Merge "[Media TTT] Add the click listener for the undo button."
diff --git a/Android.bp b/Android.bp
index 95cdea0..3b8ef61 100644
--- a/Android.bp
+++ b/Android.bp
@@ -69,7 +69,7 @@
         // Java/AIDL sources under frameworks/base
         ":framework-annotations",
         ":framework-blobstore-sources",
-        ":framework-connectivity-nsd-sources",
+        ":framework-connectivity-tiramisu-sources",
         ":framework-core-sources",
         ":framework-drm-sources",
         ":framework-graphics-nonupdatable-sources",
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
index 005c447..a6a007f 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -46,6 +46,7 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArrayMap;
+import android.util.TimeUtils;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -681,8 +682,8 @@
             final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
             final double perc = batteryLevel / 100d;
             // TODO: maybe don't give credits to bankrupt apps until battery level >= 50%
-            if (ledger.getCurrentBalance() < minBalance) {
-                final long shortfall = minBalance - getBalanceLocked(userId, pkgName);
+            final long shortfall = minBalance - ledger.getCurrentBalance();
+            if (shortfall > 0) {
                 recordTransactionLocked(userId, pkgName, ledger,
                         new Ledger.Transaction(now, now, REGULATION_BASIC_INCOME,
                                 null, (long) (perc * shortfall)), true);
@@ -1170,5 +1171,57 @@
     void dumpLocked(IndentingPrintWriter pw) {
         pw.println();
         mBalanceThresholdAlarmQueue.dump(pw);
+
+        pw.println();
+        pw.println("Ongoing events:");
+        pw.increaseIndent();
+        boolean printedEvents = false;
+        final long nowElapsed = SystemClock.elapsedRealtime();
+        for (int u = mCurrentOngoingEvents.numMaps() - 1; u >= 0; --u) {
+            final int userId = mCurrentOngoingEvents.keyAt(u);
+            for (int p = mCurrentOngoingEvents.numElementsForKey(userId) - 1; p >= 0; --p) {
+                final String pkgName = mCurrentOngoingEvents.keyAt(u, p);
+                final SparseArrayMap<String, OngoingEvent> ongoingEvents =
+                        mCurrentOngoingEvents.get(userId, pkgName);
+
+                boolean printedApp = false;
+
+                for (int e = ongoingEvents.numMaps() - 1; e >= 0; --e) {
+                    final int eventId = ongoingEvents.keyAt(e);
+                    for (int t = ongoingEvents.numElementsForKey(eventId) - 1; t >= 0; --t) {
+                        if (!printedApp) {
+                            printedApp = true;
+                            pw.println(appToString(userId, pkgName));
+                            pw.increaseIndent();
+                        }
+                        printedEvents = true;
+
+                        OngoingEvent ongoingEvent = ongoingEvents.valueAt(e, t);
+
+                        pw.print(EconomicPolicy.eventToString(ongoingEvent.eventId));
+                        if (ongoingEvent.tag != null) {
+                            pw.print("(");
+                            pw.print(ongoingEvent.tag);
+                            pw.print(")");
+                        }
+                        pw.print(" runtime=");
+                        TimeUtils.formatDuration(nowElapsed - ongoingEvent.startTimeElapsed, pw);
+                        pw.print(" delta/sec=");
+                        pw.print(ongoingEvent.deltaPerSec);
+                        pw.print(" refCount=");
+                        pw.print(ongoingEvent.refCount);
+                        pw.println();
+                    }
+                }
+
+                if (printedApp) {
+                    pw.decreaseIndent();
+                }
+            }
+        }
+        if (!printedEvents) {
+            pw.print("N/A");
+        }
+        pw.decreaseIndent();
     }
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
index 20a300a..36895a5 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -567,20 +567,23 @@
         final String pkgName = event.getPackageName();
         if (DEBUG) {
             Slog.d(TAG, "Processing event " + event.getEventType()
+                    + " (" + event.mInstanceId + ")"
                     + " for " + appToString(userId, pkgName));
         }
         final long nowElapsed = SystemClock.elapsedRealtime();
         switch (event.getEventType()) {
             case UsageEvents.Event.ACTIVITY_RESUMED:
                 mAgent.noteOngoingEventLocked(userId, pkgName,
-                        EconomicPolicy.REWARD_TOP_ACTIVITY, null, nowElapsed);
+                        EconomicPolicy.REWARD_TOP_ACTIVITY, String.valueOf(event.mInstanceId),
+                        nowElapsed);
                 break;
             case UsageEvents.Event.ACTIVITY_PAUSED:
             case UsageEvents.Event.ACTIVITY_STOPPED:
             case UsageEvents.Event.ACTIVITY_DESTROYED:
                 final long now = getCurrentTimeMillis();
                 mAgent.stopOngoingActionLocked(userId, pkgName,
-                        EconomicPolicy.REWARD_TOP_ACTIVITY, null, nowElapsed, now);
+                        EconomicPolicy.REWARD_TOP_ACTIVITY, String.valueOf(event.mInstanceId),
+                        nowElapsed, now);
                 break;
             case UsageEvents.Event.USER_INTERACTION:
             case UsageEvents.Event.CHOOSER_ACTION:
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
index a234ae6..f4917ad 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
@@ -138,6 +138,11 @@
             dumpTime(pw, transaction.endTimeMs);
             pw.print(": ");
             pw.print(EconomicPolicy.eventToString(transaction.eventId));
+            if (transaction.tag != null) {
+                pw.print("(");
+                pw.print(transaction.tag);
+                pw.print(")");
+            }
             pw.print(" --> ");
             pw.println(narcToString(transaction.delta));
         }
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index af4053f..05a06619 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -105,6 +105,7 @@
 static const char EXIT_PROP_NAME[] = "service.bootanim.exit";
 static const char PROGRESS_PROP_NAME[] = "service.bootanim.progress";
 static const char DISPLAYS_PROP_NAME[] = "persist.service.bootanim.displays";
+static const char CLOCK_ENABLED_PROP_NAME[] = "persist.sys.bootanim.clock.enabled";
 static const int ANIM_ENTRY_NAME_MAX = ANIM_PATH_MAX + 1;
 static constexpr size_t TEXT_POS_LEN_MAX = 16;
 static const int DYNAMIC_COLOR_COUNT = 4;
@@ -1320,6 +1321,8 @@
     }
     if (!anyPartHasClock) {
         mClockEnabled = false;
+    } else if (!android::base::GetBoolProperty(CLOCK_ENABLED_PROP_NAME, false)) {
+        mClockEnabled = false;
     }
 
     // Check if npot textures are supported
diff --git a/core/api/current.txt b/core/api/current.txt
index ae5c7c9..08adbcb 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10127,8 +10127,10 @@
     ctor public CompanionDeviceService();
     method @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public final void dispatchMessage(int, int, @NonNull byte[]);
     method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
-    method @MainThread public abstract void onDeviceAppeared(@NonNull String);
-    method @MainThread public abstract void onDeviceDisappeared(@NonNull String);
+    method @Deprecated @MainThread public void onDeviceAppeared(@NonNull String);
+    method @MainThread public void onDeviceAppeared(@NonNull android.companion.AssociationInfo);
+    method @Deprecated @MainThread public void onDeviceDisappeared(@NonNull String);
+    method @MainThread public void onDeviceDisappeared(@NonNull android.companion.AssociationInfo);
     method @MainThread public void onDispatchMessage(int, int, @NonNull byte[]);
     field public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService";
   }
@@ -17886,6 +17888,7 @@
     field public static final int RGB_565 = 4; // 0x4
     field public static final int RGB_888 = 3; // 0x3
     field public static final int S_UI8 = 53; // 0x35
+    field public static final long USAGE_COMPOSER_OVERLAY = 2048L; // 0x800L
     field public static final long USAGE_CPU_READ_OFTEN = 3L; // 0x3L
     field public static final long USAGE_CPU_READ_RARELY = 2L; // 0x2L
     field public static final long USAGE_CPU_WRITE_OFTEN = 48L; // 0x30L
@@ -18525,6 +18528,7 @@
     method @NonNull public android.hardware.camera2.CameraExtensionCharacteristics getCameraExtensionCharacteristics(@NonNull String) throws android.hardware.camera2.CameraAccessException;
     method @NonNull public String[] getCameraIdList() throws android.hardware.camera2.CameraAccessException;
     method @NonNull public java.util.Set<java.util.Set<java.lang.String>> getConcurrentCameraIds() throws android.hardware.camera2.CameraAccessException;
+    method public int getTorchStrengthLevel(@NonNull String) throws android.hardware.camera2.CameraAccessException;
     method @RequiresPermission(android.Manifest.permission.CAMERA) public boolean isConcurrentSessionConfigurationSupported(@NonNull java.util.Map<java.lang.String,android.hardware.camera2.params.SessionConfiguration>) throws android.hardware.camera2.CameraAccessException;
     method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, @NonNull android.hardware.camera2.CameraDevice.StateCallback, @Nullable android.os.Handler) throws android.hardware.camera2.CameraAccessException;
     method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
@@ -18533,6 +18537,7 @@
     method public void registerTorchCallback(@NonNull android.hardware.camera2.CameraManager.TorchCallback, @Nullable android.os.Handler);
     method public void registerTorchCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraManager.TorchCallback);
     method public void setTorchMode(@NonNull String, boolean) throws android.hardware.camera2.CameraAccessException;
+    method public void turnOnTorchWithStrengthLevel(@NonNull String, int) throws android.hardware.camera2.CameraAccessException;
     method public void unregisterAvailabilityCallback(@NonNull android.hardware.camera2.CameraManager.AvailabilityCallback);
     method public void unregisterTorchCallback(@NonNull android.hardware.camera2.CameraManager.TorchCallback);
   }
@@ -18550,6 +18555,7 @@
     ctor public CameraManager.TorchCallback();
     method public void onTorchModeChanged(@NonNull String, boolean);
     method public void onTorchModeUnavailable(@NonNull String);
+    method public void onTorchStrengthLevelChanged(@NonNull String, int);
   }
 
   public abstract class CameraMetadata<TKey> {
@@ -32520,7 +32526,7 @@
     method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.CREATE_USERS"}) public int getUserCount();
     method public long getUserCreationTime(android.os.UserHandle);
     method public android.os.UserHandle getUserForSerialNumber(long);
-    method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED, "android.permission.CREATE_USERS"}, conditional=true) public String getUserName();
+    method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.CREATE_USERS", "android.permission.QUERY_USERS", android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED}) public String getUserName();
     method public java.util.List<android.os.UserHandle> getUserProfiles();
     method public android.os.Bundle getUserRestrictions();
     method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.os.Bundle getUserRestrictions(android.os.UserHandle);
@@ -43280,6 +43286,7 @@
     method public int getPhoneType();
     method @RequiresPermission(anyOf={"android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE}) public int getPreferredOpportunisticDataSubscription();
     method @Nullable @RequiresPermission(allOf={android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.ACCESS_COARSE_LOCATION}) public android.telephony.ServiceState getServiceState();
+    method @Nullable @RequiresPermission(allOf={android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.ACCESS_COARSE_LOCATION}) public android.telephony.ServiceState getServiceState(boolean, boolean);
     method @Nullable public android.telephony.SignalStrength getSignalStrength();
     method public int getSimCarrierId();
     method @Nullable public CharSequence getSimCarrierIdName();
@@ -43332,8 +43339,10 @@
     method public boolean isWorldPhone();
     method @Deprecated public void listen(android.telephony.PhoneStateListener, int);
     method public void registerTelephonyCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyCallback);
+    method public void registerTelephonyCallback(boolean, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyCallback);
     method @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) public void requestCellInfoUpdate(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyManager.CellInfoCallback);
     method @RequiresPermission(allOf={android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.ACCESS_FINE_LOCATION}) public android.telephony.NetworkScan requestNetworkScan(android.telephony.NetworkScanRequest, java.util.concurrent.Executor, android.telephony.TelephonyScanManager.NetworkScanCallback);
+    method @Nullable @RequiresPermission(allOf={android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.ACCESS_FINE_LOCATION}) public android.telephony.NetworkScan requestNetworkScan(boolean, @NonNull android.telephony.NetworkScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyScanManager.NetworkScanCallback);
     method public void sendDialerSpecialCode(String);
     method public String sendEnvelopeWithStatus(String);
     method @RequiresPermission(android.Manifest.permission.CALL_PHONE) public void sendUssdRequest(String, android.telephony.TelephonyManager.UssdResponseCallback, android.os.Handler);
@@ -49041,6 +49050,7 @@
     method @NonNull public android.view.SurfaceControl build();
     method @NonNull public android.view.SurfaceControl.Builder setBufferSize(@IntRange(from=0) int, @IntRange(from=0) int);
     method @NonNull public android.view.SurfaceControl.Builder setFormat(int);
+    method @NonNull public android.view.SurfaceControl.Builder setHidden(boolean);
     method @NonNull public android.view.SurfaceControl.Builder setName(@NonNull String);
     method @NonNull public android.view.SurfaceControl.Builder setOpaque(boolean);
     method @NonNull public android.view.SurfaceControl.Builder setParent(@Nullable android.view.SurfaceControl);
@@ -49055,11 +49065,18 @@
     method @NonNull public android.view.SurfaceControl.Transaction merge(@NonNull android.view.SurfaceControl.Transaction);
     method @NonNull public android.view.SurfaceControl.Transaction reparent(@NonNull android.view.SurfaceControl, @Nullable android.view.SurfaceControl);
     method @NonNull public android.view.SurfaceControl.Transaction setAlpha(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0, to=1.0) float);
+    method @NonNull public android.view.SurfaceControl.Transaction setBuffer(@NonNull android.view.SurfaceControl, @Nullable android.hardware.HardwareBuffer);
     method @NonNull public android.view.SurfaceControl.Transaction setBufferSize(@NonNull android.view.SurfaceControl, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @NonNull public android.view.SurfaceControl.Transaction setBufferTransform(@NonNull android.view.SurfaceControl, int);
+    method @NonNull public android.view.SurfaceControl.Transaction setCrop(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect);
+    method @NonNull public android.view.SurfaceControl.Transaction setDamageRegion(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Region);
     method @NonNull public android.view.SurfaceControl.Transaction setFrameRate(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0) float, int);
     method @NonNull public android.view.SurfaceControl.Transaction setFrameRate(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0) float, int, int);
     method @NonNull public android.view.SurfaceControl.Transaction setGeometry(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect, @Nullable android.graphics.Rect, int);
     method @NonNull public android.view.SurfaceControl.Transaction setLayer(@NonNull android.view.SurfaceControl, @IntRange(from=java.lang.Integer.MIN_VALUE, to=java.lang.Integer.MAX_VALUE) int);
+    method @NonNull public android.view.SurfaceControl.Transaction setOpaque(@NonNull android.view.SurfaceControl, boolean);
+    method @NonNull public android.view.SurfaceControl.Transaction setPosition(@NonNull android.view.SurfaceControl, float, float);
+    method @NonNull public android.view.SurfaceControl.Transaction setScale(@NonNull android.view.SurfaceControl, float, float);
     method @NonNull public android.view.SurfaceControl.Transaction setVisibility(@NonNull android.view.SurfaceControl, boolean);
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.view.SurfaceControl.Transaction> CREATOR;
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index b8ce02e7..7756906 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -215,6 +215,10 @@
 
 package android.net {
 
+  public final class ConnectivityFrameworkInitializerTiramisu {
+    method public static void registerServiceWrappers();
+  }
+
   public final class EthernetNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
     ctor public EthernetNetworkSpecifier(@NonNull String);
     method public int describeContents();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 9cfa3d0..a33d0a2 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -70,6 +70,7 @@
     field public static final String BIND_TRANSLATION_SERVICE = "android.permission.BIND_TRANSLATION_SERVICE";
     field public static final String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT";
     field public static final String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE";
+    field public static final String BLUETOOTH_MAP = "android.permission.BLUETOOTH_MAP";
     field public static final String BRICK = "android.permission.BRICK";
     field public static final String BRIGHTNESS_SLIDER_USAGE = "android.permission.BRIGHTNESS_SLIDER_USAGE";
     field public static final String BROADCAST_CLOSE_SYSTEM_DIALOGS = "android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS";
@@ -219,6 +220,7 @@
     field public static final String PROVIDE_TRUST_AGENT = "android.permission.PROVIDE_TRUST_AGENT";
     field public static final String QUERY_ADMIN_POLICY = "android.permission.QUERY_ADMIN_POLICY";
     field public static final String QUERY_TIME_ZONE_RULES = "android.permission.QUERY_TIME_ZONE_RULES";
+    field public static final String QUERY_USERS = "android.permission.QUERY_USERS";
     field public static final String RADIO_SCAN_WITHOUT_LOCATION = "android.permission.RADIO_SCAN_WITHOUT_LOCATION";
     field public static final String READ_ACTIVE_EMERGENCY_SESSION = "android.permission.READ_ACTIVE_EMERGENCY_SESSION";
     field public static final String READ_APP_SPECIFIC_LOCALES = "android.permission.READ_APP_SPECIFIC_LOCALES";
@@ -2427,6 +2429,8 @@
     method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public boolean canPairWithoutPrompt(@NonNull String, @NonNull String, @NonNull android.os.UserHandle);
     method @NonNull @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public java.util.List<android.companion.AssociationInfo> getAllAssociations();
     method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public boolean isDeviceAssociatedForWifiConnection(@NonNull String, @NonNull android.net.MacAddress, @NonNull android.os.UserHandle);
+    method @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceAppeared(int);
+    method @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceDisappeared(int);
     method @RequiresPermission("android.permission.MANAGE_COMPANION_DEVICES") public void removeOnAssociationsChangedListener(@NonNull android.companion.CompanionDeviceManager.OnAssociationsChangedListener);
   }
 
@@ -3896,6 +3900,127 @@
 
 }
 
+package android.hardware.input {
+
+  public final class VirtualKeyEvent implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAction();
+    method public int getKeyCode();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int ACTION_DOWN = 0; // 0x0
+    field public static final int ACTION_UP = 1; // 0x1
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualKeyEvent> CREATOR;
+  }
+
+  public static final class VirtualKeyEvent.Builder {
+    ctor public VirtualKeyEvent.Builder();
+    method @NonNull public android.hardware.input.VirtualKeyEvent build();
+    method @NonNull public android.hardware.input.VirtualKeyEvent.Builder setAction(int);
+    method @NonNull public android.hardware.input.VirtualKeyEvent.Builder setKeyCode(int);
+  }
+
+  public class VirtualKeyboard implements java.io.Closeable {
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
+  }
+
+  public class VirtualMouse implements java.io.Closeable {
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendButtonEvent(@NonNull android.hardware.input.VirtualMouseButtonEvent);
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendRelativeEvent(@NonNull android.hardware.input.VirtualMouseRelativeEvent);
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendScrollEvent(@NonNull android.hardware.input.VirtualMouseScrollEvent);
+  }
+
+  public final class VirtualMouseButtonEvent implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAction();
+    method public int getButtonCode();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int ACTION_BUTTON_PRESS = 11; // 0xb
+    field public static final int ACTION_BUTTON_RELEASE = 12; // 0xc
+    field public static final int BUTTON_BACK = 8; // 0x8
+    field public static final int BUTTON_FORWARD = 16; // 0x10
+    field public static final int BUTTON_PRIMARY = 1; // 0x1
+    field public static final int BUTTON_SECONDARY = 2; // 0x2
+    field public static final int BUTTON_TERTIARY = 4; // 0x4
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualMouseButtonEvent> CREATOR;
+  }
+
+  public static final class VirtualMouseButtonEvent.Builder {
+    ctor public VirtualMouseButtonEvent.Builder();
+    method @NonNull public android.hardware.input.VirtualMouseButtonEvent build();
+    method @NonNull public android.hardware.input.VirtualMouseButtonEvent.Builder setAction(int);
+    method @NonNull public android.hardware.input.VirtualMouseButtonEvent.Builder setButtonCode(int);
+  }
+
+  public final class VirtualMouseRelativeEvent implements android.os.Parcelable {
+    method public int describeContents();
+    method public float getRelativeX();
+    method public float getRelativeY();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualMouseRelativeEvent> CREATOR;
+  }
+
+  public static final class VirtualMouseRelativeEvent.Builder {
+    ctor public VirtualMouseRelativeEvent.Builder();
+    method @NonNull public android.hardware.input.VirtualMouseRelativeEvent build();
+    method @NonNull public android.hardware.input.VirtualMouseRelativeEvent.Builder setRelativeX(float);
+    method @NonNull public android.hardware.input.VirtualMouseRelativeEvent.Builder setRelativeY(float);
+  }
+
+  public final class VirtualMouseScrollEvent implements android.os.Parcelable {
+    method public int describeContents();
+    method public float getXAxisMovement();
+    method public float getYAxisMovement();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualMouseScrollEvent> CREATOR;
+  }
+
+  public static final class VirtualMouseScrollEvent.Builder {
+    ctor public VirtualMouseScrollEvent.Builder();
+    method @NonNull public android.hardware.input.VirtualMouseScrollEvent build();
+    method @NonNull public android.hardware.input.VirtualMouseScrollEvent.Builder setXAxisMovement(@FloatRange(from=-1.0F, to=1.0f) float);
+    method @NonNull public android.hardware.input.VirtualMouseScrollEvent.Builder setYAxisMovement(@FloatRange(from=-1.0F, to=1.0f) float);
+  }
+
+  public final class VirtualTouchEvent implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAction();
+    method public float getMajorAxisSize();
+    method public int getPointerId();
+    method public float getPressure();
+    method public int getToolType();
+    method public float getX();
+    method public float getY();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int ACTION_CANCEL = 3; // 0x3
+    field public static final int ACTION_DOWN = 0; // 0x0
+    field public static final int ACTION_MOVE = 2; // 0x2
+    field public static final int ACTION_UP = 1; // 0x1
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualTouchEvent> CREATOR;
+    field public static final int TOOL_TYPE_FINGER = 1; // 0x1
+    field public static final int TOOL_TYPE_PALM = 5; // 0x5
+  }
+
+  public static final class VirtualTouchEvent.Builder {
+    ctor public VirtualTouchEvent.Builder();
+    method @NonNull public android.hardware.input.VirtualTouchEvent build();
+    method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setAction(int);
+    method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setMajorAxisSize(@FloatRange(from=0.0f) float);
+    method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setPointerId(int);
+    method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setPressure(@FloatRange(from=0.0f) float);
+    method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setToolType(int);
+    method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setX(float);
+    method @NonNull public android.hardware.input.VirtualTouchEvent.Builder setY(float);
+  }
+
+  public class VirtualTouchscreen implements java.io.Closeable {
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
+  }
+
+}
+
 package android.hardware.lights {
 
   public final class LightState implements android.os.Parcelable {
@@ -5751,9 +5876,11 @@
     method public android.media.AudioTrack createAudioTrackSource(android.media.audiopolicy.AudioMix) throws java.lang.IllegalArgumentException;
     method public int detachMixes(@NonNull java.util.List<android.media.audiopolicy.AudioMix>);
     method public int getFocusDuckingBehavior();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioFocusInfo> getFocusStack();
     method public int getStatus();
     method public boolean removeUidDeviceAffinity(int);
     method public boolean removeUserIdDeviceAffinity(int);
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean sendFocusLoss(@NonNull android.media.AudioFocusInfo) throws java.lang.IllegalStateException;
     method public int setFocusDuckingBehavior(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException;
     method public void setRegistration(String);
     method public boolean setUidDeviceAffinity(int, @NonNull java.util.List<android.media.AudioDeviceInfo>);
@@ -6323,6 +6450,7 @@
     method public int flush();
     method public long read(long);
     method public long read(@NonNull byte[], long, long);
+    method public long seek(long);
     method public void setFileDescriptor(@NonNull android.os.ParcelFileDescriptor);
     method public int start();
     method public int stop();
@@ -6458,6 +6586,7 @@
 
   public class DownloadEvent extends android.media.tv.tuner.filter.FilterEvent {
     method public int getDataLength();
+    method public int getDownloadId();
     method public int getItemFragmentIndex();
     method public int getItemId();
     method public int getLastItemFragmentIndex();
@@ -6467,11 +6596,13 @@
   public class DownloadSettings extends android.media.tv.tuner.filter.Settings {
     method @NonNull public static android.media.tv.tuner.filter.DownloadSettings.Builder builder(int);
     method public int getDownloadId();
+    method public boolean useDownloadId();
   }
 
   public static class DownloadSettings.Builder {
     method @NonNull public android.media.tv.tuner.filter.DownloadSettings build();
     method @NonNull public android.media.tv.tuner.filter.DownloadSettings.Builder setDownloadId(int);
+    method @NonNull public android.media.tv.tuner.filter.DownloadSettings.Builder setUseDownloadId(boolean);
   }
 
   public class Filter implements java.lang.AutoCloseable {
@@ -6570,12 +6701,14 @@
     method public long getAudioHandle();
     method public long getAvDataId();
     method public long getDataLength();
+    method public long getDts();
     method @Nullable public android.media.tv.tuner.filter.AudioDescriptor getExtraMetaData();
     method @Nullable public android.media.MediaCodec.LinearBlock getLinearBlock();
     method @IntRange(from=0) public int getMpuSequenceNumber();
     method public long getOffset();
     method public long getPts();
     method public int getStreamId();
+    method public boolean isDtsPresent();
     method public boolean isPrivateData();
     method public boolean isPtsPresent();
     method public boolean isSecureMemory();
@@ -6685,7 +6818,8 @@
   }
 
   public class SectionEvent extends android.media.tv.tuner.filter.FilterEvent {
-    method public int getDataLength();
+    method @Deprecated public int getDataLength();
+    method public long getDataLengthLong();
     method public int getSectionNumber();
     method public int getTableId();
     method public int getVersion();
@@ -7417,6 +7551,7 @@
     method public int getSignalStrength();
     method public int getSnr();
     method public int getSpectralInversion();
+    method @NonNull public int[] getStreamIdList();
     method public int getSymbolRate();
     method @IntRange(from=0, to=65535) public int getSystemId();
     method public int getTransmissionMode();
@@ -7463,6 +7598,7 @@
     field public static final int FRONTEND_STATUS_TYPE_SIGNAL_STRENGTH = 6; // 0x6
     field public static final int FRONTEND_STATUS_TYPE_SNR = 1; // 0x1
     field public static final int FRONTEND_STATUS_TYPE_SPECTRAL = 10; // 0xa
+    field public static final int FRONTEND_STATUS_TYPE_STREAM_ID_LIST = 39; // 0x27
     field public static final int FRONTEND_STATUS_TYPE_SYMBOL_RATE = 7; // 0x7
     field public static final int FRONTEND_STATUS_TYPE_T2_SYSTEM_ID = 29; // 0x1d
     field public static final int FRONTEND_STATUS_TYPE_TRANSMISSION_MODE = 27; // 0x1b
@@ -9059,31 +9195,31 @@
     method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public void clearSeedAccountData();
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.os.UserHandle createProfile(@NonNull String, @NonNull String, @NonNull java.util.Set<java.lang.String>) throws android.os.UserManager.UserOperationException;
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.os.NewUserResponse createUser(@NonNull android.os.NewUserRequest);
-    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional=true) public java.util.List<android.os.UserHandle> getAllProfiles();
-    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional=true) public java.util.List<android.os.UserHandle> getEnabledProfiles();
+    method @NonNull public java.util.List<android.os.UserHandle> getAllProfiles();
+    method @NonNull public java.util.List<android.os.UserHandle> getEnabledProfiles();
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public android.os.UserHandle getProfileParent(@NonNull android.os.UserHandle);
-    method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.os.UserHandle getRestrictedProfileParent();
+    method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public android.os.UserHandle getRestrictedProfileParent();
     method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public String getSeedAccountName();
     method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public android.os.PersistableBundle getSeedAccountOptions();
     method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public String getSeedAccountType();
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public long[] getSerialNumbersOfUsers(boolean);
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.os.UserHandle> getUserHandles(boolean);
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED}) public android.graphics.Bitmap getUserIcon();
-    method @Deprecated @android.os.UserManager.UserRestrictionSource @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public int getUserRestrictionSource(String, android.os.UserHandle);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public java.util.List<android.os.UserManager.EnforcingUser> getUserRestrictionSources(String, android.os.UserHandle);
+    method @Deprecated @android.os.UserManager.UserRestrictionSource @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public int getUserRestrictionSource(String, android.os.UserHandle);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public java.util.List<android.os.UserManager.EnforcingUser> getUserRestrictionSources(String, android.os.UserHandle);
     method @RequiresPermission(allOf={android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public int getUserSwitchability();
     method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean hasRestrictedProfiles();
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean hasUserRestrictionForUser(@NonNull String, @NonNull android.os.UserHandle);
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isAdminUser();
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isAdminUser();
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isCloneProfile();
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isGuestUser();
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isManagedProfile(int);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isGuestUser();
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isManagedProfile(int);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isMediaSharedWithParent();
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isPrimaryUser();
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isProfile();
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isPrimaryUser();
+    method public boolean isProfile();
     method public boolean isRestrictedProfile();
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional=true) public boolean isRestrictedProfile(@NonNull android.os.UserHandle);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean isSameProfileGroup(@NonNull android.os.UserHandle, @NonNull android.os.UserHandle);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSameProfileGroup(@NonNull android.os.UserHandle, @NonNull android.os.UserHandle);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED}) public boolean isUserNameSet();
     method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean isUserOfType(@NonNull String);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isUserUnlockingOrUnlocked(@NonNull android.os.UserHandle);
@@ -9576,6 +9712,7 @@
     field public static final String NAMESPACE_TELEPHONY = "telephony";
     field public static final String NAMESPACE_TETHERING = "tethering";
     field public static final String NAMESPACE_TEXTCLASSIFIER = "textclassifier";
+    field public static final String NAMESPACE_UWB = "uwb";
     field public static final String NAMESPACE_WINDOW_MANAGER_NATIVE_BOOT = "window_manager_native_boot";
   }
 
@@ -13909,12 +14046,14 @@
   }
 
   public final class RcsClientConfiguration implements android.os.Parcelable {
-    ctor public RcsClientConfiguration(@NonNull String, @NonNull String, @NonNull String, @NonNull String);
+    ctor @Deprecated public RcsClientConfiguration(@NonNull String, @NonNull String, @NonNull String, @NonNull String);
+    ctor public RcsClientConfiguration(@NonNull String, @NonNull String, @NonNull String, @NonNull String, boolean);
     method public int describeContents();
     method @NonNull public String getClientVendor();
     method @NonNull public String getClientVersion();
     method @NonNull public String getRcsProfile();
     method @NonNull public String getRcsVersion();
+    method public boolean isRcsEnabledByUser();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ims.RcsClientConfiguration> CREATOR;
     field public static final String RCS_PROFILE_1_0 = "UP_1.0";
@@ -14051,6 +14190,7 @@
     field public static final int PUBLISH_STATE_NOT_PUBLISHED = 2; // 0x2
     field public static final int PUBLISH_STATE_OK = 1; // 0x1
     field public static final int PUBLISH_STATE_OTHER_ERROR = 6; // 0x6
+    field public static final int PUBLISH_STATE_PUBLISHING = 7; // 0x7
     field public static final int PUBLISH_STATE_RCS_PROVISION_ERROR = 4; // 0x4
     field public static final int PUBLISH_STATE_REQUEST_TIMEOUT = 5; // 0x5
     field public static final int PUBLISH_STATE_VOICE_PROVISION_ERROR = 3; // 0x3
@@ -14313,6 +14453,7 @@
 package android.telephony.ims.stub {
 
   public interface CapabilityExchangeEventListener {
+    method public default void onPublishUpdated(int, @NonNull String, int, @NonNull String) throws android.telephony.ims.ImsException;
     method public void onRemoteCapabilityRequest(@NonNull android.net.Uri, @NonNull java.util.Set<java.lang.String>, @NonNull android.telephony.ims.stub.CapabilityExchangeEventListener.OptionsRequestCallback) throws android.telephony.ims.ImsException;
     method public void onRequestPublishCapabilities(int) throws android.telephony.ims.ImsException;
     method public void onUnpublish() throws android.telephony.ims.ImsException;
diff --git a/core/api/system-removed.txt b/core/api/system-removed.txt
index 9cb9ddc..2c5acf1 100644
--- a/core/api/system-removed.txt
+++ b/core/api/system-removed.txt
@@ -60,14 +60,6 @@
 
 }
 
-package android.bluetooth {
-
-  public final class BluetoothHeadset implements android.bluetooth.BluetoothProfile {
-    method @Deprecated @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.MODIFY_PHONE_STATE}) public boolean setPriority(android.bluetooth.BluetoothDevice, int);
-  }
-
-}
-
 package android.content {
 
   public class Intent implements java.lang.Cloneable android.os.Parcelable {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 9e3b374..fd11567 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -353,6 +353,10 @@
     method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void togglePanel();
   }
 
+  public static final class StatusBarManager.DisableInfo {
+    method public boolean isRotationSuggestionDisabled();
+  }
+
   public final class SyncNotedAppOp implements android.os.Parcelable {
     ctor public SyncNotedAppOp(int, @IntRange(from=0L) int, @Nullable String, @NonNull String);
   }
@@ -685,6 +689,14 @@
 
 }
 
+package android.companion {
+
+  public abstract class CompanionDeviceService extends android.app.Service {
+    method public void onBindCompanionDeviceService(@NonNull android.content.Intent);
+  }
+
+}
+
 package android.content {
 
   public final class AttributionSource implements android.os.Parcelable {
@@ -1818,7 +1830,7 @@
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createRestrictedProfile(@Nullable String);
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createUser(@Nullable String, @NonNull String, int);
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.Set<java.lang.String> getPreInstallableSystemPackages(@NonNull String);
-    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public String getUserType();
+    method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public String getUserType();
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.content.pm.UserInfo> getUsers(boolean, boolean, boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean hasBaseUserRestriction(@NonNull String, @NonNull android.os.UserHandle);
     method public static boolean isGuestUserEphemeral();
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index a642696..3573a56 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -1394,6 +1394,12 @@
          * {@link AccessibilityService#onServiceConnected()} has not yet been
          * called) or the service has been disconnected, this method will
          * return a default value of {@code 1.0f}.
+         * </p>
+         * <p>
+         * <strong>Note:</strong> This legacy API gets the scale of full-screen
+         * magnification. To get the scale of the current controlling magnifier,
+         * use {@link #getMagnificationConfig} instead.
+         * </p>
          *
          * @return the current magnification scale
          */
@@ -1422,6 +1428,12 @@
          * {@link AccessibilityService#onServiceConnected()} has not yet been
          * called) or the service has been disconnected, this method will
          * return a default value of {@code 0.0f}.
+         * </p>
+         * <p>
+         * <strong>Note:</strong> This legacy API gets the center position of full-screen
+         * magnification. To get the magnification center of the current controlling magnifier,
+         * use {@link #getMagnificationConfig} instead.
+         * </p>
          *
          * @return the unscaled screen-relative X coordinate of the center of
          *         the magnified region
@@ -1451,6 +1463,12 @@
          * {@link AccessibilityService#onServiceConnected()} has not yet been
          * called) or the service has been disconnected, this method will
          * return a default value of {@code 0.0f}.
+         * </p>
+         * <p>
+         * <strong>Note:</strong> This legacy API gets the center position of full-screen
+         * magnification. To get the magnification center of the current controlling magnifier,
+         * use {@link #getMagnificationConfig} instead.
+         * </p>
          *
          * @return the unscaled screen-relative Y coordinate of the center of
          *         the magnified region
@@ -1571,6 +1589,11 @@
          * {@link AccessibilityService#onServiceConnected()} has not yet been
          * called) or the service has been disconnected, this method will have
          * no effect and return {@code false}.
+         * <p>
+         * <strong>Note:</strong> This legacy API sets the scale of full-screen
+         * magnification. To set the scale of the specified magnifier,
+         * use {@link #setMagnificationConfig} instead.
+         * </p>
          *
          * @param scale the magnification scale to set, must be >= 1 and <= 8
          * @param animate {@code true} to animate from the current scale or
@@ -1602,6 +1625,12 @@
          * {@link AccessibilityService#onServiceConnected()} has not yet been
          * called) or the service has been disconnected, this method will have
          * no effect and return {@code false}.
+         * </p>
+         * <p>
+         * <strong>Note:</strong> This legacy API sets the center of full-screen
+         * magnification. To set the center of the specified magnifier,
+         * use {@link #setMagnificationConfig} instead.
+         * </p>
          *
          * @param centerX the unscaled screen-relative X coordinate on which to
          *                center the viewport
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 2c41e8d..3cbae99 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -23,6 +23,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 import android.os.Looper;
+import android.os.SystemProperties;
 import android.os.Trace;
 import android.util.AndroidRuntimeException;
 import android.util.Log;
@@ -74,6 +75,8 @@
 public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
     private static final String TAG = "ValueAnimator";
     private static final boolean DEBUG = false;
+    private static final boolean TRACE_ANIMATION_FRACTION = SystemProperties.getBoolean(
+            "persist.debug.animator.trace_fraction", false);
 
     /**
      * Internal constants
@@ -1554,6 +1557,10 @@
     @CallSuper
     @UnsupportedAppUsage
     void animateValue(float fraction) {
+        if (TRACE_ANIMATION_FRACTION) {
+            Trace.traceCounter(Trace.TRACE_TAG_VIEW, getNameForTrace() + hashCode(),
+                    (int) (fraction * 1000));
+        }
         fraction = mInterpolator.getInterpolation(fraction);
         mCurrentFraction = fraction;
         int numValues = mValues.length;
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 1c53fbd4..c0a8c1e 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -2700,6 +2700,16 @@
         }
     }
 
+    void onSystemUiContextCleanup(ContextImpl context) {
+        synchronized (this) {
+            if (mDisplaySystemUiContexts == null) return;
+            final int index = mDisplaySystemUiContexts.indexOfValue(context);
+            if (index >= 0) {
+                mDisplaySystemUiContexts.removeAt(index);
+            }
+        }
+    }
+
     public void installSystemApplicationInfo(ApplicationInfo info, ClassLoader classLoader) {
         synchronized (this) {
             getSystemContext().installSystemApplicationInfo(info, classLoader);
@@ -4093,12 +4103,12 @@
     }
 
     private void handleStartBinderTracking() {
-        Binder.enableTracing();
+        Binder.enableStackTracking();
     }
 
     private void handleStopBinderTrackingAndDump(ParcelFileDescriptor fd) {
         try {
-            Binder.disableTracing();
+            Binder.disableStackTracking();
             Binder.getTransactionTracker().writeTracesToFile(fd);
         } finally {
             IoUtils.closeQuietly(fd);
@@ -6637,7 +6647,7 @@
         boolean isAppProfileable = isAppDebuggable || data.appInfo.isProfileable();
         Trace.setAppTracingAllowed(isAppProfileable);
         if ((isAppProfileable || Build.IS_DEBUGGABLE) && data.enableBinderTracking) {
-            Binder.enableTracing();
+            Binder.enableStackTracking();
         }
 
         // Initialize heap profiling.
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 4a7361e..885feb1 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -3212,6 +3212,10 @@
     final void performFinalCleanup(String who, String what) {
         //Log.i(TAG, "Cleanup up context: " + this);
         mPackageInfo.removeContextRegistrations(getOuterContext(), who, what);
+        if (mContextType == CONTEXT_TYPE_SYSTEM_OR_SYSTEM_UI
+                && mToken instanceof WindowTokenClient) {
+            mMainThread.onSystemUiContextCleanup(this);
+        }
     }
 
     @UnsupportedAppUsage
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 8f904b5..a2578d6 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -520,6 +520,10 @@
     // descriptor.
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     boolean stopBinderTrackingAndDump(in ParcelFileDescriptor fd);
+
+    /** Enables server-side binder tracing for the calling uid. */
+    void enableBinderTracing();
+
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     void suppressResizeConfigChanges(boolean suppress);
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 5750484..77af474 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -1352,7 +1352,9 @@
 
         Application app = null;
 
-        String appClass = mApplicationInfo.className;
+        final String myProcessName = Process.myProcessName();
+        String appClass = mApplicationInfo.getCustomApplicationClassNameForProcess(
+                myProcessName);
         if (forceDefaultAppClass || (appClass == null)) {
             appClass = "android.app.Application";
         }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index af907af..779552f1 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5808,6 +5808,7 @@
                     p, result);
             buildCustomContentIntoTemplate(mContext, standard, customContent,
                     p, result);
+            makeHeaderExpanded(standard);
             return standard;
         }
 
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 2392c9a..ae578f5 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -165,7 +165,7 @@
      *
      * @hide
      */
-    public static final int DEFAULT_SETUP_DISABLE2_FLAGS = DISABLE2_ROTATE_SUGGESTIONS;
+    public static final int DEFAULT_SETUP_DISABLE2_FLAGS = DISABLE2_NONE;
 
     /**
      * disable flags to be applied when the device is sim-locked.
@@ -712,6 +712,7 @@
         private boolean mSystemIcons;
         private boolean mClock;
         private boolean mNotificationIcons;
+        private boolean mRotationSuggestion;
 
         /** @hide */
         public DisableInfo(int flags1, int flags2) {
@@ -723,6 +724,7 @@
             mSystemIcons = (flags1 & DISABLE_SYSTEM_INFO) != 0;
             mClock = (flags1 & DISABLE_CLOCK) != 0;
             mNotificationIcons = (flags1 & DISABLE_NOTIFICATION_ICONS) != 0;
+            mRotationSuggestion = (flags2 & DISABLE2_ROTATE_SUGGESTIONS) != 0;
         }
 
         /** @hide */
@@ -846,14 +848,24 @@
         }
 
         /**
-         * @return {@code true} if no components are disabled (default state)
+         * Returns whether the rotation suggestion is disabled.
          *
          * @hide
          */
+        @TestApi
+        public boolean isRotationSuggestionDisabled() {
+            return mRotationSuggestion;
+        }
+
+        /**
+         * @return {@code true} if no components are disabled (default state)
+         * @hide
+         */
         @SystemApi
         public boolean areAllComponentsEnabled() {
             return !mStatusBarExpansion && !mNavigateHome && !mNotificationPeeking && !mRecents
-                    && !mSearch && !mSystemIcons && !mClock && !mNotificationIcons;
+                    && !mSearch && !mSystemIcons && !mClock && !mNotificationIcons
+                    && !mRotationSuggestion;
         }
 
         /** @hide */
@@ -866,6 +878,7 @@
             mSystemIcons = false;
             mClock = false;
             mNotificationIcons = false;
+            mRotationSuggestion = false;
         }
 
         /**
@@ -875,7 +888,8 @@
          */
         public boolean areAllComponentsDisabled() {
             return mStatusBarExpansion && mNavigateHome && mNotificationPeeking
-                    && mRecents && mSearch && mSystemIcons && mClock && mNotificationIcons;
+                    && mRecents && mSearch && mSystemIcons && mClock && mNotificationIcons
+                    && mRotationSuggestion;
         }
 
         /** @hide */
@@ -888,6 +902,7 @@
             mSystemIcons = true;
             mClock = true;
             mNotificationIcons = true;
+            mRotationSuggestion = true;
         }
 
         @NonNull
@@ -904,6 +919,7 @@
             sb.append(" mSystemIcons=").append(mSystemIcons ? "disabled" : "enabled");
             sb.append(" mClock=").append(mClock ? "disabled" : "enabled");
             sb.append(" mNotificationIcons=").append(mNotificationIcons ? "disabled" : "enabled");
+            sb.append(" mRotationSuggestion=").append(mRotationSuggestion ? "disabled" : "enabled");
 
             return sb.toString();
 
@@ -927,6 +943,7 @@
             if (mSystemIcons) disable1 |= DISABLE_SYSTEM_INFO;
             if (mClock) disable1 |= DISABLE_CLOCK;
             if (mNotificationIcons) disable1 |= DISABLE_NOTIFICATION_ICONS;
+            if (mRotationSuggestion) disable2 |= DISABLE2_ROTATE_SUGGESTIONS;
 
             return new Pair<Integer, Integer>(disable1, disable2);
         }
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 81e6ae4..5002a59 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -131,6 +131,7 @@
 import android.media.tv.tunerresourcemanager.ITunerResourceManager;
 import android.media.tv.tunerresourcemanager.TunerResourceManager;
 import android.net.ConnectivityFrameworkInitializer;
+import android.net.ConnectivityFrameworkInitializerTiramisu;
 import android.net.EthernetManager;
 import android.net.IEthernetManager;
 import android.net.IIpSecService;
@@ -146,8 +147,6 @@
 import android.net.VpnManager;
 import android.net.lowpan.ILowpanManager;
 import android.net.lowpan.LowpanManager;
-import android.net.nsd.INsdManager;
-import android.net.nsd.NsdManager;
 import android.net.vcn.IVcnManagementService;
 import android.net.vcn.VcnManager;
 import android.net.wifi.WifiFrameworkInitializer;
@@ -576,15 +575,6 @@
                     ctx.mMainThread.getHandler());
             }});
 
-        registerService(Context.NSD_SERVICE, NsdManager.class,
-                new CachedServiceFetcher<NsdManager>() {
-            @Override
-            public NsdManager createService(ContextImpl ctx) throws ServiceNotFoundException {
-                IBinder b = ServiceManager.getServiceOrThrow(Context.NSD_SERVICE);
-                INsdManager service = INsdManager.Stub.asInterface(b);
-                return new NsdManager(ctx.getOuterContext(), service);
-            }});
-
         registerService(Context.PEOPLE_SERVICE, PeopleManager.class,
                 new CachedServiceFetcher<PeopleManager>() {
             @Override
@@ -1547,6 +1537,7 @@
             SupplementalProcessFrameworkInitializer.registerServiceWrappers();
             UwbFrameworkInitializer.registerServiceWrappers();
             SafetyCenterFrameworkInitializer.registerServiceWrappers();
+            ConnectivityFrameworkInitializerTiramisu.registerServiceWrappers();
         } finally {
             // If any of the above code throws, we're in a pretty bad shape and the process
             // will likely crash, but we'll reset it just in case there's an exception handler...
diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java
index c046324..8212eaa 100644
--- a/core/java/android/bluetooth/BluetoothHeadset.java
+++ b/core/java/android/bluetooth/BluetoothHeadset.java
@@ -601,49 +601,6 @@
     }
 
     /**
-     * Set priority of the profile
-     *
-     * <p> The device should already be paired.
-     * Priority can be one of {@link BluetoothProfile#PRIORITY_ON} or
-     * {@link BluetoothProfile#PRIORITY_OFF}
-     *
-     * @param device Paired bluetooth device
-     * @param priority
-     * @return true if priority is set, false on error
-     * @hide
-     * @deprecated Replaced with {@link #setConnectionPolicy(BluetoothDevice, int)}
-     * @removed
-     */
-    @Deprecated
-    @SystemApi
-    @RequiresLegacyBluetoothAdminPermission
-    @RequiresBluetoothConnectPermission
-    @RequiresPermission(allOf = {
-            android.Manifest.permission.BLUETOOTH_CONNECT,
-            android.Manifest.permission.MODIFY_PHONE_STATE,
-    })
-    public boolean setPriority(BluetoothDevice device, int priority) {
-        if (DBG) log("setPriority(" + device + ", " + priority + ")");
-        final IBluetoothHeadset service = mService;
-        if (service != null && isEnabled() && isValidDevice(device)) {
-            if (priority != BluetoothProfile.PRIORITY_OFF
-                    && priority != BluetoothProfile.PRIORITY_ON) {
-                return false;
-            }
-            try {
-                return service.setPriority(
-                        device, BluetoothAdapter.priorityToConnectionPolicy(priority),
-                        mAttributionSource);
-            } catch (RemoteException e) {
-                Log.e(TAG, Log.getStackTraceString(new Throwable()));
-                return false;
-            }
-        }
-        if (service == null) Log.w(TAG, "Proxy not attached to service");
-        return false;
-    }
-
-    /**
      * Set connection policy of the profile
      *
      * <p> The device should already be paired.
diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java
index 6e1f8b5..18a59d8 100644
--- a/core/java/android/companion/AssociationRequest.java
+++ b/core/java/android/companion/AssociationRequest.java
@@ -20,12 +20,15 @@
 
 import static com.android.internal.util.CollectionUtils.emptyIfNull;
 
+import static java.util.Objects.requireNonNull;
+
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.StringDef;
 import android.annotation.SystemApi;
+import android.annotation.UserIdInt;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 import android.os.Parcel;
@@ -52,12 +55,11 @@
  * device to be shown instead of a list to choose from
  */
 @DataClass(
+        genConstructor = false,
         genToString = true,
         genEqualsHashCode = true,
         genHiddenGetters = true,
         genParcelable = true,
-        genHiddenConstructor = true,
-        genBuilder = false,
         genConstDefs = false)
 public final class AssociationRequest implements Parcelable {
     /**
@@ -151,40 +153,76 @@
     private final boolean mForceConfirmation;
 
     /**
-     * The app package making the request.
-     *
+     * The app package name of the application the association will belong to.
      * Populated by the system.
-     *
      * @hide
      */
-    private @Nullable String mCallingPackage;
+    private @Nullable String mPackageName;
+
+    /**
+     * The UserId of the user the association will belong to.
+     * Populated by the system.
+     * @hide
+     */
+    private @UserIdInt int mUserId;
 
     /**
      * The user-readable description of the device profile's privileges.
-     *
      * Populated by the system.
-     *
      * @hide
      */
     private @Nullable String mDeviceProfilePrivilegesDescription;
 
     /**
      * The time at which his request was created
-     *
      * @hide
      */
-    private long mCreationTime;
+    private final long mCreationTime;
 
     /**
      * Whether the user-prompt may be skipped once the device is found.
-     *
      * Populated by the system.
-     *
      * @hide
      */
     private boolean mSkipPrompt;
 
     /**
+     * Creates a new AssociationRequest.
+     *
+     * @param singleDevice
+     *   Whether only a single device should match the provided filter.
+     *
+     *   When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac
+     *   address, bonded devices are also searched among. This allows to obtain the necessary app
+     *   privileges even if the device is already paired.
+     * @param deviceFilters
+     *   If set, only devices matching either of the given filters will be shown to the user
+     * @param deviceProfile
+     *   Profile of the device.
+     * @param displayName
+     *   The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for
+     *   "self-managed" association.
+     * @param selfManaged
+     *   Whether the association is to be managed by the companion application.
+     */
+    private AssociationRequest(
+            boolean singleDevice,
+            @NonNull List<DeviceFilter<?>> deviceFilters,
+            @Nullable @DeviceProfile String deviceProfile,
+            @Nullable CharSequence displayName,
+            boolean selfManaged,
+            boolean forceConfirmation) {
+        mSingleDevice = singleDevice;
+        mDeviceFilters = requireNonNull(deviceFilters);
+        mDeviceProfile = deviceProfile;
+        mDisplayName = displayName;
+        mSelfManaged = selfManaged;
+        mForceConfirmation = forceConfirmation;
+
+        mCreationTime = System.currentTimeMillis();
+    }
+
+    /**
      * @return profile of the companion device.
      */
     public @Nullable @DeviceProfile String getDeviceProfile() {
@@ -237,8 +275,13 @@
     }
 
     /** @hide */
-    public void setCallingPackage(@NonNull String pkg) {
-        mCallingPackage = pkg;
+    public void setPackageName(@NonNull String packageName) {
+        mPackageName = packageName;
+    }
+
+    /** @hide */
+    public void setUserId(@UserIdInt int userId) {
+        mUserId = userId;
     }
 
     /** @hide */
@@ -248,7 +291,7 @@
 
     /** @hide */
     public void setSkipPrompt(boolean value) {
-        mSkipPrompt = true;
+        mSkipPrompt = value;
     }
 
     /** @hide */
@@ -258,10 +301,6 @@
         return mDeviceFilters;
     }
 
-    private void onConstructed() {
-        mCreationTime = System.currentTimeMillis();
-    }
-
     /**
      * A builder for {@link AssociationRequest}
      */
@@ -325,7 +364,7 @@
         @NonNull
         public Builder setDisplayName(@NonNull CharSequence displayName) {
             checkNotUsed();
-            mDisplayName = Objects.requireNonNull(displayName);
+            mDisplayName = requireNonNull(displayName);
             return this;
         }
 
@@ -372,15 +411,13 @@
                         + "provide the display name of the device");
             }
             return new AssociationRequest(mSingleDevice, emptyIfNull(mDeviceFilters),
-                    mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation,
-                    null, null, -1L, false);
+                    mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation);
         }
     }
 
 
 
 
-
     // Code below generated by codegen v1.0.23.
     //
     // DO NOT MODIFY!
@@ -395,88 +432,29 @@
 
 
     /**
-     * Creates a new AssociationRequest.
-     *
-     * @param singleDevice
-     *   Whether only a single device should match the provided filter.
-     *
-     *   When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac
-     *   address, bonded devices are also searched among. This allows to obtain the necessary app
-     *   privileges even if the device is already paired.
-     * @param deviceFilters
-     *   If set, only devices matching either of the given filters will be shown to the user
-     * @param deviceProfile
-     *   Profile of the device.
-     * @param displayName
-     *   The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for
-     *   "self-managed" association.
-     * @param selfManaged
-     *   Whether the association is to be managed by the companion application.
-     * @param forceConfirmation
-     *   Indicates that the application would prefer the CompanionDeviceManager to collect an explicit
-     *   confirmation from the user before creating an association, even if such confirmation is not
-     *   required.
-     * @param callingPackage
-     *   The app package making the request.
-     *
-     *   Populated by the system.
-     * @param deviceProfilePrivilegesDescription
-     *   The user-readable description of the device profile's privileges.
-     *
-     *   Populated by the system.
-     * @param creationTime
-     *   The time at which his request was created
-     * @param skipPrompt
-     *   Whether the user-prompt may be skipped once the device is found.
-     *
-     *   Populated by the system.
-     * @hide
-     */
-    @DataClass.Generated.Member
-    public AssociationRequest(
-            boolean singleDevice,
-            @NonNull List<DeviceFilter<?>> deviceFilters,
-            @Nullable @DeviceProfile String deviceProfile,
-            @Nullable CharSequence displayName,
-            boolean selfManaged,
-            boolean forceConfirmation,
-            @Nullable String callingPackage,
-            @Nullable String deviceProfilePrivilegesDescription,
-            long creationTime,
-            boolean skipPrompt) {
-        this.mSingleDevice = singleDevice;
-        this.mDeviceFilters = deviceFilters;
-        com.android.internal.util.AnnotationValidations.validate(
-                NonNull.class, null, mDeviceFilters);
-        this.mDeviceProfile = deviceProfile;
-        com.android.internal.util.AnnotationValidations.validate(
-                DeviceProfile.class, null, mDeviceProfile);
-        this.mDisplayName = displayName;
-        this.mSelfManaged = selfManaged;
-        this.mForceConfirmation = forceConfirmation;
-        this.mCallingPackage = callingPackage;
-        this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription;
-        this.mCreationTime = creationTime;
-        this.mSkipPrompt = skipPrompt;
-
-        onConstructed();
-    }
-
-    /**
-     * The app package making the request.
-     *
+     * The app package name of the application the association will belong to.
      * Populated by the system.
      *
      * @hide
      */
     @DataClass.Generated.Member
-    public @Nullable String getCallingPackage() {
-        return mCallingPackage;
+    public @Nullable String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * The UserId of the user the association will belong to.
+     * Populated by the system.
+     *
+     * @hide
+     */
+    @DataClass.Generated.Member
+    public @UserIdInt int getUserId() {
+        return mUserId;
     }
 
     /**
      * The user-readable description of the device profile's privileges.
-     *
      * Populated by the system.
      *
      * @hide
@@ -498,7 +476,6 @@
 
     /**
      * Whether the user-prompt may be skipped once the device is found.
-     *
      * Populated by the system.
      *
      * @hide
@@ -521,7 +498,8 @@
                 "displayName = " + mDisplayName + ", " +
                 "selfManaged = " + mSelfManaged + ", " +
                 "forceConfirmation = " + mForceConfirmation + ", " +
-                "callingPackage = " + mCallingPackage + ", " +
+                "packageName = " + mPackageName + ", " +
+                "userId = " + mUserId + ", " +
                 "deviceProfilePrivilegesDescription = " + mDeviceProfilePrivilegesDescription + ", " +
                 "creationTime = " + mCreationTime + ", " +
                 "skipPrompt = " + mSkipPrompt +
@@ -547,7 +525,8 @@
                 && Objects.equals(mDisplayName, that.mDisplayName)
                 && mSelfManaged == that.mSelfManaged
                 && mForceConfirmation == that.mForceConfirmation
-                && Objects.equals(mCallingPackage, that.mCallingPackage)
+                && Objects.equals(mPackageName, that.mPackageName)
+                && mUserId == that.mUserId
                 && Objects.equals(mDeviceProfilePrivilegesDescription, that.mDeviceProfilePrivilegesDescription)
                 && mCreationTime == that.mCreationTime
                 && mSkipPrompt == that.mSkipPrompt;
@@ -566,7 +545,8 @@
         _hash = 31 * _hash + Objects.hashCode(mDisplayName);
         _hash = 31 * _hash + Boolean.hashCode(mSelfManaged);
         _hash = 31 * _hash + Boolean.hashCode(mForceConfirmation);
-        _hash = 31 * _hash + Objects.hashCode(mCallingPackage);
+        _hash = 31 * _hash + Objects.hashCode(mPackageName);
+        _hash = 31 * _hash + mUserId;
         _hash = 31 * _hash + Objects.hashCode(mDeviceProfilePrivilegesDescription);
         _hash = 31 * _hash + Long.hashCode(mCreationTime);
         _hash = 31 * _hash + Boolean.hashCode(mSkipPrompt);
@@ -583,16 +563,17 @@
         if (mSingleDevice) flg |= 0x1;
         if (mSelfManaged) flg |= 0x10;
         if (mForceConfirmation) flg |= 0x20;
-        if (mSkipPrompt) flg |= 0x200;
+        if (mSkipPrompt) flg |= 0x400;
         if (mDeviceProfile != null) flg |= 0x4;
         if (mDisplayName != null) flg |= 0x8;
-        if (mCallingPackage != null) flg |= 0x40;
-        if (mDeviceProfilePrivilegesDescription != null) flg |= 0x80;
+        if (mPackageName != null) flg |= 0x40;
+        if (mDeviceProfilePrivilegesDescription != null) flg |= 0x100;
         dest.writeInt(flg);
         dest.writeParcelableList(mDeviceFilters, flags);
         if (mDeviceProfile != null) dest.writeString(mDeviceProfile);
         if (mDisplayName != null) dest.writeCharSequence(mDisplayName);
-        if (mCallingPackage != null) dest.writeString(mCallingPackage);
+        if (mPackageName != null) dest.writeString(mPackageName);
+        dest.writeInt(mUserId);
         if (mDeviceProfilePrivilegesDescription != null) dest.writeString(mDeviceProfilePrivilegesDescription);
         dest.writeLong(mCreationTime);
     }
@@ -612,13 +593,14 @@
         boolean singleDevice = (flg & 0x1) != 0;
         boolean selfManaged = (flg & 0x10) != 0;
         boolean forceConfirmation = (flg & 0x20) != 0;
-        boolean skipPrompt = (flg & 0x200) != 0;
+        boolean skipPrompt = (flg & 0x400) != 0;
         List<DeviceFilter<?>> deviceFilters = new ArrayList<>();
         in.readParcelableList(deviceFilters, DeviceFilter.class.getClassLoader());
         String deviceProfile = (flg & 0x4) == 0 ? null : in.readString();
         CharSequence displayName = (flg & 0x8) == 0 ? null : (CharSequence) in.readCharSequence();
-        String callingPackage = (flg & 0x40) == 0 ? null : in.readString();
-        String deviceProfilePrivilegesDescription = (flg & 0x80) == 0 ? null : in.readString();
+        String packageName = (flg & 0x40) == 0 ? null : in.readString();
+        int userId = in.readInt();
+        String deviceProfilePrivilegesDescription = (flg & 0x100) == 0 ? null : in.readString();
         long creationTime = in.readLong();
 
         this.mSingleDevice = singleDevice;
@@ -631,12 +613,15 @@
         this.mDisplayName = displayName;
         this.mSelfManaged = selfManaged;
         this.mForceConfirmation = forceConfirmation;
-        this.mCallingPackage = callingPackage;
+        this.mPackageName = packageName;
+        this.mUserId = userId;
+        com.android.internal.util.AnnotationValidations.validate(
+                UserIdInt.class, null, mUserId);
         this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription;
         this.mCreationTime = creationTime;
         this.mSkipPrompt = skipPrompt;
 
-        onConstructed();
+        // onConstructed(); // You can define this method to get a callback
     }
 
     @DataClass.Generated.Member
@@ -654,10 +639,10 @@
     };
 
     @DataClass.Generated(
-            time = 1638368698639L,
+            time = 1638962248060L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/companion/AssociationRequest.java",
-            inputSignatures = "public static final  java.lang.String DEVICE_PROFILE_WATCH\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_APP_STREAMING\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_AUTOMOTIVE_PROJECTION\nprivate final  boolean mSingleDevice\nprivate final @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate final @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate final @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate final  boolean mSelfManaged\nprivate final  boolean mForceConfirmation\nprivate @android.annotation.Nullable java.lang.String mCallingPackage\nprivate @android.annotation.Nullable java.lang.String mDeviceProfilePrivilegesDescription\nprivate  long mCreationTime\nprivate  boolean mSkipPrompt\npublic @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String getDeviceProfile()\npublic @android.annotation.Nullable java.lang.CharSequence getDisplayName()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isSelfManaged()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isForceConfirmation()\npublic  boolean isSingleDevice()\npublic  void setCallingPackage(java.lang.String)\npublic  void setDeviceProfilePrivilegesDescription(java.lang.String)\npublic  void setSkipPrompt(boolean)\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nprivate  void onConstructed()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate  boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate  boolean mSelfManaged\nprivate  boolean mForceConfirmation\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDisplayName(java.lang.CharSequence)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setSelfManaged(boolean)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setForceConfirmation(boolean)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genHiddenConstructor=true, genBuilder=false, genConstDefs=false)")
+            inputSignatures = "public static final  java.lang.String DEVICE_PROFILE_WATCH\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_APP_STREAMING\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_AUTOMOTIVE_PROJECTION\nprivate final  boolean mSingleDevice\nprivate final @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate final @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate final @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate final  boolean mSelfManaged\nprivate final  boolean mForceConfirmation\nprivate @android.annotation.Nullable java.lang.String mPackageName\nprivate @android.annotation.UserIdInt int mUserId\nprivate @android.annotation.Nullable java.lang.String mDeviceProfilePrivilegesDescription\nprivate final  long mCreationTime\nprivate  boolean mSkipPrompt\npublic @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String getDeviceProfile()\npublic @android.annotation.Nullable java.lang.CharSequence getDisplayName()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isSelfManaged()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isForceConfirmation()\npublic  boolean isSingleDevice()\npublic  void setPackageName(java.lang.String)\npublic  void setUserId(int)\npublic  void setDeviceProfilePrivilegesDescription(java.lang.String)\npublic  void setSkipPrompt(boolean)\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate  boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate  boolean mSelfManaged\nprivate  boolean mForceConfirmation\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDisplayName(java.lang.CharSequence)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setSelfManaged(boolean)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setForceConfirmation(boolean)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genConstDefs=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 2b12f12..ae13425 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -776,6 +776,51 @@
         }
     }
 
+    /**
+     * Notify the system that the given self-managed association has just 'appeared'.
+     * This causes the system to bind to the companion app to keep it running until the association
+     * is reported as 'disappeared'
+     *
+     * <p>This API is only available for the companion apps that manage the connectivity by
+     * themselves.</p>
+     *
+     * @param associationId the unique {@link AssociationInfo#getId ID} assigned to the Association
+     * recorded by CompanionDeviceManager
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED)
+    public void notifyDeviceAppeared(int associationId) {
+        try {
+            mService.notifyDeviceAppeared(associationId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Notify the system that the given self-managed association has just 'disappeared'.
+     * This causes the system to unbind to the companion app.
+     *
+     * <p>This API is only available for the companion apps that manage the connectivity by
+     * themselves.</p>
+     *
+     * @param associationId the unique {@link AssociationInfo#getId ID} assigned to the Association
+     * recorded by CompanionDeviceManager
+
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED)
+    public void notifyDeviceDisappeared(int associationId) {
+        try {
+            mService.notifyDeviceDisappeared(associationId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     private boolean checkFeaturePresent() {
         boolean featurePresent = mService != null;
         if (!featurePresent && DEBUG) {
diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java
index 15266d6..3237f7c 100644
--- a/core/java/android/companion/CompanionDeviceService.java
+++ b/core/java/android/companion/CompanionDeviceService.java
@@ -21,12 +21,14 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.TestApi;
 import android.app.Service;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.IBinder;
 import android.util.Log;
 
+
 import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.util.Objects;
@@ -34,8 +36,9 @@
 /**
  * Service to be implemented by apps that manage a companion device.
  *
- * System will keep this service bound whenever an associated device is nearby,
- * ensuring app stays alive.
+ * System will keep this service bound whenever an associated device is nearby for Bluetooth
+ * devices or companion app manages the connectivity and reports disappeared, ensuring app stays
+ * alive
  *
  * An app must be {@link CompanionDeviceManager#associate associated} with at leas one device,
  * before it can take advantage of this service.
@@ -43,6 +46,17 @@
  * You must declare this service in your manifest with an
  * intent-filter action of {@link #SERVICE_INTERFACE} and
  * permission of {@link android.Manifest.permission#BIND_COMPANION_DEVICE_SERVICE}
+ *
+ * <p>If you want to declare more than one of these services, you must declare the meta-data in the
+ * service of your manifest with the corresponding name and value to true to indicate the
+ * primary service.
+ * Only the primary one will get the callback from
+ * {@link #onDeviceAppeared(AssociationInfo associationInfo)}.</p>
+ *
+ * Example:
+ * <meta-data
+ *   android:name="primary"
+ *   android:value="true" />
  */
 public abstract class CompanionDeviceService extends Service {
 
@@ -52,13 +66,14 @@
      * An intent action for a service to be bound whenever this app's companion device(s)
      * are nearby.
      *
-     * <p>The app will be kept alive for as long as the device is nearby.
+     * <p>The app will be kept alive for as long as the device is nearby or companion app reports
+     * appeared.
      * If the app is not running at the time device gets connected, the app will be woken up.</p>
      *
-     * <p>Shortly after the device goes out of range, the service will be unbound, and the
-     * app will be eligible for cleanup, unless any other user-visible components are running.</p>
+     * <p>Shortly after the device goes out of range or the companion app reports disappeared,
+     * the service will be unbound, and the app will be eligible for cleanup, unless any other
+     * user-visible components are running.</p>
      *
-     * <p>An app shouldn't declare more than one of these services.
      * If running in background is not essential for the devices that this app can manage,
      * app should avoid declaring this service.</p>
      *
@@ -73,9 +88,13 @@
      * Called by system whenever a device associated with this app is available.
      *
      * @param address the MAC address of the device
+     * @deprecated please override {@link #onDeviceAppeared(AssociationInfo)} instead.
      */
+    @Deprecated
     @MainThread
-    public abstract void onDeviceAppeared(@NonNull String address);
+    public void onDeviceAppeared(@NonNull String address) {
+        // Do nothing. Companion apps can override this function.
+    }
 
     /**
      * Called by system whenever a device associated with this app stops being available.
@@ -83,9 +102,13 @@
      * Usually this means the device goes out of range or is turned off.
      *
      * @param address the MAC address of the device
+     * @deprecated please override {@link #onDeviceDisappeared(AssociationInfo)} instead.
      */
+    @Deprecated
     @MainThread
-    public abstract void onDeviceDisappeared(@NonNull String address);
+    public void onDeviceDisappeared(@NonNull String address) {
+        // Do nothing. Companion apps can override this function.
+    }
 
     /**
      * Called by system whenever the system dispatches a message to the app to send it to
@@ -118,10 +141,35 @@
         companionDeviceManager.dispatchMessage(messageId, associationId, message);
     }
 
+    /**
+     * Called by system whenever a device associated with this app is connected.
+     *
+     * @param associationInfo A record for the companion device.
+     */
+    @MainThread
+    public void onDeviceAppeared(@NonNull AssociationInfo associationInfo) {
+        if (!associationInfo.isSelfManaged()) {
+            onDeviceAppeared(associationInfo.getDeviceMacAddressAsString());
+        }
+    }
+
+    /**
+     * Called by system whenever a device associated with this app is disconnected.
+     *
+     * @param associationInfo A record for the companion device.
+     */
+    @MainThread
+    public void onDeviceDisappeared(@NonNull AssociationInfo associationInfo) {
+        if (!associationInfo.isSelfManaged()) {
+            onDeviceDisappeared(associationInfo.getDeviceMacAddressAsString());
+        }
+    }
+
     @Nullable
     @Override
     public final IBinder onBind(@NonNull Intent intent) {
         if (Objects.equals(intent.getAction(), SERVICE_INTERFACE)) {
+            onBindCompanionDeviceService(intent);
             return mRemote;
         }
         Log.w(LOG_TAG,
@@ -129,20 +177,26 @@
         return null;
     }
 
+    /**
+     * Used to track the state of Binder connection in CTS tests.
+     * @hide
+     */
+    @TestApi
+    public void onBindCompanionDeviceService(@NonNull Intent intent) {
+    }
+
     class Stub extends ICompanionDeviceService.Stub {
 
         @Override
-        public void onDeviceAppeared(String address) {
-            Handler.getMain().sendMessage(PooledLambda.obtainMessage(
-                    CompanionDeviceService::onDeviceAppeared,
-                    CompanionDeviceService.this, address));
+        public void onDeviceAppeared(AssociationInfo associationInfo) {
+            Handler.getMain().post(
+                    () -> CompanionDeviceService.this.onDeviceAppeared(associationInfo));
         }
 
         @Override
-        public void onDeviceDisappeared(String address) {
-            Handler.getMain().sendMessage(PooledLambda.obtainMessage(
-                    CompanionDeviceService::onDeviceDisappeared,
-                    CompanionDeviceService.this, address));
+        public void onDeviceDisappeared(AssociationInfo associationInfo) {
+            Handler.getMain().post(
+                    () -> CompanionDeviceService.this.onDeviceDisappeared(associationInfo));
         }
 
         public void onDispatchMessage(int messageId, int associationId, @NonNull byte[] message) {
diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl
index 1558db2..68a6031f 100644
--- a/core/java/android/companion/ICompanionDeviceManager.aidl
+++ b/core/java/android/companion/ICompanionDeviceManager.aidl
@@ -65,5 +65,10 @@
     void dispatchMessage(in int messageId, in int associationId, in byte[] message);
 
     void addOnAssociationsChangedListener(IOnAssociationsChangedListener listener, int userId);
+
     void removeOnAssociationsChangedListener(IOnAssociationsChangedListener listener, int userId);
+
+    void notifyDeviceAppeared(int associationId);
+
+    void notifyDeviceDisappeared(int associationId);
 }
diff --git a/core/java/android/companion/ICompanionDeviceService.aidl b/core/java/android/companion/ICompanionDeviceService.aidl
index 25212a1..4e453573 100644
--- a/core/java/android/companion/ICompanionDeviceService.aidl
+++ b/core/java/android/companion/ICompanionDeviceService.aidl
@@ -16,9 +16,11 @@
 
 package android.companion;
 
+import android.companion.AssociationInfo;
+
 /** @hide */
 oneway interface ICompanionDeviceService {
-    void onDeviceAppeared(in String address);
-    void onDeviceDisappeared(in String address);
+    void onDeviceAppeared(in AssociationInfo associationInfo);
+    void onDeviceDisappeared(in AssociationInfo associationInfo);
     void onDispatchMessage(in int messageId, in int associationId, in byte[] message);
 }
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index dabc603..82ad150 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -16,6 +16,13 @@
 
 package android.companion.virtual;
 
+import android.graphics.Point;
+import android.hardware.input.VirtualKeyEvent;
+import android.hardware.input.VirtualMouseButtonEvent;
+import android.hardware.input.VirtualMouseRelativeEvent;
+import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualTouchEvent;
+
 /**
  * Interface for a virtual device.
  *
@@ -34,4 +41,29 @@
      * Closes the virtual device and frees all associated resources.
      */
     void close();
+    void createVirtualKeyboard(
+            int displayId,
+            String inputDeviceName,
+            int vendorId,
+            int productId,
+            IBinder token);
+    void createVirtualMouse(
+            int displayId,
+            String inputDeviceName,
+            int vendorId,
+            int productId,
+            IBinder token);
+    void createVirtualTouchscreen(
+            int displayId,
+            String inputDeviceName,
+            int vendorId,
+            int productId,
+            IBinder token,
+            in Point screenSize);
+    void unregisterInputDevice(IBinder token);
+    boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
+    boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
+    boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event);
+    boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event);
+    boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event);
 }
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 590b108..0d024b1 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -23,7 +23,13 @@
 import android.annotation.SystemService;
 import android.companion.AssociationInfo;
 import android.content.Context;
+import android.graphics.Point;
+import android.hardware.display.VirtualDisplay;
+import android.hardware.input.VirtualKeyboard;
+import android.hardware.input.VirtualMouse;
+import android.hardware.input.VirtualTouchscreen;
 import android.os.Binder;
+import android.os.IBinder;
 import android.os.RemoteException;
 
 /**
@@ -73,6 +79,8 @@
      * A virtual device has its own virtual display, audio output, microphone, and camera etc. The
      * creator of a virtual device can take the output from the virtual display and stream it over
      * to another device, and inject input events that are received from the remote device.
+     *
+     * TODO(b/204081582): Consider using a builder pattern for the input APIs.
      */
     public static class VirtualDevice implements AutoCloseable {
 
@@ -95,5 +103,88 @@
                 throw e.rethrowFromSystemServer();
             }
         }
+
+        /**
+         * Creates a virtual keyboard.
+         *
+         * @param display the display that the events inputted through this device should target
+         * @param inputDeviceName the name to call this input device
+         * @param vendorId the vendor id
+         * @param productId the product id
+         * @hide
+         */
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @NonNull
+        public VirtualKeyboard createVirtualKeyboard(
+                @NonNull VirtualDisplay display,
+                @NonNull String inputDeviceName,
+                int vendorId,
+                int productId) {
+            try {
+                final IBinder token = new Binder(
+                        "android.hardware.input.VirtualKeyboard:" + inputDeviceName);
+                mVirtualDevice.createVirtualKeyboard(display.getDisplay().getDisplayId(),
+                        inputDeviceName, vendorId, productId, token);
+                return new VirtualKeyboard(mVirtualDevice, token);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Creates a virtual mouse.
+         *
+         * @param display the display that the events inputted through this device should target
+         * @param inputDeviceName the name to call this input device
+         * @param vendorId the vendor id
+         * @param productId the product id
+         * @hide
+         */
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @NonNull
+        public VirtualMouse createVirtualMouse(
+                @NonNull VirtualDisplay display,
+                @NonNull String inputDeviceName,
+                int vendorId,
+                int productId) {
+            try {
+                final IBinder token = new Binder(
+                        "android.hardware.input.VirtualMouse:" + inputDeviceName);
+                mVirtualDevice.createVirtualMouse(display.getDisplay().getDisplayId(),
+                        inputDeviceName, vendorId, productId, token);
+                return new VirtualMouse(mVirtualDevice, token);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Creates a virtual touchscreen.
+         *
+         * @param display the display that the events inputted through this device should target
+         * @param inputDeviceName the name to call this input device
+         * @param vendorId the vendor id
+         * @param productId the product id
+         * @hide
+         */
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @NonNull
+        public VirtualTouchscreen createVirtualTouchscreen(
+                @NonNull VirtualDisplay display,
+                @NonNull String inputDeviceName,
+                int vendorId,
+                int productId) {
+            try {
+                final IBinder token = new Binder(
+                        "android.hardware.input.VirtualTouchscreen:" + inputDeviceName);
+                final Point size = new Point();
+                display.getDisplay().getSize(size);
+                mVirtualDevice.createVirtualTouchscreen(display.getDisplay().getDisplayId(),
+                        inputDeviceName, vendorId, productId, token, size);
+                return new VirtualTouchscreen(mVirtualDevice, token);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 }
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 2998f76..76b4e5c 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -33,6 +33,7 @@
 import android.os.Parcelable;
 import android.os.UserHandle;
 import android.os.storage.StorageManager;
+import android.util.ArrayMap;
 import android.util.Printer;
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
@@ -1533,6 +1534,15 @@
 
     private int mHiddenApiPolicy = HIDDEN_API_ENFORCEMENT_DEFAULT;
 
+    /**
+     * A map from a process name to an (custom) application class name in this package, derived
+     * from the <processes> tag in the app's manifest. This map may not contain all the process
+     * names. Processses not in this map will use the default app class name,
+     * which is {@link #className}, or the default class {@link android.app.Application}.
+     */
+    @Nullable
+    private ArrayMap<String, String> mAppClassNamesByProcess;
+
     public void dump(Printer pw, String prefix) {
         dump(pw, prefix, DUMP_FLAG_ALL);
     }
@@ -1540,8 +1550,14 @@
     /** @hide */
     public void dump(Printer pw, String prefix, int dumpFlags) {
         super.dumpFront(pw, prefix);
-        if ((dumpFlags & DUMP_FLAG_DETAILS) != 0 && className != null) {
-            pw.println(prefix + "className=" + className);
+        if ((dumpFlags & DUMP_FLAG_DETAILS) != 0) {
+            if (className != null) {
+                pw.println(prefix + "className=" + className);
+            }
+            for (int i = 0; i < ArrayUtils.size(mAppClassNamesByProcess); i++) {
+                pw.println(prefix + "  process=" + mAppClassNamesByProcess.keyAt(i)
+                        + " className=" + mAppClassNamesByProcess.valueAt(i));
+            }
         }
         if (permission != null) {
             pw.println(prefix + "permission=" + permission);
@@ -1967,6 +1983,16 @@
         dest.writeInt(nativeHeapZeroInitialized);
         sForBoolean.parcel(requestRawExternalStorageAccess, dest, parcelableFlags);
         dest.writeLong(createTimestamp);
+        if (mAppClassNamesByProcess == null) {
+            dest.writeInt(0);
+        } else {
+            final int size = mAppClassNamesByProcess.size();
+            dest.writeInt(size);
+            for (int i = 0; i < size; i++) {
+                dest.writeString(mAppClassNamesByProcess.keyAt(i));
+                dest.writeString(mAppClassNamesByProcess.valueAt(i));
+            }
+        }
     }
 
     public static final @android.annotation.NonNull Parcelable.Creator<ApplicationInfo> CREATOR
@@ -2055,6 +2081,13 @@
         nativeHeapZeroInitialized = source.readInt();
         requestRawExternalStorageAccess = sForBoolean.unparcel(source);
         createTimestamp = source.readLong();
+        final int allClassesSize = source.readInt();
+        if (allClassesSize > 0) {
+            mAppClassNamesByProcess = new ArrayMap<>(allClassesSize);
+            for (int i = 0; i < allClassesSize; i++) {
+                mAppClassNamesByProcess.put(source.readString(), source.readString());
+            }
+        }
     }
 
     /**
@@ -2538,6 +2571,19 @@
         requestRawExternalStorageAccess = value;
     }
 
+    /**
+     * Replaces {@link #mAppClassNamesByProcess}. This takes over the ownership of the passed map.
+     * Do not modify the argument at the callsite.
+     * {@hide}
+     */
+    public void setAppClassNamesByProcess(@Nullable ArrayMap<String, String> value) {
+        if (ArrayUtils.size(value) == 0) {
+            mAppClassNamesByProcess = null;
+        } else {
+            mAppClassNamesByProcess = value;
+        }
+    }
+
     /** {@hide} */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public String getCodePath() { return scanSourceDir; }
@@ -2568,4 +2614,21 @@
     public int getNativeHeapZeroInitialized() {
         return nativeHeapZeroInitialized;
     }
+
+    /**
+     * Return the application class name defined in the manifest. The class name set in the
+     * <processes> tag for this process, then return it. Otherwise it'll return the class
+     * name set in the <application> tag. If neither is set, it'll return null.
+     * @hide
+     */
+    @Nullable
+    public String getCustomApplicationClassNameForProcess(String processName) {
+        if (mAppClassNamesByProcess != null) {
+            String byProcess = mAppClassNamesByProcess.get(processName);
+            if (byProcess != null) {
+                return byProcess;
+            }
+        }
+        return className;
+    }
 }
diff --git a/core/java/android/content/pm/parsing/ParsingPackage.java b/core/java/android/content/pm/parsing/ParsingPackage.java
index 2fa5df7..fc9f1a5 100644
--- a/core/java/android/content/pm/parsing/ParsingPackage.java
+++ b/core/java/android/content/pm/parsing/ParsingPackage.java
@@ -118,6 +118,7 @@
 
     ParsingPackage addQueriesProvider(String authority);
 
+    /** Sets a process name -> {@link ParsedProcess} map coming from the <processes> tag. */
     ParsingPackage setProcesses(@NonNull Map<String, ParsedProcess> processes);
 
     ParsingPackage asSplit(
diff --git a/core/java/android/content/pm/parsing/ParsingPackageImpl.java b/core/java/android/content/pm/parsing/ParsingPackageImpl.java
index f03ab6a..424f477 100644
--- a/core/java/android/content/pm/parsing/ParsingPackageImpl.java
+++ b/core/java/android/content/pm/parsing/ParsingPackageImpl.java
@@ -62,6 +62,7 @@
 import android.os.Parcelable;
 import android.os.storage.StorageManager;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Pair;
 import android.util.SparseArray;
@@ -297,6 +298,9 @@
 //    @DataClass.ParcelWith(ParsingUtils.StringPairListParceler.class)
     private List<Pair<String, ParsedIntentInfo>> preferredActivityFilters = emptyList();
 
+    /**
+     * Map from a process name to a {@link ParsedProcess}.
+     */
     @NonNull
     private Map<String, ParsedProcess> processes = emptyMap();
 
@@ -1131,10 +1135,46 @@
         appInfo.setSplitCodePaths(splitCodePaths);
         appInfo.setSplitResourcePaths(splitCodePaths);
         appInfo.setVersionCode(mLongVersionCode);
+        appInfo.setAppClassNamesByProcess(buildAppClassNamesByProcess());
 
         return appInfo;
     }
 
+    /**
+     * Create a map from a process name to the custom application class for this process,
+     * which comes from <processes><process android:name="xxx">.
+     *
+     * The original information is stored in {@link #processes}, but it's stored in
+     * a form of: [process name] -[1:N]-> [package name] -[1:N]-> [class name].
+     * We scan it and collect the process names and their app class names, only for this package.
+     *
+     * The resulting map only contains processes with a custom application class set.
+     */
+    @Nullable
+    private ArrayMap<String, String> buildAppClassNamesByProcess() {
+        if (processes == null) {
+            return null;
+        }
+        final ArrayMap<String, String> ret = new ArrayMap<>(4);
+        for (String processName : processes.keySet()) {
+            final ParsedProcess process = processes.get(processName);
+            final ArrayMap<String, String> appClassesByPackage =
+                    process.getAppClassNamesByPackage();
+
+            for (int i = 0; i < appClassesByPackage.size(); i++) {
+                final String packageName = appClassesByPackage.keyAt(i);
+
+                if (this.packageName.equals(packageName)) {
+                    final String appClassName = appClassesByPackage.valueAt(i);
+                    if (!TextUtils.isEmpty(appClassName)) {
+                        ret.put(processName, appClassName);
+                    }
+                }
+            }
+        }
+        return ret;
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/core/java/android/content/pm/parsing/component/ParsedProcess.java b/core/java/android/content/pm/parsing/component/ParsedProcess.java
index c2d5163..27a540d 100644
--- a/core/java/android/content/pm/parsing/component/ParsedProcess.java
+++ b/core/java/android/content/pm/parsing/component/ParsedProcess.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.content.pm.ApplicationInfo;
 import android.os.Parcelable;
+import android.util.ArrayMap;
 
 import java.util.Set;
 
@@ -37,6 +38,15 @@
     @NonNull
     String getName();
 
+    /**
+     * The app class names in this (potentially shared) process, from a package name to
+     * the application class name.
+     * It's a map, because in shared processes, different packages can have different application
+     * classes.
+     */
+    @NonNull
+    ArrayMap<String, String> getAppClassNamesByPackage();
+
     @ApplicationInfo.NativeHeapZeroInitialized
     int getNativeHeapZeroInitialized();
 }
diff --git a/core/java/android/content/pm/parsing/component/ParsedProcessImpl.java b/core/java/android/content/pm/parsing/component/ParsedProcessImpl.java
index 3fd60eb..d404ecfd 100644
--- a/core/java/android/content/pm/parsing/component/ParsedProcessImpl.java
+++ b/core/java/android/content/pm/parsing/component/ParsedProcessImpl.java
@@ -22,6 +22,7 @@
 import android.content.pm.ApplicationInfo;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -39,6 +40,11 @@
 
     @NonNull
     private String name;
+
+    /** @see ParsedProcess#getAppClassNamesByPackage() */
+    @NonNull
+    private ArrayMap<String, String> appClassNamesByPackage = ArrayMap.EMPTY;
+
     @NonNull
     @DataClass.ParcelWith(Parcelling.BuiltIn.ForInternedStringSet.class)
     private Set<String> deniedPermissions = emptySet();
@@ -55,6 +61,8 @@
 
     public ParsedProcessImpl(@NonNull ParsedProcess other) {
         name = other.getName();
+        appClassNamesByPackage = (other.getAppClassNamesByPackage().size() == 0)
+                ? ArrayMap.EMPTY : new ArrayMap<>(other.getAppClassNamesByPackage());
         deniedPermissions = new ArraySet<>(other.getDeniedPermissions());
         gwpAsanMode = other.getGwpAsanMode();
         memtagMode = other.getMemtagMode();
@@ -66,6 +74,21 @@
         gwpAsanMode = other.getGwpAsanMode();
         memtagMode = other.getMemtagMode();
         nativeHeapZeroInitialized = other.getNativeHeapZeroInitialized();
+
+        final ArrayMap<String, String> oacn = other.getAppClassNamesByPackage();
+        for (int i = 0; i < oacn.size(); i++) {
+            appClassNamesByPackage.put(oacn.keyAt(i), oacn.valueAt(i));
+        }
+    }
+
+    /**
+     * Sets a custom application name used in this process for a given package.
+     */
+    public void putAppClassNameForPackage(String packageName, String className) {
+        if (appClassNamesByPackage.size() == 0) {
+            appClassNamesByPackage = new ArrayMap<>(4);
+        }
+        appClassNamesByPackage.put(packageName, className);
     }
 
 
@@ -83,9 +106,14 @@
     //@formatter:off
 
 
+    /**
+     * Creates a new ParsedProcessImpl.
+     *
+     */
     @DataClass.Generated.Member
     public ParsedProcessImpl(
             @NonNull String name,
+            @NonNull ArrayMap<String,String> appClassNamesByPackage,
             @NonNull Set<String> deniedPermissions,
             @ApplicationInfo.GwpAsanMode int gwpAsanMode,
             @ApplicationInfo.MemtagMode int memtagMode,
@@ -93,6 +121,9 @@
         this.name = name;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, name);
+        this.appClassNamesByPackage = appClassNamesByPackage;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, appClassNamesByPackage);
         this.deniedPermissions = deniedPermissions;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, deniedPermissions);
@@ -114,6 +145,14 @@
         return name;
     }
 
+    /**
+     * @see ParsedProcess#getAppClassNamesByPackage()
+     */
+    @DataClass.Generated.Member
+    public @NonNull ArrayMap<String,String> getAppClassNamesByPackage() {
+        return appClassNamesByPackage;
+    }
+
     @DataClass.Generated.Member
     public @NonNull Set<String> getDeniedPermissions() {
         return deniedPermissions;
@@ -142,6 +181,17 @@
         return this;
     }
 
+    /**
+     * @see ParsedProcess#getAppClassNamesByPackage()
+     */
+    @DataClass.Generated.Member
+    public @NonNull ParsedProcessImpl setAppClassNamesByPackage(@NonNull ArrayMap<String,String> value) {
+        appClassNamesByPackage = value;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, appClassNamesByPackage);
+        return this;
+    }
+
     @DataClass.Generated.Member
     public @NonNull ParsedProcessImpl setDeniedPermissions(@NonNull Set<String> value) {
         deniedPermissions = value;
@@ -192,6 +242,7 @@
         // void parcelFieldName(Parcel dest, int flags) { ... }
 
         dest.writeString(name);
+        dest.writeMap(appClassNamesByPackage);
         sParcellingForDeniedPermissions.parcel(deniedPermissions, dest, flags);
         dest.writeInt(gwpAsanMode);
         dest.writeInt(memtagMode);
@@ -210,6 +261,8 @@
         // static FieldType unparcelFieldName(Parcel in) { ... }
 
         String _name = in.readString();
+        ArrayMap<String,String> _appClassNamesByPackage = new ArrayMap();
+        in.readMap(_appClassNamesByPackage, String.class.getClassLoader());
         Set<String> _deniedPermissions = sParcellingForDeniedPermissions.unparcel(in);
         int _gwpAsanMode = in.readInt();
         int _memtagMode = in.readInt();
@@ -218,6 +271,9 @@
         this.name = _name;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, name);
+        this.appClassNamesByPackage = _appClassNamesByPackage;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, appClassNamesByPackage);
         this.deniedPermissions = _deniedPermissions;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, deniedPermissions);
@@ -249,10 +305,10 @@
     };
 
     @DataClass.Generated(
-            time = 1627605368434L,
+            time = 1639076603310L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/content/pm/parsing/component/ParsedProcessImpl.java",
-            inputSignatures = "private @android.annotation.NonNull java.lang.String name\nprivate @android.annotation.NonNull @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedStringSet.class) java.util.Set<java.lang.String> deniedPermissions\nprivate @android.content.pm.ApplicationInfo.GwpAsanMode int gwpAsanMode\nprivate @android.content.pm.ApplicationInfo.MemtagMode int memtagMode\nprivate @android.content.pm.ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized\npublic  void addStateFrom(android.content.pm.parsing.component.ParsedProcess)\nclass ParsedProcessImpl extends java.lang.Object implements [android.content.pm.parsing.component.ParsedProcess]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genParcelable=true, genAidl=false, genBuilder=false)")
+            inputSignatures = "private @android.annotation.NonNull java.lang.String name\nprivate @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.String> appClassNamesByPackage\nprivate @android.annotation.NonNull @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedStringSet.class) java.util.Set<java.lang.String> deniedPermissions\nprivate @android.content.pm.ApplicationInfo.GwpAsanMode int gwpAsanMode\nprivate @android.content.pm.ApplicationInfo.MemtagMode int memtagMode\nprivate @android.content.pm.ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized\npublic  void addStateFrom(android.content.pm.parsing.component.ParsedProcess)\npublic  void putAppClassNameForPackage(java.lang.String,java.lang.String)\nclass ParsedProcessImpl extends java.lang.Object implements [android.content.pm.parsing.component.ParsedProcess]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genParcelable=true, genAidl=false, genBuilder=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/content/pm/parsing/component/ParsedProcessUtils.java b/core/java/android/content/pm/parsing/component/ParsedProcessUtils.java
index cf83586..5e4cf66 100644
--- a/core/java/android/content/pm/parsing/component/ParsedProcessUtils.java
+++ b/core/java/android/content/pm/parsing/component/ParsedProcessUtils.java
@@ -97,7 +97,12 @@
                 return input.error(processNameResult);
             }
 
+            String packageName = pkg.getPackageName();
+            String className = ParsingUtils.buildClassName(packageName,
+                    sa.getNonConfigurationString(R.styleable.AndroidManifestProcess_name, 0));
+
             proc.setName(processNameResult.getResult());
+            proc.putAppClassNameForPackage(packageName, className);
             proc.setGwpAsanMode(sa.getInt(R.styleable.AndroidManifestProcess_gwpAsanMode, -1));
             proc.setMemtagMode(sa.getInt(R.styleable.AndroidManifestProcess_memtagMode, -1));
             if (sa.hasValue(R.styleable.AndroidManifestProcess_nativeHeapZeroInitialized)) {
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index 24c6a5a..bfd9fd0 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -792,6 +792,21 @@
     }
 
     /**
+     * To get the parent theme resource id according to the parameter theme resource id.
+     * @param resId theme resource id.
+     * @return the parent theme resource id.
+     * @hide
+     */
+    @StyleRes
+    int getParentThemeIdentifier(@StyleRes int resId) {
+        synchronized (this) {
+            ensureValidLocked();
+            // name is checked in JNI.
+            return nativeGetParentThemeIdentifier(mObject, resId);
+        }
+    }
+
+    /**
      * Enable resource resolution logging to track the steps taken to resolve the last resource
      * entry retrieved. Stores the configuration and package names for each step.
      *
@@ -1600,6 +1615,8 @@
     private static native void nativeThemeDump(long ptr, long themePtr, int priority, String tag,
             String prefix);
     static native @NativeConfig int nativeThemeGetChangingConfigurations(long themePtr);
+    @StyleRes
+    private static native int nativeGetParentThemeIdentifier(long ptr, @StyleRes int styleId);
 
     // AssetInputStream related native methods.
     private static native void nativeAssetDestroy(long assetPtr);
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index 632d8e5..cb53a2a 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -80,6 +80,7 @@
 import java.io.InputStream;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
@@ -1507,6 +1508,12 @@
      * retrieve XML attributes with style and theme information applied.
      */
     public final class Theme {
+        /**
+         * To trace parent themes needs to prevent a cycle situation.
+         * e.x. A's parent is B, B's parent is C, and C's parent is A.
+         */
+        private static final int MAX_NUMBER_OF_TRACING_PARENT_THEME = 100;
+
         private final Object mLock = new Object();
 
         @GuardedBy("mLock")
@@ -1800,6 +1807,13 @@
             }
         }
 
+        @StyleRes
+        /*package*/ int getParentThemeIdentifier(@StyleRes int resId) {
+            synchronized (mLock) {
+                return mThemeImpl.getParentThemeIdentifier(resId);
+            }
+        }
+
         /**
          * @hide
          */
@@ -1942,6 +1956,35 @@
             final Theme other = (Theme) o;
             return getKey().equals(other.getKey());
         }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append('{');
+            int themeResId = getAppliedStyleResId();
+            int i = 0;
+            sb.append("InheritanceMap=[");
+            while (themeResId > 0) {
+                if (i > MAX_NUMBER_OF_TRACING_PARENT_THEME) {
+                    sb.append(",...");
+                    break;
+                }
+
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                sb.append("id=0x").append(Integer.toHexString(themeResId));
+                sb.append(getResourcePackageName(themeResId))
+                        .append(":").append(getResourceTypeName(themeResId))
+                        .append("/").append(getResourceEntryName(themeResId));
+
+                i++;
+                themeResId = getParentThemeIdentifier(themeResId);
+            }
+            sb.append("], Themes=").append(Arrays.deepToString(getTheme()));
+            sb.append('}');
+            return sb.toString();
+        }
     }
 
     static class ThemeKey implements Cloneable {
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index b9f93b8..4d850b0c 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -1310,6 +1310,14 @@
             return mThemeResId;
         }
 
+        @StyleRes
+        /*package*/ int getParentThemeIdentifier(@StyleRes int resId) {
+            if (resId > 0) {
+                return mAssets.getParentThemeIdentifier(resId);
+            }
+            return 0;
+        }
+
         void applyStyle(int resId, boolean force) {
             mAssets.applyStyleToTheme(mTheme, resId, force);
             mThemeResId = resId;
diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java
index d6f55d6..9eb9cd5 100644
--- a/core/java/android/content/res/TypedArray.java
+++ b/core/java/android/content/res/TypedArray.java
@@ -527,11 +527,12 @@
             final TypedValue value = mValue;
             getValueAt(index, value);
             throw new UnsupportedOperationException(
-                    "Failed to resolve attribute at index " + attrIndex + ": " + value);
+                    "Failed to resolve attribute at index " + attrIndex + ": " + value
+                            + ", theme=" + mTheme);
         }
 
         throw new UnsupportedOperationException("Can't convert value at index " + attrIndex
-                + " to color: type=0x" + Integer.toHexString(type));
+                + " to color: type=0x" + Integer.toHexString(type) + ", theme=" + mTheme);
     }
 
     /**
@@ -561,7 +562,8 @@
         if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
             if (value.type == TypedValue.TYPE_ATTRIBUTE) {
                 throw new UnsupportedOperationException(
-                        "Failed to resolve attribute at index " + index + ": " + value);
+                        "Failed to resolve attribute at index " + index + ": " + value
+                                + ", theme=" + mTheme);
             }
             return mResources.loadComplexColor(value, value.resourceId, mTheme);
         }
@@ -596,7 +598,8 @@
         if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
             if (value.type == TypedValue.TYPE_ATTRIBUTE) {
                 throw new UnsupportedOperationException(
-                        "Failed to resolve attribute at index " + index + ": " + value);
+                        "Failed to resolve attribute at index " + index + ": " + value
+                                + ", theme=" + mTheme);
             }
             return mResources.loadColorStateList(value, value.resourceId, mTheme);
         }
@@ -637,11 +640,12 @@
             final TypedValue value = mValue;
             getValueAt(index, value);
             throw new UnsupportedOperationException(
-                    "Failed to resolve attribute at index " + attrIndex + ": " + value);
+                    "Failed to resolve attribute at index " + attrIndex + ": " + value
+                            + ", theme=" + mTheme);
         }
 
         throw new UnsupportedOperationException("Can't convert value at index " + attrIndex
-                + " to integer: type=0x" + Integer.toHexString(type));
+                + " to integer: type=0x" + Integer.toHexString(type) + ", theme=" + mTheme);
     }
 
     /**
@@ -684,11 +688,12 @@
             final TypedValue value = mValue;
             getValueAt(index, value);
             throw new UnsupportedOperationException(
-                    "Failed to resolve attribute at index " + attrIndex + ": " + value);
+                    "Failed to resolve attribute at index " + attrIndex + ": " + value
+                            + ", theme=" + mTheme);
         }
 
         throw new UnsupportedOperationException("Can't convert value at index " + attrIndex
-                + " to dimension: type=0x" + Integer.toHexString(type));
+                + " to dimension: type=0x" + Integer.toHexString(type) + ", theme=" + mTheme);
     }
 
     /**
@@ -732,11 +737,12 @@
             final TypedValue value = mValue;
             getValueAt(index, value);
             throw new UnsupportedOperationException(
-                    "Failed to resolve attribute at index " + attrIndex + ": " + value);
+                    "Failed to resolve attribute at index " + attrIndex + ": " + value
+                            + ", theme=" + mTheme);
         }
 
         throw new UnsupportedOperationException("Can't convert value at index " + attrIndex
-                + " to dimension: type=0x" + Integer.toHexString(type));
+                + " to dimension: type=0x" + Integer.toHexString(type) + ", theme=" + mTheme);
     }
 
     /**
@@ -781,11 +787,12 @@
             final TypedValue value = mValue;
             getValueAt(index, value);
             throw new UnsupportedOperationException(
-                    "Failed to resolve attribute at index " + attrIndex + ": " + value);
+                    "Failed to resolve attribute at index " + attrIndex + ": " + value
+                            + ", theme=" + mTheme);
         }
 
         throw new UnsupportedOperationException("Can't convert value at index " + attrIndex
-                + " to dimension: type=0x" + Integer.toHexString(type));
+                + " to dimension: type=0x" + Integer.toHexString(type) + ", theme=" + mTheme);
     }
 
     /**
@@ -825,11 +832,12 @@
             final TypedValue value = mValue;
             getValueAt(index, value);
             throw new UnsupportedOperationException(
-                    "Failed to resolve attribute at index " + attrIndex + ": " + value);
+                    "Failed to resolve attribute at index " + attrIndex + ": " + value
+                            + ", theme=" + mTheme);
         }
 
         throw new UnsupportedOperationException(getPositionDescription()
-                + ": You must supply a " + name + " attribute.");
+                + ": You must supply a " + name + " attribute." + ", theme=" + mTheme);
     }
 
     /**
@@ -900,11 +908,12 @@
             final TypedValue value = mValue;
             getValueAt(index, value);
             throw new UnsupportedOperationException(
-                    "Failed to resolve attribute at index " + attrIndex + ": " + value);
+                    "Failed to resolve attribute at index " + attrIndex + ": " + value
+                            + ", theme=" + mTheme);
         }
 
         throw new UnsupportedOperationException("Can't convert value at index " + attrIndex
-                + " to fraction: type=0x" + Integer.toHexString(type));
+                + " to fraction: type=0x" + Integer.toHexString(type) + ", theme=" + mTheme);
     }
 
     /**
@@ -996,7 +1005,8 @@
         if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
             if (value.type == TypedValue.TYPE_ATTRIBUTE) {
                 throw new UnsupportedOperationException(
-                        "Failed to resolve attribute at index " + index + ": " + value);
+                        "Failed to resolve attribute at index " + index + ": " + value
+                                + ", theme=" + mTheme);
             }
 
             if (density > 0) {
@@ -1032,7 +1042,8 @@
         if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
             if (value.type == TypedValue.TYPE_ATTRIBUTE) {
                 throw new UnsupportedOperationException(
-                        "Failed to resolve attribute at index " + index + ": " + value);
+                        "Failed to resolve attribute at index " + index + ": " + value
+                                + ", theme=" + mTheme);
             }
             return mResources.getFont(value, value.resourceId);
         }
diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java
index cad30dd..a4a8f31 100644
--- a/core/java/android/hardware/HardwareBuffer.java
+++ b/core/java/android/hardware/HardwareBuffer.java
@@ -25,6 +25,7 @@
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.view.SurfaceControl;
 
 import dalvik.annotation.optimization.CriticalNative;
 import dalvik.annotation.optimization.FastNative;
@@ -129,6 +130,15 @@
     public static final long USAGE_GPU_SAMPLED_IMAGE      = 1 << 8;
     /** Usage: The buffer will be written to by the GPU */
     public static final long USAGE_GPU_COLOR_OUTPUT       = 1 << 9;
+    /**
+     * The buffer will be used as a composer HAL overlay layer.
+     *
+     * This flag is currently only needed when using
+     * {@link android.view.SurfaceControl.Transaction#setBuffer(SurfaceControl, HardwareBuffer)}
+     * to set a buffer. In all other cases, the framework adds this flag
+     * internally to buffers that could be presented in a composer overlay.
+     */
+    public static final long USAGE_COMPOSER_OVERLAY = 1 << 11;
     /** Usage: The buffer must not be used outside of a protected hardware path */
     public static final long USAGE_PROTECTED_CONTENT      = 1 << 14;
     /** Usage: The buffer will be read by a hardware video encoder */
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 3c8b6e9..48a9121 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -2669,7 +2669,8 @@
      * </tbody>
      * </table>
      * <p>For applications targeting SDK version 31 or newer, if the mobile device declares to be
-     * {@link android.os.Build.VERSION_CDOES.MEDIA_PERFORMANCE_CLASS media performance class} S,
+     * media performance class 12 or higher by setting
+     * {@link android.os.Build.VERSION_CDOES.MEDIA_PERFORMANCE_CLASS } to be 31 or larger,
      * the primary camera devices (first rear/front camera in the camera ID list) will not
      * support JPEG sizes smaller than 1080p. If the application configures a JPEG stream
      * smaller than 1080p, the camera device will round up the JPEG image size to at least
@@ -2742,9 +2743,11 @@
      * </tbody>
      * </table>
      * <p>For applications targeting SDK version 31 or newer, if the mobile device doesn't declare
-     * to be media performance class S, or if the camera device isn't a primary rear/front
-     * camera, the minimum required output stream configurations are the same as for applications
-     * targeting SDK version older than 31.</p>
+     * to be media performance class 12 or better by setting
+     * {@link android.os.Build.VERSION_CDOES.MEDIA_PERFORMANCE_CLASS } to be 31 or larger,
+     * or if the camera device isn't a primary rear/front camera, the minimum required output
+     * stream configurations are the same as for applications targeting SDK version older than
+     * 31.</p>
      * <p>Refer to {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} for additional
      * mandatory stream configurations on a per-capability basis.</p>
      * <p>Exception on 176x144 (QCIF) resolution: camera devices usually have a fixed capability for
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 93f1d61..c12e819 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -1036,6 +1036,95 @@
     }
 
     /**
+     * Set the brightness level of the flashlight associated with the given cameraId in torch
+     * mode. If the torch is OFF and torchStrength is >= 1, torch will turn ON with the
+     * strength level specified in torchStrength.
+     *
+     * <p>Use
+     * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_MAXIMUM_LEVEL}
+     * to check whether the camera device supports flash unit strength control or not. If this value
+     * is greater than 1, applications can call this API to control the flashlight brightness level.
+     * </p>
+     *
+     * <p>If {@link #turnOnTorchWithStrengthLevel} is called to change the brightness level of the
+     * flash unit {@link CameraManager.TorchCallback#onTorchStrengthLevelChanged} will be invoked.
+     * If the new desired strength level is same as previously set level, then this callback will
+     * not be invoked.
+     * If the torch is OFF and {@link #turnOnTorchWithStrengthLevel} is called with level >= 1,
+     * the torch will be turned ON with that brightness level. In this case
+     * {@link CameraManager.TorchCallback#onTorchModeChanged} will also be invoked.
+     * </p>
+     *
+     * <p>When the torch is turned OFF via {@link #setTorchMode}, the flashlight brightness level
+     * will reset to default value
+     * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL}
+     * In this case the {@link CameraManager.TorchCallback#onTorchStrengthLevelChanged} will not be
+     * invoked.
+     * </p>
+     *
+     * <p>If torch is enabled via {@link #setTorchMode} after calling
+     * {@link #turnOnTorchWithStrengthLevel} with level N then the flash unit will have the
+     * brightness level N.
+     * Since multiple applications are free to call {@link #setTorchMode}, when the latest
+     * application that turned ON the torch mode exits, the torch mode will be turned OFF
+     * and in this case the brightness level will reset to default level.
+     * </p>
+     *
+     * @param cameraId
+     *             The unique identifier of the camera device that the flash unit belongs to.
+     * @param torchStrength
+     *             The desired brightness level to be set for the flash unit in the range 1 to
+     *             {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_MAXIMUM_LEVEL}.
+     *
+     * @throws CameraAccessException if it failed to access the flash unit.
+     *             {@link CameraAccessException#CAMERA_IN_USE} will be thrown if the camera device
+     *             is in use. {@link CameraAccessException#MAX_CAMERAS_IN_USE} will be thrown if
+     *             other camera resources needed to turn on the torch mode are in use.
+     *             {@link CameraAccessException#CAMERA_DISCONNECTED} will be thrown if camera
+     *             service is not available.
+     * @throws IllegalArgumentException if cameraId was null, cameraId doesn't match any currently
+     *              or previously available camera device, the camera device doesn't have a
+     *              flash unit or if torchStrength is not within the range i.e. is greater than
+     *              the maximum level
+     *              {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_MAXIMUM_LEVEL}
+     *              or <= 0.
+     *
+     */
+    public void turnOnTorchWithStrengthLevel(@NonNull String cameraId, int torchStrength)
+            throws CameraAccessException {
+        if (CameraManagerGlobal.sCameraServiceDisabled) {
+            throw new IllegalArgumentException("No camera available on device");
+        }
+        CameraManagerGlobal.get().turnOnTorchWithStrengthLevel(cameraId, torchStrength);
+    }
+
+    /**
+     * Returns the brightness level of the flash unit associated with the cameraId.
+     *
+     * @param cameraId
+     *              The unique identifier of the camera device that the flash unit belongs to.
+     * @return The brightness level of the flash unit associated with cameraId.
+     *         When the torch is turned OFF, the strength level will reset to a default level
+     *         {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL}.
+     *         In this case the return value will be
+     *         {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL}
+     *         rather than 0.
+     *
+     * @throws CameraAccessException if it failed to access the flash unit.
+     * @throws IllegalArgumentException if cameraId was null, cameraId doesn't match any currently
+     *              or previously available camera device, or the camera device doesn't have a
+     *              flash unit.
+     *
+     */
+    public int getTorchStrengthLevel(@NonNull String cameraId)
+            throws CameraAccessException {
+        if (CameraManagerGlobal.sCameraServiceDisabled) {
+            throw new IllegalArgumentException("No camera available on device.");
+        }
+        return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId);
+    }
+
+    /**
      * A callback for camera devices becoming available or unavailable to open.
      *
      * <p>Cameras become available when they are no longer in use, or when a new
@@ -1239,6 +1328,24 @@
         public void onTorchModeChanged(@NonNull String cameraId, boolean enabled) {
             // default empty implementation
         }
+
+        /**
+         * A camera's flash unit brightness level has been changed in torch mode via
+         * {@link #turnOnTorchWithStrengthLevel}. When the torch is turned OFF, this
+         * callback will not be triggered even though the torch strength level resets to
+         * default value
+         * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL}
+         *
+         * <p>The default implementation of this method does nothing.</p>
+         *
+         * @param cameraId The unique identifier of the camera whose flash unit brightness level has
+         * been changed.
+         *
+         * @param newStrengthLevel The brightness level of the flash unit that has been changed to.
+         */
+        public void onTorchStrengthLevelChanged(@NonNull String cameraId, int newStrengthLevel) {
+            // default empty implementation
+        }
     }
 
     /**
@@ -1642,6 +1749,10 @@
                 public void onTorchStatusChanged(int status, String id) throws RemoteException {
                 }
                 @Override
+                public void onTorchStrengthLevelChanged(String id, int newStrengthLevel)
+                        throws RemoteException {
+                }
+                @Override
                 public void onCameraAccessPrioritiesChanged() {
                 }
                 @Override
@@ -1825,6 +1936,57 @@
             }
         }
 
+        public void turnOnTorchWithStrengthLevel(String cameraId, int torchStrength) throws
+                CameraAccessException {
+            synchronized(mLock) {
+
+                if (cameraId == null) {
+                    throw new IllegalArgumentException("cameraId was null");
+                }
+
+                ICameraService cameraService = getCameraService();
+                if (cameraService == null) {
+                    throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
+                        "Camera service is currently unavailable.");
+                }
+
+                try {
+                    cameraService.turnOnTorchWithStrengthLevel(cameraId, torchStrength,
+                            mTorchClientBinder);
+                } catch(ServiceSpecificException e) {
+                    throwAsPublicException(e);
+                } catch (RemoteException e) {
+                    throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
+                            "Camera service is currently unavailable.");
+                }
+            }
+        }
+
+        public int getTorchStrengthLevel(String cameraId) throws CameraAccessException {
+            int torchStrength = 0;
+            synchronized(mLock) {
+                if (cameraId == null) {
+                    throw new IllegalArgumentException("cameraId was null");
+                }
+
+                ICameraService cameraService = getCameraService();
+                if (cameraService == null) {
+                    throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
+                        "Camera service is currently unavailable.");
+                }
+
+                try {
+                    torchStrength = cameraService.getTorchStrengthLevel(cameraId);
+                } catch(ServiceSpecificException e) {
+                    throwAsPublicException(e);
+                } catch (RemoteException e) {
+                    throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
+                            "Camera service is currently unavailable.");
+                }
+            }
+            return torchStrength;
+        }
+
         private void handleRecoverableSetupErrors(ServiceSpecificException e) {
             switch (e.errorCode) {
                 case ICameraService.ERROR_DISCONNECTED:
@@ -1984,6 +2146,18 @@
             }
         }
 
+        private void postSingleTorchStrengthLevelUpdate(final TorchCallback callback,
+                 final Executor executor, final String id, final int newStrengthLevel) {
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                executor.execute(() -> {
+                    callback.onTorchStrengthLevelChanged(id, newStrengthLevel);
+                });
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
         /**
          * Send the state of all known cameras to the provided listener, to initialize
          * the listener's knowledge of camera state.
@@ -2167,6 +2341,22 @@
             }
         } // onTorchStatusChangedLocked
 
+        private void onTorchStrengthLevelChangedLocked(String cameraId, int newStrengthLevel) {
+            if (DEBUG) {
+
+                Log.v(TAG,
+                        String.format("Camera id %s has torch strength level changed to %d",
+                            cameraId, newStrengthLevel));
+            }
+
+            final int callbackCount = mTorchCallbackMap.size();
+            for (int i = 0; i < callbackCount; i++) {
+                final Executor executor = mTorchCallbackMap.valueAt(i);
+                final TorchCallback callback = mTorchCallbackMap.keyAt(i);
+                postSingleTorchStrengthLevelUpdate(callback, executor, cameraId, newStrengthLevel);
+            }
+        } // onTorchStrengthLevelChanged
+
         /**
          * Register a callback to be notified about camera device availability with the
          * global listener singleton.
@@ -2258,6 +2448,14 @@
         }
 
         @Override
+        public void onTorchStrengthLevelChanged(String cameraId, int newStrengthLevel)
+                throws RemoteException {
+            synchronized (mLock) {
+                onTorchStrengthLevelChangedLocked(cameraId, newStrengthLevel);
+            }
+        }
+
+        @Override
         public void onCameraAccessPrioritiesChanged() {
             synchronized (mLock) {
                 final int callbackCount = mCallbackMap.size();
diff --git a/core/java/android/hardware/input/VirtualKeyEvent.aidl b/core/java/android/hardware/input/VirtualKeyEvent.aidl
new file mode 100644
index 0000000..5b3ee0c
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualKeyEvent.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.hardware.input;
+
+parcelable VirtualKeyEvent;
\ No newline at end of file
diff --git a/core/java/android/hardware/input/VirtualKeyEvent.java b/core/java/android/hardware/input/VirtualKeyEvent.java
new file mode 100644
index 0000000..d875156
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualKeyEvent.java
@@ -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 android.hardware.input;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.KeyEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event describing a keyboard interaction originating from a remote device.
+ *
+ * When the user presses a key, an {@code ACTION_DOWN} event should be reported. When the user
+ * releases the key, an {@code ACTION_UP} event should be reported.
+ *
+ * See {@link android.view.KeyEvent}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualKeyEvent implements Parcelable {
+
+    /** @hide */
+    public static final int ACTION_UNKNOWN = -1;
+    /** Action indicating the given key has been pressed. */
+    public static final int ACTION_DOWN = KeyEvent.ACTION_DOWN;
+    /** Action indicating the previously pressed key has been lifted. */
+    public static final int ACTION_UP = KeyEvent.ACTION_UP;
+
+    /** @hide */
+    @IntDef(prefix = { "ACTION_" }, value = {
+            ACTION_UNKNOWN,
+            ACTION_DOWN,
+            ACTION_UP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {
+    }
+
+    private final @Action int mAction;
+    private final int mKeyCode;
+
+    private VirtualKeyEvent(@Action int action, int keyCode) {
+        mAction = action;
+        mKeyCode = keyCode;
+    }
+
+    private VirtualKeyEvent(@NonNull Parcel parcel) {
+        mAction = parcel.readInt();
+        mKeyCode = parcel.readInt();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) {
+        parcel.writeInt(mAction);
+        parcel.writeInt(mKeyCode);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns the key code associated with this event.
+     */
+    public int getKeyCode() {
+        return mKeyCode;
+    }
+
+    /**
+     * Returns the action associated with this event.
+     */
+    public @Action int getAction() {
+        return mAction;
+    }
+
+    /**
+     * Builder for {@link VirtualKeyEvent}.
+     */
+    public static final class Builder {
+
+        private @Action int mAction = ACTION_UNKNOWN;
+        private int mKeyCode = -1;
+
+        /**
+         * Creates a {@link VirtualKeyEvent} object with the current builder configuration.
+         */
+        public @NonNull VirtualKeyEvent build() {
+            if (mAction == ACTION_UNKNOWN || mKeyCode == -1) {
+                throw new IllegalArgumentException(
+                        "Cannot build virtual key event with unset fields");
+            }
+            return new VirtualKeyEvent(mAction, mKeyCode);
+        }
+
+        /**
+         * Sets the Android key code of the event. The set of allowed characters include digits 0-9,
+         * characters A-Z, and standard punctuation, as well as numpad keys, function keys F1-F12,
+         * and meta keys (caps lock, shift, etc.).
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setKeyCode(int keyCode) {
+            mKeyCode = keyCode;
+            return this;
+        }
+
+        /**
+         * Sets the action of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setAction(@Action int action) {
+            if (action != ACTION_DOWN && action != ACTION_UP) {
+                throw new IllegalArgumentException("Unsupported action type");
+            }
+            mAction = action;
+            return this;
+        }
+    }
+
+    public static final @NonNull Parcelable.Creator<VirtualKeyEvent> CREATOR =
+            new Parcelable.Creator<VirtualKeyEvent>() {
+        public VirtualKeyEvent createFromParcel(Parcel source) {
+            return new VirtualKeyEvent(source);
+        }
+
+        public VirtualKeyEvent[] newArray(int size) {
+            return new VirtualKeyEvent[size];
+        }
+    };
+}
diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java
new file mode 100644
index 0000000..ee9b659
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualKeyboard.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.input;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.companion.virtual.IVirtualDevice;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.io.Closeable;
+
+/**
+ * A virtual keyboard representing a key input mechanism on a remote device, such as a built-in
+ * keyboard on a laptop, a software keyboard on a tablet, or a keypad on a TV remote control.
+ *
+ * This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.
+ *
+ * @hide
+ */
+@SystemApi
+public class VirtualKeyboard implements Closeable {
+
+    private final IVirtualDevice mVirtualDevice;
+    private final IBinder mToken;
+
+    /** @hide */
+    public VirtualKeyboard(IVirtualDevice virtualDevice, IBinder token) {
+        mVirtualDevice = virtualDevice;
+        mToken = token;
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void close() {
+        try {
+            mVirtualDevice.unregisterInputDevice(mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sends a key event to the system.
+     *
+     * @param event the event to send
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendKeyEvent(@NonNull VirtualKeyEvent event) {
+        try {
+            mVirtualDevice.sendKeyEvent(mToken, event);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java
new file mode 100644
index 0000000..6599dd2
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualMouse.java
@@ -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 android.hardware.input;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.companion.virtual.IVirtualDevice;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.MotionEvent;
+
+import java.io.Closeable;
+
+/**
+ * A virtual mouse representing a relative input mechanism on a remote device, such as a mouse or
+ * trackpad.
+ *
+ * This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.
+ *
+ * @hide
+ */
+@SystemApi
+public class VirtualMouse implements Closeable {
+
+    private final IVirtualDevice mVirtualDevice;
+    private final IBinder mToken;
+
+    /** @hide */
+    public VirtualMouse(IVirtualDevice virtualDevice, IBinder token) {
+        mVirtualDevice = virtualDevice;
+        mToken = token;
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void close() {
+        try {
+            mVirtualDevice.unregisterInputDevice(mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Send a mouse button event to the system.
+     *
+     * @param event the event
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendButtonEvent(@NonNull VirtualMouseButtonEvent event) {
+        try {
+            mVirtualDevice.sendButtonEvent(mToken, event);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sends a scrolling event to the system. See {@link MotionEvent#AXIS_VSCROLL} and
+     * {@link MotionEvent#AXIS_SCROLL}.
+     *
+     * @param event the event
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendScrollEvent(@NonNull VirtualMouseScrollEvent event) {
+        try {
+            mVirtualDevice.sendScrollEvent(mToken, event);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sends a relative movement event to the system.
+     *
+     * @param event the event
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendRelativeEvent(@NonNull VirtualMouseRelativeEvent event) {
+        try {
+            mVirtualDevice.sendRelativeEvent(mToken, event);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualMouseButtonEvent.aidl b/core/java/android/hardware/input/VirtualMouseButtonEvent.aidl
new file mode 100644
index 0000000..ebcf5aa
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualMouseButtonEvent.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.hardware.input;
+
+parcelable VirtualMouseButtonEvent;
\ No newline at end of file
diff --git a/core/java/android/hardware/input/VirtualMouseButtonEvent.java b/core/java/android/hardware/input/VirtualMouseButtonEvent.java
new file mode 100644
index 0000000..2e094cf
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualMouseButtonEvent.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.input;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.MotionEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event describing a mouse button click interaction originating from a remote device.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualMouseButtonEvent implements Parcelable {
+
+    /** @hide */
+    public static final int ACTION_UNKNOWN = -1;
+    /** Action indicating the mouse button has been pressed. */
+    public static final int ACTION_BUTTON_PRESS = MotionEvent.ACTION_BUTTON_PRESS;
+    /** Action indicating the mouse button has been released. */
+    public static final int ACTION_BUTTON_RELEASE = MotionEvent.ACTION_BUTTON_RELEASE;
+    /** @hide */
+    @IntDef(prefix = {"ACTION_"}, value = {
+            ACTION_UNKNOWN,
+            ACTION_BUTTON_PRESS,
+            ACTION_BUTTON_RELEASE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {}
+
+    /** @hide */
+    public static final int BUTTON_UNKNOWN = -1;
+    /** Action indicating the mouse button involved in this event is in the left position. */
+    public static final int BUTTON_PRIMARY = MotionEvent.BUTTON_PRIMARY;
+    /** Action indicating the mouse button involved in this event is in the middle position. */
+    public static final int BUTTON_TERTIARY = MotionEvent.BUTTON_TERTIARY;
+    /** Action indicating the mouse button involved in this event is in the right position. */
+    public static final int BUTTON_SECONDARY = MotionEvent.BUTTON_SECONDARY;
+    /**
+     * Action indicating the mouse button involved in this event is intended to go back to the
+     * previous.
+     */
+    public static final int BUTTON_BACK = MotionEvent.BUTTON_BACK;
+    /**
+     * Action indicating the mouse button involved in this event is intended to move forward to the
+     * next.
+     */
+    public static final int BUTTON_FORWARD = MotionEvent.BUTTON_FORWARD;
+    /** @hide */
+    @IntDef(prefix = {"BUTTON_"}, value = {
+            BUTTON_UNKNOWN,
+            BUTTON_PRIMARY,
+            BUTTON_TERTIARY,
+            BUTTON_SECONDARY,
+            BUTTON_BACK,
+            BUTTON_FORWARD,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Button {}
+
+    private final @Action int mAction;
+    private final @Button int mButtonCode;
+
+    private VirtualMouseButtonEvent(@Action int action, @Button int buttonCode) {
+        mAction = action;
+        mButtonCode = buttonCode;
+    }
+
+    private VirtualMouseButtonEvent(@NonNull Parcel parcel) {
+        mAction = parcel.readInt();
+        mButtonCode = parcel.readInt();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) {
+        parcel.writeInt(mAction);
+        parcel.writeInt(mButtonCode);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns the button code associated with this event.
+     */
+    public @Button int getButtonCode() {
+        return mButtonCode;
+    }
+
+    /**
+     * Returns the action associated with this event.
+     */
+    public @Action int getAction() {
+        return mAction;
+    }
+
+    /**
+     * Builder for {@link VirtualMouseButtonEvent}.
+     */
+    public static final class Builder {
+
+        private @Action int mAction = ACTION_UNKNOWN;
+        private @Button int mButtonCode = -1;
+
+        /**
+         * Creates a {@link VirtualMouseButtonEvent} object with the current builder configuration.
+         */
+        public @NonNull VirtualMouseButtonEvent build() {
+            if (mAction == ACTION_UNKNOWN || mButtonCode == -1) {
+                throw new IllegalArgumentException(
+                        "Cannot build virtual mouse button event with unset fields");
+            }
+            return new VirtualMouseButtonEvent(mAction, mButtonCode);
+        }
+
+        /**
+         * Sets the button code of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setButtonCode(int buttonCode) {
+            if (buttonCode != BUTTON_PRIMARY
+                    && buttonCode != BUTTON_TERTIARY
+                    && buttonCode != BUTTON_SECONDARY
+                    && buttonCode != BUTTON_BACK
+                    && buttonCode != BUTTON_FORWARD) {
+                throw new IllegalArgumentException("Unsupported mouse button code");
+            }
+            mButtonCode = buttonCode;
+            return this;
+        }
+
+        /**
+         * Sets the action of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setAction(@Action int action) {
+            if (action != ACTION_BUTTON_PRESS && action != ACTION_BUTTON_RELEASE) {
+                throw new IllegalArgumentException("Unsupported mouse button action type");
+            }
+            mAction = action;
+            return this;
+        }
+    }
+
+    public static final @NonNull Parcelable.Creator<VirtualMouseButtonEvent> CREATOR =
+            new Parcelable.Creator<VirtualMouseButtonEvent>() {
+                public VirtualMouseButtonEvent createFromParcel(Parcel source) {
+                    return new VirtualMouseButtonEvent(source);
+                }
+
+                public VirtualMouseButtonEvent[] newArray(int size) {
+                    return new VirtualMouseButtonEvent[size];
+                }
+            };
+}
diff --git a/core/java/android/hardware/input/VirtualMouseRelativeEvent.aidl b/core/java/android/hardware/input/VirtualMouseRelativeEvent.aidl
new file mode 100644
index 0000000..1095858
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualMouseRelativeEvent.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.hardware.input;
+
+parcelable VirtualMouseRelativeEvent;
\ No newline at end of file
diff --git a/core/java/android/hardware/input/VirtualMouseRelativeEvent.java b/core/java/android/hardware/input/VirtualMouseRelativeEvent.java
new file mode 100644
index 0000000..65ed1f2
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualMouseRelativeEvent.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 android.hardware.input;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * An event describing a mouse movement interaction originating from a remote device.
+ *
+ * See {@link android.view.MotionEvent}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualMouseRelativeEvent implements Parcelable {
+
+    private final float mRelativeX;
+    private final float mRelativeY;
+
+    private VirtualMouseRelativeEvent(float relativeX, float relativeY) {
+        mRelativeX = relativeX;
+        mRelativeY = relativeY;
+    }
+
+    private VirtualMouseRelativeEvent(@NonNull Parcel parcel) {
+        mRelativeX = parcel.readFloat();
+        mRelativeY = parcel.readFloat();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) {
+        parcel.writeFloat(mRelativeX);
+        parcel.writeFloat(mRelativeY);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns the relative x-axis movement, in pixels.
+     */
+    public float getRelativeX() {
+        return mRelativeX;
+    }
+
+    /**
+     * Returns the relative x-axis movement, in pixels.
+     */
+    public float getRelativeY() {
+        return mRelativeY;
+    }
+
+    /**
+     * Builder for {@link VirtualMouseRelativeEvent}.
+     */
+    public static final class Builder {
+
+        private float mRelativeX;
+        private float mRelativeY;
+
+        /**
+         * Creates a {@link VirtualMouseRelativeEvent} object with the current builder
+         * configuration.
+         */
+        public @NonNull VirtualMouseRelativeEvent build() {
+            return new VirtualMouseRelativeEvent(mRelativeX, mRelativeY);
+        }
+
+        /**
+         * Sets the relative x-axis movement, in pixels.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setRelativeX(float relativeX) {
+            mRelativeX = relativeX;
+            return this;
+        }
+
+        /**
+         * Sets the relative y-axis movement, in pixels.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setRelativeY(float relativeY) {
+            mRelativeY = relativeY;
+            return this;
+        }
+    }
+
+    public static final @NonNull Parcelable.Creator<VirtualMouseRelativeEvent> CREATOR =
+            new Parcelable.Creator<VirtualMouseRelativeEvent>() {
+                public VirtualMouseRelativeEvent createFromParcel(Parcel source) {
+                    return new VirtualMouseRelativeEvent(source);
+                }
+
+                public VirtualMouseRelativeEvent[] newArray(int size) {
+                    return new VirtualMouseRelativeEvent[size];
+                }
+            };
+}
diff --git a/core/java/android/hardware/input/VirtualMouseScrollEvent.aidl b/core/java/android/hardware/input/VirtualMouseScrollEvent.aidl
new file mode 100644
index 0000000..13177ef
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualMouseScrollEvent.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.hardware.input;
+
+parcelable VirtualMouseScrollEvent;
\ No newline at end of file
diff --git a/core/java/android/hardware/input/VirtualMouseScrollEvent.java b/core/java/android/hardware/input/VirtualMouseScrollEvent.java
new file mode 100644
index 0000000..1723259
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualMouseScrollEvent.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 android.hardware.input;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * An event describing a mouse scroll interaction originating from a remote device.
+ *
+ * See {@link android.view.MotionEvent}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualMouseScrollEvent implements Parcelable {
+
+    private final float mXAxisMovement;
+    private final float mYAxisMovement;
+
+    private VirtualMouseScrollEvent(float xAxisMovement, float yAxisMovement) {
+        mXAxisMovement = xAxisMovement;
+        mYAxisMovement = yAxisMovement;
+    }
+
+    private VirtualMouseScrollEvent(@NonNull Parcel parcel) {
+        mXAxisMovement = parcel.readFloat();
+        mYAxisMovement = parcel.readFloat();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) {
+        parcel.writeFloat(mXAxisMovement);
+        parcel.writeFloat(mYAxisMovement);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns the x-axis scroll movement, normalized from -1.0 to 1.0, inclusive. Positive values
+     * indicate scrolling upward; negative values, downward.
+     */
+    public float getXAxisMovement() {
+        return mXAxisMovement;
+    }
+
+    /**
+     * Returns the y-axis scroll movement, normalized from -1.0 to 1.0, inclusive. Positive values
+     * indicate scrolling towards the right; negative values, to the left.
+     */
+    public float getYAxisMovement() {
+        return mYAxisMovement;
+    }
+
+    /**
+     * Builder for {@link VirtualMouseScrollEvent}.
+     */
+    public static final class Builder {
+
+        private float mXAxisMovement;
+        private float mYAxisMovement;
+
+        /**
+         * Creates a {@link VirtualMouseScrollEvent} object with the current builder configuration.
+         */
+        public @NonNull VirtualMouseScrollEvent build() {
+            return new VirtualMouseScrollEvent(mXAxisMovement, mYAxisMovement);
+        }
+
+        /**
+         * Sets the x-axis scroll movement, normalized from -1.0 to 1.0, inclusive. Positive values
+         * indicate scrolling upward; negative values, downward.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setXAxisMovement(
+                @FloatRange(from = -1.0f, to = 1.0f) float xAxisMovement) {
+            Preconditions.checkArgumentInRange(xAxisMovement, -1f, 1f, "xAxisMovement");
+            mXAxisMovement = xAxisMovement;
+            return this;
+        }
+
+        /**
+         * Sets the y-axis scroll movement, normalized from -1.0 to 1.0, inclusive. Positive values
+         * indicate scrolling towards the right; negative values, to the left.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setYAxisMovement(
+                @FloatRange(from = -1.0f, to = 1.0f) float yAxisMovement) {
+            Preconditions.checkArgumentInRange(yAxisMovement, -1f, 1f, "yAxisMovement");
+            mYAxisMovement = yAxisMovement;
+            return this;
+        }
+    }
+
+    public static final @NonNull Parcelable.Creator<VirtualMouseScrollEvent> CREATOR =
+            new Parcelable.Creator<VirtualMouseScrollEvent>() {
+                public VirtualMouseScrollEvent createFromParcel(Parcel source) {
+                    return new VirtualMouseScrollEvent(source);
+                }
+
+                public VirtualMouseScrollEvent[] newArray(int size) {
+                    return new VirtualMouseScrollEvent[size];
+                }
+            };
+}
diff --git a/core/java/android/hardware/input/VirtualTouchEvent.aidl b/core/java/android/hardware/input/VirtualTouchEvent.aidl
new file mode 100644
index 0000000..03c82e3
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualTouchEvent.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.hardware.input;
+
+parcelable VirtualTouchEvent;
\ No newline at end of file
diff --git a/core/java/android/hardware/input/VirtualTouchEvent.java b/core/java/android/hardware/input/VirtualTouchEvent.java
new file mode 100644
index 0000000..c7450d8
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualTouchEvent.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.input;
+
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.MotionEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event describing a touchscreen interaction originating from a remote device.
+ *
+ * The pointer id, tool type, action, and location are required; pressure and main axis size are
+ * optional.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualTouchEvent implements Parcelable {
+
+    /** @hide */
+    public static final int TOOL_TYPE_UNKNOWN = MotionEvent.TOOL_TYPE_UNKNOWN;
+    /** Tool type indicating that the user's finger is the origin of the event. */
+    public static final int TOOL_TYPE_FINGER = MotionEvent.TOOL_TYPE_FINGER;
+    /**
+     * Tool type indicating that a user's palm (or other input mechanism to be rejected) is the
+     * origin of the event.
+     */
+    public static final int TOOL_TYPE_PALM = MotionEvent.TOOL_TYPE_PALM;
+    /** @hide */
+    @IntDef(prefix = { "TOOL_TYPE_" }, value = {
+            TOOL_TYPE_UNKNOWN,
+            TOOL_TYPE_FINGER,
+            TOOL_TYPE_PALM,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ToolType {}
+
+    /** @hide */
+    public static final int ACTION_UNKNOWN = -1;
+    /** Action indicating the tool has been pressed down to the touchscreen. */
+    public static final int ACTION_DOWN = MotionEvent.ACTION_DOWN;
+    /** Action indicating the tool has been lifted from the touchscreen. */
+    public static final int ACTION_UP = MotionEvent.ACTION_UP;
+    /** Action indicating the tool has been moved along the face of the touchscreen. */
+    public static final int ACTION_MOVE = MotionEvent.ACTION_MOVE;
+    /** Action indicating the tool cancelled the current movement. */
+    public static final int ACTION_CANCEL = MotionEvent.ACTION_CANCEL;
+    /** @hide */
+    @IntDef(prefix = { "ACTION_" }, value = {
+            ACTION_UNKNOWN,
+            ACTION_DOWN,
+            ACTION_UP,
+            ACTION_MOVE,
+            ACTION_CANCEL,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {}
+
+    private final int mPointerId;
+    private final @ToolType int mToolType;
+    private final @Action int mAction;
+    private final float mX;
+    private final float mY;
+    private final float mPressure;
+    private final float mMajorAxisSize;
+
+    private VirtualTouchEvent(int pointerId, @ToolType int toolType, @Action int action,
+            float x, float y, float pressure, float majorAxisSize) {
+        mPointerId = pointerId;
+        mToolType = toolType;
+        mAction = action;
+        mX = x;
+        mY = y;
+        mPressure = pressure;
+        mMajorAxisSize = majorAxisSize;
+    }
+
+    private VirtualTouchEvent(@NonNull Parcel parcel) {
+        mPointerId = parcel.readInt();
+        mToolType = parcel.readInt();
+        mAction = parcel.readInt();
+        mX = parcel.readFloat();
+        mY = parcel.readFloat();
+        mPressure = parcel.readFloat();
+        mMajorAxisSize = parcel.readFloat();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mPointerId);
+        dest.writeInt(mToolType);
+        dest.writeInt(mAction);
+        dest.writeFloat(mX);
+        dest.writeFloat(mY);
+        dest.writeFloat(mPressure);
+        dest.writeFloat(mMajorAxisSize);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns the pointer id associated with this event.
+     */
+    public int getPointerId() {
+        return mPointerId;
+    }
+
+    /**
+     * Returns the tool type associated with this event.
+     */
+    public @ToolType int getToolType() {
+        return mToolType;
+    }
+
+    /**
+     * Returns the action associated with this event.
+     */
+    public @Action int getAction() {
+        return mAction;
+    }
+
+    /**
+     * Returns the x-axis location associated with this event.
+     */
+    public float getX() {
+        return mX;
+    }
+
+    /**
+     * Returns the y-axis location associated with this event.
+     */
+    public float getY() {
+        return mY;
+    }
+
+    /**
+     * Returns the pressure associated with this event. Returns {@link Float#NaN} if omitted.
+     */
+    public float getPressure() {
+        return mPressure;
+    }
+
+    /**
+     * Returns the major axis size associated with this event. Returns {@link Float#NaN} if omitted.
+     */
+    public float getMajorAxisSize() {
+        return mMajorAxisSize;
+    }
+
+    /**
+     * Builder for {@link VirtualTouchEvent}.
+     */
+    public static final class Builder {
+
+        private @ToolType int mToolType = TOOL_TYPE_UNKNOWN;
+        private int mPointerId = MotionEvent.INVALID_POINTER_ID;
+        private @Action int mAction = ACTION_UNKNOWN;
+        private float mX = Float.NaN;
+        private float mY = Float.NaN;
+        private float mPressure = Float.NaN;
+        private float mMajorAxisSize = Float.NaN;
+
+        /**
+         * Creates a {@link VirtualTouchEvent} object with the current builder configuration.
+         */
+        public @NonNull VirtualTouchEvent build() {
+            if (mToolType == TOOL_TYPE_UNKNOWN || mPointerId == MotionEvent.INVALID_POINTER_ID
+                    || mAction == ACTION_UNKNOWN || Float.isNaN(mX) || Float.isNaN(mY)) {
+                throw new IllegalArgumentException(
+                        "Cannot build virtual touch event with unset required fields");
+            }
+            if ((mToolType == TOOL_TYPE_PALM && mAction != ACTION_CANCEL)
+                    || (mAction == ACTION_CANCEL && mToolType != TOOL_TYPE_PALM)) {
+                throw new IllegalArgumentException(
+                        "ACTION_CANCEL and TOOL_TYPE_PALM must always appear together");
+            }
+            return new VirtualTouchEvent(mPointerId, mToolType, mAction, mX, mY, mPressure,
+                    mMajorAxisSize);
+        }
+
+        /**
+         * Sets the pointer id of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setPointerId(int pointerId) {
+            mPointerId = pointerId;
+            return this;
+        }
+
+        /**
+         * Sets the tool type of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setToolType(@ToolType int toolType) {
+            if (toolType != TOOL_TYPE_FINGER && toolType != TOOL_TYPE_PALM) {
+                throw new IllegalArgumentException("Unsupported touch event tool type");
+            }
+            mToolType = toolType;
+            return this;
+        }
+
+        /**
+         * Sets the action of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setAction(@Action int action) {
+            if (action != ACTION_DOWN && action != ACTION_UP && action != ACTION_MOVE
+                    && action != ACTION_CANCEL) {
+                throw new IllegalArgumentException("Unsupported touch event action type");
+            }
+            mAction = action;
+            return this;
+        }
+
+        /**
+         * Sets the x-axis location of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setX(float absX) {
+            mX = absX;
+            return this;
+        }
+
+        /**
+         * Sets the y-axis location of the event.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setY(float absY) {
+            mY = absY;
+            return this;
+        }
+
+        /**
+         * Sets the pressure of the event. This field is optional and can be omitted.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setPressure(@FloatRange(from = 0f) float pressure) {
+            if (pressure < 0f) {
+                throw new IllegalArgumentException("Touch event pressure cannot be negative");
+            }
+            mPressure = pressure;
+            return this;
+        }
+
+        /**
+         * Sets the major axis size of the event. This field is optional and can be omitted.
+         *
+         * @return this builder, to allow for chaining of calls
+         */
+        public @NonNull Builder setMajorAxisSize(@FloatRange(from = 0f) float majorAxisSize) {
+            if (majorAxisSize < 0f) {
+                throw new IllegalArgumentException(
+                        "Touch event major axis size cannot be negative");
+            }
+            mMajorAxisSize = majorAxisSize;
+            return this;
+        }
+    }
+
+    public static final @NonNull Parcelable.Creator<VirtualTouchEvent> CREATOR =
+            new Parcelable.Creator<VirtualTouchEvent>() {
+        public VirtualTouchEvent createFromParcel(Parcel source) {
+            return new VirtualTouchEvent(source);
+        }
+        public VirtualTouchEvent[] newArray(int size) {
+            return new VirtualTouchEvent[size];
+        }
+    };
+}
diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java
new file mode 100644
index 0000000..c8d602a
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualTouchscreen.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 android.hardware.input;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.companion.virtual.IVirtualDevice;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.io.Closeable;
+
+/**
+ * A virtual touchscreen representing a touch-based display input mechanism on a remote device.
+ *
+ * This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.
+ *
+ * @hide
+ */
+@SystemApi
+public class VirtualTouchscreen implements Closeable {
+
+    private final IVirtualDevice mVirtualDevice;
+    private final IBinder mToken;
+
+    /** @hide */
+    public VirtualTouchscreen(IVirtualDevice virtualDevice, IBinder token) {
+        mVirtualDevice = virtualDevice;
+        mToken = token;
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void close() {
+        try {
+            mVirtualDevice.unregisterInputDevice(mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sends a touch event to the system.
+     *
+     * @param event the event to send
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void sendTouchEvent(@NonNull VirtualTouchEvent event) {
+        try {
+            mVirtualDevice.sendTouchEvent(mToken, event);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/net/NetworkPolicy.java b/core/java/android/net/NetworkPolicy.java
index 8a0211c..70fe5d6 100644
--- a/core/java/android/net/NetworkPolicy.java
+++ b/core/java/android/net/NetworkPolicy.java
@@ -16,11 +16,19 @@
 
 package android.net;
 
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkTemplate.MATCH_CARRIER;
+import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.SUBSCRIBER_ID_MATCH_RULE_EXACT;
+
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.BackupUtils;
+import android.util.Log;
 import android.util.Range;
 import android.util.RecurrenceRule;
 
@@ -42,10 +50,25 @@
  * @hide
  */
 public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
+    private static final String TAG = NetworkPolicy.class.getSimpleName();
     private static final int VERSION_INIT = 1;
     private static final int VERSION_RULE = 2;
     private static final int VERSION_RAPID = 3;
 
+    /**
+     * Initial Version of the NetworkTemplate backup serializer.
+     */
+    private static final int TEMPLATE_BACKUP_VERSION_1_INIT = 1;
+    /**
+     * Version of the NetworkTemplate backup serializer that added carrier template support.
+     */
+    private static final int TEMPLATE_BACKUP_VERSION_2_SUPPORT_CARRIER_TEMPLATE = 2;
+    /**
+     * Latest Version of the NetworkTemplate Backup Serializer.
+     */
+    private static final int TEMPLATE_BACKUP_VERSION_LATEST =
+            TEMPLATE_BACKUP_VERSION_2_SUPPORT_CARRIER_TEMPLATE;
+
     public static final int CYCLE_NONE = -1;
     public static final long WARNING_DISABLED = -1;
     public static final long LIMIT_DISABLED = -1;
@@ -255,7 +278,7 @@
         DataOutputStream out = new DataOutputStream(baos);
 
         out.writeInt(VERSION_RAPID);
-        out.write(template.getBytesForBackup());
+        out.write(getNetworkTemplateBytesForBackup());
         cycleRule.writeToStream(out);
         out.writeLong(warningBytes);
         out.writeLong(limitBytes);
@@ -274,7 +297,7 @@
             throw new BackupUtils.BadVersionException("Unknown backup version: " + version);
         }
 
-        final NetworkTemplate template = NetworkTemplate.getNetworkTemplateFromBackup(in);
+        final NetworkTemplate template = getNetworkTemplateFromBackup(in);
         final RecurrenceRule cycleRule;
         if (version >= VERSION_RULE) {
             cycleRule = new RecurrenceRule(in);
@@ -298,4 +321,61 @@
         return new NetworkPolicy(template, cycleRule, warningBytes, limitBytes, lastWarningSnooze,
                 lastLimitSnooze, lastRapidSnooze, metered, inferred);
     }
+
+    @NonNull
+    private byte[] getNetworkTemplateBytesForBackup() throws IOException {
+        if (!template.isPersistable()) {
+            Log.wtf(TAG, "Trying to backup non-persistable template: " + this);
+        }
+
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        final DataOutputStream out = new DataOutputStream(baos);
+
+        out.writeInt(TEMPLATE_BACKUP_VERSION_LATEST);
+
+        out.writeInt(template.getMatchRule());
+        BackupUtils.writeString(out, template.getSubscriberId());
+        BackupUtils.writeString(out, template.getNetworkId());
+        out.writeInt(template.getMeteredness());
+        out.writeInt(template.getSubscriberIdMatchRule());
+
+        return baos.toByteArray();
+    }
+
+    @NonNull
+    private static NetworkTemplate getNetworkTemplateFromBackup(DataInputStream in)
+            throws IOException, BackupUtils.BadVersionException {
+        int version = in.readInt();
+        if (version < TEMPLATE_BACKUP_VERSION_1_INIT || version > TEMPLATE_BACKUP_VERSION_LATEST) {
+            throw new BackupUtils.BadVersionException("Unknown Backup Serialization Version");
+        }
+
+        int matchRule = in.readInt();
+        final String subscriberId = BackupUtils.readString(in);
+        final String networkId = BackupUtils.readString(in);
+
+        final int metered;
+        final int subscriberIdMatchRule;
+        if (version >= TEMPLATE_BACKUP_VERSION_2_SUPPORT_CARRIER_TEMPLATE) {
+            metered = in.readInt();
+            subscriberIdMatchRule = in.readInt();
+        } else {
+            // For backward compatibility, fill the missing filters from match rules.
+            metered = (matchRule == MATCH_MOBILE
+                    || matchRule == NetworkTemplate.MATCH_MOBILE_WILDCARD
+                    || matchRule == MATCH_CARRIER) ? METERED_YES : METERED_ALL;
+            subscriberIdMatchRule = SUBSCRIBER_ID_MATCH_RULE_EXACT;
+        }
+
+        try {
+            return new NetworkTemplate(matchRule,
+                    subscriberId, new String[]{subscriberId},
+                    networkId, metered, NetworkStats.ROAMING_ALL,
+                    NetworkStats.DEFAULT_NETWORK_ALL, NetworkTemplate.NETWORK_TYPE_ALL,
+                    NetworkTemplate.OEM_MANAGED_ALL, subscriberIdMatchRule);
+        } catch (IllegalArgumentException e) {
+            throw new BackupUtils.BadVersionException(
+                    "Restored network template contains unknown match rule " + matchRule, e);
+        }
+    }
 }
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index b069fb3..e2d7847 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -22,9 +22,11 @@
 import android.app.AppOpsManager;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.util.ExceptionUtils;
+import android.util.IntArray;
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BinderCallHeavyHitterWatcher;
 import com.android.internal.os.BinderCallHeavyHitterWatcher.BinderCallHeavyHitterListener;
 import com.android.internal.os.BinderInternal;
@@ -140,33 +142,55 @@
     /**
      * Flag indicating whether we should be tracing transact calls.
      */
-    private static volatile boolean sTracingEnabled = false;
+    private static volatile boolean sStackTrackingEnabled = false;
+
+    private static final Object sTracingUidsWriteLock = new Object();
+    private static volatile IntArray sTracingUidsImmutable = new IntArray();
 
     /**
-     * Enable Binder IPC tracing.
+     * Enable Binder IPC stack tracking. If enabled, every binder transaction will be logged to
+     * {@link TransactionTracker}.
      *
      * @hide
      */
-    public static void enableTracing() {
-        sTracingEnabled = true;
+    public static void enableStackTracking() {
+        sStackTrackingEnabled = true;
     }
 
     /**
-     * Disable Binder IPC tracing.
+     * Disable Binder IPC stack tracking.
      *
      * @hide
      */
-    public static void disableTracing() {
-        sTracingEnabled = false;
+    public static void disableStackTracking() {
+        sStackTrackingEnabled = false;
     }
 
     /**
-     * Check if binder transaction tracing is enabled.
+     * @hide
+     */
+    public static void enableTracingForUid(int uid) {
+        synchronized (sTracingUidsWriteLock) {
+            final IntArray copy = sTracingUidsImmutable.clone();
+            copy.add(uid);
+            sTracingUidsImmutable = copy;
+        }
+    }
+
+    /**
+     * Check if binder transaction stack tracking is enabled.
      *
      * @hide
      */
-    public static boolean isTracingEnabled() {
-        return sTracingEnabled;
+    public static boolean isStackTrackingEnabled() {
+        return sStackTrackingEnabled;
+    }
+
+    /**
+     * @hide
+     */
+    public static boolean isTracingEnabled(int callingUid) {
+        return sTracingUidsImmutable.indexOf(callingUid) != -1;
     }
 
     /**
@@ -288,6 +312,9 @@
 
     private IInterface mOwner;
     private String mDescriptor;
+    private volatile String[] mTransactionTraceNames = null;
+    private volatile String mSimpleDescriptor = null;
+    private static final int TRANSACTION_TRACE_NAME_ID_LIMIT = 1024;
 
     /**
      * Return the ID of the process that sent you the current transaction
@@ -884,6 +911,53 @@
     }
 
     /**
+     * @hide
+     */
+    @VisibleForTesting
+    public final @NonNull String getTransactionTraceName(int transactionCode) {
+        if (mTransactionTraceNames == null) {
+            final String descriptor = getSimpleDescriptor();
+            final int highestId = Math.min(getMaxTransactionId(), TRANSACTION_TRACE_NAME_ID_LIMIT);
+            final String[] transactionNames = new String[highestId + 1];
+            final StringBuffer buf = new StringBuffer();
+            for (int i = 0; i <= highestId; i++) {
+                String transactionName = getTransactionName(i + FIRST_CALL_TRANSACTION);
+                if (transactionName != null) {
+                    buf.append(descriptor).append(':').append(transactionName);
+                } else {
+                    buf.append(descriptor).append('#').append(i + FIRST_CALL_TRANSACTION);
+                }
+                transactionNames[i] = buf.toString();
+                buf.setLength(0);
+            }
+            mTransactionTraceNames = transactionNames;
+            mSimpleDescriptor = descriptor;
+        }
+        final int index = transactionCode - FIRST_CALL_TRANSACTION;
+        if (index < 0 || index >= mTransactionTraceNames.length) {
+            return mSimpleDescriptor + "#" + transactionCode;
+        }
+        return mTransactionTraceNames[index];
+    }
+
+    private String getSimpleDescriptor() {
+        final int dot = mDescriptor.lastIndexOf(".");
+        if (dot > 0) {
+            // Strip the package name
+            return mDescriptor.substring(dot + 1);
+        }
+        return mDescriptor;
+    }
+
+    /**
+     * @return The highest user-defined transaction id of all transactions.
+     * @hide
+     */
+    public int getMaxTransactionId() {
+        return 0;
+    }
+
+    /**
      * Implemented to call the more convenient version
      * {@link #dump(FileDescriptor, PrintWriter, String[])}.
      */
@@ -1181,7 +1255,8 @@
         // Log any exceptions as warnings, don't silently suppress them.
         // If the call was {@link IBinder#FLAG_ONEWAY} then these exceptions
         // disappear into the ether.
-        final boolean tracingEnabled = Binder.isTracingEnabled();
+        final boolean tracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_AIDL) &&
+                (Binder.isStackTrackingEnabled() || Binder.isTracingEnabled(callingUid));
         try {
             final BinderCallHeavyHitterWatcher heavyHitterWatcher = sHeavyHitterWatcher;
             if (heavyHitterWatcher != null) {
@@ -1189,9 +1264,7 @@
                 heavyHitterWatcher.onTransaction(callingUid, getClass(), code);
             }
             if (tracingEnabled) {
-                final String transactionName = getTransactionName(code);
-                Trace.traceBegin(Trace.TRACE_TAG_ALWAYS, getClass().getName() + ":"
-                        + (transactionName != null ? transactionName : code));
+                Trace.traceBegin(Trace.TRACE_TAG_AIDL, getTransactionTraceName(code));
             }
 
             if ((flags & FLAG_COLLECT_NOTED_APP_OPS) != 0) {
@@ -1226,7 +1299,7 @@
             res = true;
         } finally {
             if (tracingEnabled) {
-                Trace.traceEnd(Trace.TRACE_TAG_ALWAYS);
+                Trace.traceEnd(Trace.TRACE_TAG_AIDL);
             }
             if (observer != null) {
                 // The parcel RPC headers have been called during onTransact so we can now access
diff --git a/core/java/android/os/BinderProxy.java b/core/java/android/os/BinderProxy.java
index 483b549..e929920 100644
--- a/core/java/android/os/BinderProxy.java
+++ b/core/java/android/os/BinderProxy.java
@@ -545,7 +545,7 @@
             }
         }
 
-        final boolean tracingEnabled = Binder.isTracingEnabled();
+        final boolean tracingEnabled = Binder.isStackTrackingEnabled();
         if (tracingEnabled) {
             final Throwable tr = new Throwable();
             Binder.getTransactionTracker().addTrace(tr);
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index 8292f26..aa4b83a 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -192,14 +192,15 @@
 
         // We only want to use ANGLE if the developer has explicitly chosen something other than
         // default driver.
-        final boolean requested = devOptIn.equals(ANGLE_GL_DRIVER_CHOICE_ANGLE);
-        if (requested) {
+        final boolean forceAngle = devOptIn.equals(ANGLE_GL_DRIVER_CHOICE_ANGLE);
+        final boolean forceNative = devOptIn.equals(ANGLE_GL_DRIVER_CHOICE_NATIVE);
+        if (forceAngle || forceNative) {
             Log.v(TAG, "ANGLE developer option for " + packageName + ": " + devOptIn);
         }
 
         final boolean gameModeEnabledAngle = isAngleEnabledByGameMode(context, packageName);
 
-        return requested || gameModeEnabledAngle;
+        return !forceNative && (forceAngle || gameModeEnabledAngle);
     }
 
     private int getVulkanVersion(PackageManager pm) {
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 29d4d78..563d6cc3 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2073,7 +2073,8 @@
     @TestApi
     @RequiresPermission(anyOf = {
             android.Manifest.permission.MANAGE_USERS,
-            android.Manifest.permission.CREATE_USERS})
+            android.Manifest.permission.CREATE_USERS,
+            android.Manifest.permission.QUERY_USERS})
     @UserHandleAware
     public @NonNull String getUserType() {
         UserInfo userInfo = getUserInfo(mUserId);
@@ -2082,15 +2083,21 @@
 
     /**
      * Returns the user name of the context user. This call is only available to applications on
-     * the system image; it requires the {@code android.permission.MANAGE_USERS} or {@code
-     * android.permission.GET_ACCOUNTS_PRIVILEGED} permissions.
+     * the system image.
      *
      * @return the user name
      */
-    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
-            android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED,
-            android.Manifest.permission.CREATE_USERS}, conditional = true)
-    @UserHandleAware
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.CREATE_USERS,
+            android.Manifest.permission.QUERY_USERS,
+            android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED})
+
+    @UserHandleAware(
+            requiresAnyOfPermissionsIfNotCaller = {
+                    android.Manifest.permission.MANAGE_USERS,
+                    android.Manifest.permission.CREATE_USERS,
+                    android.Manifest.permission.QUERY_USERS})
     public @NonNull String getUserName() {
         if (UserHandle.myUserId() == mUserId) {
             try {
@@ -2153,8 +2160,10 @@
      * @hide
      */
     @SystemApi
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS})
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS})
     @UserHandleAware(enabledSinceTargetSdkVersion = Build.VERSION_CODES.TIRAMISU)
     public boolean isPrimaryUser() {
         final UserInfo user = getUserInfo(getContextUserIfAppropriate());
@@ -2184,7 +2193,8 @@
     @SystemApi
     @RequiresPermission(anyOf = {
             Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS})
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS})
     @UserHandleAware(enabledSinceTargetSdkVersion = Build.VERSION_CODES.TIRAMISU)
     public boolean isAdminUser() {
         return isUserAdmin(getContextUserIfAppropriate());
@@ -2196,8 +2206,10 @@
      * user.
      */
     @UnsupportedAppUsage
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS})
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS})
     public boolean isUserAdmin(@UserIdInt int userId) {
         UserInfo user = getUserInfo(userId);
         return user != null && user.isAdmin();
@@ -2394,8 +2406,10 @@
      * @hide
      */
     @SystemApi
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS})
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS})
     @UserHandleAware
     public @Nullable UserHandle getRestrictedProfileParent() {
         final UserInfo info = getUserInfo(mUserId);
@@ -2412,8 +2426,10 @@
      * @hide
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS})
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS})
     public boolean isGuestUser(@UserIdInt int userId) {
         UserInfo user = getUserInfo(userId);
         return user != null && user.isGuest();
@@ -2426,8 +2442,10 @@
      * @hide
      */
     @SystemApi
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS})
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS})
     @UserHandleAware(enabledSinceTargetSdkVersion = Build.VERSION_CODES.TIRAMISU)
     public boolean isGuestUser() {
         UserInfo user = getUserInfo(getContextUserIfAppropriate());
@@ -2456,17 +2474,15 @@
     /**
      * Checks if the calling context user is running in a profile.
      *
-     * Requires {@link android.Manifest.permission#MANAGE_USERS} or
-     * {@link android.Manifest.permission#INTERACT_ACROSS_USERS} permission, otherwise the
-     * caller must be in the same profile group of specified user.
-     *
      * @return whether the caller is in a profile.
      * @hide
      */
     @SystemApi
-    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
-            android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
-    @UserHandleAware
+    @UserHandleAware(
+            requiresAnyOfPermissionsIfNotCallerProfileGroup = {
+                    android.Manifest.permission.MANAGE_USERS,
+                    android.Manifest.permission.QUERY_USERS,
+                    android.Manifest.permission.INTERACT_ACROSS_USERS})
     public boolean isProfile() {
         return isProfile(mUserId);
     }
@@ -2502,8 +2518,8 @@
             enabledSinceTargetSdkVersion = Build.VERSION_CODES.TIRAMISU,
             requiresAnyOfPermissionsIfNotCallerProfileGroup = {
                     android.Manifest.permission.MANAGE_USERS,
-                    android.Manifest.permission.INTERACT_ACROSS_USERS}
-    )
+                    android.Manifest.permission.QUERY_USERS,
+                    android.Manifest.permission.INTERACT_ACROSS_USERS})
     public boolean isManagedProfile() {
         return isManagedProfile(getContextUserIfAppropriate());
     }
@@ -2511,15 +2527,18 @@
     /**
      * Checks if the specified user is a managed profile.
      * Requires {@link android.Manifest.permission#MANAGE_USERS} or
-     * {@link android.Manifest.permission#INTERACT_ACROSS_USERS} permission, otherwise the caller
+     * {@link android.Manifest.permission#INTERACT_ACROSS_USERS} or
+     * {@link android.Manifest.permission#QUERY_USERS} permission, otherwise the caller
      * must be in the same profile group of specified user.
      *
      * @return whether the specified user is a managed profile.
      * @hide
      */
     @SystemApi
-    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
-            Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.QUERY_USERS,
+            android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
     public boolean isManagedProfile(@UserIdInt int userId) {
         if (userId == mUserId) {
             // No need for synchronization.  Once it becomes non-null, it'll be non-null forever.
@@ -2573,8 +2592,10 @@
      * @return whether the context user is an ephemeral user.
      * @hide
      */
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS})
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS})
     @UserHandleAware
     public boolean isEphemeralUser() {
         return isUserEphemeral(mUserId);
@@ -2584,8 +2605,10 @@
      * Returns whether the specified user is ephemeral.
      * @hide
      */
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS})
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS})
     public boolean isUserEphemeral(@UserIdInt int userId) {
         final UserInfo user = getUserInfo(userId);
         return user != null && user.isEphemeral();
@@ -2853,8 +2876,10 @@
      * @hide
      */
     @UnsupportedAppUsage
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS})
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS})
     public UserInfo getUserInfo(@UserIdInt int userId) {
         try {
             return mService.getUserInfo(userId);
@@ -2877,7 +2902,9 @@
     @Deprecated
     @SystemApi
     @UserRestrictionSource
-    @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.QUERY_USERS})
     public int getUserRestrictionSource(@UserRestrictionKey String restrictionKey,
             UserHandle userHandle) {
         try {
@@ -2896,7 +2923,9 @@
      * @return a list of user ids enforcing this restriction.
      */
     @SystemApi
-    @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.QUERY_USERS})
     public List<EnforcingUser> getUserRestrictionSources(
             @UserRestrictionKey String restrictionKey, UserHandle userHandle) {
         try {
@@ -3963,15 +3992,17 @@
     }
 
     /**
-     * Checks whether it's possible to add more managed profiles. Caller must hold the MANAGE_USERS
-     * permission.
+     * Checks whether it's possible to add more managed profiles.
      * if allowedToRemoveOne is true and if the user already has a managed profile, then return if
      * we could add a new managed profile to this user after removing the existing one.
      *
      * @return true if more managed profiles can be added, false if limit has been reached.
      * @hide
      */
-    @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.QUERY_USERS
+    })
     public boolean canAddMoreManagedProfiles(@UserIdInt int userId, boolean allowedToRemoveOne) {
         try {
             return mService.canAddMoreManagedProfiles(userId, allowedToRemoveOne);
@@ -3987,7 +4018,10 @@
      * @return true if more profiles can be added, false if limit has been reached.
      * @hide
      */
-    @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.QUERY_USERS
+    })
     public boolean canAddMoreProfilesToUser(@NonNull String userType, @UserIdInt int userId) {
         try {
             return mService.canAddMoreProfilesToUser(userType, userId, false);
@@ -4022,14 +4056,17 @@
      * <p>Note that this includes all profile types (not including Restricted profiles).
      *
      * <p>Requires {@link android.Manifest.permission#MANAGE_USERS} or
-     * {@link android.Manifest.permission#CREATE_USERS} if userId is not the calling user.
+     * {@link android.Manifest.permission#CREATE_USERS} or
+     * {@link android.Manifest.permission#QUERY_USERS} if userId is not the calling user.
      * @param userId profiles of this user will be returned.
      * @return the list of profiles.
      * @hide
      */
     @UnsupportedAppUsage
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS}, conditional = true)
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS}, conditional = true)
     public List<UserInfo> getProfiles(@UserIdInt int userId) {
         try {
             return mService.getProfiles(userId, false /* enabledOnly */);
@@ -4048,7 +4085,9 @@
      * @hide
      */
     @SystemApi
-    @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.QUERY_USERS})
     public boolean isSameProfileGroup(@NonNull UserHandle user, @NonNull UserHandle otherUser) {
         return isSameProfileGroup(user.getIdentifier(), otherUser.getIdentifier());
     }
@@ -4060,7 +4099,9 @@
      * @return true if the two user ids are in the same profile group.
      * @hide
      */
-    @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.QUERY_USERS})
     public boolean isSameProfileGroup(@UserIdInt int userId, int otherUserId) {
         try {
             return mService.isSameProfileGroup(userId, otherUserId);
@@ -4075,14 +4116,20 @@
      * <p>Note that this includes all profile types (not including Restricted profiles).
      *
      * <p>Requires {@link android.Manifest.permission#MANAGE_USERS} or
-     * {@link android.Manifest.permission#CREATE_USERS} if userId is not the calling user.
+     * {@link android.Manifest.permission#CREATE_USERS} or
+     * {@link android.Manifest.permission#QUERY_USERS} if userId is not the calling user.
      * @param userId profiles of this user will be returned.
      * @return the list of profiles.
+     * @deprecated use {@link #getUserProfiles()} instead.
+     *
      * @hide
      */
+    @Deprecated
     @UnsupportedAppUsage
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS}, conditional = true)
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS}, conditional = true)
     public List<UserInfo> getEnabledProfiles(@UserIdInt int userId) {
         try {
             return mService.getProfiles(userId, true /* enabledOnly */);
@@ -4102,8 +4149,8 @@
             enabledSinceTargetSdkVersion = Build.VERSION_CODES.TIRAMISU,
             requiresAnyOfPermissionsIfNotCaller = {
                     Manifest.permission.MANAGE_USERS,
-                    Manifest.permission.CREATE_USERS}
-    )
+                    Manifest.permission.CREATE_USERS,
+                    Manifest.permission.QUERY_USERS})
     public List<UserHandle> getUserProfiles() {
         int[] userIds = getProfileIds(getContextUserIfAppropriate(), true /* enabledOnly */);
         return convertUserIdsToUserHandles(userIds);
@@ -4118,9 +4165,11 @@
      * @hide
      */
     @SystemApi
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS}, conditional = true)
-    @UserHandleAware
+    @UserHandleAware(
+            requiresAnyOfPermissionsIfNotCaller = {
+                    Manifest.permission.MANAGE_USERS,
+                    Manifest.permission.CREATE_USERS,
+                    Manifest.permission.QUERY_USERS})
     public @NonNull List<UserHandle> getEnabledProfiles() {
         return getProfiles(true);
     }
@@ -4134,9 +4183,11 @@
      * @hide
      */
     @SystemApi
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS}, conditional = true)
-    @UserHandleAware
+    @UserHandleAware(
+            requiresAnyOfPermissionsIfNotCaller = {
+                    Manifest.permission.MANAGE_USERS,
+                    Manifest.permission.CREATE_USERS,
+                    Manifest.permission.QUERY_USERS})
     public @NonNull List<UserHandle> getAllProfiles() {
         return getProfiles(false);
     }
@@ -4149,9 +4200,11 @@
      * @param enabledOnly whether to return only {@link UserInfo#isEnabled() enabled} profiles
      * @return A non-empty list of UserHandles associated with the context user.
      */
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS}, conditional = true)
-    @UserHandleAware
+    @UserHandleAware(
+            requiresAnyOfPermissionsIfNotCaller = {
+                    Manifest.permission.MANAGE_USERS,
+                    Manifest.permission.CREATE_USERS,
+                    Manifest.permission.QUERY_USERS})
     private @NonNull List<UserHandle> getProfiles(boolean enabledOnly) {
         final int[] userIds = getProfileIds(mUserId, enabledOnly);
         return convertUserIdsToUserHandles(userIds);
@@ -4177,8 +4230,10 @@
      *
      * @hide
      */
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS}, conditional = true)
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS}, conditional = true)
     public @NonNull int[] getProfileIds(@UserIdInt int userId, boolean enabledOnly) {
         try {
             return mService.getProfileIds(userId, enabledOnly);
@@ -4192,8 +4247,10 @@
      * @hide
      */
     @UnsupportedAppUsage
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS}, conditional = true)
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS}, conditional = true)
     public int[] getProfileIdsWithDisabled(@UserIdInt int userId) {
         return getProfileIds(userId, false /* enabledOnly */);
     }
@@ -4202,8 +4259,10 @@
      * @see #getProfileIds(int, boolean)
      * @hide
      */
-    @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
-            Manifest.permission.CREATE_USERS}, conditional = true)
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS}, conditional = true)
     public int[] getEnabledProfileIds(@UserIdInt int userId) {
         return getProfileIds(userId, true /* enabledOnly */);
     }
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index 21c1feb..22b9578 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -671,6 +671,14 @@
     @TestApi
     public static final String NAMESPACE_APP_COMPAT_OVERRIDES = "app_compat_overrides";
 
+    /**
+     * Namespace for all ultra wideband (uwb) related features.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String NAMESPACE_UWB = "uwb";
+
     private static final Object sLock = new Object();
     @GuardedBy("sLock")
     private static ArrayMap<OnPropertiesChangedListener, Pair<String, Executor>> sListeners =
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 83a6bc0..73ffd66 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -44,7 +44,6 @@
 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;
@@ -1930,9 +1929,7 @@
 
             mScreenshotSize.set(mSurfaceSize.x, mSurfaceSize.y);
 
-            GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(hardwareBuffer);
-
-            t.setBuffer(mScreenshotSurfaceControl, graphicBuffer);
+            t.setBuffer(mScreenshotSurfaceControl, hardwareBuffer);
             t.setColorSpace(mScreenshotSurfaceControl, screenshotBuffer.getColorSpace());
             // Place on top everything else.
             t.setLayer(mScreenshotSurfaceControl, Integer.MAX_VALUE);
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index cb1cff9..859fd80 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -247,8 +247,8 @@
             } else if (listener.mSubId != null) {
                 subId = listener.mSubId;
             }
-            sRegistry.listenWithEventList(
-                    subId, pkg, featureId, listener.callback, eventsList, notifyNow);
+            sRegistry.listenWithEventList(false, false, subId, pkg, featureId,
+                    listener.callback, eventsList, notifyNow);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -263,11 +263,13 @@
      * @param events List events
      * @param notifyNow Whether to notify instantly
      */
-    private void listenFromCallback(int subId, @NonNull String pkg, @NonNull String featureId,
+    private void listenFromCallback(boolean renounceFineLocationAccess,
+            boolean renounceCoarseLocationAccess, int subId,
+            @NonNull String pkg, @NonNull String featureId,
             @NonNull TelephonyCallback telephonyCallback, @NonNull int[] events,
             boolean notifyNow) {
         try {
-            sRegistry.listenWithEventList(
+            sRegistry.listenWithEventList(renounceFineLocationAccess, renounceCoarseLocationAccess,
                     subId, pkg, featureId, telephonyCallback.callback, events, notifyNow);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -1161,14 +1163,17 @@
      *
      * @param callback The {@link TelephonyCallback} object to register.
      */
-    public void registerTelephonyCallback(@NonNull @CallbackExecutor Executor executor,
+    public void registerTelephonyCallback(boolean renounceFineLocationAccess,
+            boolean renounceCoarseLocationAccess,
+            @NonNull @CallbackExecutor Executor executor,
             int subId, String pkgName, String attributionTag, @NonNull TelephonyCallback callback,
             boolean notifyNow) {
         if (callback == null) {
             throw new IllegalStateException("telephony service is null.");
         }
         callback.init(executor);
-        listenFromCallback(subId, pkgName, attributionTag, callback,
+        listenFromCallback(renounceFineLocationAccess, renounceCoarseLocationAccess, subId,
+                pkgName, attributionTag, callback,
                 getEventsFromCallback(callback).stream().mapToInt(i -> i).toArray(), notifyNow);
     }
 
@@ -1179,6 +1184,7 @@
      */
     public void unregisterTelephonyCallback(int subId, String pkgName, String attributionTag,
             @NonNull TelephonyCallback callback, boolean notifyNow) {
-        listenFromCallback(subId, pkgName, attributionTag, callback, new int[0], notifyNow);
+        listenFromCallback(false, false, subId,
+                pkgName, attributionTag, callback, new int[0], notifyNow);
     }
 }
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 6984e4d..4789231 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -874,6 +874,18 @@
                             ? Math.max(fmDescent, Math.round(descents[breakIndex]))
                             : fmDescent;
 
+                    // The fallback ascent/descent may be larger than top/bottom of the default font
+                    // metrics. Adjust top/bottom with ascent/descent for avoiding unexpected
+                    // clipping.
+                    if (fallbackLineSpacing) {
+                        if (ascent < fmTop) {
+                            fmTop = ascent;
+                        }
+                        if (descent > fmBottom) {
+                            fmBottom = descent;
+                        }
+                    }
+
                     v = out(source, here, endPos,
                             ascent, descent, fmTop, fmBottom,
                             v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index 7e2792c..8124510 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -58,6 +58,10 @@
      */
     public static final String SETTINGS_APP_LANGUAGE_SELECTION = "settings_app_language_selection";
 
+    /** @hide */
+    public static final String SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS =
+            "settings_enable_monitor_phantom_procs";
+
     private static final Map<String, String> DEFAULT_FLAGS;
 
     static {
@@ -81,6 +85,7 @@
         DEFAULT_FLAGS.put(SETTINGS_ENABLE_SECURITY_HUB, "true");
         DEFAULT_FLAGS.put(SETTINGS_SUPPORT_LARGE_SCREEN, "true");
         DEFAULT_FLAGS.put(SETTINGS_APP_LANGUAGE_SELECTION, "false");
+        DEFAULT_FLAGS.put(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS, "true");
     }
 
     private static final Set<String> PERSISTENT_FLAGS;
@@ -89,6 +94,7 @@
         PERSISTENT_FLAGS.add(SETTINGS_APP_LANGUAGE_SELECTION);
         PERSISTENT_FLAGS.add(SETTINGS_PROVIDER_MODEL);
         PERSISTENT_FLAGS.add(SETTINGS_SUPPORT_LARGE_SCREEN);
+        PERSISTENT_FLAGS.add(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS);
     }
 
     /**
diff --git a/core/java/android/util/IntArray.java b/core/java/android/util/IntArray.java
index b77265b..7b28b8a 100644
--- a/core/java/android/util/IntArray.java
+++ b/core/java/android/util/IntArray.java
@@ -34,7 +34,7 @@
     private int[] mValues;
     private int mSize;
 
-    private  IntArray(int[] array, int size) {
+    private IntArray(int[] array, int size) {
         mValues = array;
         mSize = Preconditions.checkArgumentInRange(size, 0, array.length, "size");
     }
@@ -178,10 +178,8 @@
     }
 
     @Override
-    public IntArray clone() throws CloneNotSupportedException {
-        final IntArray clone = (IntArray) super.clone();
-        clone.mValues = mValues.clone();
-        return clone;
+    public IntArray clone() {
+        return new IntArray(mValues.clone(), mSize);
     }
 
     /**
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index b8e50fc..adb8b86 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -1495,6 +1495,15 @@
      */
     public static final int TOOL_TYPE_ERASER = 4;
 
+    /**
+     * Tool type constant: The tool is a palm and should be rejected.
+     *
+     * @see #getToolType
+     *
+     * @hide
+     */
+    public static final int TOOL_TYPE_PALM = 5;
+
     // NOTE: If you add a new tool type here you must also add it to:
     //  native/include/android/input.h
 
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index af7d86c..3b52709 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -120,6 +120,8 @@
             long relativeToObject, int zorder);
     private static native void nativeSetPosition(long transactionObj, long nativeObject,
             float x, float y);
+    private static native void nativeSetScale(long transactionObj, long nativeObject,
+            float x, float y);
     private static native void nativeSetSize(long transactionObj, long nativeObject, int w, int h);
     private static native void nativeSetTransparentRegionHint(long transactionObj,
             long nativeObject, Region region);
@@ -202,9 +204,13 @@
     private static native void nativeReparent(long transactionObj, long nativeObject,
             long newParentNativeObject);
     private static native void nativeSetBuffer(long transactionObj, long nativeObject,
-            GraphicBuffer buffer);
+            HardwareBuffer buffer);
+    private static native void nativeSetBufferTransform(long transactionObj, long nativeObject,
+            int transform);
     private static native void nativeSetColorSpace(long transactionObj, long nativeObject,
             int colorSpace);
+    private static native void nativeSetDamageRegion(long transactionObj, long nativeObject,
+            Region region);
 
     private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes);
 
@@ -1333,7 +1339,6 @@
          * Set the initial visibility for the SurfaceControl.
          *
          * @param hidden Whether the Surface is initially HIDDEN.
-         * @hide
          */
         @NonNull
         public Builder setHidden(boolean hidden) {
@@ -2840,16 +2845,38 @@
         }
 
         /**
-         * @hide
+         * Sets the SurfaceControl to the specified position relative to the parent
+         * SurfaceControl
+         *
+         * @param sc The SurfaceControl to change position
+         * @param x the X position
+         * @param y the Y position
+         * @return this transaction
          */
-        @UnsupportedAppUsage
-        public Transaction setPosition(SurfaceControl sc, float x, float y) {
+        @NonNull
+        public Transaction setPosition(@NonNull SurfaceControl sc, float x, float y) {
             checkPreconditions(sc);
             nativeSetPosition(mNativeObject, sc.mNativeObject, x, y);
             return this;
         }
 
         /**
+         * Sets the SurfaceControl to the specified scale with (0, 0) as the center point
+         * of the scale.
+         *
+         * @param sc The SurfaceControl to change scale
+         * @param scaleX the X scale
+         * @param scaleY the Y scale
+         * @return this transaction
+         */
+        @NonNull
+        public Transaction setScale(@NonNull SurfaceControl sc, float scaleX, float scaleY) {
+            checkPreconditions(sc);
+            nativeSetScale(mNativeObject, sc.mNativeObject, scaleX, scaleY);
+            return this;
+        }
+
+        /**
          * Set the default buffer size for the SurfaceControl, if there is a
          * {@link Surface} associated with the control, then
          * this will be the default size for buffers dequeued from it.
@@ -3056,7 +3083,9 @@
          * @param sc   SurfaceControl to set crop of.
          * @param crop Bounds of the crop to apply.
          * @hide
+         * @deprecated Use {@link #setCrop(SurfaceControl, Rect)} instead.
          */
+        @Deprecated
         @UnsupportedAppUsage
         public Transaction setWindowCrop(SurfaceControl sc, Rect crop) {
             checkPreconditions(sc);
@@ -3071,6 +3100,28 @@
         }
 
         /**
+         * Bounds the surface and its children to the bounds specified. Size of the surface will be
+         * ignored and only the crop and buffer size will be used to determine the bounds of the
+         * surface. If no crop is specified and the surface has no buffer, the surface bounds is
+         * only constrained by the size of its parent bounds.
+         *
+         * @param sc   SurfaceControl to set crop of.
+         * @param crop Bounds of the crop to apply.
+         * @return this This transaction for chaining
+         */
+        public @NonNull Transaction setCrop(@NonNull SurfaceControl sc, @Nullable Rect crop) {
+            checkPreconditions(sc);
+            if (crop != null) {
+                nativeSetWindowCrop(mNativeObject, sc.mNativeObject,
+                        crop.left, crop.top, crop.right, crop.bottom);
+            } else {
+                nativeSetWindowCrop(mNativeObject, sc.mNativeObject, 0, 0, 0, 0);
+            }
+
+            return this;
+        }
+
+        /**
          * Same as {@link Transaction#setWindowCrop(SurfaceControl, Rect)} but sets the crop rect
          * top left at 0, 0.
          *
@@ -3215,11 +3266,34 @@
         }
 
         /**
-         * Sets the opacity of the surface.  Setting the flag is equivalent to creating the
-         * Surface with the {@link #OPAQUE} flag.
-         * @hide
+         * Indicates whether the surface must be considered opaque, even if its pixel format is
+         * set to translucent. This can be useful if an application needs full RGBA 8888 support
+         * for instance but will still draw every pixel opaque.
+         * <p>
+         * This flag only determines whether opacity will be sampled from the alpha channel.
+         * Plane-alpha from calls to setAlpha() can still result in blended composition
+         * regardless of the opaque setting.
+         *
+         * Combined effects are (assuming a buffer format with an alpha channel):
+         * <ul>
+         * <li>OPAQUE + alpha(1.0) == opaque composition
+         * <li>OPAQUE + alpha(0.x) == blended composition
+         * <li>OPAQUE + alpha(0.0) == no composition
+         * <li>!OPAQUE + alpha(1.0) == blended composition
+         * <li>!OPAQUE + alpha(0.x) == blended composition
+         * <li>!OPAQUE + alpha(0.0) == no composition
+         * </ul>
+         * If the underlying buffer lacks an alpha channel, it is as if setOpaque(true)
+         * were set automatically.
+         *
+         * @see Builder#setOpaque(boolean)
+         *
+         * @param sc The SurfaceControl to update
+         * @param isOpaque true if the buffer's alpha should be ignored, false otherwise
+         * @return this
          */
-        public Transaction setOpaque(SurfaceControl sc, boolean isOpaque) {
+        @NonNull
+        public Transaction setOpaque(@NonNull SurfaceControl sc, boolean isOpaque) {
             checkPreconditions(sc);
             if (isOpaque) {
                 nativeSetFlags(mNativeObject, sc.mNativeObject, SURFACE_OPAQUE, SURFACE_OPAQUE);
@@ -3498,14 +3572,57 @@
          * created as type {@link #FX_SURFACE_BLAST}
          *
          * @hide
+         * @deprecated Use {@link #setBuffer(SurfaceControl, HardwareBuffer)} instead
          */
+        @Deprecated
         public Transaction setBuffer(SurfaceControl sc, GraphicBuffer buffer) {
+            return setBuffer(sc, HardwareBuffer.createFromGraphicBuffer(buffer));
+        }
+
+        /**
+         * Updates the HardwareBuffer displayed for the SurfaceControl.
+         *
+         * Note that the buffer must be allocated with {@link HardwareBuffer#USAGE_COMPOSER_OVERLAY}
+         * as well as {@link HardwareBuffer#USAGE_GPU_SAMPLED_IMAGE} as the surface control might
+         * be composited using either an overlay or using the GPU.
+         *
+         * @param sc The SurfaceControl to update
+         * @param buffer The buffer to be displayed
+         * @return this
+         */
+        public @NonNull Transaction setBuffer(@NonNull SurfaceControl sc,
+                @Nullable HardwareBuffer buffer) {
             checkPreconditions(sc);
             nativeSetBuffer(mNativeObject, sc.mNativeObject, buffer);
             return this;
         }
 
         /**
+         * Sets the buffer transform that should be applied to the current buffer.
+         *
+         * @param sc The SurfaceControl to update
+         * @param transform The transform to apply to the buffer.
+         * @return this
+         */
+        public @NonNull Transaction setBufferTransform(@NonNull SurfaceControl sc,
+                /* TODO: Mark the intdef */ int transform) {
+            checkPreconditions(sc);
+            nativeSetBufferTransform(mNativeObject, sc.mNativeObject, transform);
+            return this;
+        }
+
+        /**
+         * Updates the region for the content on this surface updated in this transaction.
+         *
+         * If unspecified, the complete surface is assumed to be damaged.
+         */
+        public @NonNull Transaction setDamageRegion(@NonNull SurfaceControl sc,
+                @Nullable Region region) {
+            nativeSetDamageRegion(mNativeObject, sc.mNativeObject, region);
+            return this;
+        }
+
+        /**
          * Set the color space for the SurfaceControl. The supported color spaces are SRGB
          * and Display P3, other color spaces will be treated as SRGB. This can only be used for
          * SurfaceControls that were created as type {@link #FX_SURFACE_BLAST}
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index 3b15db2..b85fe7c 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -836,20 +836,25 @@
     boolean beginBatchEdit();
 
     /**
-     * Tell the editor that you are done with a batch edit previously
-     * initiated with {@link #beginBatchEdit}. This ends the latest
-     * batch only.
+     * Tell the editor that you are done with a batch edit previously initiated with
+     * {@link #beginBatchEdit()}. This ends the latest batch only.
      *
-     * <p><strong>IME authors:</strong> make sure you call this
-     * exactly once for each call to {@link #beginBatchEdit}.</p>
+     * <p><strong>IME authors:</strong> make sure you call this exactly once for each call to
+     * {@link #beginBatchEdit()}.</p>
      *
-     * <p><strong>Editor authors:</strong> please be careful about
-     * batch edit nesting. Updates still to be held back until the end
-     * of the last batch edit.</p>
+     * <p><strong>Editor authors:</strong> please be careful about batch edit nesting. Updates still
+     * to be held back until the end of the last batch edit.  In case you are delegating this API
+     * call to the one obtained from
+     * {@link android.widget.EditText#onCreateInputConnection(EditorInfo)}, there was an off-by-one
+     * that had returned {@code true} when its nested batch edit count becomes {@code 0} as a result
+     * of invoking this API.  This bug is fixed in {@link android.os.Build.VERSION_CODES#TIRAMISU}.
+     * </p>
      *
-     * @return true if there is still a batch edit in progress after closing
-     * the latest one (in other words, if the nesting count is > 0), false
-     * otherwise or if the input connection is no longer valid.
+     * @return For editor authors, you must return {@code true} if a batch edit is still in progress
+     *         after closing the latest one (in other words, if the nesting count is still a
+     *         positive number). Return {@code false} otherwise.  For IME authors, you will
+     *         always receive {@code true} as long as the request was sent to the editor, and
+     *         receive {@code false} only if the input connection is no longer valid.
      */
     boolean endBatchEdit();
 
diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java
index 8532763..2702c2d 100644
--- a/core/java/android/view/translation/UiTranslationController.java
+++ b/core/java/android/view/translation/UiTranslationController.java
@@ -437,7 +437,10 @@
                     if (view.getViewTranslationResponse() != null
                             && view.getViewTranslationResponse().equals(response)) {
                         if (callback instanceof TextViewTranslationCallback) {
-                            if (((TextViewTranslationCallback) callback).isShowingTranslation()) {
+                            TextViewTranslationCallback textViewCallback =
+                                    (TextViewTranslationCallback) callback;
+                            if (textViewCallback.isShowingTranslation()
+                                    || textViewCallback.isAnimationRunning()) {
                                 if (DEBUG) {
                                     Log.d(TAG, "Duplicate ViewTranslationResponse for " + autofillId
                                             + ". Ignoring.");
diff --git a/core/java/android/widget/TextViewTranslationCallback.java b/core/java/android/widget/TextViewTranslationCallback.java
index 4a78f3e..942be21 100644
--- a/core/java/android/widget/TextViewTranslationCallback.java
+++ b/core/java/android/widget/TextViewTranslationCallback.java
@@ -46,6 +46,7 @@
 
     private TranslationTransformationMethod mTranslationTransformation;
     private boolean mIsShowingTranslation = false;
+    private boolean mAnimationRunning = false;
     private boolean mIsTextPaddingEnabled = false;
     private CharSequence mPaddedText;
     private int mAnimationDurationMillis = 250; // default value
@@ -92,6 +93,7 @@
                 (TextView) view,
                 () -> {
                     mIsShowingTranslation = true;
+                    mAnimationRunning = false;
                     // TODO(b/178353965): well-handle setTransformationMethod.
                     ((TextView) view).setTransformationMethod(transformation);
                 });
@@ -124,6 +126,7 @@
                     (TextView) view,
                     () -> {
                         mIsShowingTranslation = false;
+                        mAnimationRunning = false;
                         ((TextView) view).setTransformationMethod(transformation);
                     });
             if (!TextUtils.isEmpty(mContentDescription)) {
@@ -162,6 +165,13 @@
         return mIsShowingTranslation;
     }
 
+    /**
+     * Returns whether the view is running animation to show or hide the translation.
+     */
+    public boolean isAnimationRunning() {
+        return mAnimationRunning;
+    }
+
     @Override
     public void enableContentPadding() {
         mIsTextPaddingEnabled = true;
@@ -230,6 +240,7 @@
             mAnimator.end();
             // Note: mAnimator is now null; do not use again here.
         }
+        mAnimationRunning = true;
         int fadedOutColor = colorWithAlpha(view.getCurrentTextColor(), 0);
         mAnimator = ValueAnimator.ofArgb(view.getCurrentTextColor(), fadedOutColor);
         mAnimator.addUpdateListener(
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index b331a9e..4ba7ef2 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -15,7 +15,6 @@
  */
 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;
@@ -222,14 +221,7 @@
                         () -> 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);
-
+            final int diff = mConfiguration.diffPublicOnly(newConfig);
             if (shouldReportConfigChange && diff != 0
                     && context instanceof WindowProviderService) {
                 final WindowProviderService windowProviderService = (WindowProviderService) context;
diff --git a/core/java/com/android/internal/inputmethod/EditableInputConnection.java b/core/java/com/android/internal/inputmethod/EditableInputConnection.java
index 410e68b..29bb311 100644
--- a/core/java/com/android/internal/inputmethod/EditableInputConnection.java
+++ b/core/java/com/android/internal/inputmethod/EditableInputConnection.java
@@ -89,7 +89,7 @@
                 // contribution to mTextView's nested batch edit count is zero.
                 mTextView.endBatchEdit();
                 mBatchEditNesting--;
-                return true;
+                return mBatchEditNesting > 0;
             }
         }
         return false;
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index a0a0f32..6ba0279 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -43,7 +43,8 @@
     void removeOnSubscriptionsChangedListener(String pkg,
             IOnSubscriptionsChangedListener callback);
 
-    void listenWithEventList(in int subId, String pkg, String featureId,
+    void listenWithEventList(in boolean renounceFineLocationAccess,
+            in boolean renounceCoarseLocationAccess, in int subId, String pkg, String featureId,
             IPhoneStateListener callback, in int[] events, boolean notifyNow);
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     void notifyCallStateForAllSubs(int state, String incomingNumber);
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index e6deada..78bb53d 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -871,6 +871,10 @@
             if (newGroup == null) {
                 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
                 mAddedGroups.add(newGroup);
+            } else if (newGroup.getParent() != mMessagingLinearLayout) {
+                throw new IllegalStateException(
+                        "group parent was " + newGroup.getParent() + " but expected "
+                                + mMessagingLinearLayout);
             }
             newGroup.setImageDisplayLocation(mIsCollapsed
                     ? IMAGE_DISPLAY_LOCATION_EXTERNAL
diff --git a/core/java/com/android/internal/widget/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java
index f30b844..9e06e33 100644
--- a/core/java/com/android/internal/widget/MessagingGroup.java
+++ b/core/java/com/android/internal/widget/MessagingGroup.java
@@ -32,7 +32,6 @@
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
-import android.util.Pools;
 import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -57,8 +56,8 @@
  */
 @RemoteViews.RemoteView
 public class MessagingGroup extends LinearLayout implements MessagingLinearLayout.MessagingChild {
-    private static Pools.SimplePool<MessagingGroup> sInstancePool
-            = new Pools.SynchronizedPool<>(10);
+    private static final MessagingPool<MessagingGroup> sInstancePool =
+            new MessagingPool<>(10);
 
     /**
      * Images are displayed inline.
@@ -338,7 +337,7 @@
     }
 
     public static void dropCache() {
-        sInstancePool = new Pools.SynchronizedPool<>(10);
+        sInstancePool.clear();
     }
 
     @Override
diff --git a/core/java/com/android/internal/widget/MessagingImageMessage.java b/core/java/com/android/internal/widget/MessagingImageMessage.java
index 27689d4..f7955c3 100644
--- a/core/java/com/android/internal/widget/MessagingImageMessage.java
+++ b/core/java/com/android/internal/widget/MessagingImageMessage.java
@@ -28,7 +28,6 @@
 import android.net.Uri;
 import android.util.AttributeSet;
 import android.util.Log;
-import android.util.Pools;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 import android.widget.ImageView;
@@ -44,8 +43,8 @@
 @RemoteViews.RemoteView
 public class MessagingImageMessage extends ImageView implements MessagingMessage {
     private static final String TAG = "MessagingImageMessage";
-    private static Pools.SimplePool<MessagingImageMessage> sInstancePool
-            = new Pools.SynchronizedPool<>(10);
+    private static final MessagingPool<MessagingImageMessage> sInstancePool =
+            new MessagingPool<>(10);
     private final MessagingMessageState mState = new MessagingMessageState(this);
     private final int mMinImageHeight;
     private final Path mPath = new Path();
@@ -194,7 +193,7 @@
     }
 
     public static void dropCache() {
-        sInstancePool = new Pools.SynchronizedPool<>(10);
+        sInstancePool.clear();
     }
 
     @Override
diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java
index e1602a9..21ca196 100644
--- a/core/java/com/android/internal/widget/MessagingLayout.java
+++ b/core/java/com/android/internal/widget/MessagingLayout.java
@@ -419,6 +419,10 @@
             if (newGroup == null) {
                 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
                 mAddedGroups.add(newGroup);
+            } else if (newGroup.getParent() != mMessagingLinearLayout) {
+                throw new IllegalStateException(
+                        "group parent was " + newGroup.getParent() + " but expected "
+                                + mMessagingLinearLayout);
             }
             newGroup.setImageDisplayLocation(mIsCollapsed
                     ? IMAGE_DISPLAY_LOCATION_EXTERNAL
diff --git a/core/java/com/android/internal/widget/MessagingPool.java b/core/java/com/android/internal/widget/MessagingPool.java
new file mode 100644
index 0000000..9c0fe4b
--- /dev/null
+++ b/core/java/com/android/internal/widget/MessagingPool.java
@@ -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.internal.widget;
+
+import android.util.Log;
+import android.util.Pools;
+import android.view.View;
+
+/**
+ * A trivial wrapper around Pools.SynchronizedPool which allows clearing the pool, as well as
+ * disabling the pool class altogether.
+ * @param <T> the type of object in the pool
+ */
+public class MessagingPool<T extends View> implements Pools.Pool<T> {
+    private static final boolean ENABLED = false;  // disabled to test b/208508846
+    private static final String TAG = "MessagingPool";
+    private final int mMaxPoolSize;
+    private Pools.SynchronizedPool<T> mCurrentPool;
+
+    public MessagingPool(int maxPoolSize) {
+        mMaxPoolSize = maxPoolSize;
+        if (ENABLED) {
+            mCurrentPool = new Pools.SynchronizedPool<>(mMaxPoolSize);
+        }
+    }
+
+    @Override
+    public T acquire() {
+        if (!ENABLED) {
+            return null;
+        }
+        T instance = mCurrentPool.acquire();
+        if (instance.getParent() != null) {
+            Log.wtf(TAG, "acquired " + instance + " with parent " + instance.getParent());
+            return null;
+        }
+        return instance;
+    }
+
+    @Override
+    public boolean release(T instance) {
+        if (instance.getParent() != null) {
+            Log.wtf(TAG, "releasing " + instance + " with parent " + instance.getParent());
+            return false;
+        }
+        if (!ENABLED) {
+            return false;
+        }
+        return mCurrentPool.release(instance);
+    }
+
+    /** Clear the pool */
+    public void clear() {
+        if (ENABLED) {
+            mCurrentPool = new Pools.SynchronizedPool<>(mMaxPoolSize);
+        }
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/MessagingTextMessage.java b/core/java/com/android/internal/widget/MessagingTextMessage.java
index d778c59..19791db 100644
--- a/core/java/com/android/internal/widget/MessagingTextMessage.java
+++ b/core/java/com/android/internal/widget/MessagingTextMessage.java
@@ -24,7 +24,6 @@
 import android.content.Context;
 import android.text.Layout;
 import android.util.AttributeSet;
-import android.util.Pools;
 import android.view.LayoutInflater;
 import android.widget.RemoteViews;
 
@@ -36,8 +35,8 @@
 @RemoteViews.RemoteView
 public class MessagingTextMessage extends ImageFloatingTextView implements MessagingMessage {
 
-    private static Pools.SimplePool<MessagingTextMessage> sInstancePool
-            = new Pools.SynchronizedPool<>(20);
+    private static final MessagingPool<MessagingTextMessage> sInstancePool =
+            new MessagingPool<>(20);
     private final MessagingMessageState mState = new MessagingMessageState(this);
 
     public MessagingTextMessage(@NonNull Context context) {
@@ -92,7 +91,7 @@
     }
 
     public static void dropCache() {
-        sInstancePool = new Pools.SynchronizedPool<>(10);
+        sInstancePool.clear();
     }
 
     @Override
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index 86d7810..8c23b21 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -881,6 +881,12 @@
   return static_cast<jint>(bag->entry_count);
 }
 
+static jint NativeGetParentThemeIdentifier(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid) {
+  ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
+  const auto parentThemeResId = assetmanager->GetParentThemeResourceId(resid);
+  return parentThemeResId.value_or(0);
+}
+
 static jint NativeGetResourceIdentifier(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring name,
                                         jstring def_type, jstring def_package) {
   ScopedUtfChars name_utf8(env, name);
@@ -1464,6 +1470,8 @@
     {"nativeGetResourceIntArray", "(JI)[I", (void*)NativeGetResourceIntArray},
     {"nativeGetResourceArraySize", "(JI)I", (void*)NativeGetResourceArraySize},
     {"nativeGetResourceArray", "(JI[I)I", (void*)NativeGetResourceArray},
+    {"nativeGetParentThemeIdentifier", "(JI)I",
+     (void*)NativeGetParentThemeIdentifier},
 
     // AssetManager resource name/ID methods.
     {"nativeGetResourceIdentifier", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index fb5fded..67d0c52 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -597,6 +597,14 @@
     transaction->setPosition(ctrl, x, y);
 }
 
+static void nativeSetScale(JNIEnv* env, jclass clazz, jlong transactionObj, jlong nativeObject,
+                           jfloat xScale, jfloat yScale) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+
+    SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
+    transaction->setMatrix(ctrl, xScale, 0, 0, yScale);
+}
+
 static void nativeSetGeometry(JNIEnv* env, jclass clazz, jlong transactionObj, jlong nativeObject,
         jobject sourceObj, jobject dstObj, jlong orientation) {
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
@@ -620,9 +628,19 @@
                             jobject bufferObject) {
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
     SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
-    sp<GraphicBuffer> buffer(
-            android_graphics_GraphicBuffer_getNativeGraphicsBuffer(env, bufferObject));
-    transaction->setBuffer(ctrl, buffer);
+    sp<GraphicBuffer> graphicBuffer(GraphicBuffer::fromAHardwareBuffer(
+            android_hardware_HardwareBuffer_getNativeHardwareBuffer(env, bufferObject)));
+    transaction->setBuffer(ctrl, graphicBuffer);
+}
+
+static void nativeSetBufferTransform(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                     jlong nativeObject, jint transform) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+    SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
+    transaction->setTransform(ctrl, transform);
+    bool transformToInverseDisplay = (NATIVE_WINDOW_TRANSFORM_INVERSE_DISPLAY & transform) ==
+            NATIVE_WINDOW_TRANSFORM_INVERSE_DISPLAY;
+    transaction->setTransformToDisplayInverse(ctrl, transformToInverseDisplay);
 }
 
 static void nativeSetColorSpace(JNIEnv* env, jclass clazz, jlong transactionObj, jlong nativeObject,
@@ -740,6 +758,37 @@
     }
 }
 
+static void nativeSetDamageRegion(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                  jlong nativeObject, jobject regionObj) {
+    SurfaceControl* const surfaceControl = reinterpret_cast<SurfaceControl*>(nativeObject);
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+
+    if (regionObj == nullptr) {
+        transaction->setSurfaceDamageRegion(surfaceControl, Region::INVALID_REGION);
+        return;
+    }
+
+    graphics::RegionIterator iterator(env, regionObj);
+    if (!iterator.isValid()) {
+        transaction->setSurfaceDamageRegion(surfaceControl, Region::INVALID_REGION);
+        return;
+    }
+
+    Region region;
+    while (!iterator.isDone()) {
+        ARect rect = iterator.getRect();
+        region.orSelf(static_cast<const Rect&>(rect));
+        iterator.next();
+    }
+
+    if (region.getBounds().isEmpty()) {
+        transaction->setSurfaceDamageRegion(surfaceControl, Region::INVALID_REGION);
+        return;
+    }
+
+    transaction->setSurfaceDamageRegion(surfaceControl, region);
+}
+
 static void nativeSetAlpha(JNIEnv* env, jclass clazz, jlong transactionObj,
         jlong nativeObject, jfloat alpha) {
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
@@ -1905,10 +1954,14 @@
             (void*)nativeSetRelativeLayer },
     {"nativeSetPosition", "(JJFF)V",
             (void*)nativeSetPosition },
+    {"nativeSetScale", "(JJFF)V",
+            (void*)nativeSetScale },
     {"nativeSetSize", "(JJII)V",
             (void*)nativeSetSize },
     {"nativeSetTransparentRegionHint", "(JJLandroid/graphics/Region;)V",
             (void*)nativeSetTransparentRegionHint },
+    { "nativeSetDamageRegion", "(JJLandroid/graphics/Region;)V",
+            (void*)nativeSetDamageRegion },
     {"nativeSetAlpha", "(JJF)V",
             (void*)nativeSetAlpha },
     {"nativeSetColor", "(JJ[F)V",
@@ -2018,8 +2071,9 @@
             (void*)nativeGetDisplayedContentSample },
     {"nativeSetGeometry", "(JJLandroid/graphics/Rect;Landroid/graphics/Rect;J)V",
             (void*)nativeSetGeometry },
-    {"nativeSetBuffer", "(JJLandroid/graphics/GraphicBuffer;)V",
+    {"nativeSetBuffer", "(JJLandroid/hardware/HardwareBuffer;)V",
             (void*)nativeSetBuffer },
+    {"nativeSetBufferTransform", "(JJI)V", (void*) nativeSetBufferTransform},
     {"nativeSetColorSpace", "(JJI)V",
             (void*)nativeSetColorSpace },
     {"nativeSyncInputWindows", "(J)V",
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 7018df2..0894877 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2066,7 +2066,7 @@
     <permission android:name="android.permission.BLUETOOTH_PRIVILEGED"
         android:protectionLevel="signature|privileged" />
 
-    <!-- Control access to email providers exclusively for Bluetooth
+    <!-- @SystemApi Control access to email providers exclusively for Bluetooth
          @hide
     -->
     <permission android:name="android.permission.BLUETOOTH_MAP"
@@ -2791,6 +2791,11 @@
     <permission android:name="android.permission.CREATE_USERS"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi @hide Allows an application to call APIs that allow it to query users on the
+         device. -->
+    <permission android:name="android.permission.QUERY_USERS"
+                android:protectionLevel="signature|role" />
+
     <!-- Allows an application to access data blobs across users. -->
     <permission android:name="android.permission.ACCESS_BLOBS_ACROSS_USERS"
         android:protectionLevel="signature|privileged|development|role" />
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 0a6ef7c..06f347f 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -2584,6 +2584,9 @@
     <declare-styleable name="AndroidManifestProcess" parent="AndroidManifestProcesses">
         <!-- Required name of the process that is allowed -->
         <attr name="process" />
+        <!-- custom Application class name. We use call it "name", not "className", to be
+             consistent with the Application tag. -->
+        <attr name="name" />
         <attr name="gwpAsanMode" />
         <attr name="memtagMode" />
         <attr name="nativeHeapZeroInitialized" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5b7d1b9..ddec469 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2383,7 +2383,7 @@
     <!-- If supported and enabled, are dreams activated when asleep and charging? (by default) -->
     <bool name="config_dreamsActivatedOnSleepByDefault">false</bool>
     <!-- ComponentName of the default dream (Settings.Secure.DEFAULT_SCREENSAVER_COMPONENT) -->
-    <string name="config_dreamsDefaultComponent" translatable="false">com.google.android.deskclock/com.android.deskclock.Screensaver</string>
+    <string name="config_dreamsDefaultComponent" translatable="false">com.android.deskclock/com.android.deskclock.Screensaver</string>
 
     <!-- Are we allowed to dream while not plugged in? -->
     <bool name="config_dreamsEnabledOnBattery">false</bool>
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index bcd794e..32d72b37 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -23,6 +23,7 @@
     ],
 
     aidl: {
+        generate_get_transaction_name: true,
         local_include_dirs: ["aidl"],
     },
 
@@ -46,6 +47,7 @@
         "androidx.test.ext.junit",
         "androidx.test.runner",
         "androidx.test.rules",
+        "kotlin-test",
         "mockito-target-minus-junit4",
         "ub-uiautomator",
         "platform-test-annotations",
diff --git a/core/tests/coretests/src/android/hardware/input/VirtualKeyEventTest.java b/core/tests/coretests/src/android/hardware/input/VirtualKeyEventTest.java
new file mode 100644
index 0000000..37cc9b7
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/input/VirtualKeyEventTest.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 android.hardware.input;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import android.view.KeyEvent;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class VirtualKeyEventTest {
+
+    @Test
+    public void keyEvent_emptyBuilder() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualKeyEvent.Builder().build());
+    }
+
+    @Test
+    public void keyEvent_noKeyCode() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new VirtualKeyEvent.Builder().setAction(VirtualKeyEvent.ACTION_DOWN).build());
+    }
+
+    @Test
+    public void keyEvent_noAction() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new VirtualKeyEvent.Builder().setKeyCode(KeyEvent.KEYCODE_A).build());
+    }
+
+    @Test
+    public void keyEvent_created() {
+        final VirtualKeyEvent event = new VirtualKeyEvent.Builder()
+                .setAction(VirtualKeyEvent.ACTION_DOWN)
+                .setKeyCode(KeyEvent.KEYCODE_A).build();
+        assertWithMessage("Incorrect key code").that(event.getKeyCode()).isEqualTo(
+                KeyEvent.KEYCODE_A);
+        assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo(
+                VirtualKeyEvent.ACTION_DOWN);
+    }
+}
diff --git a/core/tests/coretests/src/android/hardware/input/VirtualMouseButtonEventTest.java b/core/tests/coretests/src/android/hardware/input/VirtualMouseButtonEventTest.java
new file mode 100644
index 0000000..789e0bb
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/input/VirtualMouseButtonEventTest.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 android.hardware.input;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class VirtualMouseButtonEventTest {
+
+    @Test
+    public void buttonEvent_emptyBuilder() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new VirtualMouseButtonEvent.Builder().build());
+    }
+
+    @Test
+    public void buttonEvent_noButtonCode() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualMouseButtonEvent.Builder()
+                .setAction(VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE).build());
+    }
+
+    @Test
+    public void buttonEvent_noAction() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualMouseButtonEvent.Builder()
+                .setButtonCode(VirtualMouseButtonEvent.BUTTON_BACK).build());
+    }
+
+    @Test
+    public void buttonEvent_created() {
+        final VirtualMouseButtonEvent event = new VirtualMouseButtonEvent.Builder()
+                .setAction(VirtualMouseButtonEvent.ACTION_BUTTON_PRESS)
+                .setButtonCode(VirtualMouseButtonEvent.BUTTON_BACK).build();
+        assertWithMessage("Incorrect button code").that(event.getButtonCode()).isEqualTo(
+                VirtualMouseButtonEvent.BUTTON_BACK);
+        assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo(
+                VirtualMouseButtonEvent.ACTION_BUTTON_PRESS);
+    }
+}
diff --git a/core/tests/coretests/src/android/hardware/input/VirtualMouseRelativeEventTest.java b/core/tests/coretests/src/android/hardware/input/VirtualMouseRelativeEventTest.java
new file mode 100644
index 0000000..c050816
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/input/VirtualMouseRelativeEventTest.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 android.hardware.input;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class VirtualMouseRelativeEventTest {
+
+    @Test
+    public void relativeEvent_created() {
+        final VirtualMouseRelativeEvent event = new VirtualMouseRelativeEvent.Builder()
+                .setRelativeX(-5f)
+                .setRelativeY(8f).build();
+        assertWithMessage("Incorrect x value").that(event.getRelativeX()).isEqualTo(-5f);
+        assertWithMessage("Incorrect y value").that(event.getRelativeY()).isEqualTo(8f);
+    }
+}
diff --git a/core/tests/coretests/src/android/hardware/input/VirtualMouseScrollEventTest.java b/core/tests/coretests/src/android/hardware/input/VirtualMouseScrollEventTest.java
new file mode 100644
index 0000000..2259c74
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/input/VirtualMouseScrollEventTest.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.hardware.input;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class VirtualMouseScrollEventTest {
+
+    @Test
+    public void scrollEvent_xOutOfRange() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualMouseScrollEvent.Builder()
+                .setXAxisMovement(1.5f)
+                .setYAxisMovement(1.0f));
+    }
+
+    @Test
+    public void scrollEvent_yOutOfRange() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualMouseScrollEvent.Builder()
+                .setXAxisMovement(0.5f)
+                .setYAxisMovement(1.1f));
+    }
+
+    @Test
+    public void scrollEvent_created() {
+        final VirtualMouseScrollEvent event = new VirtualMouseScrollEvent.Builder()
+                .setXAxisMovement(-1f)
+                .setYAxisMovement(1f).build();
+        assertWithMessage("Incorrect x value").that(event.getXAxisMovement()).isEqualTo(-1f);
+        assertWithMessage("Incorrect y value").that(event.getYAxisMovement()).isEqualTo(1f);
+    }
+}
+
diff --git a/core/tests/coretests/src/android/hardware/input/VirtualTouchEventTest.java b/core/tests/coretests/src/android/hardware/input/VirtualTouchEventTest.java
new file mode 100644
index 0000000..3f504a0
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/input/VirtualTouchEventTest.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 android.hardware.input;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class VirtualTouchEventTest {
+
+    @Test
+    public void touchEvent_emptyBuilder() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder().build());
+    }
+
+    @Test
+    public void touchEvent_noAction() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder()
+                .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER)
+                .setX(0f)
+                .setY(1f)
+                .setPointerId(1)
+                .build());
+    }
+
+    @Test
+    public void touchEvent_noPointerId() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder()
+                .setAction(VirtualTouchEvent.ACTION_DOWN)
+                .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER)
+                .setX(0f)
+                .setY(1f)
+                .build());
+    }
+
+    @Test
+    public void touchEvent_noToolType() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder()
+                .setAction(VirtualTouchEvent.ACTION_DOWN)
+                .setX(0f)
+                .setY(1f)
+                .setPointerId(1)
+                .build());
+    }
+
+    @Test
+    public void touchEvent_noX() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder()
+                .setAction(VirtualTouchEvent.ACTION_DOWN)
+                .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER)
+                .setY(1f)
+                .setPointerId(1)
+                .build());
+    }
+
+
+    @Test
+    public void touchEvent_noY() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder()
+                .setAction(VirtualTouchEvent.ACTION_DOWN)
+                .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER)
+                .setX(0f)
+                .setPointerId(1)
+                .build());
+    }
+
+    @Test
+    public void touchEvent_created() {
+        final VirtualTouchEvent event = new VirtualTouchEvent.Builder()
+                .setAction(VirtualTouchEvent.ACTION_DOWN)
+                .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER)
+                .setX(0f)
+                .setY(1f)
+                .setPointerId(1)
+                .build();
+        assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo(
+                VirtualTouchEvent.ACTION_DOWN);
+        assertWithMessage("Incorrect tool type").that(event.getToolType()).isEqualTo(
+                VirtualTouchEvent.TOOL_TYPE_FINGER);
+        assertWithMessage("Incorrect x").that(event.getX()).isEqualTo(0f);
+        assertWithMessage("Incorrect y").that(event.getY()).isEqualTo(1f);
+        assertWithMessage("Incorrect pointer id").that(event.getPointerId()).isEqualTo(1);
+    }
+
+    @Test
+    public void touchEvent_created_withPressureAndAxis() {
+        final VirtualTouchEvent event = new VirtualTouchEvent.Builder()
+                .setAction(VirtualTouchEvent.ACTION_DOWN)
+                .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER)
+                .setX(0f)
+                .setY(1f)
+                .setPointerId(1)
+                .setPressure(0.5f)
+                .setMajorAxisSize(10f)
+                .build();
+        assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo(
+                VirtualTouchEvent.ACTION_DOWN);
+        assertWithMessage("Incorrect tool type").that(event.getToolType()).isEqualTo(
+                VirtualTouchEvent.TOOL_TYPE_FINGER);
+        assertWithMessage("Incorrect x").that(event.getX()).isEqualTo(0f);
+        assertWithMessage("Incorrect y").that(event.getY()).isEqualTo(1f);
+        assertWithMessage("Incorrect pointer id").that(event.getPointerId()).isEqualTo(1);
+        assertWithMessage("Incorrect pressure").that(event.getPressure()).isEqualTo(0.5f);
+        assertWithMessage("Incorrect major axis size").that(event.getMajorAxisSize()).isEqualTo(
+                10f);
+    }
+
+    @Test
+    public void touchEvent_cancelUsedImproperly() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder()
+                .setAction(VirtualTouchEvent.ACTION_CANCEL)
+                .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER)
+                .setX(0f)
+                .setY(1f)
+                .setPointerId(1)
+                .build());
+    }
+
+    @Test
+    public void touchEvent_palmUsedImproperly() {
+        assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder()
+                .setAction(VirtualTouchEvent.ACTION_MOVE)
+                .setToolType(VirtualTouchEvent.TOOL_TYPE_PALM)
+                .setX(0f)
+                .setY(1f)
+                .setPointerId(1)
+                .build());
+    }
+
+    @Test
+    public void touchEvent_palmAndCancelUsedProperly() {
+        final VirtualTouchEvent event = new VirtualTouchEvent.Builder()
+                .setAction(VirtualTouchEvent.ACTION_CANCEL)
+                .setToolType(VirtualTouchEvent.TOOL_TYPE_PALM)
+                .setX(0f)
+                .setY(1f)
+                .setPointerId(1)
+                .setPressure(0.5f)
+                .setMajorAxisSize(10f)
+                .build();
+        assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo(
+                VirtualTouchEvent.ACTION_CANCEL);
+        assertWithMessage("Incorrect tool type").that(event.getToolType()).isEqualTo(
+                VirtualTouchEvent.TOOL_TYPE_PALM);
+        assertWithMessage("Incorrect x").that(event.getX()).isEqualTo(0f);
+        assertWithMessage("Incorrect y").that(event.getY()).isEqualTo(1f);
+        assertWithMessage("Incorrect pointer id").that(event.getPointerId()).isEqualTo(1);
+        assertWithMessage("Incorrect pressure").that(event.getPressure()).isEqualTo(0.5f);
+        assertWithMessage("Incorrect major axis size").that(event.getMajorAxisSize()).isEqualTo(
+                10f);
+    }
+}
diff --git a/core/tests/coretests/src/android/net/NetworkPolicyTest.kt b/core/tests/coretests/src/android/net/NetworkPolicyTest.kt
new file mode 100644
index 0000000..d936cad
--- /dev/null
+++ b/core/tests/coretests/src/android/net/NetworkPolicyTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.net
+
+import android.text.format.Time.TIMEZONE_UTC
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.ByteArrayInputStream
+import java.io.DataInputStream
+import java.time.ZoneId
+import kotlin.test.assertEquals
+
+private const val TEST_IMSI1 = "TESTIMSI1"
+private const val TEST_SSID1 = "TESTISSID1"
+
+@RunWith(AndroidJUnit4::class)
+class NetworkPolicyTest {
+    @Test
+    fun testTemplateBackupRestore() {
+        assertPolicyBackupRestore(createTestPolicyForTemplate(
+                NetworkTemplate.buildTemplateWifi(TEST_SSID1)))
+        assertPolicyBackupRestore(createTestPolicyForTemplate(
+                NetworkTemplate.buildTemplateMobileAll(TEST_IMSI1)))
+        assertPolicyBackupRestore(createTestPolicyForTemplate(
+                NetworkTemplate.buildTemplateCarrierMetered(TEST_IMSI1)))
+    }
+
+    private fun createTestPolicyForTemplate(template: NetworkTemplate): NetworkPolicy {
+        return NetworkPolicy(template, NetworkPolicy.buildRule(5, ZoneId.of(TIMEZONE_UTC)),
+                NetworkPolicy.WARNING_DISABLED, NetworkPolicy.LIMIT_DISABLED,
+                NetworkPolicy.SNOOZE_NEVER, NetworkPolicy.SNOOZE_NEVER, NetworkPolicy.SNOOZE_NEVER,
+                /*metered*/ false, /*inferred*/ true)
+    }
+
+    private fun assertPolicyBackupRestore(policy: NetworkPolicy) {
+        val bytes = policy.bytesForBackup
+        val stream = DataInputStream(ByteArrayInputStream(bytes))
+        val restored = NetworkPolicy.getNetworkPolicyFromBackup(stream)
+        assertEquals(policy, restored)
+    }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/os/AidlTest.java b/core/tests/coretests/src/android/os/AidlTest.java
index 4c51415..5f54b09 100644
--- a/core/tests/coretests/src/android/os/AidlTest.java
+++ b/core/tests/coretests/src/android/os/AidlTest.java
@@ -27,11 +27,12 @@
 public class AidlTest extends TestCase {
 
     private IAidlTest mRemote;
+    private AidlObject mLocal;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        AidlObject mLocal = new AidlObject();
+        mLocal = new AidlObject();
         mRemote = IAidlTest.Stub.asInterface(mLocal);
     }
 
@@ -417,5 +418,24 @@
         }
         assertEquals(good, true);
     }
+
+    @SmallTest
+    public void testGetTransactionName() throws Exception {
+        assertEquals(15, mLocal.getMaxTransactionId());
+
+        assertEquals("booleanArray",
+                mLocal.getTransactionName(IAidlTest.Stub.TRANSACTION_booleanArray));
+        assertEquals("voidSecurityException",
+                mLocal.getTransactionName(IAidlTest.Stub.TRANSACTION_voidSecurityException));
+        assertEquals("parcelableIn",
+                mLocal.getTransactionName(IAidlTest.Stub.TRANSACTION_parcelableIn));
+
+        assertEquals("IAidlTest:booleanArray",
+                mLocal.getTransactionTraceName(IAidlTest.Stub.TRANSACTION_booleanArray));
+        assertEquals("IAidlTest:voidSecurityException",
+                mLocal.getTransactionTraceName(IAidlTest.Stub.TRANSACTION_voidSecurityException));
+        assertEquals("IAidlTest:parcelableIn",
+                mLocal.getTransactionTraceName(IAidlTest.Stub.TRANSACTION_parcelableIn));
+    }
 }
 
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index 756425e..0b8dc3f 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -180,6 +180,7 @@
     <assign-permission name="android.permission.WATCH_APPOPS" uid="cameraserver" />
     <assign-permission name="android.permission.MANAGE_APP_OPS_MODES" uid="cameraserver" />
     <assign-permission name="android.permission.OBSERVE_SENSOR_PRIVACY" uid="cameraserver" />
+    <assign-permission name="android.permission.REAL_GET_TASKS" uid="cameraserver" />
 
     <assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="graphics" />
 
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 81db63e..624940b 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -394,6 +394,8 @@
         <!-- Permissions required for Incremental CTS tests -->
         <permission name="com.android.permission.USE_INSTALLER_V2"/>
         <permission name="android.permission.LOADER_USAGE_STATS"/>
+        <!-- Permissions required for Package Verifier tests -->
+        <permission name="android.permission.PACKAGE_VERIFICATION_AGENT" />
         <!-- Permission required to test system only camera devices. -->
         <permission name="android.permission.SYSTEM_CAMERA" />
         <!-- Permission required to test ExplicitHealthCheckServiceImpl. -->
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index af19bd0..b8e8b01 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -295,11 +295,12 @@
             @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity,
             @NonNull TaskFragmentContainer secondaryContainer,
             @NonNull SplitRule splitRule) {
+        SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity,
+                secondaryContainer, splitRule);
+        // Remove container later to prevent pinning escaping toast showing in lock task mode.
         if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) {
             removeExistingSecondaryContainers(wct, primaryContainer);
         }
-        SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity,
-                secondaryContainer, splitRule);
         mSplitContainers.add(splitContainer);
     }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 81be21c..ade5731 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -112,8 +112,7 @@
         secondaryContainer.setLastRequestedBounds(secondaryRectBounds);
 
         // Set adjacent to each other so that the containers below will be invisible.
-        setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
-                secondaryContainer.getTaskFragmentToken(), rule);
+        setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
 
         mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
 
@@ -149,8 +148,7 @@
                 secondaryActivity, secondaryRectBounds, primaryContainer);
 
         // Set adjacent to each other so that the containers below will be invisible.
-        setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
-                secondaryContainer.getTaskFragmentToken(), rule);
+        setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
 
         mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
 
@@ -269,8 +267,22 @@
         final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
         resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds);
 
-        setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
-                secondaryContainer.getTaskFragmentToken(), rule);
+        setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
+    }
+
+    private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer primaryContainer,
+            @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule) {
+        final Rect parentBounds = getParentContainerBounds(primaryContainer);
+        // Clear adjacent TaskFragments if the container is shown in fullscreen, or the
+        // secondaryContainer could not be finished.
+        if (!shouldShowSideBySide(parentBounds, splitRule)) {
+            setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
+                    null /* secondary */, null /* splitRule */);
+        } else {
+            setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
+                    secondaryContainer.getTaskFragmentToken(), splitRule);
+        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_hint_bubble.xml b/libs/WindowManager/Shell/res/drawable/compat_hint_bubble.xml
similarity index 85%
rename from libs/WindowManager/Shell/res/drawable/size_compat_hint_bubble.xml
rename to libs/WindowManager/Shell/res/drawable/compat_hint_bubble.xml
index 22cd384..26848b1 100644
--- a/libs/WindowManager/Shell/res/drawable/size_compat_hint_bubble.xml
+++ b/libs/WindowManager/Shell/res/drawable/compat_hint_bubble.xml
@@ -16,6 +16,6 @@
   -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
-    <solid android:color="@color/size_compat_background"/>
-    <corners android:radius="@dimen/size_compat_hint_corner_radius"/>
+    <solid android:color="@color/compat_controls_background"/>
+    <corners android:radius="@dimen/compat_hint_corner_radius"/>
 </shape>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml b/libs/WindowManager/Shell/res/drawable/compat_hint_point.xml
similarity index 88%
rename from libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml
rename to libs/WindowManager/Shell/res/drawable/compat_hint_point.xml
index af9063a..0e0ca37 100644
--- a/libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml
+++ b/libs/WindowManager/Shell/res/drawable/compat_hint_point.xml
@@ -15,11 +15,11 @@
   ~ limitations under the License.
   -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="@dimen/size_compat_hint_point_width"
+        android:width="@dimen/compat_hint_point_width"
         android:height="8dp"
         android:viewportWidth="10"
         android:viewportHeight="8">
     <path
-        android:fillColor="@color/size_compat_background"
+        android:fillColor="@color/compat_controls_background"
         android:pathData="M10,0 l-4.1875,6.6875 a1,1 0 0,1 -1.625,0 l-4.1875,-6.6875z"/>
 </vector>
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
index 18caa35..ab74e43 100644
--- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
+++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
@@ -20,16 +20,16 @@
         android:viewportWidth="48"
         android:viewportHeight="48">
     <path
-        android:fillColor="@color/size_compat_background"
+        android:fillColor="@color/compat_controls_background"
         android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0" />
     <group
         android:translateX="12"
         android:translateY="12">
         <path
-            android:fillColor="@color/size_compat_text"
+            android:fillColor="@color/compat_controls_text"
             android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/>
         <path
-            android:fillColor="@color/size_compat_text"
+            android:fillColor="@color/compat_controls_text"
             android:pathData="M20,13c0,-4.42 -3.58,-8 -8,-8c-0.06,0 -0.12,0.01 -0.18,0.01v0l1.09,-1.09L11.5,2.5L8,6l3.5,3.5l1.41,-1.41l-1.08,-1.08C11.89,7.01 11.95,7 12,7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02C16.95,20.44 20,17.08 20,13z"/>
     </group>
 </vector>
diff --git a/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml
new file mode 100644
index 0000000..c04e258e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:clipToPadding="false"
+    android:paddingEnd="@dimen/compat_hint_padding_end"
+    android:paddingBottom="5dp"
+    android:clickable="true">
+
+    <TextView
+        android:id="@+id/compat_mode_hint_text"
+        android:layout_width="188dp"
+        android:layout_height="wrap_content"
+        android:lineSpacingExtra="4sp"
+        android:background="@drawable/compat_hint_bubble"
+        android:padding="16dp"
+        android:textAlignment="viewStart"
+        android:textColor="@color/compat_controls_text"
+        android:textSize="14sp"/>
+
+    <ImageView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="end"
+        android:src="@drawable/compat_hint_point"
+        android:paddingHorizontal="@dimen/compat_hint_corner_radius"
+        android:contentDescription="@null"/>
+
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/size_compat_ui.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
similarity index 82%
rename from libs/WindowManager/Shell/res/layout/size_compat_ui.xml
rename to libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
index 82ebee2..6f946b2 100644
--- a/libs/WindowManager/Shell/res/layout/size_compat_ui.xml
+++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
@@ -14,10 +14,15 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<com.android.wm.shell.sizecompatui.SizeCompatRestartButton
+<com.android.wm.shell.compatui.CompatUILayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:gravity="bottom|end">
+
+    <include android:id="@+id/size_compat_hint"
+         layout="@layout/compat_mode_hint"/>
 
     <FrameLayout
         android:layout_width="@dimen/size_compat_button_width"
@@ -36,4 +41,4 @@
 
     </FrameLayout>
 
-</com.android.wm.shell.sizecompatui.SizeCompatRestartButton>
+</com.android.wm.shell.compatui.CompatUILayout>
diff --git a/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml
deleted file mode 100644
index d0e7c42..0000000
--- a/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<com.android.wm.shell.sizecompatui.SizeCompatHintPopup
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-
-    <FrameLayout
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:clipToPadding="false"
-        android:paddingBottom="5dp">
-
-        <LinearLayout
-            android:id="@+id/size_compat_hint_popup"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:orientation="vertical"
-            android:clickable="true">
-
-            <TextView
-                android:layout_width="188dp"
-                android:layout_height="wrap_content"
-                android:lineSpacingExtra="4sp"
-                android:background="@drawable/size_compat_hint_bubble"
-                android:padding="16dp"
-                android:text="@string/restart_button_description"
-                android:textAlignment="viewStart"
-                android:textColor="@color/size_compat_text"
-                android:textSize="14sp"/>
-
-            <ImageView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="end"
-                android:src="@drawable/size_compat_hint_point"
-                android:paddingHorizontal="@dimen/size_compat_hint_corner_radius"
-                android:contentDescription="@null"/>
-
-        </LinearLayout>
-
-    </FrameLayout>
-
-</com.android.wm.shell.sizecompatui.SizeCompatHintPopup>
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index 23a2172..cf596f7 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -30,9 +30,9 @@
     <color name="bubbles_dark">@color/GM2_grey_800</color>
     <color name="bubbles_icon_tint">@color/GM2_grey_700</color>
 
-    <!-- Size Compat Restart Button -->
-    <color name="size_compat_background">@android:color/system_neutral1_800</color>
-    <color name="size_compat_text">@android:color/system_neutral1_50</color>
+    <!-- Compat controls UI -->
+    <color name="compat_controls_background">@android:color/system_neutral1_800</color>
+    <color name="compat_controls_text">@android:color/system_neutral1_50</color>
 
     <!-- GM2 colors -->
     <color name="GM2_grey_200">#E8EAED</color>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 9e77578..18e91f4 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -206,11 +206,15 @@
     <!-- The height of the size compat restart button including padding. -->
     <dimen name="size_compat_button_height">64dp</dimen>
 
-    <!-- The radius of the corners of the size compat hint bubble. -->
-    <dimen name="size_compat_hint_corner_radius">28dp</dimen>
+    <!-- The radius of the corners of the compat hint bubble. -->
+    <dimen name="compat_hint_corner_radius">28dp</dimen>
 
-    <!-- The width of the size compat hint point. -->
-    <dimen name="size_compat_hint_point_width">10dp</dimen>
+    <!-- The width of the compat hint point. -->
+    <dimen name="compat_hint_point_width">10dp</dimen>
+
+    <!-- The end padding for the compat hint. Computed as (size_compat_button_width / 2
+         - compat_hint_corner_radius - compat_hint_point_width /2). -->
+    <dimen name="compat_hint_padding_end">7dp</dimen>
 
     <!-- The width of the brand image on staring surface. -->
     <dimen name="starting_surface_brand_image_width">200dp</dimen>
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 8e98b82..8b3a356 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -52,8 +52,8 @@
 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.compatui.CompatUIController;
 import com.android.wm.shell.recents.RecentTasksController;
-import com.android.wm.shell.sizecompatui.SizeCompatUIController;
 import com.android.wm.shell.startingsurface.StartingWindowController;
 
 import java.io.PrintWriter;
@@ -69,7 +69,7 @@
  * TODO(b/167582004): may consider consolidating this class and TaskOrganizer
  */
 public class ShellTaskOrganizer extends TaskOrganizer implements
-        SizeCompatUIController.SizeCompatUICallback {
+        CompatUIController.CompatUICallback {
 
     // Intentionally using negative numbers here so the positive numbers can be used
     // for task id specific listeners that will be added later.
@@ -98,9 +98,9 @@
         default void onTaskInfoChanged(RunningTaskInfo taskInfo) {}
         default void onTaskVanished(RunningTaskInfo taskInfo) {}
         default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {}
-        /** Whether this task listener supports size compat UI. */
-        default boolean supportSizeCompatUI() {
-            // All TaskListeners should support size compat except PIP.
+        /** Whether this task listener supports  compat UI. */
+        default boolean supportCompatUI() {
+            // All TaskListeners should support compat UI except PIP.
             return true;
         }
         /** Attaches the a child window surface to the task surface. */
@@ -159,11 +159,11 @@
     private StartingWindowController mStartingWindow;
 
     /**
-     * In charge of showing size compat UI. Can be {@code null} if device doesn't support size
+     * In charge of showing compat UI. Can be {@code null} if device doesn't support size
      * compat.
      */
     @Nullable
-    private final SizeCompatUIController mSizeCompatUI;
+    private final CompatUIController mCompatUI;
 
     @Nullable
     private final Optional<RecentTasksController> mRecentTasks;
@@ -172,32 +172,32 @@
     private RunningTaskInfo mLastFocusedTaskInfo;
 
     public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) {
-        this(null /* taskOrganizerController */, mainExecutor, context, null /* sizeCompatUI */,
+        this(null /* taskOrganizerController */, mainExecutor, context, null /* compatUI */,
                 Optional.empty() /* recentTasksController */);
     }
 
     public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
-            SizeCompatUIController sizeCompatUI) {
-        this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI,
+            CompatUIController compatUI) {
+        this(null /* taskOrganizerController */, mainExecutor, context, compatUI,
                 Optional.empty() /* recentTasksController */);
     }
 
     public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
-            SizeCompatUIController sizeCompatUI,
+            CompatUIController compatUI,
             Optional<RecentTasksController> recentTasks) {
-        this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI,
+        this(null /* taskOrganizerController */, mainExecutor, context, compatUI,
                 recentTasks);
     }
 
     @VisibleForTesting
     ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, ShellExecutor mainExecutor,
-            Context context, @Nullable SizeCompatUIController sizeCompatUI,
+            Context context, @Nullable CompatUIController compatUI,
             Optional<RecentTasksController> recentTasks) {
         super(taskOrganizerController, mainExecutor);
-        mSizeCompatUI = sizeCompatUI;
+        mCompatUI = compatUI;
         mRecentTasks = recentTasks;
-        if (sizeCompatUI != null) {
-            sizeCompatUI.setSizeCompatUICallback(this);
+        if (compatUI != null) {
+            compatUI.setCompatUICallback(this);
         }
     }
 
@@ -428,7 +428,7 @@
             listener.onTaskAppeared(info.getTaskInfo(), info.getLeash());
         }
         notifyLocusVisibilityIfNeeded(info.getTaskInfo());
-        notifySizeCompatUI(info.getTaskInfo(), listener);
+        notifyCompatUI(info.getTaskInfo(), listener);
     }
 
     /**
@@ -459,8 +459,8 @@
             }
             notifyLocusVisibilityIfNeeded(taskInfo);
             if (updated || !taskInfo.equalsForSizeCompat(data.getTaskInfo())) {
-                // Notify the size compat UI if the listener or task info changed.
-                notifySizeCompatUI(taskInfo, newListener);
+                // Notify the compat UI if the listener or task info changed.
+                notifyCompatUI(taskInfo, newListener);
             }
             if (data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode()) {
                 // Notify the recent tasks when a task changes windowing modes
@@ -504,8 +504,8 @@
                 listener.onTaskVanished(taskInfo);
             }
             notifyLocusVisibilityIfNeeded(taskInfo);
-            // Pass null for listener to remove the size compat UI on this task if there is any.
-            notifySizeCompatUI(taskInfo, null /* taskListener */);
+            // Pass null for listener to remove the compat UI on this task if there is any.
+            notifyCompatUI(taskInfo, null /* taskListener */);
             // Notify the recent tasks that a task has been removed
             mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskRemoved(taskInfo));
         }
@@ -618,28 +618,28 @@
     }
 
     /**
-     * Notifies {@link SizeCompatUIController} about the size compat info changed on the give Task
+     * Notifies {@link CompatUIController} about the compat info changed on the give Task
      * to update the UI accordingly.
      *
      * @param taskInfo the new Task info
      * @param taskListener listener to handle the Task Surface placement. {@code null} if task is
      *                     vanished.
      */
-    private void notifySizeCompatUI(RunningTaskInfo taskInfo, @Nullable TaskListener taskListener) {
-        if (mSizeCompatUI == null) {
+    private void notifyCompatUI(RunningTaskInfo taskInfo, @Nullable TaskListener taskListener) {
+        if (mCompatUI == null) {
             return;
         }
 
-        // The task is vanished or doesn't support size compat UI, notify to remove size compat UI
+        // The task is vanished or doesn't support compat UI, notify to remove compat UI
         // on this Task if there is any.
-        if (taskListener == null || !taskListener.supportSizeCompatUI()
+        if (taskListener == null || !taskListener.supportCompatUI()
                 || !taskInfo.topActivityInSizeCompat || !taskInfo.isVisible) {
-            mSizeCompatUI.onSizeCompatInfoChanged(taskInfo.displayId, taskInfo.taskId,
+            mCompatUI.onCompatInfoChanged(taskInfo.displayId, taskInfo.taskId,
                     null /* taskConfig */, null /* taskListener */);
             return;
         }
 
-        mSizeCompatUI.onSizeCompatInfoChanged(taskInfo.displayId, taskInfo.taskId,
+        mCompatUI.onCompatInfoChanged(taskInfo.displayId, taskInfo.taskId,
                 taskInfo.configuration, taskListener);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
index d92e2cc..686fbbf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
@@ -15,23 +15,22 @@
  */
 package com.android.wm.shell.bubbles;
 
-import static android.graphics.Paint.ANTI_ALIAS_FLAG;
-import static android.graphics.Paint.DITHER_FLAG;
-import static android.graphics.Paint.FILTER_BITMAP_FLAG;
-
+import android.annotation.DrawableRes;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Outline;
-import android.graphics.Paint;
-import android.graphics.PaintFlagsDrawFilter;
 import android.graphics.Path;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.util.PathParser;
+import android.view.Gravity;
 import android.view.View;
 import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
 import android.widget.ImageView;
 
 import com.android.launcher3.icons.DotRenderer;
@@ -47,7 +46,7 @@
  * Badge = the icon associated with the app that created this bubble, this will show work profile
  * badge if appropriate.
  */
-public class BadgedImageView extends ImageView {
+public class BadgedImageView extends FrameLayout {
 
     /** Same value as Launcher3 dot code */
     public static final float WHITE_SCRIM_ALPHA = 0.54f;
@@ -74,6 +73,9 @@
     private final EnumSet<SuppressionFlag> mDotSuppressionFlags =
             EnumSet.of(SuppressionFlag.FLYOUT_VISIBLE);
 
+    private final ImageView mBubbleIcon;
+    private final ImageView mAppIcon;
+
     private float mDotScale = 0f;
     private float mAnimatingToDotScale = 0f;
     private boolean mDotIsAnimating = false;
@@ -86,7 +88,6 @@
     private DotRenderer.DrawParams mDrawParams;
     private int mDotColor;
 
-    private Paint mPaint = new Paint(ANTI_ALIAS_FLAG);
     private Rect mTempBounds = new Rect();
 
     public BadgedImageView(Context context) {
@@ -104,6 +105,17 @@
     public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
+
+        mBubbleIcon = new ImageView(context);
+        addView(mBubbleIcon);
+        mAppIcon = new ImageView(context);
+        addView(mAppIcon);
+
+        final TypedArray ta = mContext.obtainStyledAttributes(attrs, new int[]{android.R.attr.src},
+                defStyleAttr, defStyleRes);
+        mBubbleIcon.setImageResource(ta.getResourceId(0, 0));
+        ta.recycle();
+
         mDrawParams = new DotRenderer.DrawParams();
 
         setFocusable(true);
@@ -135,7 +147,6 @@
     public void showDotAndBadge(boolean onLeft) {
         removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK);
         animateDotBadgePositions(onLeft);
-
     }
 
     public void hideDotAndBadge(boolean onLeft) {
@@ -149,6 +160,7 @@
      */
     public void setRenderedBubble(BubbleViewProvider bubble) {
         mBubble = bubble;
+        mBubbleIcon.setImageBitmap(bubble.getBubbleIcon());
         if (mDotSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK)) {
             hideBadge();
         } else {
@@ -176,6 +188,20 @@
         mDotRenderer.draw(canvas, mDrawParams);
     }
 
+    /**
+     * Set drawable resource shown as the icon
+     */
+    public void setIconImageResource(@DrawableRes int drawable) {
+        mBubbleIcon.setImageResource(drawable);
+    }
+
+    /**
+     * Get icon drawable
+     */
+    public Drawable getIconDrawable() {
+        return mBubbleIcon.getDrawable();
+    }
+
     /** Adds a dot suppression flag, updating dot visibility if needed. */
     void addDotSuppressionFlag(SuppressionFlag flag) {
         if (mDotSuppressionFlags.add(flag)) {
@@ -279,7 +305,6 @@
         showBadge();
     }
 
-
     /** Whether to draw the dot in onDraw(). */
     private boolean shouldDrawDot() {
         // Always render the dot if it's animating, since it could be animating out. Otherwise, show
@@ -325,29 +350,28 @@
     void showBadge() {
         Bitmap badge = mBubble.getAppBadge();
         if (badge == null) {
-            setImageBitmap(mBubble.getBubbleIcon());
+            mAppIcon.setVisibility(GONE);
             return;
         }
-        Canvas bubbleCanvas = new Canvas();
-        Bitmap noBadgeBubble = mBubble.getBubbleIcon();
-        Bitmap bubble = noBadgeBubble.copy(noBadgeBubble.getConfig(), /* isMutable */ true);
 
-        bubbleCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
-        bubbleCanvas.setBitmap(bubble);
-        final int bubbleSize = bubble.getWidth();
+        final int bubbleSize = mBubble.getBubbleIcon().getWidth();
         final int badgeSize = (int) (ICON_BADGE_SCALE * bubbleSize);
-        Rect dest = new Rect();
+
+        FrameLayout.LayoutParams appIconParams = (LayoutParams) mAppIcon.getLayoutParams();
+        appIconParams.height = badgeSize;
+        appIconParams.width = badgeSize;
         if (mOnLeft) {
-            dest.set(0, bubbleSize - badgeSize, badgeSize, bubbleSize);
+            appIconParams.gravity = Gravity.BOTTOM | Gravity.LEFT;
         } else {
-            dest.set(bubbleSize - badgeSize, bubbleSize - badgeSize, bubbleSize, bubbleSize);
+            appIconParams.gravity = Gravity.BOTTOM | Gravity.RIGHT;
         }
-        bubbleCanvas.drawBitmap(badge, null /* src */, dest, mPaint);
-        bubbleCanvas.setBitmap(null);
-        setImageBitmap(bubble);
+        mAppIcon.setLayoutParams(appIconParams);
+
+        mAppIcon.setImageBitmap(badge);
+        mAppIcon.setVisibility(VISIBLE);
     }
 
     void hideBadge() {
-        setImageBitmap(mBubble.getBubbleIcon());
+        mAppIcon.setVisibility(GONE);
     }
 }
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 519a856..51b7eaa 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
@@ -328,6 +328,7 @@
         if (prevBubble == null) {
             // Create a new bubble
             bubble.setSuppressFlyout(suppressFlyout);
+            bubble.markUpdatedAt(mTimeSource.currentTimeMillis());
             doAdd(bubble);
             trim();
         } else {
@@ -561,17 +562,10 @@
         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
-                // if we're already expanded or always showing.
-                setShowingOverflow(true);
-                setSelectedBubbleInternal(mOverflow);
-            } else {
-                setExpandedInternal(false);
-                // Don't use setSelectedBubbleInternal because we don't want to trigger an
-                // applyUpdate
-                mSelectedBubble = null;
-            }
+            setExpandedInternal(false);
+            // Don't use setSelectedBubbleInternal because we don't want to trigger an
+            // applyUpdate
+            mSelectedBubble = null;
         }
         if (indexToRemove < mBubbles.size() - 1) {
             // Removing anything but the last bubble means positions will change.
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 5161092..a175929 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
@@ -66,7 +66,7 @@
         updateResources()
         getExpandedView()?.applyThemeAttrs()
         // Apply inset and new style to fresh icon drawable.
-        getIconView()?.setImageResource(R.drawable.bubble_ic_overflow_button)
+        getIconView()?.setIconImageResource(R.drawable.bubble_ic_overflow_button)
         updateBtnTheme()
     }
 
@@ -89,19 +89,19 @@
         dotColor = colorAccent
 
         val shapeColor = res.getColor(android.R.color.system_accent1_1000)
-        overflowBtn?.drawable?.setTint(shapeColor)
+        overflowBtn?.iconDrawable?.setTint(shapeColor)
 
         val iconFactory = BubbleIconFactory(context)
 
         // Update bitmap
-        val fg = InsetDrawable(overflowBtn?.drawable, overflowIconInset)
+        val fg = InsetDrawable(overflowBtn?.iconDrawable, overflowIconInset)
         bitmap = iconFactory.createBadgedIconBitmap(
                 AdaptiveIconDrawable(ColorDrawable(colorAccent), fg)).icon
 
         // Update dot path
         dotPath = PathParser.createPathFromPathData(
             res.getString(com.android.internal.R.string.config_icon_mask))
-        val scale = iconFactory.normalizer.getScale(iconView!!.drawable,
+        val scale = iconFactory.normalizer.getScale(iconView!!.iconDrawable,
             null /* outBounds */, null /* path */, null /* outMaskShape */)
         val radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f
         val matrix = Matrix()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java
index eea6e3c..c4bd73b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.common;
 
-import android.graphics.GraphicBuffer;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.view.SurfaceControl;
@@ -63,8 +62,6 @@
             if (buffer == null || buffer.getHardwareBuffer() == null) {
                 return;
             }
-            final GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(
-                buffer.getHardwareBuffer());
             mScreenshot = new SurfaceControl.Builder()
                 .setName("ScreenshotUtils screenshot")
                 .setFormat(PixelFormat.TRANSLUCENT)
@@ -73,7 +70,7 @@
                 .setBLASTLayer()
                 .build();
 
-            mTransaction.setBuffer(mScreenshot, graphicBuffer);
+            mTransaction.setBuffer(mScreenshot, buffer.getHardwareBuffer());
             mTransaction.setColorSpace(mScreenshot, buffer.getColorSpace());
             mTransaction.reparent(mScreenshot, mSurfaceControl);
             mTransaction.setLayer(mScreenshot, mLayer);
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 040fffae..b8ac87f 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
@@ -93,7 +93,7 @@
     private final InsetsState mInsetsState = new InsetsState();
 
     private Context mContext;
-    private DividerSnapAlgorithm mDividerSnapAlgorithm;
+    @VisibleForTesting DividerSnapAlgorithm mDividerSnapAlgorithm;
     private WindowContainerToken mWinToken1;
     private WindowContainerToken mWinToken2;
     private int mDividePosition;
@@ -294,20 +294,22 @@
         mSplitLayoutHandler.onLayoutSizeChanging(this);
     }
 
-    void setDividePosition(int position) {
+    void setDividePosition(int position, boolean applyLayoutChange) {
         mDividePosition = position;
         updateBounds(mDividePosition);
-        mSplitLayoutHandler.onLayoutSizeChanged(this);
+        if (applyLayoutChange) {
+            mSplitLayoutHandler.onLayoutSizeChanged(this);
+        }
     }
 
-    /** Sets divide position base on the ratio within root bounds. */
+    /** Updates divide position and split bounds base on the ratio within root bounds. */
     public void setDivideRatio(float ratio) {
         final int position = isLandscape()
                 ? mRootBounds.left + (int) (mRootBounds.width() * ratio)
                 : mRootBounds.top + (int) (mRootBounds.height() * ratio);
-        DividerSnapAlgorithm.SnapTarget snapTarget =
+        final DividerSnapAlgorithm.SnapTarget snapTarget =
                 mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(position);
-        setDividePosition(snapTarget.position);
+        setDividePosition(snapTarget.position, false /* applyLayoutChange */);
     }
 
     /** Resets divider position. */
@@ -336,7 +338,7 @@
                 break;
             default:
                 flingDividePosition(currentPosition, snapTarget.position,
-                        () -> setDividePosition(snapTarget.position));
+                        () -> setDividePosition(snapTarget.position, true /* applyLayoutChange */));
                 break;
         }
     }
@@ -389,7 +391,7 @@
 
             @Override
             public void onAnimationCancel(Animator animation) {
-                setDividePosition(to);
+                setDividePosition(to, true /* applyLayoutChange */);
             }
         });
         animator.start();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUI.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java
similarity index 86%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUI.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java
index a703114..99dbfe0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUI.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java
@@ -14,17 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.sizecompatui;
+package com.android.wm.shell.compatui;
 
 import com.android.wm.shell.common.annotations.ExternalThread;
 
 /**
- * Interface to engage size compat UI.
+ * Interface to engage compat UI.
  */
 @ExternalThread
-public interface SizeCompatUI {
+public interface CompatUI {
     /**
-     * Called when the keyguard occluded state changes. Removes all size compat UIs if the
+     * Called when the keyguard occluded state changes. Removes all compat UIs if the
      * keyguard is now occluded.
      * @param occluded indicates if the keyguard is now occluded.
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
similarity index 81%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index e06070a..e0b2387 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.sizecompatui;
+package com.android.wm.shell.compatui;
 
 import android.annotation.Nullable;
 import android.content.Context;
@@ -48,20 +48,20 @@
 
 /**
  * Controls to show/update restart-activity buttons on Tasks based on whether the foreground
- * activities are in size compatibility mode.
+ * activities are in compatibility mode.
  */
-public class SizeCompatUIController implements OnDisplaysChangedListener,
+public class CompatUIController implements OnDisplaysChangedListener,
         DisplayImeController.ImePositionProcessor {
 
     /** Callback for size compat UI interaction. */
-    public interface SizeCompatUICallback {
+    public interface CompatUICallback {
         /** 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);
     }
 
-    private static final String TAG = "SizeCompatUIController";
+    private static final String TAG = "CompatUIController";
 
     /** Whether the IME is shown on display id. */
     private final Set<Integer> mDisplaysWithIme = new ArraySet<>(1);
@@ -71,7 +71,7 @@
             new SparseArray<>(0);
 
     /** The showing UIs by task id. */
-    private final SparseArray<SizeCompatUILayout> mActiveLayouts = new SparseArray<>(0);
+    private final SparseArray<CompatUIWindowManager> mActiveLayouts = new SparseArray<>(0);
 
     /** Avoid creating display context frequently for non-default display. */
     private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0);
@@ -82,17 +82,17 @@
     private final DisplayImeController mImeController;
     private final SyncTransactionQueue mSyncQueue;
     private final ShellExecutor mMainExecutor;
-    private final SizeCompatUIImpl mImpl = new SizeCompatUIImpl();
+    private final CompatUIImpl mImpl = new CompatUIImpl();
 
-    private SizeCompatUICallback mCallback;
+    private CompatUICallback mCallback;
 
     /** Only show once automatically in the process life. */
     private boolean mHasShownHint;
-    /** Indicates if the keyguard is currently occluded, in which case size compat UIs shouldn't
+    /** Indicates if the keyguard is currently occluded, in which case compat UIs shouldn't
      * be shown. */
     private boolean mKeyguardOccluded;
 
-    public SizeCompatUIController(Context context,
+    public CompatUIController(Context context,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
             DisplayImeController imeController,
@@ -108,35 +108,36 @@
         mImeController.addPositionProcessor(this);
     }
 
-    public SizeCompatUI asSizeCompatUI() {
+    /** Returns implementation of {@link CompatUI}. */
+    public CompatUI asCompatUI() {
         return mImpl;
     }
 
     /** Sets the callback for UI interactions. */
-    public void setSizeCompatUICallback(SizeCompatUICallback callback) {
+    public void setCompatUICallback(CompatUICallback callback) {
         mCallback = callback;
     }
 
     /**
-     * Called when the Task info changed. Creates and updates the size compat UI if there is an
+     * Called when the Task info changed. Creates and updates the compat UI if there is an
      * activity in size compat, or removes the UI if there is no size compat activity.
      *
      * @param displayId display the task and activity are in.
      * @param taskId task the activity is in.
-     * @param taskConfig task config to place the size compat UI with.
+     * @param taskConfig task config to place the compat UI with.
      * @param taskListener listener to handle the Task Surface placement.
      */
-    public void onSizeCompatInfoChanged(int displayId, int taskId,
+    public void onCompatInfoChanged(int displayId, int taskId,
             @Nullable Configuration taskConfig,
             @Nullable ShellTaskOrganizer.TaskListener taskListener) {
         if (taskConfig == null || taskListener == null) {
-            // Null token means the current foreground activity is not in size compatibility mode.
+            // Null token means the current foreground activity is not in compatibility mode.
             removeLayout(taskId);
         } else if (mActiveLayouts.contains(taskId)) {
             // UI already exists, update the UI layout.
             updateLayout(taskId, taskConfig, taskListener);
         } else {
-            // Create a new size compat UI.
+            // Create a new compat UI.
             createLayout(displayId, taskId, taskConfig, taskListener);
         }
     }
@@ -151,7 +152,7 @@
         mDisplayContextCache.remove(displayId);
         removeOnInsetsChangedListener(displayId);
 
-        // Remove all size compat UIs on the removed display.
+        // Remove all compat UIs on the removed display.
         final List<Integer> toRemoveTaskIds = new ArrayList<>();
         forAllLayoutsOnDisplay(displayId, layout -> toRemoveTaskIds.add(layout.getTaskId()));
         for (int i = toRemoveTaskIds.size() - 1; i >= 0; i--) {
@@ -194,7 +195,7 @@
             mDisplaysWithIme.remove(displayId);
         }
 
-        // Hide the size compat UIs when input method is showing.
+        // Hide the compat UIs when input method is showing.
         forAllLayoutsOnDisplay(displayId,
                 layout -> layout.updateVisibility(showOnDisplay(displayId)));
     }
@@ -202,7 +203,7 @@
     @VisibleForTesting
     void onKeyguardOccludedChanged(boolean occluded) {
         mKeyguardOccluded = occluded;
-        // Hide the size compat UIs when keyguard is occluded.
+        // Hide the compat UIs when keyguard is occluded.
         forAllLayouts(layout -> layout.updateVisibility(showOnDisplay(layout.getDisplayId())));
     }
 
@@ -222,34 +223,34 @@
             return;
         }
 
-        final SizeCompatUILayout layout = createLayout(context, displayId, taskId, taskConfig,
-                taskListener);
-        mActiveLayouts.put(taskId, layout);
-        layout.createSizeCompatButton(showOnDisplay(displayId));
+        final CompatUIWindowManager compatUIWindowManager =
+                createLayout(context, displayId, taskId, taskConfig, taskListener);
+        mActiveLayouts.put(taskId, compatUIWindowManager);
+        compatUIWindowManager.createLayout(showOnDisplay(displayId));
     }
 
     @VisibleForTesting
-    SizeCompatUILayout createLayout(Context context, int displayId, int taskId,
+    CompatUIWindowManager createLayout(Context context, int displayId, int taskId,
             Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener) {
-        final SizeCompatUILayout layout = new SizeCompatUILayout(mSyncQueue, mCallback, context,
-                taskConfig, taskId, taskListener, mDisplayController.getDisplayLayout(displayId),
-                mHasShownHint);
+        final CompatUIWindowManager compatUIWindowManager = new CompatUIWindowManager(context,
+                taskConfig, mSyncQueue, mCallback, taskId, taskListener,
+                mDisplayController.getDisplayLayout(displayId), mHasShownHint);
         // Only show hint for the first time.
         mHasShownHint = true;
-        return layout;
+        return compatUIWindowManager;
     }
 
     private void updateLayout(int taskId, Configuration taskConfig,
             ShellTaskOrganizer.TaskListener taskListener) {
-        final SizeCompatUILayout layout = mActiveLayouts.get(taskId);
+        final CompatUIWindowManager layout = mActiveLayouts.get(taskId);
         if (layout == null) {
             return;
         }
-        layout.updateSizeCompatInfo(taskConfig, taskListener, showOnDisplay(layout.getDisplayId()));
+        layout.updateCompatInfo(taskConfig, taskListener, showOnDisplay(layout.getDisplayId()));
     }
 
     private void removeLayout(int taskId) {
-        final SizeCompatUILayout layout = mActiveLayouts.get(taskId);
+        final CompatUIWindowManager layout = mActiveLayouts.get(taskId);
         if (layout != null) {
             layout.release();
             mActiveLayouts.remove(taskId);
@@ -275,19 +276,19 @@
         return context;
     }
 
-    private void forAllLayoutsOnDisplay(int displayId, Consumer<SizeCompatUILayout> callback) {
+    private void forAllLayoutsOnDisplay(int displayId, Consumer<CompatUIWindowManager> callback) {
         forAllLayouts(layout -> layout.getDisplayId() == displayId, callback);
     }
 
-    private void forAllLayouts(Consumer<SizeCompatUILayout> callback) {
+    private void forAllLayouts(Consumer<CompatUIWindowManager> callback) {
         forAllLayouts(layout -> true, callback);
     }
 
-    private void forAllLayouts(Predicate<SizeCompatUILayout> condition,
-            Consumer<SizeCompatUILayout> callback) {
+    private void forAllLayouts(Predicate<CompatUIWindowManager> condition,
+            Consumer<CompatUIWindowManager> callback) {
         for (int i = 0; i < mActiveLayouts.size(); i++) {
             final int taskId = mActiveLayouts.keyAt(i);
-            final SizeCompatUILayout layout = mActiveLayouts.get(taskId);
+            final CompatUIWindowManager layout = mActiveLayouts.get(taskId);
             if (layout != null && condition.test(layout)) {
                 callback.accept(layout);
             }
@@ -298,11 +299,11 @@
      * The interface for calls from outside the Shell, within the host process.
      */
     @ExternalThread
-    private class SizeCompatUIImpl implements SizeCompatUI {
+    private class CompatUIImpl implements CompatUI {
         @Override
         public void onKeyguardOccludedChanged(boolean occluded) {
             mMainExecutor.execute(() -> {
-                SizeCompatUIController.this.onKeyguardOccludedChanged(occluded);
+                CompatUIController.this.onKeyguardOccludedChanged(occluded);
             });
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
new file mode 100644
index 0000000..ea4f209
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.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.compatui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.wm.shell.R;
+
+/**
+ * Container for compat UI controls.
+ */
+public class CompatUILayout extends LinearLayout {
+
+    private CompatUIWindowManager mWindowManager;
+
+    public CompatUILayout(Context context) {
+        this(context, null);
+    }
+
+    public CompatUILayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public CompatUILayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public CompatUILayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    void inject(CompatUIWindowManager windowManager) {
+        mWindowManager = windowManager;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        // Need to relayout after changes like hiding / showing a hint since they affect size.
+        // Doing this directly in setSizeCompatHintVisibility can result in flaky animation.
+        mWindowManager.relayout();
+    }
+
+    void setSizeCompatHintVisibility(boolean show) {
+        final LinearLayout sizeCompatHint = findViewById(R.id.size_compat_hint);
+        int visibility = show ? View.VISIBLE : View.GONE;
+        if (sizeCompatHint.getVisibility() == visibility) {
+            return;
+        }
+        sizeCompatHint.setVisibility(visibility);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        final ImageButton restartButton = findViewById(R.id.size_compat_restart_button);
+        restartButton.setOnClickListener(view -> mWindowManager.onRestartButtonClicked());
+        restartButton.setOnLongClickListener(view -> {
+            mWindowManager.onRestartButtonLongClicked();
+            return true;
+        });
+
+        final LinearLayout sizeCompatHint = findViewById(R.id.size_compat_hint);
+        ((TextView) sizeCompatHint.findViewById(R.id.compat_mode_hint_text))
+                .setText(R.string.restart_button_description);
+        sizeCompatHint.setOnClickListener(view -> setSizeCompatHintVisibility(/* show= */ false));
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
new file mode 100644
index 0000000..997ad04
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.compatui;
+
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+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.util.Log;
+import android.view.IWindow;
+import android.view.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.SurfaceSession;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+/**
+ * Holds view hierarchy of a root surface and helps to inflate and manage layout for compat
+ * controls.
+ */
+class CompatUIWindowManager extends WindowlessWindowManager {
+
+    private static final String TAG = "CompatUIWindowManager";
+
+    private final SyncTransactionQueue mSyncQueue;
+    private final CompatUIController.CompatUICallback mCallback;
+    private final int mDisplayId;
+    private final int mTaskId;
+    private final Rect mStableBounds;
+
+    private Context mContext;
+    private Configuration mTaskConfig;
+    private ShellTaskOrganizer.TaskListener mTaskListener;
+    private DisplayLayout mDisplayLayout;
+
+    @VisibleForTesting
+    boolean mShouldShowHint;
+
+    @Nullable
+    @VisibleForTesting
+    CompatUILayout mCompatUILayout;
+
+    @Nullable
+    private SurfaceControlViewHost mViewHost;
+    @Nullable
+    private SurfaceControl mLeash;
+
+    CompatUIWindowManager(Context context, Configuration taskConfig,
+            SyncTransactionQueue syncQueue, CompatUIController.CompatUICallback callback,
+            int taskId, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout,
+            boolean hasShownHint) {
+        super(taskConfig, null /* rootSurface */, null /* hostInputToken */);
+        mContext = context;
+        mSyncQueue = syncQueue;
+        mCallback = callback;
+        mTaskConfig = taskConfig;
+        mDisplayId = mContext.getDisplayId();
+        mTaskId = taskId;
+        mTaskListener = taskListener;
+        mDisplayLayout = displayLayout;
+        mShouldShowHint = !hasShownHint;
+        mStableBounds = new Rect();
+        mDisplayLayout.getStableBounds(mStableBounds);
+    }
+
+    @Override
+    public void setConfiguration(Configuration configuration) {
+        super.setConfiguration(configuration);
+        mContext = mContext.createConfigurationContext(configuration);
+    }
+
+    @Override
+    protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+        // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
+        final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+                .setContainerLayer()
+                .setName("CompatUILeash")
+                .setHidden(false)
+                .setCallsite("CompatUIWindowManager#attachToParentSurface");
+        attachToParentSurface(builder);
+        mLeash = builder.build();
+        b.setParent(mLeash);
+    }
+
+    /** Creates the layout for compat controls. */
+    void createLayout(boolean show) {
+        if (!show || mCompatUILayout != null) {
+            // Wait until compat controls should be visible.
+            return;
+        }
+
+        initCompatUi();
+        updateSurfacePosition();
+
+        mCallback.onSizeCompatRestartButtonAppeared(mTaskId);
+    }
+
+    /** Called when compat info changed. */
+    void updateCompatInfo(Configuration taskConfig,
+            ShellTaskOrganizer.TaskListener taskListener, boolean show) {
+        final Configuration prevTaskConfig = mTaskConfig;
+        final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener;
+        mTaskConfig = taskConfig;
+        mTaskListener = taskListener;
+
+        // Update configuration.
+        mContext = mContext.createConfigurationContext(taskConfig);
+        setConfiguration(taskConfig);
+
+        if (mCompatUILayout == null || prevTaskListener != taskListener) {
+            // TaskListener changed, recreate the layout for new surface parent.
+            release();
+            createLayout(show);
+            return;
+        }
+
+        if (!taskConfig.windowConfiguration.getBounds()
+                .equals(prevTaskConfig.windowConfiguration.getBounds())) {
+            // Reposition the UI surfaces.
+            updateSurfacePosition();
+        }
+
+        if (taskConfig.getLayoutDirection() != prevTaskConfig.getLayoutDirection()) {
+            // Update layout for RTL.
+            mCompatUILayout.setLayoutDirection(taskConfig.getLayoutDirection());
+            updateSurfacePosition();
+        }
+    }
+
+    /** Called when the visibility of the UI should change. */
+    void updateVisibility(boolean show) {
+        if (mCompatUILayout == null) {
+            // Layout may not have been created because it was hidden previously.
+            createLayout(show);
+            return;
+        }
+
+        // Hide compat UIs when IME is showing.
+        final int newVisibility = show ? View.VISIBLE : View.GONE;
+        if (mCompatUILayout.getVisibility() != newVisibility) {
+            mCompatUILayout.setVisibility(newVisibility);
+        }
+    }
+
+    /** Called when display layout changed. */
+    void updateDisplayLayout(DisplayLayout displayLayout) {
+        final Rect prevStableBounds = mStableBounds;
+        final Rect curStableBounds = new Rect();
+        displayLayout.getStableBounds(curStableBounds);
+        mDisplayLayout = displayLayout;
+        if (!prevStableBounds.equals(curStableBounds)) {
+            // Stable bounds changed, update UI surface positions.
+            updateSurfacePosition();
+            mStableBounds.set(curStableBounds);
+        }
+    }
+
+    /** Called when it is ready to be placed compat UI surface. */
+    void attachToParentSurface(SurfaceControl.Builder b) {
+        mTaskListener.attachChildSurfaceToTask(mTaskId, b);
+    }
+
+    /** Called when the restart button is clicked. */
+    void onRestartButtonClicked() {
+        mCallback.onSizeCompatRestartButtonClicked(mTaskId);
+    }
+
+    /** Called when the restart button is long clicked. */
+    void onRestartButtonLongClicked() {
+        if (mCompatUILayout == null) {
+            return;
+        }
+        mCompatUILayout.setSizeCompatHintVisibility(/* show= */ true);
+    }
+
+    int getDisplayId() {
+        return mDisplayId;
+    }
+
+    int getTaskId() {
+        return mTaskId;
+    }
+
+    /** Releases the surface control and tears down the view hierarchy. */
+    void release() {
+        mCompatUILayout = null;
+
+        if (mViewHost != null) {
+            mViewHost.release();
+            mViewHost = null;
+        }
+
+        if (mLeash != null) {
+            final SurfaceControl leash = mLeash;
+            mSyncQueue.runInSync(t -> t.remove(leash));
+            mLeash = null;
+        }
+    }
+
+    void relayout() {
+        mViewHost.relayout(getWindowLayoutParams());
+        updateSurfacePosition();
+    }
+
+    @VisibleForTesting
+    void updateSurfacePosition() {
+        if (mCompatUILayout == null || mLeash == null) {
+            return;
+        }
+
+        // Use stable bounds to prevent controls from overlapping with system bars.
+        final Rect taskBounds = mTaskConfig.windowConfiguration.getBounds();
+        final Rect stableBounds = new Rect();
+        mDisplayLayout.getStableBounds(stableBounds);
+        stableBounds.intersect(taskBounds);
+
+        // Position of the button in the container coordinate.
+        final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+                ? stableBounds.left - taskBounds.left
+                : stableBounds.right - taskBounds.left - mCompatUILayout.getMeasuredWidth();
+        final int positionY = stableBounds.bottom - taskBounds.top
+                - mCompatUILayout.getMeasuredHeight();
+
+        updateSurfacePosition(positionX, positionY);
+    }
+
+    private int getLayoutDirection() {
+        return mContext.getResources().getConfiguration().getLayoutDirection();
+    }
+
+    private void updateSurfacePosition(int positionX, int positionY) {
+        mSyncQueue.runInSync(t -> {
+            if (mLeash == null || !mLeash.isValid()) {
+                Log.w(TAG, "The leash has been released.");
+                return;
+            }
+            t.setPosition(mLeash, positionX, positionY);
+            // The compat UI should be the topmost child of the Task in case there can be more
+            // than one children.
+            t.setLayer(mLeash, Integer.MAX_VALUE);
+        });
+    }
+
+    /** Inflates {@link CompatUILayout} on to the root surface. */
+    private void initCompatUi() {
+        if (mViewHost != null) {
+            throw new IllegalStateException(
+                    "A UI has already been created with this window manager.");
+        }
+
+        // Construction extracted into the separate methods to allow injection for tests.
+        mViewHost = createSurfaceViewHost();
+        mCompatUILayout = inflateCompatUILayout();
+        mCompatUILayout.inject(this);
+
+        mCompatUILayout.setSizeCompatHintVisibility(mShouldShowHint);
+
+        mViewHost.setView(mCompatUILayout, getWindowLayoutParams());
+
+        // Only show by default for the first time.
+        mShouldShowHint = false;
+    }
+
+    @VisibleForTesting
+    CompatUILayout inflateCompatUILayout() {
+        return (CompatUILayout) LayoutInflater.from(mContext)
+                .inflate(R.layout.compat_ui_layout, null);
+    }
+
+    @VisibleForTesting
+    SurfaceControlViewHost createSurfaceViewHost() {
+        return new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
+    }
+
+    /** Gets the layout params. */
+    private WindowManager.LayoutParams getWindowLayoutParams() {
+        // Measure how big the hint is since its size depends on the text size.
+        mCompatUILayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+        final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams(
+                // Cannot be wrap_content as this determines the actual window size
+                mCompatUILayout.getMeasuredWidth(), mCompatUILayout.getMeasuredHeight(),
+                TYPE_APPLICATION_OVERLAY,
+                FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL,
+                PixelFormat.TRANSLUCENT);
+        winParams.token = new Binder();
+        winParams.setTitle(CompatUILayout.class.getSimpleName() + mTaskId);
+        winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
+        return winParams;
+    }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 7bc8d23..6d158d5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -55,6 +55,8 @@
 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.compatui.CompatUI;
+import com.android.wm.shell.compatui.CompatUIController;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
 import com.android.wm.shell.draganddrop.DragAndDrop;
@@ -76,8 +78,6 @@
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.recents.RecentTasks;
 import com.android.wm.shell.recents.RecentTasksController;
-import com.android.wm.shell.sizecompatui.SizeCompatUI;
-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.startingsurface.StartingSurface;
@@ -174,25 +174,25 @@
     @Provides
     static ShellTaskOrganizer provideShellTaskOrganizer(@ShellMainThread ShellExecutor mainExecutor,
             Context context,
-            SizeCompatUIController sizeCompatUI,
+            CompatUIController compatUI,
             Optional<RecentTasksController> recentTasksOptional
     ) {
-        return new ShellTaskOrganizer(mainExecutor, context, sizeCompatUI, recentTasksOptional);
+        return new ShellTaskOrganizer(mainExecutor, context, compatUI, recentTasksOptional);
     }
 
     @WMSingleton
     @Provides
-    static SizeCompatUI provideSizeCompatUI(SizeCompatUIController sizeCompatUIController) {
-        return sizeCompatUIController.asSizeCompatUI();
+    static CompatUI provideCompatUI(CompatUIController compatUIController) {
+        return compatUIController.asCompatUI();
     }
 
     @WMSingleton
     @Provides
-    static SizeCompatUIController provideSizeCompatUIController(Context context,
+    static CompatUIController provideCompatUIController(Context context,
             DisplayController displayController, DisplayInsetsController displayInsetsController,
             DisplayImeController imeController, SyncTransactionQueue syncQueue,
             @ShellMainThread ShellExecutor mainExecutor) {
-        return new SizeCompatUIController(context, displayController, displayInsetsController,
+        return new CompatUIController(context, displayController, displayInsetsController,
                 imeController, syncQueue, mainExecutor);
     }
 
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 b65a2e4..8e6c05d 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
@@ -151,7 +151,13 @@
                 final Rect rightHitRegion = new Rect();
                 final Rect rightDrawRegion = bottomOrRightBounds;
 
-                displayRegion.splitVertically(leftHitRegion, rightHitRegion);
+                // If we have existing split regions use those bounds, otherwise split it 50/50
+                if (inSplitScreen) {
+                    leftHitRegion.set(topOrLeftBounds);
+                    rightHitRegion.set(bottomOrRightBounds);
+                } else {
+                    displayRegion.splitVertically(leftHitRegion, rightHitRegion);
+                }
 
                 mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, leftDrawRegion));
                 mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, rightDrawRegion));
@@ -162,8 +168,13 @@
                 final Rect bottomHitRegion = new Rect();
                 final Rect bottomDrawRegion = bottomOrRightBounds;
 
-                displayRegion.splitHorizontally(
-                        topHitRegion, bottomHitRegion);
+                // If we have existing split regions use those bounds, otherwise split it 50/50
+                if (inSplitScreen) {
+                    topHitRegion.set(topOrLeftBounds);
+                    bottomHitRegion.set(bottomOrRightBounds);
+                } else {
+                    displayRegion.splitHorizontally(topHitRegion, bottomHitRegion);
+                }
 
                 mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topDrawRegion));
                 mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomDrawRegion));
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 67f9062..fd3be2b 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
@@ -17,7 +17,9 @@
 package com.android.wm.shell.draganddrop;
 
 import static android.app.StatusBarManager.DISABLE_NONE;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 
 import android.animation.Animator;
@@ -32,11 +34,11 @@
 import android.content.res.Configuration;
 import android.graphics.Color;
 import android.graphics.Insets;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.RemoteException;
 import android.view.DragEvent;
 import android.view.SurfaceControl;
-import android.view.ViewGroup;
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type;
 import android.widget.LinearLayout;
@@ -73,6 +75,7 @@
     private DropZoneView mDropZoneView2;
 
     private int mDisplayMargin;
+    private int mDividerSize;
     private Insets mInsets = Insets.NONE;
 
     private boolean mIsShowing;
@@ -89,13 +92,15 @@
 
         mDisplayMargin = context.getResources().getDimensionPixelSize(
                 R.dimen.drop_layout_display_margin);
+        mDividerSize = context.getResources().getDimensionPixelSize(
+                R.dimen.split_divider_bar_width);
 
         mDropZoneView1 = new DropZoneView(context);
         mDropZoneView2 = new DropZoneView(context);
-        addView(mDropZoneView1, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                ViewGroup.LayoutParams.MATCH_PARENT));
-        addView(mDropZoneView2, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                ViewGroup.LayoutParams.MATCH_PARENT));
+        addView(mDropZoneView1, new LinearLayout.LayoutParams(MATCH_PARENT,
+                MATCH_PARENT));
+        addView(mDropZoneView2, new LinearLayout.LayoutParams(MATCH_PARENT,
+                MATCH_PARENT));
         ((LayoutParams) mDropZoneView1.getLayoutParams()).weight = 1;
         ((LayoutParams) mDropZoneView2.getLayoutParams()).weight = 1;
         updateContainerMargins();
@@ -165,44 +170,82 @@
         mHasDropped = false;
         mCurrentTarget = null;
 
-        List<ActivityManager.RunningTaskInfo> tasks = null;
-        // Figure out the splashscreen info for the existing task(s).
-        try {
-            tasks = ActivityTaskManager.getService().getTasks(2,
-                            false /* filterOnlyVisibleRecents */,
-                            false /* keepIntentExtra */);
-        } catch (RemoteException e) {
-            // don't show an icon / will just use the defaults
-        }
-        if (tasks != null && !tasks.isEmpty()) {
-            ActivityManager.RunningTaskInfo taskInfo1 = tasks.get(0);
-            Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
-            int bgColor1 = getResizingBackgroundColor(taskInfo1);
-
-            boolean alreadyInSplit = mSplitScreenController != null
-                    && mSplitScreenController.isSplitScreenVisible();
-            if (alreadyInSplit && tasks.size() > 1) {
-                ActivityManager.RunningTaskInfo taskInfo2 = tasks.get(1);
-                Drawable icon2 = mIconProvider.getIcon(taskInfo2.topActivityInfo);
-                int bgColor2 = getResizingBackgroundColor(taskInfo2);
-
-                // figure out which task is on which side
-                int splitPosition1 = mSplitScreenController.getSplitPosition(taskInfo1.taskId);
-                boolean isTask1TopOrLeft = splitPosition1 == SPLIT_POSITION_TOP_OR_LEFT;
-                if (isTask1TopOrLeft) {
-                    mDropZoneView1.setAppInfo(bgColor1, icon1);
-                    mDropZoneView2.setAppInfo(bgColor2, icon2);
-                } else {
-                    mDropZoneView2.setAppInfo(bgColor1, icon1);
-                    mDropZoneView1.setAppInfo(bgColor2, icon2);
-                }
-            } else {
+        boolean alreadyInSplit = mSplitScreenController != null
+                && mSplitScreenController.isSplitScreenVisible();
+        if (!alreadyInSplit) {
+            List<ActivityManager.RunningTaskInfo> tasks = null;
+            // Figure out the splashscreen info for the existing task.
+            try {
+                tasks = ActivityTaskManager.getService().getTasks(1,
+                        false /* filterOnlyVisibleRecents */,
+                        false /* keepIntentExtra */);
+            } catch (RemoteException e) {
+                // don't show an icon / will just use the defaults
+            }
+            if (tasks != null && !tasks.isEmpty()) {
+                ActivityManager.RunningTaskInfo taskInfo1 = tasks.get(0);
+                Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
+                int bgColor1 = getResizingBackgroundColor(taskInfo1);
                 mDropZoneView1.setAppInfo(bgColor1, icon1);
                 mDropZoneView2.setAppInfo(bgColor1, icon1);
+                updateDropZoneSizes(null, null); // passing null splits the views evenly
             }
+        } else {
+            // We're already in split so get taskInfo from the controller to populate icon / color.
+            ActivityManager.RunningTaskInfo topOrLeftTask =
+                    mSplitScreenController.getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+            ActivityManager.RunningTaskInfo bottomOrRightTask =
+                    mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+            if (topOrLeftTask != null && bottomOrRightTask != null) {
+                Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo);
+                int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask);
+                Drawable bottomOrRightIcon = mIconProvider.getIcon(
+                        bottomOrRightTask.topActivityInfo);
+                int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask);
+                mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon);
+                mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon);
+            }
+
+            // Update the dropzones to match existing split sizes
+            Rect topOrLeftBounds = new Rect();
+            Rect bottomOrRightBounds = new Rect();
+            mSplitScreenController.getStageBounds(topOrLeftBounds, bottomOrRightBounds);
+            updateDropZoneSizes(topOrLeftBounds, bottomOrRightBounds);
         }
     }
 
+    /**
+     * Sets the size of the two drop zones based on the provided bounds. The divider sits between
+     * the views and its size is included in the calculations.
+     *
+     * @param bounds1 bounds to apply to the first dropzone view, null if split in half.
+     * @param bounds2 bounds to apply to the second dropzone view, null if split in half.
+     */
+    private void updateDropZoneSizes(Rect bounds1, Rect bounds2) {
+        final int orientation = getResources().getConfiguration().orientation;
+        final boolean isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT;
+        final int halfDivider = mDividerSize / 2;
+        final LinearLayout.LayoutParams dropZoneView1 =
+                (LayoutParams) mDropZoneView1.getLayoutParams();
+        final LinearLayout.LayoutParams dropZoneView2 =
+                (LayoutParams) mDropZoneView2.getLayoutParams();
+        if (isPortrait) {
+            dropZoneView1.width = MATCH_PARENT;
+            dropZoneView2.width = MATCH_PARENT;
+            dropZoneView1.height = bounds1 != null ? bounds1.height() + halfDivider : MATCH_PARENT;
+            dropZoneView2.height = bounds2 != null ? bounds2.height() + halfDivider : MATCH_PARENT;
+        } else {
+            dropZoneView1.width = bounds1 != null ? bounds1.width() + halfDivider : MATCH_PARENT;
+            dropZoneView2.width = bounds2 != null ? bounds2.width() + halfDivider : MATCH_PARENT;
+            dropZoneView1.height = MATCH_PARENT;
+            dropZoneView2.height = MATCH_PARENT;
+        }
+        dropZoneView1.weight = bounds1 != null ? 0 : 1;
+        dropZoneView2.weight = bounds2 != null ? 0 : 1;
+        mDropZoneView1.setLayoutParams(dropZoneView1);
+        mDropZoneView2.setLayoutParams(dropZoneView2);
+    }
+
     public void show() {
         mIsShowing = true;
         recomputeDropTargets();
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 854fc60e..f0b2716 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
@@ -778,8 +778,8 @@
     }
 
     @Override
-    public boolean supportSizeCompatUI() {
-        // PIP doesn't support size compat.
+    public boolean supportCompatUI() {
+        // PIP doesn't support compat.
         return false;
     }
 
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 eb512af..101a55d 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
@@ -191,6 +191,7 @@
         mSystemWindows.addView(mPipMenuView,
                 getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
                 0, SHELL_ROOT_LAYER_PIP);
+        setShellRootAccessibilityWindow();
     }
 
     private void detachPipMenuView() {
@@ -546,6 +547,10 @@
             mListeners.forEach(l -> l.onPipMenuStateChangeFinish(menuState));
         }
         mMenuState = menuState;
+        setShellRootAccessibilityWindow();
+    }
+
+    private void setShellRootAccessibilityWindow() {
         switch (mMenuState) {
             case MENU_STATE_NONE:
                 mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, null);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java
deleted file mode 100644
index ff6f913..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.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.wm.shell.sizecompatui;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-
-import androidx.annotation.Nullable;
-
-import com.android.wm.shell.R;
-
-/** Popup to show the hint about the {@link SizeCompatRestartButton}. */
-public class SizeCompatHintPopup extends FrameLayout implements View.OnClickListener {
-
-    private SizeCompatUILayout mLayout;
-
-    public SizeCompatHintPopup(Context context) {
-        super(context);
-    }
-
-    public SizeCompatHintPopup(Context context, @Nullable AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public SizeCompatHintPopup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-    }
-
-    public SizeCompatHintPopup(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-    }
-
-    void inject(SizeCompatUILayout layout) {
-        mLayout = layout;
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        final LinearLayout hintPopup = findViewById(R.id.size_compat_hint_popup);
-        hintPopup.setOnClickListener(this);
-    }
-
-    @Override
-    public void onClick(View v) {
-        mLayout.dismissHint();
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java
deleted file mode 100644
index d75fe517..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java
+++ /dev/null
@@ -1,76 +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.sizecompatui;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.ImageButton;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.wm.shell.R;
-
-/** Button to restart the size compat activity. */
-public class SizeCompatRestartButton extends FrameLayout implements View.OnClickListener,
-        View.OnLongClickListener {
-
-    private SizeCompatUILayout mLayout;
-
-    public SizeCompatRestartButton(@NonNull Context context) {
-        super(context);
-    }
-
-    public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs,
-            int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-    }
-
-    public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs,
-            int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-    }
-
-    void inject(SizeCompatUILayout layout) {
-        mLayout = layout;
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        final ImageButton restartButton = findViewById(R.id.size_compat_restart_button);
-        restartButton.setOnClickListener(this);
-        restartButton.setOnLongClickListener(this);
-    }
-
-    @Override
-    public void onClick(View v) {
-        mLayout.onRestartButtonClicked();
-    }
-
-    @Override
-    public boolean onLongClick(View v) {
-        mLayout.onRestartButtonLongClicked();
-        return true;
-    }
-}
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
deleted file mode 100644
index c35b89a..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
+++ /dev/null
@@ -1,344 +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.sizecompatui;
-
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
-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.content.res.Resources;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.os.Binder;
-import android.util.Log;
-import android.view.SurfaceControl;
-import android.view.View;
-import android.view.WindowManager;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.wm.shell.R;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.DisplayLayout;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-/**
- * Records and handles layout of size compat UI on a task with size compat activity. Helps to
- * calculate proper bounds when configuration or UI position changes.
- */
-class SizeCompatUILayout {
-    private static final String TAG = "SizeCompatUILayout";
-
-    final SyncTransactionQueue mSyncQueue;
-    private final SizeCompatUIController.SizeCompatUICallback mCallback;
-    private Context mContext;
-    private Configuration mTaskConfig;
-    private final int mDisplayId;
-    private final int mTaskId;
-    private ShellTaskOrganizer.TaskListener mTaskListener;
-    private DisplayLayout mDisplayLayout;
-    private final Rect mStableBounds;
-    private final int mButtonWidth;
-    private final int mButtonHeight;
-    private final int mPopupOffsetX;
-    private final int mPopupOffsetY;
-
-    @VisibleForTesting
-    final SizeCompatUIWindowManager mButtonWindowManager;
-    @VisibleForTesting
-    @Nullable
-    SizeCompatUIWindowManager mHintWindowManager;
-    @VisibleForTesting
-    @Nullable
-    SizeCompatRestartButton mButton;
-    @VisibleForTesting
-    @Nullable
-    SizeCompatHintPopup mHint;
-    @VisibleForTesting
-    boolean mShouldShowHint;
-
-    SizeCompatUILayout(SyncTransactionQueue syncQueue,
-            SizeCompatUIController.SizeCompatUICallback callback, Context context,
-            Configuration taskConfig, int taskId, ShellTaskOrganizer.TaskListener taskListener,
-            DisplayLayout displayLayout, boolean hasShownHint) {
-        mSyncQueue = syncQueue;
-        mCallback = callback;
-        mContext = context.createConfigurationContext(taskConfig);
-        mTaskConfig = taskConfig;
-        mDisplayId = mContext.getDisplayId();
-        mTaskId = taskId;
-        mTaskListener = taskListener;
-        mDisplayLayout = displayLayout;
-        mShouldShowHint = !hasShownHint;
-        mButtonWindowManager = new SizeCompatUIWindowManager(mContext, taskConfig, this);
-
-        mStableBounds = new Rect();
-        mDisplayLayout.getStableBounds(mStableBounds);
-
-        final Resources resources = mContext.getResources();
-        mButtonWidth = resources.getDimensionPixelSize(R.dimen.size_compat_button_width);
-        mButtonHeight = resources.getDimensionPixelSize(R.dimen.size_compat_button_height);
-        mPopupOffsetX = (mButtonWidth / 2) - resources.getDimensionPixelSize(
-                R.dimen.size_compat_hint_corner_radius) - (resources.getDimensionPixelSize(
-                R.dimen.size_compat_hint_point_width) / 2);
-        mPopupOffsetY = mButtonHeight;
-    }
-
-    /** Creates the activity restart button window. */
-    void createSizeCompatButton(boolean show) {
-        if (!show || mButton != null) {
-            // Wait until button should be visible.
-            return;
-        }
-        mButton = mButtonWindowManager.createSizeCompatButton();
-        updateButtonSurfacePosition();
-
-        if (mShouldShowHint) {
-            // Only show by default for the first time.
-            mShouldShowHint = false;
-            createSizeCompatHint();
-        }
-
-        mCallback.onSizeCompatRestartButtonAppeared(mTaskId);
-    }
-
-    /** Creates the restart button hint window. */
-    private void createSizeCompatHint() {
-        if (mHint != null) {
-            // Hint already shown.
-            return;
-        }
-        mHintWindowManager = createHintWindowManager();
-        mHint = mHintWindowManager.createSizeCompatHint();
-        updateHintSurfacePosition();
-    }
-
-    @VisibleForTesting
-    SizeCompatUIWindowManager createHintWindowManager() {
-        return new SizeCompatUIWindowManager(mContext, mTaskConfig, this);
-    }
-
-    /** Dismisses the hint window. */
-    void dismissHint() {
-        mHint = null;
-        if (mHintWindowManager != null) {
-            mHintWindowManager.release();
-            mHintWindowManager = null;
-        }
-    }
-
-    /** Releases the UI windows. */
-    void release() {
-        dismissHint();
-        mButton = null;
-        mButtonWindowManager.release();
-    }
-
-    /** Called when size compat info changed. */
-    void updateSizeCompatInfo(Configuration taskConfig,
-            ShellTaskOrganizer.TaskListener taskListener, boolean show) {
-        final Configuration prevTaskConfig = mTaskConfig;
-        final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener;
-        mTaskConfig = taskConfig;
-        mTaskListener = taskListener;
-
-        // Update configuration.
-        mContext = mContext.createConfigurationContext(taskConfig);
-        mButtonWindowManager.setConfiguration(taskConfig);
-        if (mHintWindowManager != null) {
-            mHintWindowManager.setConfiguration(taskConfig);
-        }
-
-        if (mButton == null || prevTaskListener != taskListener) {
-            // TaskListener changed, recreate the button for new surface parent.
-            release();
-            createSizeCompatButton(show);
-            return;
-        }
-
-        if (!taskConfig.windowConfiguration.getBounds()
-                .equals(prevTaskConfig.windowConfiguration.getBounds())) {
-            // Reposition the UI surfaces.
-            updateAllSurfacePositions();
-        }
-
-        if (taskConfig.getLayoutDirection() != prevTaskConfig.getLayoutDirection()) {
-            // Update layout for RTL.
-            mButton.setLayoutDirection(taskConfig.getLayoutDirection());
-            updateButtonSurfacePosition();
-            if (mHint != null) {
-                mHint.setLayoutDirection(taskConfig.getLayoutDirection());
-                updateHintSurfacePosition();
-            }
-        }
-    }
-
-    /** Called when display layout changed. */
-    void updateDisplayLayout(DisplayLayout displayLayout) {
-        final Rect prevStableBounds = mStableBounds;
-        final Rect curStableBounds = new Rect();
-        displayLayout.getStableBounds(curStableBounds);
-        mDisplayLayout = displayLayout;
-        if (!prevStableBounds.equals(curStableBounds)) {
-            // Stable bounds changed, update UI surface positions.
-            updateAllSurfacePositions();
-            mStableBounds.set(curStableBounds);
-        }
-    }
-
-    /** Called when the visibility of the UI should change. */
-    void updateVisibility(boolean show) {
-        if (mButton == null) {
-            // Button may not have been created because it was hidden previously.
-            createSizeCompatButton(show);
-            return;
-        }
-
-        // Hide size compat UIs when IME is showing.
-        final int newVisibility = show ? View.VISIBLE : View.GONE;
-        if (mButton.getVisibility() != newVisibility) {
-            mButton.setVisibility(newVisibility);
-        }
-        if (mHint != null && mHint.getVisibility() != newVisibility) {
-            mHint.setVisibility(newVisibility);
-        }
-    }
-
-    /** Gets the layout params for restart button. */
-    WindowManager.LayoutParams getButtonWindowLayoutParams() {
-        final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams(
-                // Cannot be wrap_content as this determines the actual window size
-                mButtonWidth, mButtonHeight,
-                TYPE_APPLICATION_OVERLAY,
-                FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL,
-                PixelFormat.TRANSLUCENT);
-        winParams.token = new Binder();
-        winParams.setTitle(SizeCompatRestartButton.class.getSimpleName() + getTaskId());
-        winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
-        return winParams;
-    }
-
-    /** Gets the layout params for hint popup. */
-    WindowManager.LayoutParams getHintWindowLayoutParams(SizeCompatHintPopup hint) {
-        final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams(
-                // Cannot be wrap_content as this determines the actual window size
-                hint.getMeasuredWidth(), hint.getMeasuredHeight(),
-                TYPE_APPLICATION_OVERLAY,
-                FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL,
-                PixelFormat.TRANSLUCENT);
-        winParams.token = new Binder();
-        winParams.setTitle(SizeCompatHintPopup.class.getSimpleName() + getTaskId());
-        winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
-        winParams.windowAnimations = android.R.style.Animation_InputMethod;
-        return winParams;
-    }
-
-    /** Called when it is ready to be placed size compat UI surface. */
-    void attachToParentSurface(SurfaceControl.Builder b) {
-        mTaskListener.attachChildSurfaceToTask(mTaskId, b);
-    }
-
-    /** Called when the restart button is clicked. */
-    void onRestartButtonClicked() {
-        mCallback.onSizeCompatRestartButtonClicked(mTaskId);
-    }
-
-    /** Called when the restart button is long clicked. */
-    void onRestartButtonLongClicked() {
-        createSizeCompatHint();
-    }
-
-    private void updateAllSurfacePositions() {
-        updateButtonSurfacePosition();
-        updateHintSurfacePosition();
-    }
-
-    @VisibleForTesting
-    void updateButtonSurfacePosition() {
-        if (mButton == null || mButtonWindowManager.getSurfaceControl() == null) {
-            return;
-        }
-        final SurfaceControl leash = mButtonWindowManager.getSurfaceControl();
-
-        // Use stable bounds to prevent the button from overlapping with system bars.
-        final Rect taskBounds = mTaskConfig.windowConfiguration.getBounds();
-        final Rect stableBounds = new Rect();
-        mDisplayLayout.getStableBounds(stableBounds);
-        stableBounds.intersect(taskBounds);
-
-        // Position of the button in the container coordinate.
-        final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
-                ? stableBounds.left - taskBounds.left
-                : stableBounds.right - taskBounds.left - mButtonWidth;
-        final int positionY = stableBounds.bottom - taskBounds.top - mButtonHeight;
-
-        updateSurfacePosition(leash, positionX, positionY);
-    }
-
-    @VisibleForTesting
-    void updateHintSurfacePosition() {
-        if (mHint == null || mHintWindowManager == null
-                || mHintWindowManager.getSurfaceControl() == null) {
-            return;
-        }
-        final SurfaceControl leash = mHintWindowManager.getSurfaceControl();
-
-        // Use stable bounds to prevent the hint from overlapping with system bars.
-        final Rect taskBounds = mTaskConfig.windowConfiguration.getBounds();
-        final Rect stableBounds = new Rect();
-        mDisplayLayout.getStableBounds(stableBounds);
-        stableBounds.intersect(taskBounds);
-
-        // Position of the hint in the container coordinate.
-        final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
-                ? stableBounds.left - taskBounds.left + mPopupOffsetX
-                : stableBounds.right - taskBounds.left - mPopupOffsetX - mHint.getMeasuredWidth();
-        final int positionY =
-                stableBounds.bottom - taskBounds.top - mPopupOffsetY - mHint.getMeasuredHeight();
-
-        updateSurfacePosition(leash, positionX, positionY);
-    }
-
-    private void updateSurfacePosition(SurfaceControl leash, int positionX, int positionY) {
-        mSyncQueue.runInSync(t -> {
-            if (!leash.isValid()) {
-                Log.w(TAG, "The leash has been released.");
-                return;
-            }
-            t.setPosition(leash, positionX, positionY);
-            // The size compat UI should be the topmost child of the Task in case there can be more
-            // than one children.
-            t.setLayer(leash, Integer.MAX_VALUE);
-        });
-    }
-
-    int getDisplayId() {
-        return mDisplayId;
-    }
-
-    int getTaskId() {
-        return mTaskId;
-    }
-
-    private int getLayoutDirection() {
-        return mContext.getResources().getConfiguration().getLayoutDirection();
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIWindowManager.java
deleted file mode 100644
index 82f69c3..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIWindowManager.java
+++ /dev/null
@@ -1,127 +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.sizecompatui;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.view.IWindow;
-import android.view.LayoutInflater;
-import android.view.SurfaceControl;
-import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
-import android.view.View;
-import android.view.WindowlessWindowManager;
-
-import com.android.wm.shell.R;
-
-/**
- * Holds view hierarchy of a root surface and helps to inflate {@link SizeCompatRestartButton} or
- * {@link SizeCompatHintPopup}.
- */
-class SizeCompatUIWindowManager extends WindowlessWindowManager {
-
-    private Context mContext;
-    private final SizeCompatUILayout mLayout;
-
-    @Nullable
-    private SurfaceControlViewHost mViewHost;
-    @Nullable
-    private SurfaceControl mLeash;
-
-    SizeCompatUIWindowManager(Context context, Configuration config, SizeCompatUILayout layout) {
-        super(config, null /* rootSurface */, null /* hostInputToken */);
-        mContext = context;
-        mLayout = layout;
-    }
-
-    @Override
-    public void setConfiguration(Configuration configuration) {
-        super.setConfiguration(configuration);
-        mContext = mContext.createConfigurationContext(configuration);
-    }
-
-    @Override
-    protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
-        // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
-        final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
-                .setContainerLayer()
-                .setName("SizeCompatUILeash")
-                .setHidden(false)
-                .setCallsite("SizeCompatUIWindowManager#attachToParentSurface");
-        mLayout.attachToParentSurface(builder);
-        mLeash = builder.build();
-        b.setParent(mLeash);
-    }
-
-    /** Inflates {@link SizeCompatRestartButton} on to the root surface. */
-    SizeCompatRestartButton createSizeCompatButton() {
-        if (mViewHost != null) {
-            throw new IllegalStateException(
-                    "A UI has already been created with this window manager.");
-        }
-
-        mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
-
-        final SizeCompatRestartButton button = (SizeCompatRestartButton)
-                LayoutInflater.from(mContext).inflate(R.layout.size_compat_ui, null);
-        button.inject(mLayout);
-        mViewHost.setView(button, mLayout.getButtonWindowLayoutParams());
-        return button;
-    }
-
-    /** Inflates {@link SizeCompatHintPopup} on to the root surface. */
-    SizeCompatHintPopup createSizeCompatHint() {
-        if (mViewHost != null) {
-            throw new IllegalStateException(
-                    "A UI has already been created with this window manager.");
-        }
-
-        mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
-
-        final SizeCompatHintPopup hint = (SizeCompatHintPopup)
-                LayoutInflater.from(mContext).inflate(R.layout.size_compat_mode_hint, null);
-        // Measure how big the hint is.
-        hint.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
-        hint.inject(mLayout);
-        mViewHost.setView(hint, mLayout.getHintWindowLayoutParams(hint));
-        return hint;
-    }
-
-    /** Releases the surface control and tears down the view hierarchy. */
-    void release() {
-        if (mViewHost != null) {
-            mViewHost.release();
-            mViewHost = null;
-        }
-
-        if (mLeash != null) {
-            final SurfaceControl leash = mLeash;
-            mLayout.mSyncQueue.runInSync(t -> t.remove(leash));
-            mLeash = null;
-        }
-    }
-
-    /**
-     * Gets {@link SurfaceControl} of the surface holding size compat UI view. @return {@code null}
-     * if not feasible.
-     */
-    @Nullable
-    SurfaceControl getSurfaceControl() {
-        return mLeash;
-    }
-}
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 cf4e56e..46c4a40 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
@@ -186,6 +186,15 @@
         return mStageCoordinator.isSplitScreenVisible();
     }
 
+    @Nullable
+    public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) {
+        if (isSplitScreenVisible()) {
+            int taskId = mStageCoordinator.getTaskId(splitPosition);
+            return mTaskOrganizer.getRunningTaskInfo(taskId);
+        }
+        return null;
+    }
+
     public boolean isTaskInSplitScreen(int taskId) {
         return isSplitScreenVisible()
                 && mStageCoordinator.getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED;
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 ed5de0d..dd27017 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
@@ -342,12 +342,12 @@
         sideOptions = sideOptions != null ? sideOptions : new Bundle();
         setSideStagePosition(sidePosition, wct);
 
+        mSplitLayout.setDivideRatio(splitRatio);
         // 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);
 
-        mSplitLayout.setDivideRatio(splitRatio);
         // Make sure the launch options will put tasks in the corresponding split roots
         addActivityOptions(mainOptions, mMainStage);
         addActivityOptions(sideOptions, mSideStage);
@@ -522,6 +522,14 @@
         return SplitLayout.reversePosition(mSideStagePosition);
     }
 
+    int getTaskId(@SplitPosition int splitPosition) {
+        if (mSideStagePosition == splitPosition) {
+            return mSideStage.getTopVisibleChildTaskId();
+        } else {
+            return mMainStage.getTopVisibleChildTaskId();
+        }
+    }
+
     void setSideStagePosition(@SplitPosition int sideStagePosition,
             @Nullable WindowContainerTransaction wct) {
         setSideStagePosition(sideStagePosition, true /* updateBounds */, wct);
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 6643ca1..4ecc0b6 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
@@ -380,9 +380,7 @@
     }
 
     private void drawSizeMatchSnapshot() {
-        GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(
-                mSnapshot.getHardwareBuffer());
-        mTransaction.setBuffer(mSurfaceControl, graphicBuffer)
+        mTransaction.setBuffer(mSurfaceControl, mSnapshot.getHardwareBuffer())
                 .setColorSpace(mSurfaceControl, mSnapshot.getColorSpace())
                 .apply();
     }
@@ -428,20 +426,20 @@
         // Scale the mismatch dimensions to fill the task bounds
         mSnapshotMatrix.setRectToRect(mTmpSnapshotSize, mTmpDstFrame, Matrix.ScaleToFit.FILL);
         mTransaction.setMatrix(childSurfaceControl, mSnapshotMatrix, mTmpFloat9);
-        GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(
-                mSnapshot.getHardwareBuffer());
         mTransaction.setColorSpace(childSurfaceControl, mSnapshot.getColorSpace());
-        mTransaction.setBuffer(childSurfaceControl, graphicBuffer);
+        mTransaction.setBuffer(childSurfaceControl, mSnapshot.getHardwareBuffer());
 
         if (aspectRatioMismatch) {
             GraphicBuffer background = GraphicBuffer.create(mFrame.width(), mFrame.height(),
                     PixelFormat.RGBA_8888,
                     GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_HW_COMPOSER
                             | GraphicBuffer.USAGE_SW_WRITE_RARELY);
+            // TODO: Support this on HardwareBuffer
             final Canvas c = background.lockCanvas();
             drawBackgroundAndBars(c, frame);
             background.unlockCanvasAndPost(c);
-            mTransaction.setBuffer(mSurfaceControl, background);
+            mTransaction.setBuffer(mSurfaceControl,
+                    HardwareBuffer.createFromGraphicBuffer(background));
         }
         mTransaction.apply();
         childSurfaceControl.release();
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 1fcbf14..a3b98a8f 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
@@ -56,7 +56,7 @@
 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.sizecompatui.SizeCompatUIController;
+import com.android.wm.shell.compatui.CompatUIController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -82,7 +82,7 @@
     @Mock
     private Context mContext;
     @Mock
-    private SizeCompatUIController mSizeCompatUI;
+    private CompatUIController mCompatUI;
 
     ShellTaskOrganizer mOrganizer;
     private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class);
@@ -132,7 +132,7 @@
                     .when(mTaskOrganizerController).registerTaskOrganizer(any());
         } catch (RemoteException e) {}
         mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext,
-                mSizeCompatUI, Optional.empty()));
+                mCompatUI, Optional.empty()));
     }
 
     @Test
@@ -334,34 +334,34 @@
         mOrganizer.onTaskAppeared(taskInfo1, null);
 
         // sizeCompatActivity is null if top activity is not in size compat.
-        verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
+        verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
                 null /* taskConfig */, null /* taskListener */);
 
         // sizeCompatActivity is non-null if top activity is in size compat.
-        clearInvocations(mSizeCompatUI);
+        clearInvocations(mCompatUI);
         final RunningTaskInfo taskInfo2 =
                 createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
         taskInfo2.displayId = taskInfo1.displayId;
         taskInfo2.topActivityInSizeCompat = true;
         taskInfo2.isVisible = true;
         mOrganizer.onTaskInfoChanged(taskInfo2);
-        verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
+        verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
                 taskInfo1.configuration, taskListener);
 
         // Not show size compat UI if task is not visible.
-        clearInvocations(mSizeCompatUI);
+        clearInvocations(mCompatUI);
         final RunningTaskInfo taskInfo3 =
                 createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
         taskInfo3.displayId = taskInfo1.displayId;
         taskInfo3.topActivityInSizeCompat = true;
         taskInfo3.isVisible = false;
         mOrganizer.onTaskInfoChanged(taskInfo3);
-        verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
+        verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
                 null /* taskConfig */, null /* taskListener */);
 
-        clearInvocations(mSizeCompatUI);
+        clearInvocations(mCompatUI);
         mOrganizer.onTaskVanished(taskInfo1);
-        verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
+        verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
                 null /* taskConfig */, null /* taskListener */);
     }
 
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 bc701d0..185479b 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
@@ -39,6 +39,7 @@
 import android.service.notification.StatusBarNotification;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.util.Log;
 import android.util.Pair;
 import android.view.WindowManager;
 
@@ -793,7 +794,7 @@
     }
 
     @Test
-    public void test_expanded_removeLastBubble_showsOverflowIfNotEmpty() {
+    public void test_expanded_removeLastBubble_collapsesIfOverflowNotEmpty() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         changeExpandedStateAtTime(true, 2000);
@@ -803,7 +804,7 @@
         mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertThat(mBubbleData.getOverflowBubbles().size()).isGreaterThan(0);
-        assertSelectionChangedTo(mBubbleData.getOverflow());
+        assertExpandedChangedTo(false);
     }
 
     @Test
@@ -913,6 +914,31 @@
         assertSelectionChangedTo(mBubbleA2);
     }
 
+    /**
+      * - have a maxed out bubble stack & all of the bubbles have been recently accessed
+      * - bubble a notification that was posted before any of those bubbles were accessed
+      * => that bubble should be added
+     *
+      */
+    @Test
+    public void test_addOldNotifWithNewerBubbles() {
+        sendUpdatedEntryAtTime(mEntryA1, 2000);
+        sendUpdatedEntryAtTime(mEntryA2, 3000);
+        sendUpdatedEntryAtTime(mEntryA3, 4000);
+        sendUpdatedEntryAtTime(mEntryB1, 5000);
+        sendUpdatedEntryAtTime(mEntryB2, 6000);
+
+        mBubbleData.setListener(mListener);
+        sendUpdatedEntryAtTime(mEntryB3, 1000 /* postTime */, 7000 /* currentTime */);
+        verifyUpdateReceived();
+
+        // B3 is in the stack
+        assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleB3.getKey())).isNotNull();
+        // A1 is the oldest so it's in the overflow
+        assertThat(mBubbleData.getOverflowBubbleWithKey(mEntryA1.getKey())).isNotNull();
+        assertOrderChangedTo(mBubbleB3, mBubbleB2, mBubbleB1, mBubbleA3, mBubbleA2);
+    }
+
     private void verifyUpdateReceived() {
         verify(mListener).applyUpdate(mUpdateCaptor.capture());
         reset(mListener);
@@ -1014,6 +1040,12 @@
     }
 
     private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime) {
+        setCurrentTime(postTime);
+        sendUpdatedEntryAtTime(entry, postTime, true /* isTextChanged */);
+    }
+
+    private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime, long currentTime) {
+        setCurrentTime(currentTime);
         sendUpdatedEntryAtTime(entry, postTime, true /* isTextChanged */);
     }
 
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 453050f..83d5f04 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,6 +24,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
@@ -101,14 +102,21 @@
 
     @Test
     public void testSetDividePosition() {
-        mSplitLayout.setDividePosition(anyInt());
+        mSplitLayout.setDividePosition(100, false /* applyLayoutChange */);
+        assertThat(mSplitLayout.getDividePosition()).isEqualTo(100);
+        verify(mSplitLayoutHandler, never()).onLayoutSizeChanged(any(SplitLayout.class));
+
+        mSplitLayout.setDividePosition(200, true /* applyLayoutChange */);
+        assertThat(mSplitLayout.getDividePosition()).isEqualTo(200);
         verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class));
     }
 
     @Test
     public void testSetDivideRatio() {
+        mSplitLayout.setDividePosition(200, false /* applyLayoutChange */);
         mSplitLayout.setDivideRatio(0.5f);
-        verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class));
+        assertThat(mSplitLayout.getDividePosition()).isEqualTo(
+                mSplitLayout.mDividerSnapAlgorithm.getMiddleTarget().position);
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
similarity index 83%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 877b192..f622edb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.sizecompatui;
+package com.android.wm.shell.compatui;
 
 import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
 
@@ -56,18 +56,18 @@
 import org.mockito.MockitoAnnotations;
 
 /**
- * Tests for {@link SizeCompatUIController}.
+ * Tests for {@link CompatUIController}.
  *
  * Build/Install/Run:
- *  atest WMShellUnitTests:SizeCompatUIControllerTest
+ *  atest WMShellUnitTests:CompatUIControllerTest
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class SizeCompatUIControllerTest extends ShellTestCase {
+public class CompatUIControllerTest extends ShellTestCase {
     private static final int DISPLAY_ID = 0;
     private static final int TASK_ID = 12;
 
-    private SizeCompatUIController mController;
+    private CompatUIController mController;
     private @Mock DisplayController mMockDisplayController;
     private @Mock DisplayInsetsController mMockDisplayInsetsController;
     private @Mock DisplayLayout mMockDisplayLayout;
@@ -75,7 +75,7 @@
     private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener;
     private @Mock SyncTransactionQueue mMockSyncQueue;
     private @Mock ShellExecutor mMockExecutor;
-    private @Mock SizeCompatUILayout mMockLayout;
+    private @Mock CompatUIWindowManager mMockLayout;
 
     @Captor
     ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor;
@@ -87,10 +87,10 @@
         doReturn(mMockDisplayLayout).when(mMockDisplayController).getDisplayLayout(anyInt());
         doReturn(DISPLAY_ID).when(mMockLayout).getDisplayId();
         doReturn(TASK_ID).when(mMockLayout).getTaskId();
-        mController = new SizeCompatUIController(mContext, mMockDisplayController,
+        mController = new CompatUIController(mContext, mMockDisplayController,
                 mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor) {
             @Override
-            SizeCompatUILayout createLayout(Context context, int displayId, int taskId,
+            CompatUIWindowManager createLayout(Context context, int displayId, int taskId,
                     Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener) {
                 return mMockLayout;
             }
@@ -105,24 +105,24 @@
     }
 
     @Test
-    public void testOnSizeCompatInfoChanged() {
+    public void testOnCompatInfoChanged() {
         final Configuration taskConfig = new Configuration();
 
         // Verify that the restart button is added with non-null size compat info.
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
 
         verify(mController).createLayout(any(), eq(DISPLAY_ID), eq(TASK_ID), eq(taskConfig),
                 eq(mMockTaskListener));
 
         // Verify that the restart button is updated with non-null new size compat info.
         final Configuration newTaskConfig = new Configuration();
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, newTaskConfig, mMockTaskListener);
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, newTaskConfig, mMockTaskListener);
 
-        verify(mMockLayout).updateSizeCompatInfo(taskConfig, mMockTaskListener,
+        verify(mMockLayout).updateCompatInfo(taskConfig, mMockTaskListener,
                 true /* show */);
 
         // Verify that the restart button is removed with null size compat info.
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, null, mMockTaskListener);
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, null, mMockTaskListener);
 
         verify(mMockLayout).release();
     }
@@ -140,7 +140,7 @@
     public void testOnDisplayRemoved() {
         mController.onDisplayAdded(DISPLAY_ID);
         final Configuration taskConfig = new Configuration();
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig,
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig,
                 mMockTaskListener);
 
         mController.onDisplayRemoved(DISPLAY_ID + 1);
@@ -158,7 +158,7 @@
     @Test
     public void testOnDisplayConfigurationChanged() {
         final Configuration taskConfig = new Configuration();
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig,
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig,
                 mMockTaskListener);
 
         final Configuration newTaskConfig = new Configuration();
@@ -175,7 +175,7 @@
     public void testInsetsChanged() {
         mController.onDisplayAdded(DISPLAY_ID);
         final Configuration taskConfig = new Configuration();
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig,
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig,
                 mMockTaskListener);
         InsetsState insetsState = new InsetsState();
         InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
@@ -197,7 +197,7 @@
     @Test
     public void testChangeButtonVisibilityOnImeShowHide() {
         final Configuration taskConfig = new Configuration();
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
 
         // Verify that the restart button is hidden after IME is showing.
         mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */);
@@ -205,9 +205,9 @@
         verify(mMockLayout).updateVisibility(false);
 
         // Verify button remains hidden while IME is showing.
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
 
-        verify(mMockLayout).updateSizeCompatInfo(taskConfig, mMockTaskListener,
+        verify(mMockLayout).updateCompatInfo(taskConfig, mMockTaskListener,
                 false /* show */);
 
         // Verify button is shown after IME is hidden.
@@ -219,7 +219,7 @@
     @Test
     public void testChangeButtonVisibilityOnKeyguardOccludedChanged() {
         final Configuration taskConfig = new Configuration();
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
 
         // Verify that the restart button is hidden after keyguard becomes occluded.
         mController.onKeyguardOccludedChanged(true);
@@ -227,9 +227,9 @@
         verify(mMockLayout).updateVisibility(false);
 
         // Verify button remains hidden while keyguard is occluded.
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
 
-        verify(mMockLayout).updateSizeCompatInfo(taskConfig, mMockTaskListener,
+        verify(mMockLayout).updateCompatInfo(taskConfig, mMockTaskListener,
                 false /* show */);
 
         // Verify button is shown after keyguard becomes not occluded.
@@ -241,7 +241,7 @@
     @Test
     public void testButtonRemainsHiddenOnKeyguardOccludedFalseWhenImeIsShowing() {
         final Configuration taskConfig = new Configuration();
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
 
         mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */);
         mController.onKeyguardOccludedChanged(true);
@@ -264,7 +264,7 @@
     @Test
     public void testButtonRemainsHiddenOnImeHideWhenKeyguardIsOccluded() {
         final Configuration taskConfig = new Configuration();
-        mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
+        mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener);
 
         mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */);
         mController.onKeyguardOccludedChanged(true);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
new file mode 100644
index 0000000..2c3987b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.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 com.android.wm.shell.compatui;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.content.res.Configuration;
+import android.testing.AndroidTestingRunner;
+import android.view.LayoutInflater;
+import android.view.SurfaceControlViewHost;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link CompatUILayout}.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:CompatUILayoutTest
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class CompatUILayoutTest extends ShellTestCase {
+
+    private static final int TASK_ID = 1;
+
+    @Mock private SyncTransactionQueue mSyncTransactionQueue;
+    @Mock private CompatUIController.CompatUICallback mCallback;
+    @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
+    @Mock private SurfaceControlViewHost mViewHost;
+
+    private CompatUIWindowManager mWindowManager;
+    private CompatUILayout mCompatUILayout;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mWindowManager = new CompatUIWindowManager(mContext, new Configuration(),
+                mSyncTransactionQueue, mCallback, TASK_ID, mTaskListener, new DisplayLayout(),
+                false /* hasShownHint */);
+
+        mCompatUILayout = (CompatUILayout)
+                LayoutInflater.from(mContext).inflate(R.layout.compat_ui_layout, null);
+        mCompatUILayout.inject(mWindowManager);
+
+        spyOn(mWindowManager);
+        spyOn(mCompatUILayout);
+        doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost();
+    }
+
+    @Test
+    public void testOnClickForRestartButton() {
+        final ImageButton button = mCompatUILayout.findViewById(R.id.size_compat_restart_button);
+        button.performClick();
+
+        verify(mWindowManager).onRestartButtonClicked();
+        doReturn(mCompatUILayout).when(mWindowManager).inflateCompatUILayout();
+        verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID);
+    }
+
+    @Test
+    public void testOnLongClickForRestartButton() {
+        doNothing().when(mWindowManager).onRestartButtonLongClicked();
+
+        final ImageButton button = mCompatUILayout.findViewById(R.id.size_compat_restart_button);
+        button.performLongClick();
+
+        verify(mWindowManager).onRestartButtonLongClicked();
+    }
+
+    @Test
+    public void testOnClickForSizeCompatHint() {
+        mWindowManager.createLayout(true /* show */);
+        final LinearLayout sizeCompatHint = mCompatUILayout.findViewById(R.id.size_compat_hint);
+        sizeCompatHint.performClick();
+
+        verify(mCompatUILayout).setSizeCompatHintVisibility(/* show= */ false);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
new file mode 100644
index 0000000..d5dcf2e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.compatui;
+
+import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+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.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.view.DisplayInfo;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link CompatUIWindowManager}.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:CompatUIWindowManagerTest
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class CompatUIWindowManagerTest extends ShellTestCase {
+
+    private static final int TASK_ID = 1;
+
+    @Mock private SyncTransactionQueue mSyncTransactionQueue;
+    @Mock private CompatUIController.CompatUICallback mCallback;
+    @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
+    @Mock private CompatUILayout mCompatUILayout;
+    @Mock private SurfaceControlViewHost mViewHost;
+    private Configuration mTaskConfig;
+
+    private CompatUIWindowManager mWindowManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mTaskConfig = new Configuration();
+
+        mWindowManager = new CompatUIWindowManager(mContext, new Configuration(),
+                mSyncTransactionQueue, mCallback, TASK_ID, mTaskListener, new DisplayLayout(),
+                false /* hasShownHint */);
+
+        spyOn(mWindowManager);
+        doReturn(mCompatUILayout).when(mWindowManager).inflateCompatUILayout();
+        doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost();
+    }
+
+    @Test
+    public void testCreateSizeCompatButton() {
+        // Not create layout if show is false.
+        mWindowManager.createLayout(false /* show */);
+
+        verify(mWindowManager, never()).inflateCompatUILayout();
+
+        // Not create hint popup.
+        mWindowManager.mShouldShowHint = false;
+        mWindowManager.createLayout(true /* show */);
+
+        verify(mWindowManager).inflateCompatUILayout();
+        verify(mCompatUILayout).setSizeCompatHintVisibility(false /* show */);
+
+        // Create hint popup.
+        mWindowManager.release();
+        mWindowManager.mShouldShowHint = true;
+        mWindowManager.createLayout(true /* show */);
+
+        verify(mWindowManager, times(2)).inflateCompatUILayout();
+        assertNotNull(mCompatUILayout);
+        verify(mCompatUILayout).setSizeCompatHintVisibility(true /* show */);
+        assertFalse(mWindowManager.mShouldShowHint);
+    }
+
+    @Test
+    public void testRelease() {
+        mWindowManager.createLayout(true /* show */);
+
+        verify(mWindowManager).inflateCompatUILayout();
+
+        mWindowManager.release();
+
+        verify(mViewHost).release();
+    }
+
+    @Test
+    public void testUpdateCompatInfo() {
+        mWindowManager.createLayout(true /* show */);
+
+        // No diff
+        clearInvocations(mWindowManager);
+        mWindowManager.updateCompatInfo(mTaskConfig, mTaskListener, true /* show */);
+
+        verify(mWindowManager, never()).updateSurfacePosition();
+        verify(mWindowManager, never()).release();
+        verify(mWindowManager, never()).createLayout(anyBoolean());
+
+        // Change task listener, recreate button.
+        clearInvocations(mWindowManager);
+        final ShellTaskOrganizer.TaskListener newTaskListener = mock(
+                ShellTaskOrganizer.TaskListener.class);
+        mWindowManager.updateCompatInfo(mTaskConfig, newTaskListener,
+                true /* show */);
+
+        verify(mWindowManager).release();
+        verify(mWindowManager).createLayout(anyBoolean());
+
+        // Change task bounds, update position.
+        clearInvocations(mWindowManager);
+        final Configuration newTaskConfiguration = new Configuration();
+        newTaskConfiguration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000));
+        mWindowManager.updateCompatInfo(newTaskConfiguration, newTaskListener,
+                true /* show */);
+
+        verify(mWindowManager).updateSurfacePosition();
+    }
+
+    @Test
+    public void testUpdateDisplayLayout() {
+        final DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.logicalWidth = 1000;
+        displayInfo.logicalHeight = 2000;
+        final DisplayLayout displayLayout1 = new DisplayLayout(displayInfo,
+                mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false);
+
+        mWindowManager.updateDisplayLayout(displayLayout1);
+        verify(mWindowManager).updateSurfacePosition();
+
+        // No update if the display bounds is the same.
+        clearInvocations(mWindowManager);
+        final DisplayLayout displayLayout2 = new DisplayLayout(displayInfo,
+                mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false);
+        mWindowManager.updateDisplayLayout(displayLayout2);
+        verify(mWindowManager, never()).updateSurfacePosition();
+    }
+
+    @Test
+    public void testUpdateDisplayLayoutInsets() {
+        final DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.logicalWidth = 1000;
+        displayInfo.logicalHeight = 2000;
+        final DisplayLayout displayLayout = new DisplayLayout(displayInfo,
+                mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false);
+
+        mWindowManager.updateDisplayLayout(displayLayout);
+        verify(mWindowManager).updateSurfacePosition();
+
+        // Update if the insets change on the existing display layout
+        clearInvocations(mWindowManager);
+        InsetsState insetsState = new InsetsState();
+        InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
+        insetsSource.setFrame(0, 0, 1000, 1000);
+        insetsState.addSource(insetsSource);
+        displayLayout.setInsets(mContext.getResources(), insetsState);
+        mWindowManager.updateDisplayLayout(displayLayout);
+        verify(mWindowManager).updateSurfacePosition();
+    }
+
+    @Test
+    public void testUpdateVisibility() {
+        // Create button if it is not created.
+        mWindowManager.mCompatUILayout = null;
+        mWindowManager.updateVisibility(true /* show */);
+
+        verify(mWindowManager).createLayout(true /* show */);
+
+        // Hide button.
+        clearInvocations(mWindowManager);
+        doReturn(View.VISIBLE).when(mCompatUILayout).getVisibility();
+        mWindowManager.updateVisibility(false /* show */);
+
+        verify(mWindowManager, never()).createLayout(anyBoolean());
+        verify(mCompatUILayout).setVisibility(View.GONE);
+
+        // Show button.
+        doReturn(View.GONE).when(mCompatUILayout).getVisibility();
+        mWindowManager.updateVisibility(true /* show */);
+
+        verify(mWindowManager, never()).createLayout(anyBoolean());
+        verify(mCompatUILayout).setVisibility(View.VISIBLE);
+    }
+
+    @Test
+    public void testAttachToParentSurface() {
+        final SurfaceControl.Builder b = new SurfaceControl.Builder();
+        mWindowManager.attachToParentSurface(b);
+
+        verify(mTaskListener).attachChildSurfaceToTask(TASK_ID, b);
+    }
+
+    @Test
+    public void testOnRestartButtonClicked() {
+        mWindowManager.onRestartButtonClicked();
+
+        verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID);
+    }
+
+    @Test
+    public void testOnRestartButtonLongClicked_showHint() {
+       // Not create hint popup.
+        mWindowManager.mShouldShowHint = false;
+        mWindowManager.createLayout(true /* show */);
+
+        verify(mWindowManager).inflateCompatUILayout();
+        verify(mCompatUILayout).setSizeCompatHintVisibility(false /* show */);
+
+        mWindowManager.onRestartButtonLongClicked();
+
+        verify(mCompatUILayout).setSizeCompatHintVisibility(true /* show */);
+    }
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java
deleted file mode 100644
index 3a14a33..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java
+++ /dev/null
@@ -1,85 +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.sizecompatui;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.verify;
-
-import android.content.res.Configuration;
-import android.testing.AndroidTestingRunner;
-import android.view.LayoutInflater;
-import android.widget.LinearLayout;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.R;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.DisplayLayout;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Tests for {@link SizeCompatHintPopup}.
- *
- * Build/Install/Run:
- *  atest WMShellUnitTests:SizeCompatHintPopupTest
- */
-@RunWith(AndroidTestingRunner.class)
-@SmallTest
-public class SizeCompatHintPopupTest extends ShellTestCase {
-
-    @Mock private SyncTransactionQueue mSyncTransactionQueue;
-    @Mock private SizeCompatUIController.SizeCompatUICallback mCallback;
-    @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
-    @Mock private DisplayLayout mDisplayLayout;
-
-    private SizeCompatUILayout mLayout;
-    private SizeCompatHintPopup mHint;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
-        final int taskId = 1;
-        mLayout = new SizeCompatUILayout(mSyncTransactionQueue, mCallback, mContext,
-                new Configuration(), taskId, mTaskListener, mDisplayLayout,
-                false /* hasShownHint */);
-        mHint = (SizeCompatHintPopup)
-                LayoutInflater.from(mContext).inflate(R.layout.size_compat_mode_hint, null);
-        mHint.inject(mLayout);
-
-        spyOn(mLayout);
-    }
-
-    @Test
-    public void testOnClick() {
-        doNothing().when(mLayout).dismissHint();
-
-        final LinearLayout hintPopup = mHint.findViewById(R.id.size_compat_hint_popup);
-        hintPopup.performClick();
-
-        verify(mLayout).dismissHint();
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButtonTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButtonTest.java
deleted file mode 100644
index a20a5e9..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButtonTest.java
+++ /dev/null
@@ -1,95 +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.sizecompatui;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.verify;
-
-import android.content.res.Configuration;
-import android.testing.AndroidTestingRunner;
-import android.view.LayoutInflater;
-import android.widget.ImageButton;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.R;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.DisplayLayout;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Tests for {@link SizeCompatRestartButton}.
- *
- * Build/Install/Run:
- *  atest WMShellUnitTests:SizeCompatRestartButtonTest
- */
-@RunWith(AndroidTestingRunner.class)
-@SmallTest
-public class SizeCompatRestartButtonTest extends ShellTestCase {
-
-    private static final int TASK_ID = 1;
-
-    @Mock private SyncTransactionQueue mSyncTransactionQueue;
-    @Mock private SizeCompatUIController.SizeCompatUICallback mCallback;
-    @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
-    @Mock private DisplayLayout mDisplayLayout;
-
-    private SizeCompatUILayout mLayout;
-    private SizeCompatRestartButton mButton;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
-        mLayout = new SizeCompatUILayout(mSyncTransactionQueue, mCallback, mContext,
-                new Configuration(), TASK_ID, mTaskListener, mDisplayLayout,
-                false /* hasShownHint */);
-        mButton = (SizeCompatRestartButton)
-                LayoutInflater.from(mContext).inflate(R.layout.size_compat_ui, null);
-        mButton.inject(mLayout);
-
-        spyOn(mLayout);
-    }
-
-    @Test
-    public void testOnClick() {
-        final ImageButton button = mButton.findViewById(R.id.size_compat_restart_button);
-        button.performClick();
-
-        verify(mLayout).onRestartButtonClicked();
-        verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID);
-    }
-
-    @Test
-    public void testOnLongClick() {
-        doNothing().when(mLayout).onRestartButtonLongClicked();
-
-        final ImageButton button = mButton.findViewById(R.id.size_compat_restart_button);
-        button.performLongClick();
-
-        verify(mLayout).onRestartButtonLongClicked();
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java
deleted file mode 100644
index eb9305b..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java
+++ /dev/null
@@ -1,284 +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.sizecompatui;
-
-import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-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.times;
-import static org.mockito.Mockito.verify;
-
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.testing.AndroidTestingRunner;
-import android.view.DisplayInfo;
-import android.view.InsetsSource;
-import android.view.InsetsState;
-import android.view.SurfaceControl;
-import android.view.View;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.common.DisplayLayout;
-import com.android.wm.shell.common.SyncTransactionQueue;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Tests for {@link SizeCompatUILayout}.
- *
- * Build/Install/Run:
- *  atest WMShellUnitTests:SizeCompatUILayoutTest
- */
-@RunWith(AndroidTestingRunner.class)
-@SmallTest
-public class SizeCompatUILayoutTest extends ShellTestCase {
-
-    private static final int TASK_ID = 1;
-
-    @Mock private SyncTransactionQueue mSyncTransactionQueue;
-    @Mock private SizeCompatUIController.SizeCompatUICallback mCallback;
-    @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
-    @Mock private DisplayLayout mDisplayLayout;
-    @Mock private SizeCompatRestartButton mButton;
-    @Mock private SizeCompatHintPopup mHint;
-    private Configuration mTaskConfig;
-
-    private SizeCompatUILayout mLayout;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mTaskConfig = new Configuration();
-
-        mLayout = new SizeCompatUILayout(mSyncTransactionQueue, mCallback, mContext,
-                new Configuration(), TASK_ID, mTaskListener, new DisplayLayout(),
-                false /* hasShownHint */);
-
-        spyOn(mLayout);
-        spyOn(mLayout.mButtonWindowManager);
-        doReturn(mButton).when(mLayout.mButtonWindowManager).createSizeCompatButton();
-
-        final SizeCompatUIWindowManager hintWindowManager = mLayout.createHintWindowManager();
-        spyOn(hintWindowManager);
-        doReturn(mHint).when(hintWindowManager).createSizeCompatHint();
-        doReturn(hintWindowManager).when(mLayout).createHintWindowManager();
-    }
-
-    @Test
-    public void testCreateSizeCompatButton() {
-        // Not create button if show is false.
-        mLayout.createSizeCompatButton(false /* show */);
-
-        verify(mLayout.mButtonWindowManager, never()).createSizeCompatButton();
-        assertNull(mLayout.mButton);
-        assertNull(mLayout.mHintWindowManager);
-        assertNull(mLayout.mHint);
-
-        // Not create hint popup.
-        mLayout.mShouldShowHint = false;
-        mLayout.createSizeCompatButton(true /* show */);
-
-        verify(mLayout.mButtonWindowManager).createSizeCompatButton();
-        assertNotNull(mLayout.mButton);
-        assertNull(mLayout.mHintWindowManager);
-        assertNull(mLayout.mHint);
-
-        // Create hint popup.
-        mLayout.release();
-        mLayout.mShouldShowHint = true;
-        mLayout.createSizeCompatButton(true /* show */);
-
-        verify(mLayout.mButtonWindowManager, times(2)).createSizeCompatButton();
-        assertNotNull(mLayout.mButton);
-        assertNotNull(mLayout.mHintWindowManager);
-        verify(mLayout.mHintWindowManager).createSizeCompatHint();
-        assertNotNull(mLayout.mHint);
-        assertFalse(mLayout.mShouldShowHint);
-    }
-
-    @Test
-    public void testRelease() {
-        mLayout.createSizeCompatButton(true /* show */);
-        final SizeCompatUIWindowManager hintWindowManager = mLayout.mHintWindowManager;
-
-        mLayout.release();
-
-        assertNull(mLayout.mButton);
-        assertNull(mLayout.mHint);
-        verify(hintWindowManager).release();
-        assertNull(mLayout.mHintWindowManager);
-        verify(mLayout.mButtonWindowManager).release();
-    }
-
-    @Test
-    public void testUpdateSizeCompatInfo() {
-        mLayout.createSizeCompatButton(true /* show */);
-
-        // No diff
-        clearInvocations(mLayout);
-        mLayout.updateSizeCompatInfo(mTaskConfig, mTaskListener, true /* show */);
-
-        verify(mLayout, never()).updateButtonSurfacePosition();
-        verify(mLayout, never()).release();
-        verify(mLayout, never()).createSizeCompatButton(anyBoolean());
-
-        // Change task listener, recreate button.
-        clearInvocations(mLayout);
-        final ShellTaskOrganizer.TaskListener newTaskListener = mock(
-                ShellTaskOrganizer.TaskListener.class);
-        mLayout.updateSizeCompatInfo(mTaskConfig, newTaskListener,
-                true /* show */);
-
-        verify(mLayout).release();
-        verify(mLayout).createSizeCompatButton(anyBoolean());
-
-        // Change task bounds, update position.
-        clearInvocations(mLayout);
-        final Configuration newTaskConfiguration = new Configuration();
-        newTaskConfiguration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000));
-        mLayout.updateSizeCompatInfo(newTaskConfiguration, newTaskListener,
-                true /* show */);
-
-        verify(mLayout).updateButtonSurfacePosition();
-        verify(mLayout).updateHintSurfacePosition();
-    }
-
-    @Test
-    public void testUpdateDisplayLayout() {
-        final DisplayInfo displayInfo = new DisplayInfo();
-        displayInfo.logicalWidth = 1000;
-        displayInfo.logicalHeight = 2000;
-        final DisplayLayout displayLayout1 = new DisplayLayout(displayInfo,
-                mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false);
-
-        mLayout.updateDisplayLayout(displayLayout1);
-        verify(mLayout).updateButtonSurfacePosition();
-        verify(mLayout).updateHintSurfacePosition();
-
-        // No update if the display bounds is the same.
-        clearInvocations(mLayout);
-        final DisplayLayout displayLayout2 = new DisplayLayout(displayInfo,
-                mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false);
-        mLayout.updateDisplayLayout(displayLayout2);
-        verify(mLayout, never()).updateButtonSurfacePosition();
-        verify(mLayout, never()).updateHintSurfacePosition();
-    }
-
-    @Test
-    public void testUpdateDisplayLayoutInsets() {
-        final DisplayInfo displayInfo = new DisplayInfo();
-        displayInfo.logicalWidth = 1000;
-        displayInfo.logicalHeight = 2000;
-        final DisplayLayout displayLayout = new DisplayLayout(displayInfo,
-                mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false);
-
-        mLayout.updateDisplayLayout(displayLayout);
-        verify(mLayout).updateButtonSurfacePosition();
-        verify(mLayout).updateHintSurfacePosition();
-
-        // Update if the insets change on the existing display layout
-        clearInvocations(mLayout);
-        InsetsState insetsState = new InsetsState();
-        InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
-        insetsSource.setFrame(0, 0, 1000, 1000);
-        insetsState.addSource(insetsSource);
-        displayLayout.setInsets(mContext.getResources(), insetsState);
-        mLayout.updateDisplayLayout(displayLayout);
-        verify(mLayout).updateButtonSurfacePosition();
-        verify(mLayout).updateHintSurfacePosition();
-    }
-
-    @Test
-    public void testUpdateVisibility() {
-        // Create button if it is not created.
-        mLayout.mButton = null;
-        mLayout.updateVisibility(true /* show */);
-
-        verify(mLayout).createSizeCompatButton(true /* show */);
-
-        // Hide button.
-        clearInvocations(mLayout);
-        doReturn(View.VISIBLE).when(mButton).getVisibility();
-        mLayout.updateVisibility(false /* show */);
-
-        verify(mLayout, never()).createSizeCompatButton(anyBoolean());
-        verify(mButton).setVisibility(View.GONE);
-
-        // Show button.
-        doReturn(View.GONE).when(mButton).getVisibility();
-        mLayout.updateVisibility(true /* show */);
-
-        verify(mLayout, never()).createSizeCompatButton(anyBoolean());
-        verify(mButton).setVisibility(View.VISIBLE);
-    }
-
-    @Test
-    public void testAttachToParentSurface() {
-        final SurfaceControl.Builder b = new SurfaceControl.Builder();
-        mLayout.attachToParentSurface(b);
-
-        verify(mTaskListener).attachChildSurfaceToTask(TASK_ID, b);
-    }
-
-    @Test
-    public void testOnRestartButtonClicked() {
-        mLayout.onRestartButtonClicked();
-
-        verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID);
-    }
-
-    @Test
-    public void testOnRestartButtonLongClicked_showHint() {
-        mLayout.dismissHint();
-
-        assertNull(mLayout.mHint);
-
-        mLayout.onRestartButtonLongClicked();
-
-        assertNotNull(mLayout.mHint);
-    }
-
-    @Test
-    public void testDismissHint() {
-        mLayout.onRestartButtonLongClicked();
-        final SizeCompatUIWindowManager hintWindowManager = mLayout.mHintWindowManager;
-        assertNotNull(mLayout.mHint);
-        assertNotNull(hintWindowManager);
-
-        mLayout.dismissHint();
-
-        assertNull(mLayout.mHint);
-        assertNull(mLayout.mHintWindowManager);
-        verify(hintWindowManager).release();
-    }
-}
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index 22904a0..136fc6c 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -902,6 +902,27 @@
   return log_stream.str();
 }
 
+base::expected<uint32_t, NullOrIOError> AssetManager2::GetParentThemeResourceId(uint32_t resid)
+const {
+  auto entry = FindEntry(resid, 0u /* density_override */,
+                         false /* stop_at_first_match */,
+                         false /* ignore_configuration */);
+  if (!entry.has_value()) {
+    return base::unexpected(entry.error());
+  }
+
+  auto entry_map = std::get_if<incfs::verified_map_ptr<ResTable_map_entry>>(&entry->entry);
+  if (entry_map == nullptr) {
+    // Not a bag, nothing to do.
+    return base::unexpected(std::nullopt);
+  }
+
+  auto map = *entry_map;
+  const uint32_t parent_resid = dtohl(map->parent.ident);
+
+  return parent_resid;
+}
+
 base::expected<AssetManager2::ResourceName, NullOrIOError> AssetManager2::GetResourceName(
     uint32_t resid) const {
   auto result = FindEntry(resid, 0u /* density_override */, true /* stop_at_first_match */,
diff --git a/libs/androidfw/TEST_MAPPING b/libs/androidfw/TEST_MAPPING
index 9ebc996..8abe79d 100644
--- a/libs/androidfw/TEST_MAPPING
+++ b/libs/androidfw/TEST_MAPPING
@@ -2,6 +2,9 @@
   "presubmit": [
     {
       "name": "CtsResourcesLoaderTests"
+    },
+    {
+      "name": "libandroidfw_tests"
     }
   ]
 }
diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h
index a3b42df..1bde792 100644
--- a/libs/androidfw/include/androidfw/AssetManager2.h
+++ b/libs/androidfw/include/androidfw/AssetManager2.h
@@ -192,6 +192,12 @@
   std::unique_ptr<Asset> OpenNonAsset(const std::string& filename, ApkAssetsCookie cookie,
                                       Asset::AccessMode mode) const;
 
+  // Returns the resource id of parent style of the specified theme.
+  //
+  // Returns a null error if the name is missing/corrupt, or an I/O error if reading resource data
+  // failed.
+  base::expected<uint32_t, NullOrIOError> GetParentThemeResourceId(uint32_t resid) const;
+
   // Returns the resource name of the specified resource ID.
   //
   // Utf8 strings are preferred, and only if they are unavailable are the Utf16 variants populated.
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 147e735..7f6fb90 100755
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -460,4 +460,8 @@
             boolean register);
 
     void setTestDeviceConnectionState(in AudioDeviceAttributes device, boolean connected);
+
+    List<AudioFocusInfo> getFocusStack();
+
+    boolean sendFocusLoss(in AudioFocusInfo focusLoser, in IAudioPolicyCallback apcb);
 }
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 0f08d79..3ba1d1f 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -19,6 +19,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.annotation.UserIdInt;
@@ -230,7 +231,7 @@
          * If set to {@code true}, it is mandatory to set an
          * {@link AudioPolicy.AudioPolicyFocusListener} in order to successfully build
          * an {@code AudioPolicy} instance.
-         * @param enforce true if the policy will govern audio focus decisions.
+         * @param isFocusPolicy true if the policy will govern audio focus decisions.
          * @return the same Builder instance.
          */
         @NonNull
@@ -723,6 +724,45 @@
     }
 
     /**
+     * Returns the list of entries in the focus stack.
+     * The list is ordered with increasing rank of focus ownership, where the last entry is at the
+     * top of the focus stack and is the current focus owner.
+     * @return the ordered list of focus owners
+     * @see AudioManager#registerAudioPolicy(AudioPolicy)
+     */
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public @NonNull List<AudioFocusInfo> getFocusStack() {
+        try {
+            return getService().getFocusStack();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Send AUDIOFOCUS_LOSS to a specific stack entry, causing it to be notified of the focus
+     * loss, and for it to exit the focus stack (its focus listener will not be invoked after that).
+     * This operation is only valid for a registered policy (with
+     * {@link AudioManager#registerAudioPolicy(AudioPolicy)}) that is also set as the policy focus
+     * listener (with {@link Builder#setAudioPolicyFocusListener(AudioPolicyFocusListener)}.
+     * @param focusLoser the stack entry that is exiting the stack through a focus loss
+     * @return false if the focusLoser wasn't found in the stack, true otherwise
+     * @throws IllegalStateException if used on an unregistered policy, or a registered policy
+     *     with no {@link AudioPolicyFocusListener} set
+     * @see AudioManager#registerAudioPolicy(AudioPolicy)
+     * @see Builder#setAudioPolicyStatusListener(AudioPolicyStatusListener)
+     */
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public boolean sendFocusLoss(@NonNull AudioFocusInfo focusLoser) throws IllegalStateException {
+        Objects.requireNonNull(focusLoser);
+        try {
+            return getService().sendFocusLoss(focusLoser, cb());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Create an {@link AudioRecord} instance that is associated with the given {@link AudioMix}.
      * Audio buffers recorded through the created instance will contain the mix of the audio
      * streams that fed the given mixer.
diff --git a/media/java/android/media/tv/interactive/ITvIAppManager.aidl b/media/java/android/media/tv/interactive/ITvIAppManager.aidl
index cd87a09..9859a5f 100644
--- a/media/java/android/media/tv/interactive/ITvIAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvIAppManager.aidl
@@ -22,6 +22,7 @@
 import android.media.tv.interactive.ITvIAppManagerCallback;
 import android.media.tv.interactive.TvIAppInfo;
 import android.net.Uri;
+import android.os.Bundle;
 import android.view.Surface;
 
 /**
@@ -31,6 +32,7 @@
 interface ITvIAppManager {
     List<TvIAppInfo> getTvIAppServiceList(int userId);
     void prepare(String tiasId, int type, int userId);
+    void notifyAppLinkInfo(String tiasId, in Bundle info, int userId);
     void startIApp(in IBinder sessionToken, int userId);
     void createSession(
             in ITvIAppClient client, in String iAppServiceId, int type, int seq, int userId);
diff --git a/media/java/android/media/tv/interactive/ITvIAppService.aidl b/media/java/android/media/tv/interactive/ITvIAppService.aidl
index af15dd8..72db3fa 100644
--- a/media/java/android/media/tv/interactive/ITvIAppService.aidl
+++ b/media/java/android/media/tv/interactive/ITvIAppService.aidl
@@ -18,6 +18,7 @@
 
 import android.media.tv.interactive.ITvIAppServiceCallback;
 import android.media.tv.interactive.ITvIAppSessionCallback;
+import android.os.Bundle;
 import android.view.InputChannel;
 
 /**
@@ -31,4 +32,5 @@
     void createSession(in InputChannel channel, in ITvIAppSessionCallback callback,
             in String iAppServiceId, int type);
     void prepare(int type);
+    void notifyAppLinkInfo(in Bundle info);
 }
\ No newline at end of file
diff --git a/media/java/android/media/tv/interactive/TvIAppManager.java b/media/java/android/media/tv/interactive/TvIAppManager.java
index 2272084..8766055 100644
--- a/media/java/android/media/tv/interactive/TvIAppManager.java
+++ b/media/java/android/media/tv/interactive/TvIAppManager.java
@@ -26,6 +26,7 @@
 import android.media.tv.BroadcastInfoResponse;
 import android.media.tv.TvInputManager;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -87,6 +88,51 @@
      */
     public static final int TV_IAPP_RTE_STATE_ERROR = 4;
 
+    /**
+     * Key for package name in app link.
+     * <p>Type: String
+     *
+     * @see #notifyAppLinkInfo(String, Bundle)
+     * @hide
+     */
+    public static final String KEY_PACKAGE_NAME = "package_name";
+
+    /**
+     * Key for class name in app link.
+     * <p>Type: String
+     *
+     * @see #notifyAppLinkInfo(String, Bundle)
+     * @hide
+     */
+    public static final String KEY_CLASS_NAME = "class_name";
+
+    /**
+     * Key for URI scheme in app link.
+     * <p>Type: String
+     *
+     * @see #notifyAppLinkInfo(String, Bundle)
+     * @hide
+     */
+    public static final String KEY_URI_SCHEME = "uri_scheme";
+
+    /**
+     * Key for URI host in app link.
+     * <p>Type: String
+     *
+     * @see #notifyAppLinkInfo(String, Bundle)
+     * @hide
+     */
+    public static final String KEY_URI_HOST = "uri_host";
+
+    /**
+     * Key for URI prefix in app link.
+     * <p>Type: String
+     *
+     * @see #notifyAppLinkInfo(String, Bundle)
+     * @hide
+     */
+    public static final String KEY_URI_PREFIX = "uri_prefix";
+
     private final ITvIAppManager mService;
     private final int mUserId;
 
@@ -418,6 +464,18 @@
     }
 
     /**
+     * Notifies app link info.
+     * @hide
+     */
+    public void notifyAppLinkInfo(String tvIAppServiceId, Bundle appLinkInfo) {
+        try {
+            mService.notifyAppLinkInfo(tvIAppServiceId, appLinkInfo, mUserId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Registers a {@link TvIAppManager.TvIAppCallback}.
      *
      * @param callback A callback used to monitor status of the TV IApp services.
diff --git a/media/java/android/media/tv/interactive/TvIAppService.java b/media/java/android/media/tv/interactive/TvIAppService.java
index f93b597..6bf8028 100644
--- a/media/java/android/media/tv/interactive/TvIAppService.java
+++ b/media/java/android/media/tv/interactive/TvIAppService.java
@@ -30,6 +30,7 @@
 import android.media.tv.BroadcastInfoResponse;
 import android.net.Uri;
 import android.os.AsyncTask;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -123,6 +124,11 @@
             public void prepare(int type) {
                 onPrepare(type);
             }
+
+            @Override
+            public void notifyAppLinkInfo(Bundle appLinkInfo) {
+                onAppLinkInfo(appLinkInfo);
+            }
         };
         return tvIAppServiceBinder;
     }
@@ -135,6 +141,14 @@
         // TODO: make it abstract when unhide
     }
 
+    /**
+     * Registers App link info.
+     * @hide
+     */
+    public void onAppLinkInfo(Bundle appLinkInfo) {
+        // TODO: make it abstract when unhide
+    }
+
 
     /**
      * Returns a concrete implementation of {@link Session}.
diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java
index 94de7fa..255b391b 100644
--- a/media/java/android/media/tv/tuner/Tuner.java
+++ b/media/java/android/media/tv/tuner/Tuner.java
@@ -285,7 +285,7 @@
     @Nullable
     private FrontendInfo mFrontendInfo;
     private Integer mFrontendHandle;
-    private Boolean mIsSharedFrontend = false;
+    private Tuner mFeOwnerTuner = null;
     private int mFrontendType = FrontendSettings.TYPE_UNDEFINED;
     private int mUserId;
     private Lnb mLnb;
@@ -442,11 +442,10 @@
         mFrontendLock.lock();
         try {
             mTunerResourceManager.shareFrontend(mClientId, tuner.mClientId);
-            synchronized (mIsSharedFrontend) {
-                mFrontendHandle = tuner.mFrontendHandle;
-                mFrontend = tuner.mFrontend;
-                mIsSharedFrontend = true;
-            }
+            mFeOwnerTuner = tuner;
+            mFeOwnerTuner.registerFrontendCallbackListener(this);
+            mFrontendHandle = mFeOwnerTuner.mFrontendHandle;
+            mFrontend = mFeOwnerTuner.mFrontend;
             nativeShareFrontend(mFrontend.mId);
         } finally {
             releaseTRMSLock();
@@ -513,6 +512,27 @@
     private long mNativeContext; // used by native jMediaTuner
 
     /**
+     * Registers a tuner as a listener for frontend callbacks.
+     */
+    private void registerFrontendCallbackListener(Tuner tuner) {
+        nativeRegisterFeCbListener(tuner.getNativeContext());
+    }
+
+    /**
+     * Unregisters a tuner as a listener for frontend callbacks.
+     */
+    private void unregisterFrontendCallbackListener(Tuner tuner) {
+        nativeUnregisterFeCbListener(tuner.getNativeContext());
+    }
+
+    /**
+     * Returns the pointer to the associated JTuner.
+     */
+    long getNativeContext() {
+        return mNativeContext;
+    }
+
+    /**
      * Releases the Tuner instance.
      */
     @Override
@@ -526,19 +546,21 @@
         }
     }
 
-    private void releaseAll() {
+    private void releaseFrontend() {
         mFrontendLock.lock();
         try {
             if (mFrontendHandle != null) {
-                synchronized (mIsSharedFrontend) {
-                    if (!mIsSharedFrontend) {
-                        int res = nativeCloseFrontend(mFrontendHandle);
-                        if (res != Tuner.RESULT_SUCCESS) {
-                            TunerUtils.throwExceptionForResult(res, "failed to close frontend");
-                        }
-                        mTunerResourceManager.releaseFrontend(mFrontendHandle, mClientId);
+                if (mFeOwnerTuner != null) {
+                    // unregister self from the Frontend callback
+                    mFeOwnerTuner.unregisterFrontendCallbackListener(this);
+                    mFeOwnerTuner = null;
+                } else {
+                    // close resource as owner
+                    int res = nativeCloseFrontend(mFrontendHandle);
+                    if (res != Tuner.RESULT_SUCCESS) {
+                        TunerUtils.throwExceptionForResult(res, "failed to close frontend");
                     }
-                    mIsSharedFrontend = false;
+                    mTunerResourceManager.releaseFrontend(mFrontendHandle, mClientId);
                 }
                 FrameworkStatsLog
                         .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
@@ -549,9 +571,14 @@
         } finally {
             mFrontendLock.unlock();
         }
+    }
+
+    private void releaseAll() {
+        releaseFrontend();
 
         mLnbLock.lock();
         try {
+            // mLnb will be non-null only for owner tuner
             if (mLnb != null) {
                 mLnb.close();
             }
@@ -641,6 +668,8 @@
      */
     private native Frontend nativeOpenFrontendByHandle(int handle);
     private native int nativeShareFrontend(int id);
+    private native void nativeRegisterFeCbListener(long nativeContext);
+    private native void nativeUnregisterFeCbListener(long nativeContext);
     @Result
     private native int nativeTune(int type, FrontendSettings settings);
     private native int nativeStopTune();
@@ -1276,7 +1305,7 @@
     }
 
     private void onFrontendEvent(int eventType) {
-        Log.d(TAG, "Got event from tuning. Event type: " + eventType);
+        Log.d(TAG, "Got event from tuning. Event type: " + eventType + " for " + this);
         synchronized (mOnTuneEventLock) {
             if (mOnTuneEventExecutor != null && mOnTuneEventListener != null) {
                 mOnTuneEventExecutor.execute(() -> {
diff --git a/media/java/android/media/tv/tuner/dvr/DvrPlayback.java b/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
index cfd8583..6f3ab03 100644
--- a/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
+++ b/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
@@ -98,6 +98,7 @@
     private native void nativeSetFileDescriptor(int fd);
     private native long nativeRead(long size);
     private native long nativeRead(byte[] bytes, long offset, long size);
+    private native long nativeSeek(long pos);
 
     private DvrPlayback() {
         mUserId = Process.myUid();
@@ -243,7 +244,7 @@
      *
      * @param fd the file descriptor to read data.
      * @see #read(long)
-     * @see #read(byte[], long, long)
+     * @see #seek(long)
      */
     public void setFileDescriptor(@NonNull ParcelFileDescriptor fd) {
         nativeSetFileDescriptor(fd.getFd());
@@ -261,19 +262,30 @@
     }
 
     /**
-     * Reads data from the buffer for DVR playback and copies to the given byte array.
+     * Reads data from the buffer for DVR playback.
      *
-     * @param bytes the byte array to store the data.
-     * @param offset the index of the first byte in {@code bytes} to copy to.
+     * @param buffer the byte array where DVR reads data from.
+     * @param offset the index of the first byte in {@code buffer} to read.
      * @param size the maximum number of bytes to read.
      * @return the number of bytes read.
      */
     @BytesLong
-    public long read(@NonNull byte[] bytes, @BytesLong long offset, @BytesLong long size) {
-        if (size + offset > bytes.length) {
+    public long read(@NonNull byte[] buffer, @BytesLong long offset, @BytesLong long size) {
+        if (size + offset > buffer.length) {
             throw new ArrayIndexOutOfBoundsException(
-                    "Array length=" + bytes.length + ", offset=" + offset + ", size=" + size);
+                    "Array length=" + buffer.length + ", offset=" + offset + ", size=" + size);
         }
-        return nativeRead(bytes, offset, size);
+        return nativeRead(buffer, offset, size);
+    }
+
+    /**
+     * Sets the file pointer offset of the file descriptor.
+     *
+     * @param pos the offset position, measured in bytes from the beginning of the file.
+     * @return the new offset position.
+     */
+    @BytesLong
+    public long seek(@BytesLong long pos) {
+        return nativeSeek(pos);
     }
 }
diff --git a/media/java/android/media/tv/tuner/dvr/DvrRecorder.java b/media/java/android/media/tv/tuner/dvr/DvrRecorder.java
index 212a713..e72026a 100644
--- a/media/java/android/media/tv/tuner/dvr/DvrRecorder.java
+++ b/media/java/android/media/tv/tuner/dvr/DvrRecorder.java
@@ -216,7 +216,6 @@
      *
      * @param fd the file descriptor to write data.
      * @see #write(long)
-     * @see #write(byte[], long, long)
      */
     public void setFileDescriptor(@NonNull ParcelFileDescriptor fd) {
         nativeSetFileDescriptor(fd.getFd());
@@ -236,17 +235,17 @@
     /**
      * Writes recording data to buffer.
      *
-     * @param bytes the byte array stores the data to be written to DVR.
-     * @param offset the index of the first byte in {@code bytes} to be written to DVR.
+     * @param buffer the byte array stores the data from DVR.
+     * @param offset the index of the first byte in {@code buffer} to write the data from DVR.
      * @param size the maximum number of bytes to write.
      * @return the number of bytes written.
      */
     @BytesLong
-    public long write(@NonNull byte[] bytes, @BytesLong long offset, @BytesLong long size) {
-        if (size + offset > bytes.length) {
+    public long write(@NonNull byte[] buffer, @BytesLong long offset, @BytesLong long size) {
+        if (size + offset > buffer.length) {
             throw new ArrayIndexOutOfBoundsException(
-                    "Array length=" + bytes.length + ", offset=" + offset + ", size=" + size);
+                    "Array length=" + buffer.length + ", offset=" + offset + ", size=" + size);
         }
-        return nativeWrite(bytes, offset, size);
+        return nativeWrite(buffer, offset, size);
     }
 }
diff --git a/media/java/android/media/tv/tuner/filter/DownloadEvent.java b/media/java/android/media/tv/tuner/filter/DownloadEvent.java
index 394211be..25989db 100644
--- a/media/java/android/media/tv/tuner/filter/DownloadEvent.java
+++ b/media/java/android/media/tv/tuner/filter/DownloadEvent.java
@@ -27,15 +27,17 @@
 @SystemApi
 public class DownloadEvent extends FilterEvent {
     private final int mItemId;
+    private final int mDownloadId;
     private final int mMpuSequenceNumber;
     private final int mItemFragmentIndex;
     private final int mLastItemFragmentIndex;
     private final int mDataLength;
 
     // This constructor is used by JNI code only
-    private DownloadEvent(int itemId, int mpuSequenceNumber, int itemFragmentIndex,
+    private DownloadEvent(int itemId, int downloadId, int mpuSequenceNumber, int itemFragmentIndex,
             int lastItemFragmentIndex, int dataLength) {
         mItemId = itemId;
+        mDownloadId = downloadId;
         mMpuSequenceNumber = mpuSequenceNumber;
         mItemFragmentIndex = itemFragmentIndex;
         mLastItemFragmentIndex = lastItemFragmentIndex;
@@ -50,6 +52,15 @@
     }
 
     /**
+     * Gets download ID.
+     *
+     * <p>This query is only supported in Tuner 2.0 or higher version. Unsupported version will
+     * return {@code -1}.
+     * Use {@link TunerVersionChecker#getTunerVersion()} to get the version information.
+     */
+    public int getDownloadId() { return mDownloadId; }
+
+    /**
      * Gets MPU sequence number of filtered data.
      */
     @IntRange(from = 0)
@@ -80,4 +91,3 @@
         return mDataLength;
     }
 }
-
diff --git a/media/java/android/media/tv/tuner/filter/DownloadSettings.java b/media/java/android/media/tv/tuner/filter/DownloadSettings.java
index 7ba923e..e2cfd7c 100644
--- a/media/java/android/media/tv/tuner/filter/DownloadSettings.java
+++ b/media/java/android/media/tv/tuner/filter/DownloadSettings.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.media.tv.tuner.TunerUtils;
+import android.media.tv.tuner.TunerVersionChecker;
 
 /**
  * Filter Settings for a Download.
@@ -27,10 +28,12 @@
  */
 @SystemApi
 public class DownloadSettings extends Settings {
+    private final boolean mUseDownloadId;
     private final int mDownloadId;
 
-    private DownloadSettings(int mainType, int downloadId) {
+    private DownloadSettings(int mainType, boolean useDownloadId, int downloadId) {
         super(TunerUtils.getFilterSubtype(mainType, Filter.SUBTYPE_DOWNLOAD));
+        mUseDownloadId = useDownloadId;
         mDownloadId = downloadId;
     }
 
@@ -42,6 +45,15 @@
     }
 
     /**
+     * Gets whether download ID is used.
+     *
+     * <p>This query is only supported in Tuner 2.0 or higher version. Unsupported version will
+     * return {@code false}.
+     * Use {@link TunerVersionChecker#getTunerVersion()} to get the version information.
+     */
+    public boolean useDownloadId() { return mUseDownloadId; }
+
+    /**
      * Creates a builder for {@link DownloadSettings}.
      *
      * @param mainType the filter main type.
@@ -56,6 +68,7 @@
      */
     public static class Builder {
         private final int mMainType;
+        private boolean mUseDownloadId = false;
         private int mDownloadId;
 
         private Builder(int mainType) {
@@ -63,6 +76,24 @@
         }
 
         /**
+         * Sets whether download ID is used or not.
+         *
+         * <p>This configuration is only supported in Tuner 2.0 or higher version. Unsupported
+         * version will cause no-op. Use {@link TunerVersionChecker#getTunerVersion()} to get the
+         * version information.
+         *
+         * <p>Default value is {@code false}.
+         */
+        @NonNull
+        public Builder setUseDownloadId(boolean useDownloadId) {
+            if (TunerVersionChecker.checkHigherOrEqualVersionTo(
+                        TunerVersionChecker.TUNER_VERSION_2_0, "setUseDownloadId")) {
+                mUseDownloadId = useDownloadId;
+            }
+            return this;
+        }
+
+        /**
          * Sets download ID.
          */
         @NonNull
@@ -76,7 +107,7 @@
          */
         @NonNull
         public DownloadSettings build() {
-            return new DownloadSettings(mMainType, mDownloadId);
+            return new DownloadSettings(mMainType, mUseDownloadId, mDownloadId);
         }
     }
 }
diff --git a/media/java/android/media/tv/tuner/filter/MediaEvent.java b/media/java/android/media/tv/tuner/filter/MediaEvent.java
index dbd85e9..79d4062 100644
--- a/media/java/android/media/tv/tuner/filter/MediaEvent.java
+++ b/media/java/android/media/tv/tuner/filter/MediaEvent.java
@@ -40,6 +40,8 @@
     private final int mStreamId;
     private final boolean mIsPtsPresent;
     private final long mPts;
+    private final boolean mIsDtsPresent;
+    private final long mDts;
     private final long mDataLength;
     private final long mOffset;
     private LinearBlock mLinearBlock;
@@ -50,12 +52,14 @@
     private final AudioDescriptor mExtraMetaData;
 
     // This constructor is used by JNI code only
-    private MediaEvent(int streamId, boolean isPtsPresent, long pts, long dataLength, long offset,
-            LinearBlock buffer, boolean isSecureMemory, long dataId, int mpuSequenceNumber,
-            boolean isPrivateData, AudioDescriptor extraMetaData) {
+    private MediaEvent(int streamId, boolean isPtsPresent, long pts, boolean isDtsPresent, long dts,
+            long dataLength, long offset, LinearBlock buffer, boolean isSecureMemory, long dataId,
+            int mpuSequenceNumber, boolean isPrivateData, AudioDescriptor extraMetaData) {
         mStreamId = streamId;
         mIsPtsPresent = isPtsPresent;
         mPts = pts;
+        mIsDtsPresent = isDtsPresent;
+        mDts = dts;
         mDataLength = dataLength;
         mOffset = offset;
         mLinearBlock = buffer;
@@ -90,6 +94,26 @@
     }
 
     /**
+     * Returns whether DTS (Decode Time Stamp) is present.
+     *
+     * <p>This query is only supported in Tuner 2.0 or higher version. Unsupported version will
+     * return {@code false}.
+     * Use {@link TunerVersionChecker#getTunerVersion()} to get the version information.
+     *
+     * @return {@code true} if DTS is present in PES header; {@code false} otherwise.
+     */
+    public boolean isDtsPresent() { return mIsDtsPresent; }
+
+    /**
+     * Gets DTS (Decode Time Stamp) for audio or video frame.
+     *
+     * * <p>This query is only supported in Tuner 2.0 or higher version. Unsupported version will
+     * return {@code -1}.
+     * Use {@link TunerVersionChecker#getTunerVersion()} to get the version information.
+     */
+    public long getDts() { return mDts; }
+
+    /**
      * Gets data size in bytes of audio or video frame.
      */
     @BytesLong
diff --git a/media/java/android/media/tv/tuner/filter/SectionEvent.java b/media/java/android/media/tv/tuner/filter/SectionEvent.java
index ff12492..182bb94 100644
--- a/media/java/android/media/tv/tuner/filter/SectionEvent.java
+++ b/media/java/android/media/tv/tuner/filter/SectionEvent.java
@@ -28,10 +28,10 @@
     private final int mTableId;
     private final int mVersion;
     private final int mSectionNum;
-    private final int mDataLength;
+    private final long mDataLength;
 
     // This constructor is used by JNI code only
-    private SectionEvent(int tableId, int version, int sectionNum, int dataLength) {
+    private SectionEvent(int tableId, int version, int sectionNum, long dataLength) {
         mTableId = tableId;
         mVersion = version;
         mSectionNum = sectionNum;
@@ -61,8 +61,13 @@
 
     /**
      * Gets data size in bytes of filtered data.
+     *
+     * @deprecated Use {@link #getDataLengthLong()}
      */
+    @Deprecated
     public int getDataLength() {
-        return mDataLength;
+        return (int) getDataLengthLong();
     }
+
+    public long getDataLengthLong() { return mDataLength; }
 }
diff --git a/media/java/android/media/tv/tuner/frontend/FrontendStatus.java b/media/java/android/media/tv/tuner/frontend/FrontendStatus.java
index 31f1a63..582e4f5 100644
--- a/media/java/android/media/tv/tuner/frontend/FrontendStatus.java
+++ b/media/java/android/media/tv/tuner/frontend/FrontendStatus.java
@@ -19,10 +19,10 @@
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.media.tv.tuner.Lnb;
 import android.media.tv.tuner.TunerVersionChecker;
-
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -53,7 +53,7 @@
             FRONTEND_STATUS_TYPE_MODULATIONS_EXT, FRONTEND_STATUS_TYPE_ROLL_OFF,
             FRONTEND_STATUS_TYPE_IS_MISO_ENABLED, FRONTEND_STATUS_TYPE_IS_LINEAR,
             FRONTEND_STATUS_TYPE_IS_SHORT_FRAMES_ENABLED, FRONTEND_STATUS_TYPE_ISDBT_MODE,
-            FRONTEND_STATUS_TYPE_ISDBT_PARTIAL_RECEPTION_FLAG})
+            FRONTEND_STATUS_TYPE_ISDBT_PARTIAL_RECEPTION_FLAG, FRONTEND_STATUS_TYPE_STREAM_ID_LIST})
     @Retention(RetentionPolicy.SOURCE)
     public @interface FrontendStatusType {}
 
@@ -254,6 +254,12 @@
     public static final int FRONTEND_STATUS_TYPE_ISDBT_PARTIAL_RECEPTION_FLAG =
             android.hardware.tv.tuner.FrontendStatusType.ISDBT_PARTIAL_RECEPTION_FLAG;
 
+    /**
+     * Stream ID list included in a transponder.
+     */
+    public static final int FRONTEND_STATUS_TYPE_STREAM_ID_LIST =
+            android.hardware.tv.tuner.FrontendStatusType.STREAM_ID_LIST;
+
     /** @hide */
     @IntDef(value = {
             AtscFrontendSettings.MODULATION_UNDEFINED,
@@ -493,6 +499,7 @@
     private Boolean mIsShortFrames;
     private Integer mIsdbtMode;
     private Integer mIsdbtPartialReceptionFlag;
+    private int[] mStreamIds;
 
     // Constructed and fields set by JNI code.
     private FrontendStatus() {
@@ -1001,6 +1008,24 @@
     }
 
     /**
+     * Gets stream id list included in a transponder.
+     *
+     * <p>This query is only supported by Tuner HAL 2.0 or higher. Unsupported version or if HAL
+     * doesn't return stream id list status will throw IllegalStateException. Use
+     * {@link TunerVersionChecker#getTunerVersion()} to check the version.
+     */
+    @SuppressLint("ArrayReturn")
+    @NonNull
+    public int[] getStreamIdList() {
+        TunerVersionChecker.checkHigherOrEqualVersionTo(
+                TunerVersionChecker.TUNER_VERSION_2_0, "stream id list status");
+        if (mStreamIds == null) {
+            throw new IllegalStateException("stream id list status is empty");
+        }
+        return mStreamIds;
+    }
+
+    /**
      * Information of each tuning Physical Layer Pipes.
      */
     public static class Atsc3PlpTuningInfo {
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index ded9652..e91e238 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -588,13 +588,13 @@
                                                const DemuxFilterEvent &event) {
     JNIEnv *env = AndroidRuntime::getJNIEnv();
     jclass eventClazz = env->FindClass("android/media/tv/tuner/filter/SectionEvent");
-    jmethodID eventInit = env->GetMethodID(eventClazz, "<init>", "(IIII)V");
+    jmethodID eventInit = env->GetMethodID(eventClazz, "<init>", "(IIIJ)V");
 
     const DemuxFilterSectionEvent &sectionEvent = event.get<DemuxFilterEvent::Tag::section>();
     jint tableId = sectionEvent.tableId;
     jint version = sectionEvent.version;
     jint sectionNum = sectionEvent.sectionNum;
-    jint dataLength = sectionEvent.dataLength;
+    jlong dataLength = sectionEvent.dataLength;
 
     jobject obj = env->NewObject(eventClazz, eventInit, tableId, version, sectionNum, dataLength);
     env->SetObjectArrayElement(arr, size, obj);
@@ -604,10 +604,9 @@
                                              const DemuxFilterEvent &event) {
     JNIEnv *env = AndroidRuntime::getJNIEnv();
     jclass eventClazz = env->FindClass("android/media/tv/tuner/filter/MediaEvent");
-    jmethodID eventInit = env->GetMethodID(eventClazz,
-            "<init>",
-            "(IZJJJLandroid/media/MediaCodec$LinearBlock;"
-            "ZJIZLandroid/media/tv/tuner/filter/AudioDescriptor;)V");
+    jmethodID eventInit = env->GetMethodID(eventClazz, "<init>",
+                                           "(IZJZJJJLandroid/media/MediaCodec$LinearBlock;"
+                                           "ZJIZLandroid/media/tv/tuner/filter/AudioDescriptor;)V");
     jfieldID eventContext = env->GetFieldID(eventClazz, "mNativeContext", "J");
 
     const DemuxFilterMediaEvent &mediaEvent = event.get<DemuxFilterEvent::Tag::media>();
@@ -633,15 +632,17 @@
     jint streamId = mediaEvent.streamId;
     jboolean isPtsPresent = mediaEvent.isPtsPresent;
     jlong pts = mediaEvent.pts;
+    jboolean isDtsPresent = mediaEvent.isDtsPresent;
+    jlong dts = mediaEvent.dts;
     jlong offset = mediaEvent.offset;
     jboolean isSecureMemory = mediaEvent.isSecureMemory;
     jlong avDataId = mediaEvent.avDataId;
     jint mpuSequenceNumber = mediaEvent.mpuSequenceNumber;
     jboolean isPesPrivateData = mediaEvent.isPesPrivateData;
 
-    jobject obj = env->NewObject(eventClazz, eventInit, streamId, isPtsPresent, pts, dataLength,
-                                 offset, nullptr, isSecureMemory, avDataId, mpuSequenceNumber,
-                                 isPesPrivateData, audioDescriptor);
+    jobject obj = env->NewObject(eventClazz, eventInit, streamId, isPtsPresent, pts, isDtsPresent,
+                                 dts, dataLength, offset, nullptr, isSecureMemory, avDataId,
+                                 mpuSequenceNumber, isPesPrivateData, audioDescriptor);
 
     uint64_t avSharedMemSize = mFilterClient->getAvSharedHandleInfo().size;
     if (mediaEvent.avMemory.fds.size() > 0 || mediaEvent.avDataId != 0 ||
@@ -733,16 +734,17 @@
                                                 const DemuxFilterEvent &event) {
     JNIEnv *env = AndroidRuntime::getJNIEnv();
     jclass eventClazz = env->FindClass("android/media/tv/tuner/filter/DownloadEvent");
-    jmethodID eventInit = env->GetMethodID(eventClazz, "<init>", "(IIIII)V");
+    jmethodID eventInit = env->GetMethodID(eventClazz, "<init>", "(IIIIII)V");
 
     const DemuxFilterDownloadEvent &downloadEvent = event.get<DemuxFilterEvent::Tag::download>();
     jint itemId = downloadEvent.itemId;
+    jint downloadId = downloadEvent.downloadId;
     jint mpuSequenceNumber = downloadEvent.mpuSequenceNumber;
     jint itemFragmentIndex = downloadEvent.itemFragmentIndex;
     jint lastItemFragmentIndex = downloadEvent.lastItemFragmentIndex;
     jint dataLength = downloadEvent.dataLength;
 
-    jobject obj = env->NewObject(eventClazz, eventInit, itemId, mpuSequenceNumber,
+    jobject obj = env->NewObject(eventClazz, eventInit, itemId, downloadId, mpuSequenceNumber,
                                  itemFragmentIndex, lastItemFragmentIndex, dataLength);
     env->SetObjectArrayElement(arr, size, obj);
 }
@@ -951,20 +953,45 @@
 }
 
 /////////////// FrontendClientCallbackImpl ///////////////////////
-FrontendClientCallbackImpl::FrontendClientCallbackImpl(jweak tunerObj) : mObject(tunerObj) {}
+FrontendClientCallbackImpl::FrontendClientCallbackImpl(JTuner* jtuner, jweak listener) {
+    ALOGV("FrontendClientCallbackImpl() with listener:%p", listener);
+    addCallbackListener(jtuner, listener);
+}
+
+void FrontendClientCallbackImpl::addCallbackListener(JTuner* jtuner, jweak listener) {
+    JNIEnv *env = AndroidRuntime::getJNIEnv();
+    jweak listenerRef = env->NewWeakGlobalRef(listener);
+    ALOGV("addCallbackListener() with listener:%p and ref:%p @%p", listener, listenerRef, this);
+    std::scoped_lock<std::mutex> lock(mMutex);
+    mListenersMap[jtuner] = listenerRef;
+}
+
+void FrontendClientCallbackImpl::removeCallbackListener(JTuner* listener) {
+    ALOGV("removeCallbackListener for listener:%p", listener);
+    JNIEnv *env = AndroidRuntime::getJNIEnv();
+    std::scoped_lock<std::mutex> lock(mMutex);
+    if (mListenersMap.find(listener) != mListenersMap.end() && mListenersMap[listener]) {
+        env->DeleteWeakGlobalRef(mListenersMap[listener]);
+        mListenersMap.erase(listener);
+    }
+}
 
 void FrontendClientCallbackImpl::onEvent(FrontendEventType frontendEventType) {
     ALOGV("FrontendClientCallbackImpl::onEvent, type=%d", frontendEventType);
     JNIEnv *env = AndroidRuntime::getJNIEnv();
-    jobject frontend(env->NewLocalRef(mObject));
-    if (!env->IsSameObject(frontend, nullptr)) {
-        env->CallVoidMethod(
-                frontend,
-                gFields.onFrontendEventID,
-                (jint)frontendEventType);
-    } else {
-        ALOGE("FrontendClientCallbackImpl::onEvent:"
-                "Frontend object has been freed. Ignoring callback.");
+    std::scoped_lock<std::mutex> lock(mMutex);
+    for (const auto& mapEntry : mListenersMap) {
+        ALOGV("JTuner:%p, jweak:%p", mapEntry.first, mapEntry.second);
+        jobject frontend(env->NewLocalRef(mapEntry.second));
+        if (!env->IsSameObject(frontend, nullptr)) {
+            env->CallVoidMethod(
+                    frontend,
+                    gFields.onFrontendEventID,
+                    (jint)frontendEventType);
+        } else {
+            ALOGW("FrontendClientCallbackImpl::onEvent:"
+                    "Frontend object has been freed. Ignoring callback.");
+        }
     }
 }
 
@@ -973,12 +1000,25 @@
     ALOGV("FrontendClientCallbackImpl::onScanMessage, type=%d", type);
     JNIEnv *env = AndroidRuntime::getJNIEnv();
     jclass clazz = env->FindClass("android/media/tv/tuner/Tuner");
-    jobject frontend(env->NewLocalRef(mObject));
-    if (env->IsSameObject(frontend, nullptr)) {
-        ALOGE("FrontendClientCallbackImpl::onScanMessage:"
-                "Frontend object has been freed. Ignoring callback.");
-        return;
+
+    std::scoped_lock<std::mutex> lock(mMutex);
+    for (const auto& mapEntry : mListenersMap) {
+        jobject frontend(env->NewLocalRef(mapEntry.second));
+        if (env->IsSameObject(frontend, nullptr)) {
+            ALOGE("FrontendClientCallbackImpl::onScanMessage:"
+                    "Tuner object has been freed. Ignoring callback.");
+            continue;
+        }
+        executeOnScanMessage(env, clazz, frontend, type, message);
     }
+}
+
+void FrontendClientCallbackImpl::executeOnScanMessage(
+         JNIEnv *env, const jclass& clazz, const jobject& frontend,
+         FrontendScanMessageType type,
+         const FrontendScanMessage& message) {
+    ALOGV("FrontendClientCallbackImpl::executeOnScanMessage, type=%d", type);
+
     switch(type) {
         case FrontendScanMessageType::LOCKED: {
             if (message.get<FrontendScanMessage::Tag::isLocked>()) {
@@ -1157,11 +1197,14 @@
 }
 
 FrontendClientCallbackImpl::~FrontendClientCallbackImpl() {
-    JNIEnv *env = AndroidRuntime::getJNIEnv();
-    if (mObject != nullptr) {
-        env->DeleteWeakGlobalRef(mObject);
-        mObject = nullptr;
+    JNIEnv *env = android::AndroidRuntime::getJNIEnv();
+    ALOGV("~FrontendClientCallbackImpl()");
+    std::scoped_lock<std::mutex> lock(mMutex);
+    for (const auto& mapEntry : mListenersMap) {
+        ALOGV("deleteRef :%p at @ %p", mapEntry.second, this);
+        env->DeleteWeakGlobalRef(mapEntry.second);
     }
+    mListenersMap.clear();
 }
 
 /////////////// Tuner ///////////////////////
@@ -1180,6 +1223,10 @@
     mSharedFeId = (int)Constant::INVALID_FRONTEND_ID;
 }
 
+jweak JTuner::getObject() {
+    return mObject;
+}
+
 JTuner::~JTuner() {
     if (mFeClient != nullptr) {
         mFeClient->close();
@@ -1192,6 +1239,7 @@
     env->DeleteWeakGlobalRef(mObject);
     env->DeleteGlobalRef(mClass);
     mFeClient = nullptr;
+    mFeClientCb = nullptr;
     mDemuxClient = nullptr;
     mClass = nullptr;
     mObject = nullptr;
@@ -1247,9 +1295,8 @@
         return nullptr;
     }
 
-    sp<FrontendClientCallbackImpl> feClientCb =
-            new FrontendClientCallbackImpl(env->NewWeakGlobalRef(mObject));
-    mFeClient->setCallback(feClientCb);
+    mFeClientCb = new FrontendClientCallbackImpl(this, mObject);
+    mFeClient->setCallback(mFeClientCb);
     // TODO: add more fields to frontend
     return env->NewObject(
             env->FindClass("android/media/tv/tuner/Tuner$Frontend"),
@@ -1269,6 +1316,18 @@
     return (int)Result::SUCCESS;
 }
 
+void JTuner::registerFeCbListener(JTuner* jtuner) {
+    if (mFeClientCb != nullptr && jtuner != nullptr) {
+        mFeClientCb->addCallbackListener(jtuner, jtuner->getObject());
+    }
+}
+
+void JTuner::unregisterFeCbListener(JTuner* jtuner) {
+    if (mFeClientCb != nullptr && jtuner != nullptr) {
+        mFeClientCb->removeCallbackListener(jtuner);
+    }
+}
+
 jobject JTuner::getAnalogFrontendCaps(JNIEnv *env, FrontendCapabilities &caps) {
     jclass clazz = env->FindClass("android/media/tv/tuner/frontend/AnalogFrontendCapabilities");
     jmethodID capsInit = env->GetMethodID(clazz, "<init>", "(II)V");
@@ -2450,7 +2509,14 @@
                 env->SetObjectField(statusObj, field, newIntegerObj);
                 break;
             }
-            default: {
+            case FrontendStatus::Tag::streamIdList: {
+                jfieldID field = env->GetFieldID(clazz, "mStreamIds", "[I");
+                std::vector<int32_t> ids = s.get<FrontendStatus::Tag::streamIdList>();
+
+                jintArray valObj = env->NewIntArray(v.size());
+                env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&ids[0]));
+
+                env->SetObjectField(statusObj, field, valObj);
                 break;
             }
         }
@@ -3188,6 +3254,20 @@
     return tuner->shareFrontend(id);
 }
 
+static void android_media_tv_Tuner_register_fe_cb_listener(
+        JNIEnv *env, jobject thiz, jlong shareeJTuner) {
+    sp<JTuner> tuner = getTuner(env, thiz);
+    JTuner *jtuner = (JTuner *)shareeJTuner;
+    tuner->registerFeCbListener(jtuner);
+}
+
+static void android_media_tv_Tuner_unregister_fe_cb_listener(
+        JNIEnv *env, jobject thiz, jlong shareeJTuner) {
+    sp<JTuner> tuner = getTuner(env, thiz);
+    JTuner *jtuner = (JTuner *)shareeJTuner;
+    tuner->unregisterFeCbListener(jtuner);
+}
+
 static int android_media_tv_Tuner_tune(JNIEnv *env, jobject thiz, jint type, jobject settings) {
     sp<JTuner> tuner = getTuner(env, thiz);
     FrontendSettings setting = getFrontendSettings(env, type, settings);
@@ -3469,10 +3549,13 @@
 
 static DemuxFilterDownloadSettings getFilterDownloadSettings(JNIEnv *env, const jobject& settings) {
     jclass clazz = env->FindClass("android/media/tv/tuner/filter/DownloadSettings");
+    bool useDownloadId =
+            env->GetBooleanField(settings, env->GetFieldID(clazz, "mUseDownloadId", "Z"));
     int32_t downloadId = env->GetIntField(settings, env->GetFieldID(clazz, "mDownloadId", "I"));
 
-    DemuxFilterDownloadSettings filterDownloadSettings {
-        .downloadId = downloadId,
+    DemuxFilterDownloadSettings filterDownloadSettings{
+            .useDownloadId = useDownloadId,
+            .downloadId = downloadId,
     };
     return filterDownloadSettings;
 }
@@ -4253,6 +4336,17 @@
     return (jlong)dvrClient->readFromFile(size);
 }
 
+static jlong android_media_tv_Tuner_seek_dvr(JNIEnv *env, jobject dvr, jlong pos) {
+    sp<DvrClient> dvrClient = getDvrClient(env, dvr);
+    if (dvrClient == nullptr) {
+        jniThrowException(env, "java/lang/IllegalStateException",
+                          "Failed to seek dvr: dvr client not found");
+        return -1;
+    }
+
+    return (jlong)dvrClient->seekFile(pos);
+}
+
 static jlong android_media_tv_Tuner_read_dvr_from_array(
         JNIEnv* env, jobject dvr, jbyteArray buffer, jlong offset, jlong size) {
     sp<DvrClient> dvrClient = getDvrClient(env, dvr);
@@ -4361,6 +4455,10 @@
             (void *)android_media_tv_Tuner_open_frontend_by_handle },
     { "nativeShareFrontend", "(I)I",
             (void *)android_media_tv_Tuner_share_frontend },
+    { "nativeRegisterFeCbListener", "(J)V",
+            (void*)android_media_tv_Tuner_register_fe_cb_listener },
+    { "nativeUnregisterFeCbListener", "(J)V",
+            (void*)android_media_tv_Tuner_unregister_fe_cb_listener },
     { "nativeTune", "(ILandroid/media/tv/tuner/frontend/FrontendSettings;)I",
             (void *)android_media_tv_Tuner_tune },
     { "nativeStopTune", "()I", (void *)android_media_tv_Tuner_stop_tune },
@@ -4403,38 +4501,37 @@
     { "nativeClose", "()I", (void *)android_media_tv_Tuner_close_tuner },
     { "nativeCloseFrontend", "(I)I", (void *)android_media_tv_Tuner_close_frontend },
     { "nativeCloseDemux", "(I)I", (void *)android_media_tv_Tuner_close_demux },
-    {"nativeOpenSharedFilter",
+    { "nativeOpenSharedFilter",
             "(Ljava/lang/String;)Landroid/media/tv/tuner/filter/SharedFilter;",
             (void *)android_media_tv_Tuner_open_shared_filter},
 };
 
 static const JNINativeMethod gFilterMethods[] = {
     { "nativeConfigureFilter", "(IILandroid/media/tv/tuner/filter/FilterConfiguration;)I",
-            (void *)android_media_tv_Tuner_configure_filter },
-    { "nativeGetId", "()I", (void *)android_media_tv_Tuner_get_filter_id },
-    { "nativeGetId64Bit", "()J",
-            (void *)android_media_tv_Tuner_get_filter_64bit_id },
+            (void *)android_media_tv_Tuner_configure_filter},
+    { "nativeGetId", "()I", (void *)android_media_tv_Tuner_get_filter_id},
+    { "nativeGetId64Bit", "()J", (void *)android_media_tv_Tuner_get_filter_64bit_id},
     { "nativeConfigureMonitorEvent", "(I)I",
-            (void *)android_media_tv_Tuner_configure_monitor_event },
+            (void *)android_media_tv_Tuner_configure_monitor_event},
     { "nativeSetDataSource", "(Landroid/media/tv/tuner/filter/Filter;)I",
-            (void *)android_media_tv_Tuner_set_filter_data_source },
-    { "nativeStartFilter", "()I", (void *)android_media_tv_Tuner_start_filter },
-    { "nativeStopFilter", "()I", (void *)android_media_tv_Tuner_stop_filter },
-    { "nativeFlushFilter", "()I", (void *)android_media_tv_Tuner_flush_filter },
-    { "nativeRead", "([BJJ)I", (void *)android_media_tv_Tuner_read_filter_fmq },
-    { "nativeClose", "()I", (void *)android_media_tv_Tuner_close_filter },
-    {"nativeAcquireSharedFilterToken", "()Ljava/lang/String;",
+            (void *)android_media_tv_Tuner_set_filter_data_source},
+    { "nativeStartFilter", "()I", (void *)android_media_tv_Tuner_start_filter},
+    { "nativeStopFilter", "()I", (void *)android_media_tv_Tuner_stop_filter},
+    { "nativeFlushFilter", "()I", (void *)android_media_tv_Tuner_flush_filter},
+    { "nativeRead", "([BJJ)I", (void *)android_media_tv_Tuner_read_filter_fmq},
+    { "nativeClose", "()I", (void *)android_media_tv_Tuner_close_filter},
+    { "nativeAcquireSharedFilterToken", "()Ljava/lang/String;",
             (void *)android_media_tv_Tuner_acquire_shared_filter_token},
-    {"nativeFreeSharedFilterToken", "(Ljava/lang/String;)V",
+    { "nativeFreeSharedFilterToken", "(Ljava/lang/String;)V",
             (void *)android_media_tv_Tuner_free_shared_filter_token},
 };
 
 static const JNINativeMethod gSharedFilterMethods[] = {
-    {"nativeStartSharedFilter", "()I", (void *)android_media_tv_Tuner_start_filter},
-    {"nativeStopSharedFilter", "()I", (void *)android_media_tv_Tuner_stop_filter},
-    {"nativeFlushSharedFilter", "()I", (void *)android_media_tv_Tuner_flush_filter},
-    {"nativeSharedRead", "([BJJ)I", (void *)android_media_tv_Tuner_read_filter_fmq},
-    {"nativeSharedClose", "()I", (void *)android_media_tv_Tuner_close_filter},
+    { "nativeStartSharedFilter", "()I", (void *)android_media_tv_Tuner_start_filter},
+    { "nativeStopSharedFilter", "()I", (void *)android_media_tv_Tuner_stop_filter},
+    { "nativeFlushSharedFilter", "()I", (void *)android_media_tv_Tuner_flush_filter},
+    { "nativeSharedRead", "([BJJ)I", (void *)android_media_tv_Tuner_read_filter_fmq},
+    { "nativeSharedClose", "()I", (void *)android_media_tv_Tuner_close_filter},
 };
 
 static const JNINativeMethod gTimeFilterMethods[] = {
@@ -4474,18 +4571,19 @@
 
 static const JNINativeMethod gDvrPlaybackMethods[] = {
     { "nativeAttachFilter", "(Landroid/media/tv/tuner/filter/Filter;)I",
-            (void *)android_media_tv_Tuner_attach_filter },
+            (void *)android_media_tv_Tuner_attach_filter},
     { "nativeDetachFilter", "(Landroid/media/tv/tuner/filter/Filter;)I",
-            (void *)android_media_tv_Tuner_detach_filter },
+            (void *)android_media_tv_Tuner_detach_filter},
     { "nativeConfigureDvr", "(Landroid/media/tv/tuner/dvr/DvrSettings;)I",
-            (void *)android_media_tv_Tuner_configure_dvr },
-    { "nativeStartDvr", "()I", (void *)android_media_tv_Tuner_start_dvr },
-    { "nativeStopDvr", "()I", (void *)android_media_tv_Tuner_stop_dvr },
-    { "nativeFlushDvr", "()I", (void *)android_media_tv_Tuner_flush_dvr },
-    { "nativeClose", "()I", (void *)android_media_tv_Tuner_close_dvr },
-    { "nativeSetFileDescriptor", "(I)V", (void *)android_media_tv_Tuner_dvr_set_fd },
-    { "nativeRead", "(J)J", (void *)android_media_tv_Tuner_read_dvr },
-    { "nativeRead", "([BJJ)J", (void *)android_media_tv_Tuner_read_dvr_from_array },
+            (void *)android_media_tv_Tuner_configure_dvr},
+    { "nativeStartDvr", "()I", (void *)android_media_tv_Tuner_start_dvr},
+    { "nativeStopDvr", "()I", (void *)android_media_tv_Tuner_stop_dvr},
+    { "nativeFlushDvr", "()I", (void *)android_media_tv_Tuner_flush_dvr},
+    { "nativeClose", "()I", (void *)android_media_tv_Tuner_close_dvr},
+    { "nativeSetFileDescriptor", "(I)V", (void *)android_media_tv_Tuner_dvr_set_fd},
+    { "nativeRead", "(J)J", (void *)android_media_tv_Tuner_read_dvr},
+    { "nativeRead", "([BJJ)J", (void *)android_media_tv_Tuner_read_dvr_from_array},
+    { "nativeSeek", "(J)J", (void *)android_media_tv_Tuner_seek_dvr},
 };
 
 static const JNINativeMethod gLnbMethods[] = {
diff --git a/media/jni/android_media_tv_Tuner.h b/media/jni/android_media_tv_Tuner.h
index 31d24ee..06e2492 100644
--- a/media/jni/android_media_tv_Tuner.h
+++ b/media/jni/android_media_tv_Tuner.h
@@ -149,14 +149,21 @@
     void getRestartEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event);
 };
 
+struct JTuner;
 struct FrontendClientCallbackImpl : public FrontendClientCallback {
-    FrontendClientCallbackImpl(jweak tunerObj);
+    FrontendClientCallbackImpl(JTuner*, jweak);
     ~FrontendClientCallbackImpl();
     virtual void onEvent(FrontendEventType frontendEventType);
     virtual void onScanMessage(
             FrontendScanMessageType type, const FrontendScanMessage& message);
 
-    jweak mObject;
+    void executeOnScanMessage(JNIEnv *env, const jclass& clazz, const jobject& frontend,
+                              FrontendScanMessageType type,
+                              const FrontendScanMessage& message);
+    void addCallbackListener(JTuner*, jweak obj);
+    void removeCallbackListener(JTuner* jtuner);
+    std::unordered_map<JTuner*, jweak> mListenersMap;
+    std::mutex mMutex;
 };
 
 struct JTuner : public RefBase {
@@ -171,6 +178,8 @@
     jobject getFrontendIds();
     jobject openFrontendByHandle(int feHandle);
     int shareFrontend(int feId);
+    void registerFeCbListener(JTuner* jtuner);
+    void unregisterFeCbListener(JTuner* jtuner);
     jint closeFrontendById(int id);
     jobject getFrontendInfo(int id);
     int tune(const FrontendSettings& settings);
@@ -192,6 +201,8 @@
     jint closeFrontend();
     jint closeDemux();
 
+    jweak getObject();
+
 protected:
     virtual ~JTuner();
 
@@ -200,6 +211,7 @@
     jweak mObject;
     static sp<TunerClient> mTunerClient;
     sp<FrontendClient> mFeClient;
+    sp<FrontendClientCallbackImpl> mFeClientCb;
     int mFeId;
     int mSharedFeId;
     sp<LnbClient> mLnbClient;
diff --git a/media/jni/soundpool/SoundPool.cpp b/media/jni/soundpool/SoundPool.cpp
index 253b4e3..00c4a97 100644
--- a/media/jni/soundpool/SoundPool.cpp
+++ b/media/jni/soundpool/SoundPool.cpp
@@ -185,7 +185,7 @@
     auto apiLock = kUseApiLock ? std::make_unique<std::lock_guard<std::mutex>>(mApiLock) : nullptr;
     soundpool::Stream* stream = mStreamManager.findStream(streamID);
     if (stream != nullptr && stream->requestStop(streamID)) {
-        mStreamManager.moveToRestartQueue(stream);
+        mStreamManager.moveToRestartQueue(stream, streamID);
     }
 }
 
diff --git a/media/jni/tuner/DvrClient.cpp b/media/jni/tuner/DvrClient.cpp
index 3027838..05683b6 100644
--- a/media/jni/tuner/DvrClient.cpp
+++ b/media/jni/tuner/DvrClient.cpp
@@ -22,6 +22,8 @@
 #include <aidl/android/hardware/tv/tuner/DemuxQueueNotifyBits.h>
 #include <android-base/logging.h>
 #include <inttypes.h>
+#include <sys/types.h>
+#include <unistd.h>
 #include <utils/Log.h>
 
 #include "ClientHelper.h"
@@ -200,6 +202,14 @@
     return size;
 }
 
+int64_t DvrClient::seekFile(int64_t pos) {
+    if (mFd < 0) {
+        ALOGE("Failed to seekFile. File is not configured");
+        return -1;
+    }
+    return lseek64(mFd, pos, SEEK_SET);
+}
+
 Result DvrClient::configure(DvrSettings settings) {
     if (mTunerDvr != nullptr) {
         Status s = mTunerDvr->configure(settings);
diff --git a/media/jni/tuner/DvrClient.h b/media/jni/tuner/DvrClient.h
index 9080c72..61c0325 100644
--- a/media/jni/tuner/DvrClient.h
+++ b/media/jni/tuner/DvrClient.h
@@ -82,6 +82,11 @@
     int64_t writeToFile(int64_t size);
 
     /**
+     * Seeks the Dvr file descriptor from the beginning of the file.
+     */
+    int64_t seekFile(int64_t pos);
+
+    /**
      * Write data to the given buffer with given size. Return the actual write size.
      */
     int64_t writeToBuffer(int8_t* buffer, int64_t size);
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
index 6dc05ad..a2eae2c 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
@@ -303,6 +303,11 @@
         public void onCameraClosed(String cameraId) {
             Log.v(TAG, String.format("Camera %s is closed", cameraId));
         }
+        @Override
+        public void onTorchStrengthLevelChanged(String cameraId, int torchStrength) {
+            Log.v(TAG, String.format("Camera " + cameraId + " torch strength level changed to "
+                    + torchStrength ));
+        }
     }
 
     /**
diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp
index ceba4d6..f76811e 100644
--- a/native/android/surface_control.cpp
+++ b/native/android/surface_control.cpp
@@ -364,7 +364,7 @@
     sp<SurfaceControl> surfaceControl = ASurfaceControl_to_SurfaceControl(aSurfaceControl);
     Transaction* transaction = ASurfaceTransaction_to_Transaction(aSurfaceTransaction);
 
-    sp<GraphicBuffer> graphic_buffer(reinterpret_cast<GraphicBuffer*>(buffer));
+    sp<GraphicBuffer> graphic_buffer(GraphicBuffer::fromAHardwareBuffer(buffer));
 
     std::optional<sp<Fence>> fence = std::nullopt;
     if (acquire_fence_fd != -1) {
diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml
index 3e65df2..632dfb3 100644
--- a/packages/CarrierDefaultApp/AndroidManifest.xml
+++ b/packages/CarrierDefaultApp/AndroidManifest.xml
@@ -27,6 +27,7 @@
     <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
     <uses-permission android:name="android.permission.NETWORK_BYPASS_PRIVATE_DNS" />
     <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" />
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
     <application
         android:label="@string/app_name"
diff --git a/packages/CompanionDeviceManager/AndroidManifest.xml b/packages/CompanionDeviceManager/AndroidManifest.xml
index c5926a5..06f2d9d 100644
--- a/packages/CompanionDeviceManager/AndroidManifest.xml
+++ b/packages/CompanionDeviceManager/AndroidManifest.xml
@@ -19,10 +19,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.companiondevicemanager">
 
-    <permission
-        android:name="com.android.companiondevicemanager.permission.BIND"
-        android:protectionLevel="signature" />
-
     <uses-permission android:name="android.permission.BLUETOOTH"/>
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
     <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
@@ -43,23 +39,17 @@
         android:forceQueryable="true"
         android:supportsRtl="true">
 
-        <service
-            android:name=".CompanionDeviceDiscoveryService"
-            android:permission="android.permission.BIND_COMPANION_DEVICE_MANAGER_SERVICE"
-            android:exported="true">
-        </service>
-
         <activity
             android:name=".CompanionDeviceActivity"
-            android:theme="@style/ChooserActivity"
+            android:exported="true"
+            android:launchMode="singleInstance"
+            android:excludeFromRecents="true"
             android:permission="android.permission.BIND_COMPANION_DEVICE_MANAGER_SERVICE"
-            android:exported="true">
-            <!--TODO include url scheme filter similar to PrintSpooler -->
-            <intent-filter>
-                <action android:name="android.companiondevice.START_DISCOVERY" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </activity>
+            android:theme="@style/ChooserActivity"/>
+
+        <service
+            android:name=".CompanionDeviceDiscoveryService"
+            android:exported="false" />
 
     </application>
 
diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
new file mode 100644
index 0000000..c87bac6
--- /dev/null
+++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
@@ -0,0 +1,81 @@
+<?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="match_parent"
+              android:layout_height="wrap_content"
+              android:background="@drawable/dialog_background"
+              android:elevation="16dp"
+              android:maxHeight="400dp"
+              android:orientation="vertical"
+              android:padding="18dp"
+              android:layout_gravity="center">
+
+    <TextView
+            android:id="@+id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:paddingHorizontal="12dp"
+            style="@*android:style/TextAppearance.Widget.Toolbar.Title"/>
+    <!-- style="@*android:style/TextAppearance.Widget.Toolbar.Title" -->
+
+    <TextView
+            android:id="@+id/summary"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="12dp"
+            android:layout_marginBottom="12dp"
+            android:gravity="center"
+            android:textColor="?android:attr/textColorSecondary"
+            android:textSize="14sp" />
+
+    <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1">
+
+        <ListView
+                android:id="@+id/device_list"
+                style="@android:style/Widget.Material.ListView"
+                android:layout_width="match_parent"
+                android:layout_height="200dp" />
+
+    </RelativeLayout>
+
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:gravity="end">
+
+        <Button
+                android:id="@+id/button_cancel"
+                style="@android:style/Widget.Material.Button.Borderless.Colored"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/consent_no"
+                android:textColor="?android:attr/textColorSecondary" />
+
+        <Button
+                android:id="@+id/button_allow"
+                style="@android:style/Widget.Material.Button.Borderless.Colored"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/consent_yes" />
+
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/buttons.xml b/packages/CompanionDeviceManager/res/layout/buttons.xml
deleted file mode 100644
index a80720c..0000000
--- a/packages/CompanionDeviceManager/res/layout/buttons.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/buttons"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="horizontal"
-    android:layout_alignParentBottom="true"
-    android:layout_alignParentEnd="true"
-    android:gravity="end"
->
-    <Button
-        android:id="@+id/button_cancel"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="@string/consent_no"
-        android:textColor="?android:attr/textColorSecondary"
-        style="@android:style/Widget.Material.Button.Borderless.Colored"
-    />
-    <Button
-        android:id="@+id/button_pair"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="@string/consent_yes"
-        style="@android:style/Widget.Material.Button.Borderless.Colored"
-    />
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/device_chooser.xml b/packages/CompanionDeviceManager/res/layout/device_chooser.xml
deleted file mode 100644
index 273347a..0000000
--- a/packages/CompanionDeviceManager/res/layout/device_chooser.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<RelativeLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/container"
-    android:layout_height="400dp"
-    style="@style/ContainerLayout"
-    >
-
-    <include layout="@layout/title" />
-
-    <include layout="@layout/profile_summary" />
-
-    <ListView
-        android:id="@+id/device_list"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_below="@+id/profile_summary"
-        android:layout_above="@+id/buttons"
-        style="@android:style/Widget.Material.ListView"
-    />
-
-    <include layout="@layout/buttons" />
-
-</RelativeLayout>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/device_confirmation.xml b/packages/CompanionDeviceManager/res/layout/device_confirmation.xml
deleted file mode 100644
index 1336e79..0000000
--- a/packages/CompanionDeviceManager/res/layout/device_confirmation.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/container"
-    android:layout_height="wrap_content"
-    style="@style/ContainerLayout"
-    >
-
-    <include layout="@layout/title" />
-
-    <include layout="@layout/profile_summary" />
-
-    <include layout="@layout/buttons" />
-
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/profile_summary.xml b/packages/CompanionDeviceManager/res/layout/profile_summary.xml
deleted file mode 100644
index 80fec59..0000000
--- a/packages/CompanionDeviceManager/res/layout/profile_summary.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-
-
-<TextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/profile_summary"
-    android:layout_below="@+id/title"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:layout_marginStart="16dp"
-    android:layout_marginEnd="16dp"
-    android:textColor="?android:attr/textColorSecondary"
-    android:textSize="14sp"
-    android:gravity="center"
-/>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/title.xml b/packages/CompanionDeviceManager/res/layout/title.xml
deleted file mode 100644
index 9a50366..0000000
--- a/packages/CompanionDeviceManager/res/layout/title.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-
-<TextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/title"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:gravity="center"
-    style="@*android:style/TextAppearance.Widget.Toolbar.Title"
-/>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/values/dimens.xml b/packages/CompanionDeviceManager/res/values/dimens.xml
deleted file mode 100644
index da7b0d1..0000000
--- a/packages/CompanionDeviceManager/res/values/dimens.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
-    <!--  Padding applied on most UI elements  -->
-    <dimen name="padding">12dp</dimen>
-
-</resources>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index 44748e9..cb8b616 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -19,25 +19,58 @@
     <!-- Title of the CompanionDeviceManager application. [CHAR LIMIT=50] -->
     <string name="app_label">Companion Device Manager</string>
 
-    <!-- Title of the device selection dialog. -->
-    <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%2$s</xliff:g>&lt;/strong&gt;</string>
+    <!-- Title of the device association confirmation dialog. -->
+    <string name="confirmation_title">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to manage your &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;</string>
 
-    <!-- The generic placeholder for a device type when nothing specific is known about it [CHAR LIMIT=30] -->
-    <string name="profile_name_generic">device</string>
+    <!-- ================= DEVICE_PROFILE_WATCH and null profile ================= -->
 
     <!-- The name of the "watch" device type [CHAR LIMIT=30] -->
     <string name="profile_name_watch">watch</string>
 
-    <!-- Title of the device association confirmation dialog. -->
-    <string name="confirmation_title">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to manage your &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;</string>
+    <!-- Title of the device selection dialog. -->
+    <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%2$s</xliff:g>&lt;/strong&gt;</string>
 
-    <!-- Text of the device profile permissions explanation in the association dialog. -->
-    <string name="profile_summary">This app is needed to manage your <xliff:g id="profile_name" example="watch">%1$s</xliff:g>. <xliff:g id="privileges_discplaimer" example="Android Wear will get access to your Notifications, Calendar and Contacts.">%2$s</xliff:g></string>
+    <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_watch" product="default"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_watch" product="tablet"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string>
+
+    <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
+
+    <!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
+    <string name="title_app_streaming">Allow &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to stream applications?</string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_app_streaming" product="default">Let &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to provide &lt;strong&gt;<xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g>&lt;/strong&gt; remote access to access to applications installed on this phone when connected.</string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_app_streaming" product="tablet">Let &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to provide &lt;strong&gt;<xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g>&lt;/strong&gt; remote access to access to applications installed on this tablet when connected.</string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_app_streaming" product="device">Let &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to provide &lt;strong&gt;<xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g>&lt;/strong&gt; remote access to access to applications installed on this device when connected.</string>
+
+    <!-- ================= DEVICE_PROFILE_AUTOMOTIVE_PROJECTION ================= -->
+
+    <!-- Confirmation for associating an application with a companion device of AUTOMOTIVE_PROJECTION profile (type) [CHAR LIMIT=NONE] -->
+    <string name="title_automotive_projection"></string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of AUTOMOTIVE_PROJECTION profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_automotive_projection"></string>
+
+    <!-- ================= null profile ================= -->
+
+    <!-- A noun for a companion device with unspecified profile (type) [CHAR LIMIT=30] -->
+    <string name="profile_name_generic">device</string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_generic"></string>
+
+    <!-- ================= Buttons ================= -->
 
     <!-- Positive button for the device-app association consent dialog [CHAR LIMIT=30] -->
     <string name="consent_yes">Allow</string>
 
     <!-- Negative button for the device-app association consent dialog [CHAR LIMIT=30] -->
     <string name="consent_no">Don\u2019t allow</string>
-
 </resources>
diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml
deleted file mode 100644
index 9dced47b..0000000
--- a/packages/CompanionDeviceManager/res/values/styles.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<resources>
-    <style name="ContainerLayout">
-        <item name="android:orientation">vertical</item>
-        <item name="android:layout_width">match_parent</item>
-        <item name="android:elevation">16dp</item>
-        <item name="android:background">@drawable/dialog_background</item>
-        <item name="android:paddingTop">18dip</item>
-        <item name="android:paddingStart">20dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:paddingBottom">16dip</item>
-        <item name="android:layout_gravity">center</item>
-    </style>
-</resources>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index a5168cc..cc887c3 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -16,327 +16,356 @@
 
 package com.android.companiondevicemanager;
 
-import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
-import static android.text.TextUtils.emptyIfNull;
-import static android.text.TextUtils.isEmpty;
-import static android.text.TextUtils.withoutPrefix;
+import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
+import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
+import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
 
+import static com.android.companiondevicemanager.Utils.getApplicationLabel;
+import static com.android.companiondevicemanager.Utils.getHtmlFromResources;
+import static com.android.companiondevicemanager.Utils.prepareResultReceiverForIpc;
+
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Activity;
+import android.companion.AssociationInfo;
 import android.companion.AssociationRequest;
 import android.companion.CompanionDeviceManager;
+import android.companion.IAssociationRequestCallback;
 import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.database.DataSetObserver;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
+import android.net.MacAddress;
 import android.os.Bundle;
-import android.text.Html;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.text.Spanned;
 import android.util.Log;
-import android.util.SparseArray;
-import android.util.TypedValue;
-import android.view.Gravity;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
+import android.widget.Button;
 import android.widget.ListView;
-import android.widget.ProgressBar;
 import android.widget.TextView;
 
-import com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DeviceFilterPair;
-import com.android.internal.util.Preconditions;
-
 public class CompanionDeviceActivity extends Activity {
-
     private static final boolean DEBUG = false;
-    private static final String LOG_TAG = CompanionDeviceActivity.class.getSimpleName();
+    private static final String TAG = CompanionDeviceActivity.class.getSimpleName();
 
-    static CompanionDeviceActivity sInstance;
+    // Keep the following constants in sync with
+    // frameworks/base/services/companion/java/
+    // com/android/server/companion/AssociationRequestsProcessor.java
 
-    View mLoadingIndicator = null;
-    ListView mDeviceListView;
-    private View mPairButton;
-    private View mCancelButton;
+    // AssociationRequestsProcessor <-> UI
+    private static final String EXTRA_APPLICATION_CALLBACK = "application_callback";
+    private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
+    private static final String EXTRA_RESULT_RECEIVER = "result_receiver";
 
-    DevicesAdapter mDevicesAdapter;
+    // AssociationRequestsProcessor -> UI
+    private static final int RESULT_CODE_ASSOCIATION_CREATED = 0;
+    private static final String EXTRA_ASSOCIATION = "association";
+
+    // UI -> AssociationRequestsProcessor
+    private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0;
+    private static final String EXTRA_MAC_ADDRESS = "mac_address";
+
+    private AssociationRequest mRequest;
+    private IAssociationRequestCallback mAppCallback;
+    private ResultReceiver mCdmServiceReceiver;
+
+    // Always present widgets.
+    private TextView mTitle;
+    private TextView mSummary;
+
+    // Progress indicator is only shown while we are looking for the first suitable device for a
+    // "regular" (ie. not self-managed) association.
+    private View mProgressIndicator;
+
+    // Present for self-managed association requests and "single-device" regular association
+    // regular.
+    private Button mButtonAllow;
+
+    // The list is only shown for multiple-device regular association request, after at least one
+    // matching device is found.
+    private @Nullable ListView mListView;
+    private @Nullable DeviceListAdapter mAdapter;
+
+    // The flag used to prevent double taps, that may lead to sending several requests for creating
+    // an association to CDM.
+    private boolean mAssociationApproved;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
-        Log.i(LOG_TAG, "Starting UI for " + getService().mRequest);
-
-        if (getService().mDevicesFound.isEmpty()) {
-            Log.e(LOG_TAG, "About to show UI, but no devices to show");
-        }
-
         getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
-        sInstance = this;
-        getService().mActivity = this;
-
-        String deviceProfile = getRequest().getDeviceProfile();
-        String profilePrivacyDisclaimer = emptyIfNull(getRequest()
-                .getDeviceProfilePrivilegesDescription())
-                .replace("APP_NAME", getCallingAppName());
-        boolean useDeviceProfile = deviceProfile != null && !isEmpty(profilePrivacyDisclaimer);
-        String profileName = useDeviceProfile
-                ? getDeviceProfileName(deviceProfile)
-                : getString(R.string.profile_name_generic);
-
-        if (getRequest().isSingleDevice()) {
-            setContentView(R.layout.device_confirmation);
-            final DeviceFilterPair selectedDevice = getService().mDevicesFound.get(0);
-            setTitle(Html.fromHtml(getString(
-                    R.string.confirmation_title,
-                    Html.escapeHtml(getCallingAppName()),
-                    Html.escapeHtml(selectedDevice.getDisplayName())), 0));
-
-            mPairButton = findViewById(R.id.button_pair);
-            mPairButton.setOnClickListener(v -> onDeviceConfirmed(getService().mSelectedDevice));
-            getService().mSelectedDevice = selectedDevice;
-            onSelectionUpdate();
-            if (getRequest().isSkipPrompt()) {
-                onDeviceConfirmed(selectedDevice);
-            }
-        } else {
-            setContentView(R.layout.device_chooser);
-            mPairButton = findViewById(R.id.button_pair);
-            mPairButton.setVisibility(View.GONE);
-            setTitle(Html.fromHtml(getString(R.string.chooser_title,
-                    Html.escapeHtml(profileName),
-                    Html.escapeHtml(getCallingAppName())), 0));
-            mDeviceListView = findViewById(R.id.device_list);
-            mDevicesAdapter = new DevicesAdapter();
-            mDeviceListView.setAdapter(mDevicesAdapter);
-            mDeviceListView.setOnItemClickListener((adapterView, view, pos, l) -> {
-                getService().mSelectedDevice =
-                        (DeviceFilterPair) adapterView.getItemAtPosition(pos);
-                mDevicesAdapter.notifyDataSetChanged();
-            });
-            mDevicesAdapter.registerDataSetObserver(new DataSetObserver() {
-                @Override
-                public void onChanged() {
-                    onSelectionUpdate();
-                }
-            });
-            mDeviceListView.addFooterView(mLoadingIndicator = getProgressBar(), null, false);
-        }
-
-        TextView profileSummary = findViewById(R.id.profile_summary);
-
-        if (useDeviceProfile) {
-            profileSummary.setVisibility(View.VISIBLE);
-            String deviceRef = getRequest().isSingleDevice()
-                    ? getService().mDevicesFound.get(0).getDisplayName()
-                    : profileName;
-            profileSummary.setText(getString(R.string.profile_summary,
-                    deviceRef,
-                    profilePrivacyDisclaimer));
-        } else {
-            profileSummary.setVisibility(View.GONE);
-        }
-
-        mCancelButton = findViewById(R.id.button_cancel);
-        mCancelButton.setOnClickListener(v -> cancel());
     }
 
-    static void notifyDevicesChanged() {
-        if (sInstance != null && sInstance.mDevicesAdapter != null && !sInstance.isFinishing()) {
-            sInstance.mDevicesAdapter.notifyDataSetChanged();
-        }
-    }
+    @Override
+    protected void onStart() {
+        super.onStart();
+        if (DEBUG) Log.d(TAG, "onStart()");
 
-    private AssociationRequest getRequest() {
-        return getService().mRequest;
-    }
+        final Intent intent = getIntent();
+        mRequest = intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST);
+        mAppCallback = IAssociationRequestCallback.Stub.asInterface(
+                intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK));
+        mCdmServiceReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
 
-    private String getDeviceProfileName(@Nullable String deviceProfile) {
-        if (deviceProfile == null) {
-            return getString(R.string.profile_name_generic);
-        }
-        switch (deviceProfile) {
-            case AssociationRequest.DEVICE_PROFILE_WATCH: {
-                return getString(R.string.profile_name_watch);
-            }
-            default: {
-                Log.w(LOG_TAG,
-                        "No localized profile name found for device profile: " + deviceProfile);
-                return withoutPrefix("android.app.role.COMPANION_DEVICE_", deviceProfile)
-                        .toLowerCase()
-                        .replace('_', ' ');
-            }
-        }
-    }
+        requireNonNull(mRequest);
+        requireNonNull(mAppCallback);
+        requireNonNull(mCdmServiceReceiver);
 
-    private void cancel() {
-        Log.i(LOG_TAG, "cancel()");
-        getService().onCancel();
-        setResult(RESULT_CANCELED);
-        finish();
+        // Start discovery services if needed.
+        if (!mRequest.isSelfManaged()) {
+            CompanionDeviceDiscoveryService.startForRequest(this, mRequest);
+        }
+        // Init UI.
+        initUI();
     }
 
     @Override
     protected void onStop() {
         super.onStop();
-        if (!isFinishing() && !isChangingConfigurations()) {
-            Log.i(LOG_TAG, "onStop() - cancelling");
-            cancel();
+        if (DEBUG) Log.d(TAG, "onStop(), finishing=" + isFinishing());
+
+        // TODO: handle config changes without cancelling.
+        if (!isFinishing()) {
+            cancel(); // will finish()
         }
+
+        // mAdapter may be observing - need to remove it.
+        CompanionDeviceDiscoveryService.SCAN_RESULTS_OBSERVABLE.deleteObservers();
     }
 
     @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        getService().mActivity = null;
-        if (sInstance == this) {
-            sInstance = null;
-        }
-    }
+    protected void onNewIntent(Intent intent) {
+        // Handle another incoming request (while we are not done with the original - mRequest -
+        // yet).
+        final AssociationRequest request = requireNonNull(
+                intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST));
+        if (DEBUG) Log.d(TAG, "onNewIntent(), request=" + request);
 
-    private CharSequence getCallingAppName() {
+        // We can only "process" one request at a time.
+        final IAssociationRequestCallback appCallback = IAssociationRequestCallback.Stub
+                .asInterface(intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK));
         try {
-            final PackageManager packageManager = getPackageManager();
-            String callingPackage = Preconditions.checkStringNotEmpty(
-                    getCallingPackage(),
-                    "This activity must be called for result");
-            return packageManager.getApplicationLabel(
-                    packageManager.getApplicationInfo(callingPackage, 0));
-        } catch (PackageManager.NameNotFoundException e) {
-            throw new RuntimeException(e);
+            requireNonNull(appCallback).onFailure("Busy.");
+        } catch (RemoteException ignore) {
         }
     }
 
-    @Override
-    public String getCallingPackage() {
-        return requireNonNull(getRequest().getCallingPackage());
-    }
+    private void initUI() {
+        if (DEBUG) Log.d(TAG, "initUI(), request=" + mRequest);
 
-    @Override
-    public void setTitle(CharSequence title) {
-        final TextView titleView = findViewById(R.id.title);
-        final int padding = getPadding(getResources());
-        titleView.setPadding(padding, padding, padding, padding);
-        titleView.setText(title);
-    }
+        setContentView(R.layout.activity_confirmation);
 
-    private ProgressBar getProgressBar() {
-        final ProgressBar progressBar = new ProgressBar(this);
-        progressBar.setForegroundGravity(Gravity.CENTER_HORIZONTAL);
-        final int padding = getPadding(getResources());
-        progressBar.setPadding(padding, padding, padding, padding);
-        return progressBar;
-    }
+        mTitle = findViewById(R.id.title);
+        mSummary = findViewById(R.id.summary);
 
-    static int getPadding(Resources r) {
-        return r.getDimensionPixelSize(R.dimen.padding);
-    }
+        mListView = findViewById(R.id.device_list);
+        mListView.setOnItemClickListener((av, iv, position, id) -> onListItemClick(position));
 
-    private void onSelectionUpdate() {
-        DeviceFilterPair selectedDevice = getService().mSelectedDevice;
-        if (mPairButton.getVisibility() != View.VISIBLE && selectedDevice != null) {
-            onDeviceConfirmed(selectedDevice);
+        mButtonAllow = findViewById(R.id.button_allow);
+        mButtonAllow.setOnClickListener(this::onAllowButtonClick);
+
+        findViewById(R.id.button_cancel).setOnClickListener(v -> cancel());
+
+        final CharSequence appLabel = getApplicationLabel(this, mRequest.getPackageName());
+        if (mRequest.isSelfManaged()) {
+            initUiForSelfManagedAssociation(appLabel);
+        } else if (mRequest.isSingleDevice()) {
+            initUiForSingleDevice(appLabel);
         } else {
-            mPairButton.setEnabled(selectedDevice != null);
+            initUiForMultipleDevices(appLabel);
         }
     }
 
-    private CompanionDeviceDiscoveryService getService() {
-        return CompanionDeviceDiscoveryService.sInstance;
+    private void onAssociationCreated(@NonNull AssociationInfo association) {
+        if (DEBUG) Log.i(TAG, "onAssociationCreated(), association=" + association);
+
+        // Don't need to notify the app, CdmService has already done that. Just finish.
+        setResultAndFinish(association);
     }
 
-    protected void onDeviceConfirmed(DeviceFilterPair selectedDevice) {
-        Log.i(LOG_TAG, "onDeviceConfirmed(selectedDevice = " + selectedDevice + ")");
-        getService().onDeviceSelected(
-                getCallingPackage(), getDeviceMacAddress(selectedDevice.device));
+    private void cancel() {
+        if (DEBUG) Log.i(TAG, "cancel()");
+
+        // Stop discovery service if it was used.
+        if (!mRequest.isSelfManaged()) {
+            CompanionDeviceDiscoveryService.stop(this);
+        }
+
+        // First send callback to the app directly...
+        try {
+            mAppCallback.onFailure("Cancelled.");
+        } catch (RemoteException ignore) {
+        }
+
+        // ... then set result and finish ("sending" onActivityResult()).
+        setResultAndFinish(null);
     }
 
-    void setResultAndFinish() {
-        Log.i(LOG_TAG, "setResultAndFinish(selectedDevice = "
-                + getService().mSelectedDevice.device + ")");
-        setResult(RESULT_OK,
-                new Intent().putExtra(
-                        CompanionDeviceManager.EXTRA_DEVICE, getService().mSelectedDevice.device));
+    private void setResultAndFinish(@Nullable AssociationInfo association) {
+        if (DEBUG) Log.i(TAG, "setResultAndFinish(), association=" + association);
+
+        final Intent data = new Intent();
+        if (association != null) {
+            data.putExtra(CompanionDeviceManager.EXTRA_ASSOCIATION, association);
+            if (!association.isSelfManaged()) {
+                data.putExtra(CompanionDeviceManager.EXTRA_DEVICE,
+                        association.getDeviceMacAddressAsString());
+            }
+        }
+        setResult(association != null ? RESULT_OK : RESULT_CANCELED, data);
+
         finish();
     }
 
-    class DevicesAdapter extends BaseAdapter {
-        private final Drawable mBluetoothIcon = icon(android.R.drawable.stat_sys_data_bluetooth);
-        private final Drawable mWifiIcon = icon(com.android.internal.R.drawable.ic_wifi_signal_3);
+    private void initUiForSelfManagedAssociation(CharSequence appLabel) {
+        if (DEBUG) Log.i(TAG, "initUiFor_SelfManaged_Association()");
 
-        private SparseArray<Integer> mColors = new SparseArray();
+        final CharSequence deviceName = mRequest.getDisplayName(); // "<device>";
+        final String deviceProfile = mRequest.getDeviceProfile(); // DEVICE_PROFILE_APP_STREAMING;
 
-        private Drawable icon(int drawableRes) {
-            Drawable icon = getResources().getDrawable(drawableRes, null);
-            icon.setTint(Color.DKGRAY);
-            return icon;
+        final Spanned title;
+        final Spanned summary;
+        switch (deviceProfile) {
+            case DEVICE_PROFILE_APP_STREAMING:
+                title = getHtmlFromResources(this, R.string.title_app_streaming, appLabel);
+                summary = getHtmlFromResources(
+                        this, R.string.summary_app_streaming, appLabel, deviceName);
+                break;
+
+            case DEVICE_PROFILE_AUTOMOTIVE_PROJECTION:
+                title = getHtmlFromResources(this, R.string.title_automotive_projection, appLabel);
+                summary = getHtmlFromResources(
+                        this, R.string.summary_automotive_projection, appLabel, deviceName);
+                break;
+
+            default:
+                throw new RuntimeException("Unsupported profile " + deviceProfile);
         }
+        mTitle.setText(title);
+        mSummary.setText(summary);
 
-        @Override
-        public View getView(
-                int position,
-                @Nullable View convertView,
-                @NonNull ViewGroup parent) {
-            TextView view = convertView instanceof TextView
-                    ? (TextView) convertView
-                    : newView();
-            bind(view, getItem(position));
-            return view;
-        }
-
-        private void bind(TextView textView, DeviceFilterPair device) {
-            textView.setText(device.getDisplayName());
-            textView.setBackgroundColor(
-                    device.equals(getService().mSelectedDevice)
-                            ? getColor(android.R.attr.colorControlHighlight)
-                            : Color.TRANSPARENT);
-            textView.setCompoundDrawablesWithIntrinsicBounds(
-                    device.device instanceof android.net.wifi.ScanResult
-                            ? mWifiIcon
-                            : mBluetoothIcon,
-                    null, null, null);
-            textView.getCompoundDrawables()[0].setTint(getColor(android.R.attr.colorForeground));
-        }
-
-        private TextView newView() {
-            final TextView textView = new TextView(CompanionDeviceActivity.this);
-            textView.setTextColor(getColor(android.R.attr.colorForeground));
-            final int padding = CompanionDeviceActivity.getPadding(getResources());
-            textView.setPadding(padding, padding, padding, padding);
-            textView.setCompoundDrawablePadding(padding);
-            return textView;
-        }
-
-        private int getColor(int colorAttr) {
-            if (mColors.contains(colorAttr)) {
-                return mColors.get(colorAttr);
-            }
-            TypedValue typedValue = new TypedValue();
-            TypedArray a = obtainStyledAttributes(typedValue.data, new int[] { colorAttr });
-            int result = a.getColor(0, 0);
-            a.recycle();
-            mColors.put(colorAttr, result);
-            return result;
-        }
-
-        @Override
-        public int getCount() {
-            return getService().mDevicesFound.size();
-        }
-
-        @Override
-        public DeviceFilterPair getItem(int position) {
-            return getService().mDevicesFound.get(position);
-        }
-
-        @Override
-        public long getItemId(int position) {
-            return position;
-        }
+        mListView.setVisibility(View.GONE);
     }
+
+    private void initUiForSingleDevice(CharSequence appLabel) {
+        if (DEBUG) Log.i(TAG, "initUiFor_SingleDevice()");
+
+        // TODO: use real name
+        final String deviceName = "<device>";
+        final String deviceProfile = mRequest.getDeviceProfile();
+
+        final Spanned title = getHtmlFromResources(
+                this, R.string.confirmation_title, appLabel, deviceName);
+        final Spanned summary;
+        if (deviceProfile == null) {
+            summary = getHtmlFromResources(this, R.string.summary_generic);
+        } else if (deviceProfile.equals(DEVICE_PROFILE_WATCH)) {
+            summary = getHtmlFromResources(this, R.string.summary_watch, appLabel, deviceName);
+        } else {
+            throw new RuntimeException("Unsupported profile " + deviceProfile);
+        }
+
+        mTitle.setText(title);
+        mSummary.setText(summary);
+
+        mListView.setVisibility(View.GONE);
+    }
+
+    private void initUiForMultipleDevices(CharSequence appLabel) {
+        if (DEBUG) Log.i(TAG, "initUiFor_MultipleDevices()");
+
+        final String deviceProfile = mRequest.getDeviceProfile();
+
+        final String profileName;
+        final Spanned summary;
+        if (deviceProfile == null) {
+            profileName = getString(R.string.profile_name_generic);
+            summary = getHtmlFromResources(this, R.string.summary_generic);
+        } else if (deviceProfile.equals(DEVICE_PROFILE_WATCH)) {
+            profileName = getString(R.string.profile_name_watch);
+            summary = getHtmlFromResources(this, R.string.summary_watch, appLabel);
+        } else {
+            throw new RuntimeException("Unsupported profile " + deviceProfile);
+        }
+        final Spanned title = getHtmlFromResources(
+                this, R.string.chooser_title, profileName, appLabel);
+
+        mTitle.setText(title);
+        mSummary.setText(summary);
+
+        mAdapter = new DeviceListAdapter(this);
+        CompanionDeviceDiscoveryService.SCAN_RESULTS_OBSERVABLE.addObserver(mAdapter);
+        // TODO: hide the list and show a spinner until a first device matching device is found.
+        mListView.setAdapter(mAdapter);
+
+        // "Remove" consent button: users would need to click on the list item.
+        mButtonAllow.setVisibility(View.GONE);
+    }
+
+    private void onListItemClick(int position) {
+        if (DEBUG) Log.d(TAG, "onListItemClick() " + position);
+
+        final DeviceFilterPair<?> selectedDevice = mAdapter.getItem(position);
+        final MacAddress macAddress = selectedDevice.getMacAddress();
+        onAssociationApproved(macAddress);
+    }
+
+    private void onAllowButtonClick(View v) {
+        if (DEBUG) Log.d(TAG, "onAllowButtonClick()");
+
+        // Disable the button, to prevent more clicks.
+        v.setEnabled(false);
+
+        final MacAddress macAddress;
+        if (mRequest.isSelfManaged()) {
+            macAddress = null;
+        } else {
+            // TODO: implement.
+            throw new UnsupportedOperationException(
+                    "isSingleDevice() requests are not supported yet.");
+        }
+        onAssociationApproved(macAddress);
+    }
+
+    private void onAssociationApproved(@Nullable MacAddress macAddress) {
+        if (mAssociationApproved) return;
+        mAssociationApproved = true;
+
+        if (DEBUG) Log.i(TAG, "onAssociationApproved() macAddress=" + macAddress);
+
+        if (!mRequest.isSelfManaged()) {
+            requireNonNull(macAddress);
+            CompanionDeviceDiscoveryService.stop(this);
+        }
+
+        final Bundle data = new Bundle();
+        data.putParcelable(EXTRA_ASSOCIATION_REQUEST, mRequest);
+        data.putBinder(EXTRA_APPLICATION_CALLBACK, mAppCallback.asBinder());
+        if (macAddress != null) {
+            data.putParcelable(EXTRA_MAC_ADDRESS, macAddress);
+        }
+
+        data.putParcelable(EXTRA_RESULT_RECEIVER,
+                prepareResultReceiverForIpc(mOnAssociationCreatedReceiver));
+
+        mCdmServiceReceiver.send(RESULT_CODE_ASSOCIATION_APPROVED, data);
+    }
+
+    private final ResultReceiver mOnAssociationCreatedReceiver =
+            new ResultReceiver(Handler.getMain()) {
+                @Override
+                protected void onReceiveResult(int resultCode, Bundle data) {
+                    if (resultCode != RESULT_CODE_ASSOCIATION_CREATED) {
+                        throw new RuntimeException("Unknown result code: " + resultCode);
+                    }
+
+                    final AssociationInfo association = data.getParcelable(EXTRA_ASSOCIATION);
+                    requireNonNull(association);
+
+                    onAssociationCreated(association);
+                }
+            };
 }
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
index 126b823..a4ff1dc 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
@@ -16,18 +16,17 @@
 
 package com.android.companiondevicemanager;
 
-import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
-import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
-
+import static com.android.companiondevicemanager.Utils.runOnMainThread;
 import static com.android.internal.util.ArrayUtils.isEmpty;
-import static com.android.internal.util.CollectionUtils.emptyIfNull;
-import static com.android.internal.util.CollectionUtils.size;
-import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+import static com.android.internal.util.CollectionUtils.filter;
+import static com.android.internal.util.CollectionUtils.find;
+import static com.android.internal.util.CollectionUtils.map;
+
+import static java.util.Objects.requireNonNull;
 
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.PendingIntent;
 import android.app.Service;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -42,8 +41,6 @@
 import android.companion.BluetoothDeviceFilter;
 import android.companion.BluetoothLeDeviceFilter;
 import android.companion.DeviceFilter;
-import android.companion.IAssociationRequestCallback;
-import android.companion.ICompanionDeviceDiscoveryService;
 import android.companion.WifiDeviceFilter;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -53,417 +50,411 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Parcelable;
-import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.internal.infra.AndroidFuture;
-import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.CollectionUtils;
-import com.android.internal.util.Preconditions;
-
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.Observable;
 
 public class CompanionDeviceDiscoveryService extends Service {
-
     private static final boolean DEBUG = false;
-    private static final String LOG_TAG = CompanionDeviceDiscoveryService.class.getSimpleName();
+    private static final String TAG = CompanionDeviceDiscoveryService.class.getSimpleName();
 
-    private static final long SCAN_TIMEOUT = 20000;
+    private static final String ACTION_START_DISCOVERY =
+            "com.android.companiondevicemanager.action.START_DISCOVERY";
+    private static final String ACTION_STOP_DISCOVERY =
+            "com.android.companiondevicemanager.action.ACTION_STOP_DISCOVERY";
+    private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
 
-    static CompanionDeviceDiscoveryService sInstance;
+    private static final long SCAN_TIMEOUT = 20_000L; // 20 seconds
 
-    private BluetoothManager mBluetoothManager;
-    private BluetoothAdapter mBluetoothAdapter;
+    // TODO: replace with LiveData-s?
+    static final Observable TIMEOUT_OBSERVABLE = new MyObservable();
+    static final Observable SCAN_RESULTS_OBSERVABLE = new MyObservable();
+
+    private static CompanionDeviceDiscoveryService sInstance;
+
+    private BluetoothManager mBtManager;
+    private BluetoothAdapter mBtAdapter;
     private WifiManager mWifiManager;
-    @Nullable private BluetoothLeScanner mBLEScanner;
-    private ScanSettings mDefaultScanSettings = new ScanSettings.Builder()
-            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
-            .build();
+    private BluetoothLeScanner mBleScanner;
 
-    private List<DeviceFilter<?>> mFilters;
-    private List<BluetoothLeDeviceFilter> mBLEFilters;
-    private List<BluetoothDeviceFilter> mBluetoothFilters;
-    private List<WifiDeviceFilter> mWifiFilters;
-    private List<ScanFilter> mBLEScanFilters;
+    private ScanCallback mBleScanCallback;
+    private BluetoothBroadcastReceiver mBtReceiver;
+    private WifiBroadcastReceiver mWifiReceiver;
 
-    AssociationRequest mRequest;
-    List<DeviceFilterPair> mDevicesFound;
-    DeviceFilterPair mSelectedDevice;
-    IAssociationRequestCallback mApplicationCallback;
+    private boolean mDiscoveryStarted = false;
+    private boolean mDiscoveryStopped = false;
+    private final List<DeviceFilterPair<?>> mDevicesFound = new ArrayList<>();
 
-    AndroidFuture<String> mServiceCallback;
-    boolean mIsScanning = false;
-    @Nullable
-    CompanionDeviceActivity mActivity = null;
+    private final Runnable mTimeoutRunnable = this::timeout;
 
-    private final ICompanionDeviceDiscoveryService mBinder =
-            new ICompanionDeviceDiscoveryService.Stub() {
-        @Override
-        public void startDiscovery(AssociationRequest request,
-                String callingPackage,
-                IAssociationRequestCallback appCallback,
-                AndroidFuture<String> serviceCallback) {
-            Log.i(LOG_TAG,
-                    "startDiscovery() called with: filter = [" + request
-                            + "], appCallback = [" + appCallback + "]"
-                            + "], serviceCallback = [" + serviceCallback + "]");
-            mApplicationCallback = appCallback;
-            mServiceCallback = serviceCallback;
-            Handler.getMain().sendMessage(obtainMessage(
-                    CompanionDeviceDiscoveryService::startDiscovery,
-                    CompanionDeviceDiscoveryService.this, request));
-        }
+    static void startForRequest(
+            @NonNull Context context, @NonNull AssociationRequest associationRequest) {
+        requireNonNull(associationRequest);
+        final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class);
+        intent.setAction(ACTION_START_DISCOVERY);
+        intent.putExtra(EXTRA_ASSOCIATION_REQUEST, associationRequest);
+        context.startService(intent);
+    }
 
-        @Override
-        public void onAssociationCreated() {
-            Handler.getMain().post(CompanionDeviceDiscoveryService.this::onAssociationCreated);
-        }
-    };
+    static void stop(@NonNull Context context) {
+        final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class);
+        intent.setAction(ACTION_STOP_DISCOVERY);
+        context.startService(intent);
+    }
 
-    private ScanCallback mBLEScanCallback;
-    private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
-    private WifiBroadcastReceiver mWifiBroadcastReceiver;
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        Log.i(LOG_TAG, "onBind(" + intent + ")");
-        return mBinder.asBinder();
+    @MainThread
+    static @NonNull List<DeviceFilterPair<?>> getScanResults() {
+        return sInstance != null ? new ArrayList<>(sInstance.mDevicesFound)
+                : Collections.emptyList();
     }
 
     @Override
     public void onCreate() {
         super.onCreate();
-
-        Log.i(LOG_TAG, "onCreate()");
-
-        mBluetoothManager = getSystemService(BluetoothManager.class);
-        mBluetoothAdapter = mBluetoothManager.getAdapter();
-        mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
-        mWifiManager = getSystemService(WifiManager.class);
-
-        mDevicesFound = new ArrayList<>();
+        if (DEBUG) Log.d(TAG, "onCreate()");
 
         sInstance = this;
-    }
 
-    @MainThread
-    private void startDiscovery(AssociationRequest request) {
-        if (!request.equals(mRequest)) {
-            mRequest = request;
-
-            mFilters = request.getDeviceFilters();
-            mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class);
-            mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class);
-            mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLeDeviceFilter.class);
-            mBLEScanFilters
-                    = CollectionUtils.map(mBLEFilters, BluetoothLeDeviceFilter::getScanFilter);
-
-            reset();
-        } else {
-            Log.i(LOG_TAG, "startDiscovery: duplicate request: " + request);
-        }
-
-        if (!ArrayUtils.isEmpty(mDevicesFound)) {
-            onReadyToShowUI();
-        }
-
-        // If filtering to get single device by mac address, also search in the set of already
-        // bonded devices to allow linking those directly
-        String singleMacAddressFilter = null;
-        if (mRequest.isSingleDevice()) {
-            int numFilters = size(mBluetoothFilters);
-            for (int i = 0; i < numFilters; i++) {
-                BluetoothDeviceFilter filter = mBluetoothFilters.get(i);
-                if (!TextUtils.isEmpty(filter.getAddress())) {
-                    singleMacAddressFilter = filter.getAddress();
-                    break;
-                }
-            }
-        }
-        if (singleMacAddressFilter != null) {
-            for (BluetoothDevice dev : emptyIfNull(mBluetoothAdapter.getBondedDevices())) {
-                onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters));
-            }
-            for (BluetoothDevice dev : emptyIfNull(
-                    mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT))) {
-                onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters));
-            }
-            for (BluetoothDevice dev : emptyIfNull(
-                    mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER))) {
-                onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters));
-            }
-        }
-
-        if (shouldScan(mBluetoothFilters)) {
-            final IntentFilter intentFilter = new IntentFilter();
-            intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
-
-            Log.i(LOG_TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)");
-            mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
-            registerReceiver(mBluetoothBroadcastReceiver, intentFilter);
-            mBluetoothAdapter.startDiscovery();
-        }
-
-        if (shouldScan(mBLEFilters) && mBLEScanner != null) {
-            Log.i(LOG_TAG, "BLEScanner.startScan");
-            mBLEScanCallback = new BLEScanCallback();
-            mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback);
-        }
-
-        if (shouldScan(mWifiFilters)) {
-            Log.i(LOG_TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)");
-            mWifiBroadcastReceiver = new WifiBroadcastReceiver();
-            registerReceiver(mWifiBroadcastReceiver,
-                    new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
-            mWifiManager.startScan();
-        }
-        mIsScanning = true;
-        Handler.getMain().sendMessageDelayed(
-                obtainMessage(CompanionDeviceDiscoveryService::stopScan, this),
-                SCAN_TIMEOUT);
-    }
-
-    @MainThread
-    private void onAssociationCreated() {
-        mActivity.setResultAndFinish();
-    }
-
-    private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) {
-        return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters);
-    }
-
-    @MainThread
-    private void reset() {
-        Log.i(LOG_TAG, "reset()");
-        stopScan();
-        mDevicesFound.clear();
-        mSelectedDevice = null;
-        CompanionDeviceActivity.notifyDevicesChanged();
+        mBtManager = getSystemService(BluetoothManager.class);
+        mBtAdapter = mBtManager.getAdapter();
+        mBleScanner = mBtAdapter.getBluetoothLeScanner();
+        mWifiManager = getSystemService(WifiManager.class);
     }
 
     @Override
-    public boolean onUnbind(Intent intent) {
-        Log.i(LOG_TAG, "onUnbind(intent = " + intent + ")");
-        stopScan();
-        return super.onUnbind(intent);
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        final String action = intent.getAction();
+        if (DEBUG) Log.d(TAG, "onStartCommand() action=" + action);
+
+        switch (action) {
+            case ACTION_START_DISCOVERY:
+                final AssociationRequest request =
+                        intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST);
+                startDiscovery(request);
+                break;
+
+            case ACTION_STOP_DISCOVERY:
+                stopDiscoveryAndFinish();
+                break;
+        }
+        return START_NOT_STICKY;
     }
 
-    private void stopScan() {
-        Log.i(LOG_TAG, "stopScan()");
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (DEBUG) Log.d(TAG, "onDestroy()");
 
-        if (!mIsScanning) return;
-        mIsScanning = false;
-
-        if (mActivity != null && mActivity.mDeviceListView != null) {
-            mActivity.mDeviceListView.removeFooterView(mActivity.mLoadingIndicator);
-        }
-
-        mBluetoothAdapter.cancelDiscovery();
-        if (mBluetoothBroadcastReceiver != null) {
-            unregisterReceiver(mBluetoothBroadcastReceiver);
-            mBluetoothBroadcastReceiver = null;
-        }
-        if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback);
-        if (mWifiBroadcastReceiver != null) {
-            unregisterReceiver(mWifiBroadcastReceiver);
-            mWifiBroadcastReceiver = null;
-        }
-    }
-
-    private void onDeviceFound(@Nullable DeviceFilterPair device) {
-        if (device == null) return;
-
-        Handler.getMain().sendMessage(obtainMessage(
-                CompanionDeviceDiscoveryService::onDeviceFoundMainThread, this, device));
+        sInstance = null;
     }
 
     @MainThread
-    void onDeviceFoundMainThread(@NonNull DeviceFilterPair device) {
-        if (mDevicesFound.contains(device)) {
-            Log.i(LOG_TAG, "Skipping device " + device + " - already among found devices");
-            return;
-        }
+    private void startDiscovery(@NonNull AssociationRequest request) {
+        if (DEBUG) Log.i(TAG, "startDiscovery() request=" + request);
+        requireNonNull(request);
 
-        Log.i(LOG_TAG, "Found device " + device);
+        if (mDiscoveryStarted) throw new RuntimeException("Discovery in progress.");
+        mDiscoveryStarted = true;
 
-        if (mDevicesFound.isEmpty()) {
-            onReadyToShowUI();
-        }
-        mDevicesFound.add(device);
-        CompanionDeviceActivity.notifyDevicesChanged();
-    }
+        final List<DeviceFilter<?>> allFilters = request.getDeviceFilters();
+        final List<BluetoothDeviceFilter> btFilters =
+                filter(allFilters, BluetoothDeviceFilter.class);
+        final List<BluetoothLeDeviceFilter> bleFilters =
+                filter(allFilters, BluetoothLeDeviceFilter.class);
+        final List<WifiDeviceFilter> wifiFilters = filter(allFilters, WifiDeviceFilter.class);
 
-    //TODO also, on timeout -> call onFailure
-    private void onReadyToShowUI() {
-        try {
-            mApplicationCallback.onAssociationPending(PendingIntent.getActivity(
-                    this, 0,
-                    new Intent(this, CompanionDeviceActivity.class),
-                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
-                            | PendingIntent.FLAG_IMMUTABLE));
-        } catch (RemoteException e) {
-            throw new RuntimeException(e);
-        }
-    }
+        checkBoundDevicesIfNeeded(request, btFilters);
 
-    private void onDeviceLost(@Nullable DeviceFilterPair device) {
-        Log.i(LOG_TAG, "Lost device " + device.getDisplayName());
-        Handler.getMain().sendMessage(obtainMessage(
-                CompanionDeviceDiscoveryService::onDeviceLostMainThread, this, device));
+        // If no filters are specified: look for everything.
+        final boolean forceStartScanningAll = isEmpty(allFilters);
+        // Start BT scanning (if needed)
+        mBtReceiver = startBtScanningIfNeeded(btFilters, forceStartScanningAll);
+        // Start Wi-Fi scanning (if needed)
+        mWifiReceiver = startWifiScanningIfNeeded(wifiFilters, forceStartScanningAll);
+        // Start BLE scanning (if needed)
+        mBleScanCallback = startBleScanningIfNeeded(bleFilters, forceStartScanningAll);
+
+        // Schedule a time-out.
+        Handler.getMain().postDelayed(mTimeoutRunnable, SCAN_TIMEOUT);
     }
 
     @MainThread
-    void onDeviceLostMainThread(@Nullable DeviceFilterPair device) {
-        mDevicesFound.remove(device);
-        CompanionDeviceActivity.notifyDevicesChanged();
-    }
+    private void stopDiscoveryAndFinish() {
+        if (DEBUG) Log.i(TAG, "stopDiscovery()");
 
-    void onDeviceSelected(String callingPackage, String deviceAddress) {
-        if (callingPackage == null || deviceAddress == null) {
+        if (!mDiscoveryStarted) {
+            stopSelf();
             return;
         }
-        mServiceCallback.complete(deviceAddress);
-    }
 
-    void onCancel() {
-        if (DEBUG) Log.i(LOG_TAG, "onCancel()");
-        mActivity = null;
-        mServiceCallback.cancel(true);
-    }
+        if (mDiscoveryStopped) return;
+        mDiscoveryStopped = true;
 
-    /**
-     * A pair of device and a filter that matched this device if any.
-     *
-     * @param <T> device type
-     */
-    static class DeviceFilterPair<T extends Parcelable> {
-        public final T device;
-        @Nullable
-        public final DeviceFilter<T> filter;
-
-        private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) {
-            this.device = device;
-            this.filter = filter;
+        // Stop BT discovery.
+        if (mBtReceiver != null) {
+            // Cancel discovery.
+            mBtAdapter.cancelDiscovery();
+            // Unregister receiver.
+            unregisterReceiver(mBtReceiver);
+            mBtReceiver = null;
         }
 
-        /**
-         * {@code (device, null)} if the filters list is empty or null
-         * {@code null} if none of the provided filters match the device
-         * {@code (device, filter)} where filter is among the list of filters and matches the device
-         */
-        @Nullable
-        public static <T extends Parcelable> DeviceFilterPair<T> findMatch(
-                T dev, @Nullable List<? extends DeviceFilter<T>> filters) {
-            if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null);
-            final DeviceFilter<T> matchingFilter
-                    = CollectionUtils.find(filters, f -> f.matches(dev));
-
-            DeviceFilterPair<T> result = matchingFilter != null
-                    ? new DeviceFilterPair<>(dev, matchingFilter)
-                    : null;
-            if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters +
-                    ") -> " + result);
-            return result;
+        // Stop Wi-Fi scanning.
+        if (mWifiReceiver != null) {
+            // TODO: need to stop scan?
+            // Unregister receiver.
+            unregisterReceiver(mWifiReceiver);
+            mWifiReceiver = null;
         }
 
-        public String getDisplayName() {
-            if (filter == null) {
-                Preconditions.checkNotNull(device);
-                if (device instanceof BluetoothDevice) {
-                    return getDeviceDisplayNameInternal((BluetoothDevice) device);
-                } else if (device instanceof android.net.wifi.ScanResult) {
-                    return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device);
-                } else if (device instanceof ScanResult) {
-                    return getDeviceDisplayNameInternal(((ScanResult) device).getDevice());
-                } else {
-                    throw new IllegalArgumentException("Unknown device type: " + device.getClass());
-                }
+        // Stop BLE scanning.
+        if (mBleScanCallback != null) {
+            mBleScanner.stopScan(mBleScanCallback);
+        }
+
+        Handler.getMain().removeCallbacks(mTimeoutRunnable);
+
+        // "Finish".
+        stopSelf();
+    }
+
+    private void checkBoundDevicesIfNeeded(@NonNull AssociationRequest request,
+            @NonNull List<BluetoothDeviceFilter> btFilters) {
+        // If filtering to get single device by mac address, also search in the set of already
+        // bonded devices to allow linking those directly
+        if (btFilters.isEmpty() || !request.isSingleDevice()) return;
+
+        final BluetoothDeviceFilter singleMacAddressFilter =
+                find(btFilters, filter -> !TextUtils.isEmpty(filter.getAddress()));
+
+        if (singleMacAddressFilter == null) return;
+
+        findAndReportMatches(mBtAdapter.getBondedDevices(), btFilters);
+        findAndReportMatches(mBtManager.getConnectedDevices(BluetoothProfile.GATT), btFilters);
+        findAndReportMatches(
+                mBtManager.getConnectedDevices(BluetoothProfile.GATT_SERVER), btFilters);
+    }
+
+    private void findAndReportMatches(@Nullable Collection<BluetoothDevice> devices,
+            @NonNull List<BluetoothDeviceFilter> filters) {
+        if (devices == null) return;
+
+        for (BluetoothDevice device : devices) {
+            final DeviceFilterPair<BluetoothDevice> match = findMatch(device, filters);
+            if (match != null) {
+                onDeviceFound(match);
             }
-            return filter.getDeviceDisplayName(device);
+        }
+    }
+
+    private BluetoothBroadcastReceiver startBtScanningIfNeeded(
+            List<BluetoothDeviceFilter> filters, boolean force) {
+        if (isEmpty(filters) && !force) return null;
+        if (DEBUG) Log.d(TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)");
+
+        final BluetoothBroadcastReceiver receiver = new BluetoothBroadcastReceiver(filters);
+
+        final IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
+        registerReceiver(receiver, intentFilter);
+
+        mBtAdapter.startDiscovery();
+
+        return receiver;
+    }
+
+    private WifiBroadcastReceiver startWifiScanningIfNeeded(
+            List<WifiDeviceFilter> filters, boolean force) {
+        if (isEmpty(filters) && !force) return null;
+        if (DEBUG) Log.d(TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)");
+
+        final WifiBroadcastReceiver receiver = new WifiBroadcastReceiver(filters);
+
+        final IntentFilter intentFilter = new IntentFilter(
+                WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+        registerReceiver(receiver, intentFilter);
+
+        mWifiManager.startScan();
+
+        return receiver;
+    }
+
+    private ScanCallback startBleScanningIfNeeded(
+            List<BluetoothLeDeviceFilter> filters, boolean force) {
+        if (isEmpty(filters) && !force) return null;
+        if (DEBUG) Log.d(TAG, "BLEScanner.startScan");
+
+        if (mBleScanner == null) {
+            Log.w(TAG, "BLE Scanner is not available.");
+            return null;
         }
 
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            DeviceFilterPair<?> that = (DeviceFilterPair<?>) o;
-            return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device));
-        }
+        final BLEScanCallback callback = new BLEScanCallback(filters);
 
-        @Override
-        public int hashCode() {
-            return Objects.hash(getDeviceMacAddress(device));
-        }
+        final List<ScanFilter> scanFilters = map(
+                filters, BluetoothLeDeviceFilter::getScanFilter);
+        final ScanSettings scanSettings = new ScanSettings.Builder()
+                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+                .build();
+        mBleScanner.startScan(scanFilters, scanSettings, callback);
 
-        @Override
-        public String toString() {
-            return "DeviceFilterPair{"
-                    + "device=" + device + " " + getDisplayName()
-                    + ", filter=" + filter
-                    + '}';
-        }
+        return callback;
+    }
+
+    private void onDeviceFound(@NonNull DeviceFilterPair<?> device) {
+        runOnMainThread(() -> {
+            if (DEBUG) Log.v(TAG, "onDeviceFound() " + device);
+            if (mDevicesFound.contains(device)) {
+                // TODO: update the device instead of ignoring (new found device may contain
+                //  additional/updated info, eg. name of the device).
+                if (DEBUG) {
+                    Log.d(TAG, "onDeviceFound() " + device.toShortString()
+                            + " - Already seen: ignore.");
+                }
+                return;
+            }
+            if (DEBUG) Log.i(TAG, "onDeviceFound() " + device.toShortString() + " - New device.");
+
+            // First: make change.
+            mDevicesFound.add(device);
+            // Then: notify observers.
+            SCAN_RESULTS_OBSERVABLE.notifyObservers();
+        });
+    }
+
+    private void onDeviceLost(@Nullable DeviceFilterPair<?> device) {
+        runOnMainThread(() -> {
+            if (DEBUG) Log.i(TAG, "onDeviceLost(), device=" + device.toShortString());
+
+            // First: make change.
+            mDevicesFound.remove(device);
+            // Then: notify observers.
+            SCAN_RESULTS_OBSERVABLE.notifyObservers();
+        });
+    }
+
+    private void timeout() {
+        if (DEBUG) Log.i(TAG, "timeout()");
+        stopDiscoveryAndFinish();
+        TIMEOUT_OBSERVABLE.notifyObservers();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
     }
 
     private class BLEScanCallback extends ScanCallback {
+        final List<BluetoothLeDeviceFilter> mFilters;
 
-        public BLEScanCallback() {
-            if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this);
+        BLEScanCallback(List<BluetoothLeDeviceFilter> filters) {
+            mFilters = filters;
         }
 
         @Override
         public void onScanResult(int callbackType, ScanResult result) {
             if (DEBUG) {
-                Log.i(LOG_TAG,
-                        "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result
-                                + ")");
+                Log.v(TAG, "BLE.onScanResult() callback=" + callbackType + ", result=" + result);
             }
-            final DeviceFilterPair<ScanResult> deviceFilterPair
-                    = DeviceFilterPair.findMatch(result, mBLEFilters);
-            if (deviceFilterPair == null) return;
+
+            final DeviceFilterPair<ScanResult> match = findMatch(result, mFilters);
+            if (match == null) return;
+
             if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
-                onDeviceLost(deviceFilterPair);
+                onDeviceLost(match);
             } else {
-                onDeviceFound(deviceFilterPair);
+                // TODO: check this logic.
+                onDeviceFound(match);
             }
         }
     }
 
     private class BluetoothBroadcastReceiver extends BroadcastReceiver {
+        final List<BluetoothDeviceFilter> mFilters;
+
+        BluetoothBroadcastReceiver(List<BluetoothDeviceFilter> filters) {
+            this.mFilters = filters;
+        }
+
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (DEBUG) {
-                Log.i(LOG_TAG,
-                        "BL.onReceive(context = " + context + ", intent = " + intent + ")");
-            }
+            final String action = intent.getAction();
             final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-            final DeviceFilterPair<BluetoothDevice> deviceFilterPair
-                    = DeviceFilterPair.findMatch(device, mBluetoothFilters);
-            if (deviceFilterPair == null) return;
-            if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) {
-                onDeviceFound(deviceFilterPair);
+
+            if (DEBUG) Log.v(TAG, action + ", device=" + device);
+
+            if (action == null) return;
+
+            final DeviceFilterPair<BluetoothDevice> match = findMatch(device, mFilters);
+            if (match == null) return;
+
+            if (action.equals(BluetoothDevice.ACTION_FOUND)) {
+                onDeviceFound(match);
             } else {
-                onDeviceLost(deviceFilterPair);
+                // TODO: check this logic.
+                onDeviceLost(match);
             }
         }
     }
 
     private class WifiBroadcastReceiver extends BroadcastReceiver {
+        final List<WifiDeviceFilter> mFilters;
+
+        private WifiBroadcastReceiver(List<WifiDeviceFilter> filters) {
+            this.mFilters = filters;
+        }
+
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
-                List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults();
+            if (!Objects.equals(intent.getAction(), WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
+                return;
+            }
 
-                if (DEBUG) {
-                    Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults));
-                }
+            final List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults();
+            if (DEBUG) {
+                Log.v(TAG, "WifiManager.SCAN_RESULTS_AVAILABLE_ACTION, results:\n  "
+                        + TextUtils.join("\n  ", scanResults));
+            }
 
-                for (int i = 0; i < scanResults.size(); i++) {
-                    onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters));
+            for (int i = 0; i < scanResults.size(); i++) {
+                final android.net.wifi.ScanResult scanResult = scanResults.get(i);
+                final DeviceFilterPair<?> match = findMatch(scanResult, mFilters);
+                if (match != null) {
+                    onDeviceFound(match);
                 }
             }
         }
     }
+
+    /**
+     * {@code (device, null)} if the filters list is empty or null
+     * {@code null} if none of the provided filters match the device
+     * {@code (device, filter)} where filter is among the list of filters and matches the device
+     */
+    @Nullable
+    public static <T extends Parcelable> DeviceFilterPair<T> findMatch(
+            T dev, @Nullable List<? extends DeviceFilter<T>> filters) {
+        if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null);
+        final DeviceFilter<T> matchingFilter = find(filters, f -> f.matches(dev));
+
+        DeviceFilterPair<T> result = matchingFilter != null
+                ? new DeviceFilterPair<>(dev, matchingFilter) : null;
+        if (DEBUG) {
+            Log.v(TAG, "findMatch(dev=" + dev + ", filters=" + filters + ") -> " + result);
+        }
+        return result;
+    }
+
+    private static class MyObservable extends Observable {
+        @Override
+        public void notifyObservers() {
+            setChanged();
+            super.notifyObservers();
+        }
+    }
 }
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java
new file mode 100644
index 0000000..faca1ae
--- /dev/null
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.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.companiondevicemanager;
+
+import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
+import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothDevice;
+import android.companion.DeviceFilter;
+import android.net.MacAddress;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A pair of device and a filter that matched this device if any.
+ *
+ * @param <T> device type.
+ */
+class DeviceFilterPair<T extends Parcelable> {
+    private final T mDevice;
+    private final @Nullable DeviceFilter<T> mFilter;
+
+    DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) {
+        this.mDevice = device;
+        this.mFilter = filter;
+    }
+
+    T getDevice() {
+        return mDevice;
+    }
+
+    String getDisplayName() {
+        if (mFilter != null) mFilter.getDeviceDisplayName(mDevice);
+
+        if (mDevice instanceof BluetoothDevice) {
+            return getDeviceDisplayNameInternal((BluetoothDevice) mDevice);
+        } else if (mDevice instanceof android.bluetooth.le.ScanResult) {
+            final android.bluetooth.le.ScanResult bleScanResult =
+                    (android.bluetooth.le.ScanResult) mDevice;
+            return getDeviceDisplayNameInternal(bleScanResult.getDevice());
+        } else if (mDevice instanceof android.net.wifi.ScanResult) {
+            final android.net.wifi.ScanResult wifiScanResult =
+                    (android.net.wifi.ScanResult) mDevice;
+            return getDeviceDisplayNameInternal(wifiScanResult);
+        } else {
+            throw new IllegalArgumentException("Unknown device type: " + mDevice.getClass());
+        }
+    }
+
+    @NonNull MacAddress getMacAddress() {
+        return MacAddress.fromString(getDeviceMacAddress(getDevice()));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DeviceFilterPair<?> that = (DeviceFilterPair<?>) o;
+        return Objects.equals(getDeviceMacAddress(mDevice), getDeviceMacAddress(that.mDevice));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getDeviceMacAddress(mDevice));
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceFilterPair{"
+                + "device=" + mDevice + " " + getDisplayName()
+                + ", filter=" + mFilter
+                + '}';
+    }
+
+    @NonNull String toShortString() {
+        return '(' + getDeviceTypeAsString() + ") " + getMacAddress() + " '" + getDisplayName()
+                + '\'';
+    }
+
+    private @NonNull String getDeviceTypeAsString() {
+        if (mDevice instanceof BluetoothDevice) {
+            return "BT";
+        } else if (mDevice instanceof android.bluetooth.le.ScanResult) {
+            return "BLE";
+        } else if (mDevice instanceof android.net.wifi.ScanResult) {
+            return "Wi-Fi";
+        } else {
+            return "Unknown";
+        }
+    }
+}
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java
new file mode 100644
index 0000000..cf2a2bf
--- /dev/null
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.companiondevicemanager;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import java.util.List;
+import java.util.Observable;
+import java.util.Observer;
+
+/**
+ * Adapter for the list of "found" devices.
+ */
+class DeviceListAdapter extends BaseAdapter implements Observer {
+    private final Context mContext;
+    private final Resources mResources;
+
+    private final Drawable mBluetoothIcon;
+    private final Drawable mWifiIcon;
+
+    private final @ColorInt int mTextColor;
+
+    // List if pairs (display name, address)
+    private List<DeviceFilterPair<?>> mDevices;
+
+    DeviceListAdapter(Context context) {
+        mContext = context;
+        mResources = context.getResources();
+        mBluetoothIcon = getTintedIcon(mResources, android.R.drawable.stat_sys_data_bluetooth);
+        mWifiIcon = getTintedIcon(mResources, com.android.internal.R.drawable.ic_wifi_signal_3);
+        mTextColor = getColor(context, android.R.attr.colorForeground);
+    }
+
+    @Override
+    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+        final TextView view = convertView != null ? (TextView) convertView : newView();
+        bind(view, getItem(position));
+        return view;
+    }
+
+    private void bind(TextView textView, DeviceFilterPair<?> item) {
+        textView.setText(item.getDisplayName());
+        textView.setBackgroundColor(Color.TRANSPARENT);
+        /*
+        textView.setCompoundDrawablesWithIntrinsicBounds(
+                item.getDevice() instanceof android.net.wifi.ScanResult
+                        ? mWifiIcon
+                        : mBluetoothIcon,
+                null, null, null);
+        textView.getCompoundDrawables()[0].setTint(mTextColor);
+         */
+    }
+
+    private TextView newView() {
+        final TextView textView = new TextView(mContext);
+        textView.setTextColor(mTextColor);
+        final int padding = 24;
+        textView.setPadding(padding, padding, padding, padding);
+        //textView.setCompoundDrawablePadding(padding);
+        return textView;
+    }
+
+    @Override
+    public int getCount() {
+        return mDevices != null ? mDevices.size() : 0;
+    }
+
+    @Override
+    public DeviceFilterPair<?> getItem(int position) {
+        return mDevices.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public void update(Observable o, Object arg) {
+        mDevices = CompanionDeviceDiscoveryService.getScanResults();
+        notifyDataSetChanged();
+    }
+
+    private @ColorInt int getColor(Context context, int attr) {
+        final TypedArray a = context.obtainStyledAttributes(new TypedValue().data,
+                new int[] { attr });
+        final int color = a.getColor(0, 0);
+        a.recycle();
+        return color;
+    }
+
+    private static Drawable getTintedIcon(Resources resources, int drawableRes) {
+        Drawable icon = resources.getDrawable(drawableRes, null);
+        icon.setTint(Color.DKGRAY);
+        return icon;
+    }
+}
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java
new file mode 100644
index 0000000..eab421e
--- /dev/null
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.companiondevicemanager;
+
+import android.annotation.NonNull;
+import android.annotation.StringRes;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+import android.text.Html;
+import android.text.Spanned;
+
+/**
+ * Utilities.
+ */
+class Utils {
+
+    /**
+     * Convert an instance of a "locally-defined" ResultReceiver to an instance of
+     * {@link android.os.ResultReceiver} itself, which the receiving process will be able to
+     * unmarshall.
+     */
+    static <T extends ResultReceiver> ResultReceiver prepareResultReceiverForIpc(T resultReceiver) {
+        final Parcel parcel = Parcel.obtain();
+        resultReceiver.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        return ipcFriendly;
+    }
+
+    static @NonNull CharSequence getApplicationLabel(
+            @NonNull Context context, @NonNull String packageName) {
+        final PackageManager packageManager = context.getPackageManager();
+        final ApplicationInfo appInfo;
+        try {
+            appInfo = packageManager.getApplicationInfo(packageName, 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+        return packageManager.getApplicationLabel(appInfo);
+    }
+
+    static Spanned getHtmlFromResources(
+            @NonNull Context context, @StringRes int resId, CharSequence... formatArgs) {
+        final String[] escapedArgs = new String[formatArgs.length];
+        for (int i = 0; i < escapedArgs.length; i++) {
+            escapedArgs[i] = Html.escapeHtml(formatArgs[i]);
+        }
+        final String plain = context.getString(resId, (Object[]) escapedArgs);
+        return Html.fromHtml(plain, 0);
+    }
+
+    static void runOnMainThread(Runnable runnable) {
+        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
+            runnable.run();
+        } else {
+            Handler.getMain().post(runnable);
+        }
+    }
+
+    private Utils() {
+    }
+}
diff --git a/packages/ConnectivityT/OWNERS b/packages/ConnectivityT/OWNERS
index 4862377..e267d19 100644
--- a/packages/ConnectivityT/OWNERS
+++ b/packages/ConnectivityT/OWNERS
@@ -1 +1,2 @@
-file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking
\ No newline at end of file
+file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking
+per-file **IpSec* = file:/services/core/java/com/android/server/vcn/OWNERS
diff --git a/packages/ConnectivityT/framework-t/Android.bp b/packages/ConnectivityT/framework-t/Android.bp
index 2363a9f..931a55b 100644
--- a/packages/ConnectivityT/framework-t/Android.bp
+++ b/packages/ConnectivityT/framework-t/Android.bp
@@ -19,11 +19,58 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+// NetworkStats related libraries.
+
+filegroup {
+    name: "framework-connectivity-netstats-internal-sources",
+    srcs: [
+        "src/android/app/usage/*.java",
+        "src/android/net/DataUsage*.*",
+        "src/android/net/INetworkStats*.*",
+        "src/android/net/NetworkIdentity*.java",
+        "src/android/net/NetworkStateSnapshot.*",
+        "src/android/net/NetworkStats*.*",
+        "src/android/net/NetworkTemplate.*",
+        "src/android/net/TrafficStats.java",
+        "src/android/net/UnderlyingNetworkInfo.*",
+        "src/android/net/netstats/**/*.*",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+filegroup {
+    name: "framework-connectivity-netstats-aidl-export-sources",
+    srcs: [
+        "aidl-export/android/net/NetworkStats.aidl",
+        "aidl-export/android/net/NetworkTemplate.aidl",
+    ],
+    path: "aidl-export",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+filegroup {
+    name: "framework-connectivity-netstats-sources",
+    srcs: [
+        ":framework-connectivity-netstats-internal-sources",
+        ":framework-connectivity-netstats-aidl-export-sources",
+    ],
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+// Nsd related libraries.
+
 filegroup {
     name: "framework-connectivity-nsd-internal-sources",
     srcs: [
-        "src/**/*.java",
-        "src/**/*.aidl",
+        "src/android/net/nsd/*.aidl",
+        "src/android/net/nsd/*.java",
     ],
     path: "src",
     visibility: [
@@ -34,7 +81,7 @@
 filegroup {
     name: "framework-connectivity-nsd-aidl-export-sources",
     srcs: [
-        "aidl-export/**/*.aidl",
+        "aidl-export/android/net/nsd/*.aidl",
     ],
     path: "aidl-export",
     visibility: [
@@ -49,6 +96,44 @@
         ":framework-connectivity-nsd-aidl-export-sources",
     ],
     visibility: [
-        "//frameworks/base",
+        "//visibility:private",
     ],
 }
+
+// IpSec related libraries.
+
+filegroup {
+    name: "framework-connectivity-ipsec-sources",
+    srcs: [
+        "src/android/net/IIpSecService.aidl",
+        "src/android/net/IpSec*.*",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+// Connectivity-T common libraries.
+
+filegroup {
+    name: "framework-connectivity-tiramisu-internal-sources",
+    srcs: [
+        "src/android/net/ConnectivityFrameworkInitializerTiramisu.java",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+filegroup {
+    name: "framework-connectivity-tiramisu-sources",
+    srcs: [
+        ":framework-connectivity-ipsec-sources",
+        ":framework-connectivity-netstats-sources",
+        ":framework-connectivity-nsd-sources",
+        ":framework-connectivity-tiramisu-internal-sources",
+    ],
+    visibility: ["//frameworks/base"],
+}
diff --git a/core/java/android/net/NetworkStats.aidl b/packages/ConnectivityT/framework-t/aidl-export/android/net/NetworkStats.aidl
similarity index 100%
rename from core/java/android/net/NetworkStats.aidl
rename to packages/ConnectivityT/framework-t/aidl-export/android/net/NetworkStats.aidl
diff --git a/core/java/android/net/NetworkTemplate.aidl b/packages/ConnectivityT/framework-t/aidl-export/android/net/NetworkTemplate.aidl
similarity index 100%
rename from core/java/android/net/NetworkTemplate.aidl
rename to packages/ConnectivityT/framework-t/aidl-export/android/net/NetworkTemplate.aidl
diff --git a/core/java/android/app/usage/NetworkStats.java b/packages/ConnectivityT/framework-t/src/android/app/usage/NetworkStats.java
similarity index 100%
rename from core/java/android/app/usage/NetworkStats.java
rename to packages/ConnectivityT/framework-t/src/android/app/usage/NetworkStats.java
diff --git a/core/java/android/app/usage/NetworkStatsManager.java b/packages/ConnectivityT/framework-t/src/android/app/usage/NetworkStatsManager.java
similarity index 100%
rename from core/java/android/app/usage/NetworkStatsManager.java
rename to packages/ConnectivityT/framework-t/src/android/app/usage/NetworkStatsManager.java
diff --git a/packages/ConnectivityT/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/packages/ConnectivityT/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
new file mode 100644
index 0000000..630f902e
--- /dev/null
+++ b/packages/ConnectivityT/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -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.net;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+import android.net.nsd.INsdManager;
+import android.net.nsd.NsdManager;
+
+/**
+ * Class for performing registration for Connectivity services which are exposed via updatable APIs
+ * since Android T.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class ConnectivityFrameworkInitializerTiramisu {
+    private ConnectivityFrameworkInitializerTiramisu() {}
+
+    /**
+     * Called by {@link SystemServiceRegistry}'s static initializer and registers nsd services to
+     * {@link Context}, so that {@link Context#getSystemService} can return them.
+     *
+     * @throws IllegalStateException if this is called anywhere besides
+     * {@link SystemServiceRegistry}.
+     */
+    public static void registerServiceWrappers() {
+        SystemServiceRegistry.registerContextAwareService(
+                Context.NSD_SERVICE,
+                NsdManager.class,
+                (context, serviceBinder) -> {
+                    INsdManager service = INsdManager.Stub.asInterface(serviceBinder);
+                    return new NsdManager(context, service);
+                }
+        );
+    }
+}
diff --git a/core/java/android/net/DataUsageRequest.aidl b/packages/ConnectivityT/framework-t/src/android/net/DataUsageRequest.aidl
similarity index 100%
rename from core/java/android/net/DataUsageRequest.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/DataUsageRequest.aidl
diff --git a/core/java/android/net/DataUsageRequest.java b/packages/ConnectivityT/framework-t/src/android/net/DataUsageRequest.java
similarity index 100%
rename from core/java/android/net/DataUsageRequest.java
rename to packages/ConnectivityT/framework-t/src/android/net/DataUsageRequest.java
diff --git a/core/java/android/net/IIpSecService.aidl b/packages/ConnectivityT/framework-t/src/android/net/IIpSecService.aidl
similarity index 100%
rename from core/java/android/net/IIpSecService.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/IIpSecService.aidl
diff --git a/core/java/android/net/INetworkStatsService.aidl b/packages/ConnectivityT/framework-t/src/android/net/INetworkStatsService.aidl
similarity index 100%
rename from core/java/android/net/INetworkStatsService.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/INetworkStatsService.aidl
diff --git a/core/java/android/net/INetworkStatsSession.aidl b/packages/ConnectivityT/framework-t/src/android/net/INetworkStatsSession.aidl
similarity index 100%
rename from core/java/android/net/INetworkStatsSession.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/INetworkStatsSession.aidl
diff --git a/core/java/android/net/IpSecAlgorithm.java b/packages/ConnectivityT/framework-t/src/android/net/IpSecAlgorithm.java
similarity index 100%
rename from core/java/android/net/IpSecAlgorithm.java
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecAlgorithm.java
diff --git a/core/java/android/net/IpSecConfig.aidl b/packages/ConnectivityT/framework-t/src/android/net/IpSecConfig.aidl
similarity index 100%
rename from core/java/android/net/IpSecConfig.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecConfig.aidl
diff --git a/core/java/android/net/IpSecConfig.java b/packages/ConnectivityT/framework-t/src/android/net/IpSecConfig.java
similarity index 100%
rename from core/java/android/net/IpSecConfig.java
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecConfig.java
diff --git a/core/java/android/net/IpSecManager.java b/packages/ConnectivityT/framework-t/src/android/net/IpSecManager.java
similarity index 100%
rename from core/java/android/net/IpSecManager.java
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecManager.java
diff --git a/core/java/android/net/IpSecSpiResponse.aidl b/packages/ConnectivityT/framework-t/src/android/net/IpSecSpiResponse.aidl
similarity index 100%
rename from core/java/android/net/IpSecSpiResponse.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecSpiResponse.aidl
diff --git a/core/java/android/net/IpSecSpiResponse.java b/packages/ConnectivityT/framework-t/src/android/net/IpSecSpiResponse.java
similarity index 100%
rename from core/java/android/net/IpSecSpiResponse.java
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecSpiResponse.java
diff --git a/core/java/android/net/IpSecTransform.java b/packages/ConnectivityT/framework-t/src/android/net/IpSecTransform.java
similarity index 100%
rename from core/java/android/net/IpSecTransform.java
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecTransform.java
diff --git a/core/java/android/net/IpSecTransformResponse.aidl b/packages/ConnectivityT/framework-t/src/android/net/IpSecTransformResponse.aidl
similarity index 100%
rename from core/java/android/net/IpSecTransformResponse.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecTransformResponse.aidl
diff --git a/core/java/android/net/IpSecTransformResponse.java b/packages/ConnectivityT/framework-t/src/android/net/IpSecTransformResponse.java
similarity index 100%
rename from core/java/android/net/IpSecTransformResponse.java
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecTransformResponse.java
diff --git a/core/java/android/net/IpSecTunnelInterfaceResponse.aidl b/packages/ConnectivityT/framework-t/src/android/net/IpSecTunnelInterfaceResponse.aidl
similarity index 100%
rename from core/java/android/net/IpSecTunnelInterfaceResponse.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecTunnelInterfaceResponse.aidl
diff --git a/core/java/android/net/IpSecTunnelInterfaceResponse.java b/packages/ConnectivityT/framework-t/src/android/net/IpSecTunnelInterfaceResponse.java
similarity index 100%
rename from core/java/android/net/IpSecTunnelInterfaceResponse.java
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecTunnelInterfaceResponse.java
diff --git a/core/java/android/net/IpSecUdpEncapResponse.aidl b/packages/ConnectivityT/framework-t/src/android/net/IpSecUdpEncapResponse.aidl
similarity index 100%
rename from core/java/android/net/IpSecUdpEncapResponse.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecUdpEncapResponse.aidl
diff --git a/core/java/android/net/IpSecUdpEncapResponse.java b/packages/ConnectivityT/framework-t/src/android/net/IpSecUdpEncapResponse.java
similarity index 100%
rename from core/java/android/net/IpSecUdpEncapResponse.java
rename to packages/ConnectivityT/framework-t/src/android/net/IpSecUdpEncapResponse.java
diff --git a/core/java/android/net/NetworkIdentity.java b/packages/ConnectivityT/framework-t/src/android/net/NetworkIdentity.java
similarity index 100%
rename from core/java/android/net/NetworkIdentity.java
rename to packages/ConnectivityT/framework-t/src/android/net/NetworkIdentity.java
diff --git a/services/core/java/com/android/server/net/NetworkIdentitySet.java b/packages/ConnectivityT/framework-t/src/android/net/NetworkIdentitySet.java
similarity index 96%
rename from services/core/java/com/android/server/net/NetworkIdentitySet.java
rename to packages/ConnectivityT/framework-t/src/android/net/NetworkIdentitySet.java
index 22ed781..abbebef 100644
--- a/services/core/java/com/android/server/net/NetworkIdentitySet.java
+++ b/packages/ConnectivityT/framework-t/src/android/net/NetworkIdentitySet.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.server.net;
+package android.net;
 
-import android.net.NetworkIdentity;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+
 import android.service.NetworkIdentitySetProto;
 import android.util.proto.ProtoOutputStream;
 
@@ -25,8 +26,6 @@
 import java.io.IOException;
 import java.util.HashSet;
 
-import static android.net.ConnectivityManager.TYPE_MOBILE;
-
 /**
  * Identity of a {@code iface}, defined by the set of {@link NetworkIdentity}
  * active on that interface.
@@ -97,6 +96,9 @@
         }
     }
 
+    /**
+     * Method to serialize this object into a {@code DataOutput}.
+     */
     public void writeToStream(DataOutput out) throws IOException {
         out.writeInt(VERSION_ADD_OEM_MANAGED_NETWORK);
         out.writeInt(size());
@@ -179,6 +181,9 @@
         return ident.compareTo(anotherIdent);
     }
 
+    /**
+     * Method to dump this object into proto debug file.
+     */
     public void dumpDebug(ProtoOutputStream proto, long tag) {
         final long start = proto.start(tag);
 
diff --git a/core/java/android/net/NetworkStateSnapshot.aidl b/packages/ConnectivityT/framework-t/src/android/net/NetworkStateSnapshot.aidl
similarity index 100%
rename from core/java/android/net/NetworkStateSnapshot.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/NetworkStateSnapshot.aidl
diff --git a/core/java/android/net/NetworkStateSnapshot.java b/packages/ConnectivityT/framework-t/src/android/net/NetworkStateSnapshot.java
similarity index 100%
rename from core/java/android/net/NetworkStateSnapshot.java
rename to packages/ConnectivityT/framework-t/src/android/net/NetworkStateSnapshot.java
diff --git a/core/java/android/net/NetworkStats.java b/packages/ConnectivityT/framework-t/src/android/net/NetworkStats.java
similarity index 100%
rename from core/java/android/net/NetworkStats.java
rename to packages/ConnectivityT/framework-t/src/android/net/NetworkStats.java
diff --git a/services/core/java/com/android/server/net/NetworkStatsAccess.java b/packages/ConnectivityT/framework-t/src/android/net/NetworkStatsAccess.java
similarity index 97%
rename from services/core/java/com/android/server/net/NetworkStatsAccess.java
rename to packages/ConnectivityT/framework-t/src/android/net/NetworkStatsAccess.java
index d25eae4..3885a9e 100644
--- a/services/core/java/com/android/server/net/NetworkStatsAccess.java
+++ b/packages/ConnectivityT/framework-t/src/android/net/NetworkStatsAccess.java
@@ -11,10 +11,10 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT 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.
  */
 
-package com.android.server.net;
+package android.net;
 
 import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
 import static android.net.NetworkStats.UID_ALL;
@@ -37,7 +37,11 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
-/** Utility methods for controlling access to network stats APIs. */
+/**
+ * Utility methods for controlling access to network stats APIs.
+ *
+ * @hide
+ */
 public final class NetworkStatsAccess {
     private NetworkStatsAccess() {}
 
diff --git a/services/core/java/com/android/server/net/NetworkStatsCollection.java b/packages/ConnectivityT/framework-t/src/android/net/NetworkStatsCollection.java
similarity index 97%
rename from services/core/java/com/android/server/net/NetworkStatsCollection.java
rename to packages/ConnectivityT/framework-t/src/android/net/NetworkStatsCollection.java
index 213ea40..0d3b9ed 100644
--- a/services/core/java/com/android/server/net/NetworkStatsCollection.java
+++ b/packages/ConnectivityT/framework-t/src/android/net/NetworkStatsCollection.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.net;
+package android.net;
 
 import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
 import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
@@ -31,13 +31,7 @@
 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
 
 import static com.android.internal.net.NetworkUtilsInternal.multiplySafeByRational;
-import static com.android.server.net.NetworkStatsService.TAG;
 
-import android.net.NetworkIdentity;
-import android.net.NetworkStats;
-import android.net.NetworkStatsHistory;
-import android.net.NetworkTemplate;
-import android.net.TrafficStats;
 import android.os.Binder;
 import android.service.NetworkStatsCollectionKeyProto;
 import android.service.NetworkStatsCollectionProto;
@@ -85,8 +79,11 @@
 /**
  * Collection of {@link NetworkStatsHistory}, stored based on combined key of
  * {@link NetworkIdentitySet}, UID, set, and tag. Knows how to persist itself.
+ *
+ * @hide
  */
 public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.Writer {
+    private static final String TAG = NetworkStatsCollection.class.getSimpleName();
     /** File header magic number: "ANET" */
     private static final int FILE_MAGIC = 0x414E4554;
 
@@ -366,8 +363,8 @@
                 entry.uid = key.uid;
                 entry.set = key.set;
                 entry.tag = key.tag;
-                entry.defaultNetwork = key.ident.areAllMembersOnDefaultNetwork() ?
-                        DEFAULT_NETWORK_YES : DEFAULT_NETWORK_NO;
+                entry.defaultNetwork = key.ident.areAllMembersOnDefaultNetwork()
+                        ? DEFAULT_NETWORK_YES : DEFAULT_NETWORK_NO;
                 entry.metered = key.ident.isAnyMemberMetered() ? METERED_YES : METERED_NO;
                 entry.roaming = key.ident.isAnyMemberRoaming() ? ROAMING_YES : ROAMING_NO;
                 entry.rxBytes = historyEntry.rxBytes;
@@ -521,6 +518,12 @@
         }
     }
 
+    /**
+     * Read legacy network summary statistics file format into the collection,
+     * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}.
+     *
+     * @deprecated
+     */
     @Deprecated
     public void readLegacyNetwork(File file) throws IOException {
         final AtomicFile inputFile = new AtomicFile(file);
@@ -560,6 +563,12 @@
         }
     }
 
+    /**
+     * Read legacy Uid statistics file format into the collection,
+     * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}.
+     *
+     * @deprecated
+     */
     @Deprecated
     public void readLegacyUid(File file, boolean onlyTags) throws IOException {
         final AtomicFile inputFile = new AtomicFile(file);
@@ -774,19 +783,19 @@
         public final int set;
         public final int tag;
 
-        private final int hashCode;
+        private final int mHashCode;
 
-        public Key(NetworkIdentitySet ident, int uid, int set, int tag) {
+        Key(NetworkIdentitySet ident, int uid, int set, int tag) {
             this.ident = ident;
             this.uid = uid;
             this.set = set;
             this.tag = tag;
-            hashCode = Objects.hash(ident, uid, set, tag);
+            mHashCode = Objects.hash(ident, uid, set, tag);
         }
 
         @Override
         public int hashCode() {
-            return hashCode;
+            return mHashCode;
         }
 
         @Override
diff --git a/core/java/android/net/NetworkStatsHistory.aidl b/packages/ConnectivityT/framework-t/src/android/net/NetworkStatsHistory.aidl
similarity index 100%
rename from core/java/android/net/NetworkStatsHistory.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/NetworkStatsHistory.aidl
diff --git a/core/java/android/net/NetworkStatsHistory.java b/packages/ConnectivityT/framework-t/src/android/net/NetworkStatsHistory.java
similarity index 100%
rename from core/java/android/net/NetworkStatsHistory.java
rename to packages/ConnectivityT/framework-t/src/android/net/NetworkStatsHistory.java
diff --git a/core/java/android/net/NetworkTemplate.java b/packages/ConnectivityT/framework-t/src/android/net/NetworkTemplate.java
similarity index 91%
rename from core/java/android/net/NetworkTemplate.java
rename to packages/ConnectivityT/framework-t/src/android/net/NetworkTemplate.java
index c906a13..8b9c14d 100644
--- a/core/java/android/net/NetworkTemplate.java
+++ b/packages/ConnectivityT/framework-t/src/android/net/NetworkTemplate.java
@@ -44,16 +44,10 @@
 import android.telephony.Annotation.NetworkType;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
-import android.util.BackupUtils;
-import android.util.Log;
 
 import com.android.internal.util.ArrayUtils;
 import com.android.net.module.util.NetworkIdentityUtils;
 
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
@@ -69,19 +63,6 @@
 public class NetworkTemplate implements Parcelable {
     private static final String TAG = "NetworkTemplate";
 
-    /**
-     * Initial Version of the backup serializer.
-     */
-    public static final int BACKUP_VERSION_1_INIT = 1;
-    /**
-     * Version of the backup serializer that added carrier template support.
-     */
-    public static final int BACKUP_VERSION_2_SUPPORT_CARRIER_TEMPLATE = 2;
-    /**
-     * Current Version of the Backup Serializer.
-     */
-    private static final int BACKUP_VERSION = BACKUP_VERSION_2_SUPPORT_CARRIER_TEMPLATE;
-
     public static final int MATCH_MOBILE = 1;
     public static final int MATCH_WIFI = 4;
     public static final int MATCH_ETHERNET = 5;
@@ -849,58 +830,4 @@
             return new NetworkTemplate[size];
         }
     };
-
-    public byte[] getBytesForBackup() throws IOException {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        DataOutputStream out = new DataOutputStream(baos);
-
-        if (!isPersistable()) {
-            Log.wtf(TAG, "Trying to backup non-persistable template: " + this);
-        }
-
-        out.writeInt(BACKUP_VERSION);
-
-        out.writeInt(mMatchRule);
-        BackupUtils.writeString(out, mSubscriberId);
-        BackupUtils.writeString(out, mNetworkId);
-        out.writeInt(mMetered);
-        out.writeInt(mSubscriberIdMatchRule);
-
-        return baos.toByteArray();
-    }
-
-    public static NetworkTemplate getNetworkTemplateFromBackup(DataInputStream in)
-            throws IOException, BackupUtils.BadVersionException {
-        int version = in.readInt();
-        if (version < BACKUP_VERSION_1_INIT || version > BACKUP_VERSION) {
-            throw new BackupUtils.BadVersionException("Unknown Backup Serialization Version");
-        }
-
-        int matchRule = in.readInt();
-        String subscriberId = BackupUtils.readString(in);
-        String networkId = BackupUtils.readString(in);
-
-        final int metered;
-        final int subscriberIdMatchRule;
-        if (version >= BACKUP_VERSION_2_SUPPORT_CARRIER_TEMPLATE) {
-            metered = in.readInt();
-            subscriberIdMatchRule = in.readInt();
-        } else {
-            // For backward compatibility, fill the missing filters from match rules.
-            metered = (matchRule == MATCH_MOBILE || matchRule == MATCH_MOBILE_WILDCARD
-                    || matchRule == MATCH_CARRIER) ? METERED_YES : METERED_ALL;
-            subscriberIdMatchRule = SUBSCRIBER_ID_MATCH_RULE_EXACT;
-        }
-
-        try {
-            return new NetworkTemplate(matchRule,
-                    subscriberId, new String[] { subscriberId },
-                    networkId, metered, NetworkStats.ROAMING_ALL,
-                    NetworkStats.DEFAULT_NETWORK_ALL, NetworkTemplate.NETWORK_TYPE_ALL,
-                    NetworkTemplate.OEM_MANAGED_ALL, subscriberIdMatchRule);
-        } catch (IllegalArgumentException e) {
-            throw new BackupUtils.BadVersionException(
-                    "Restored network template contains unknown match rule " + matchRule, e);
-        }
-    }
 }
diff --git a/core/java/android/net/TrafficStats.java b/packages/ConnectivityT/framework-t/src/android/net/TrafficStats.java
similarity index 100%
rename from core/java/android/net/TrafficStats.java
rename to packages/ConnectivityT/framework-t/src/android/net/TrafficStats.java
diff --git a/core/java/android/net/UnderlyingNetworkInfo.aidl b/packages/ConnectivityT/framework-t/src/android/net/UnderlyingNetworkInfo.aidl
similarity index 100%
rename from core/java/android/net/UnderlyingNetworkInfo.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/UnderlyingNetworkInfo.aidl
diff --git a/core/java/android/net/UnderlyingNetworkInfo.java b/packages/ConnectivityT/framework-t/src/android/net/UnderlyingNetworkInfo.java
similarity index 100%
rename from core/java/android/net/UnderlyingNetworkInfo.java
rename to packages/ConnectivityT/framework-t/src/android/net/UnderlyingNetworkInfo.java
diff --git a/core/java/android/net/netstats/provider/INetworkStatsProvider.aidl b/packages/ConnectivityT/framework-t/src/android/net/netstats/provider/INetworkStatsProvider.aidl
similarity index 100%
rename from core/java/android/net/netstats/provider/INetworkStatsProvider.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/netstats/provider/INetworkStatsProvider.aidl
diff --git a/core/java/android/net/netstats/provider/INetworkStatsProviderCallback.aidl b/packages/ConnectivityT/framework-t/src/android/net/netstats/provider/INetworkStatsProviderCallback.aidl
similarity index 100%
rename from core/java/android/net/netstats/provider/INetworkStatsProviderCallback.aidl
rename to packages/ConnectivityT/framework-t/src/android/net/netstats/provider/INetworkStatsProviderCallback.aidl
diff --git a/core/java/android/net/netstats/provider/NetworkStatsProvider.java b/packages/ConnectivityT/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java
similarity index 100%
rename from core/java/android/net/netstats/provider/NetworkStatsProvider.java
rename to packages/ConnectivityT/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java
diff --git a/packages/ConnectivityT/framework-t/src/android/net/nsd/NsdManager.java b/packages/ConnectivityT/framework-t/src/android/net/nsd/NsdManager.java
index 6c597e2..0f21e55 100644
--- a/packages/ConnectivityT/framework-t/src/android/net/nsd/NsdManager.java
+++ b/packages/ConnectivityT/framework-t/src/android/net/nsd/NsdManager.java
@@ -16,10 +16,6 @@
 
 package android.net.nsd;
 
-import static com.android.internal.util.Preconditions.checkArgument;
-import static com.android.internal.util.Preconditions.checkNotNull;
-import static com.android.internal.util.Preconditions.checkStringNotEmpty;
-
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.SystemService;
@@ -32,11 +28,13 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.Protocol;
+
+import java.util.Objects;
 
 /**
  * The Network Service Discovery Manager class provides the API to discover services
@@ -175,65 +173,63 @@
      */
     public static final int NSD_STATE_ENABLED = 2;
 
-    private static final int BASE = Protocol.BASE_NSD_MANAGER;
+    /** @hide */
+    public static final int DISCOVER_SERVICES                       = 1;
+    /** @hide */
+    public static final int DISCOVER_SERVICES_STARTED               = 2;
+    /** @hide */
+    public static final int DISCOVER_SERVICES_FAILED                = 3;
+    /** @hide */
+    public static final int SERVICE_FOUND                           = 4;
+    /** @hide */
+    public static final int SERVICE_LOST                            = 5;
 
     /** @hide */
-    public static final int DISCOVER_SERVICES                       = BASE + 1;
+    public static final int STOP_DISCOVERY                          = 6;
     /** @hide */
-    public static final int DISCOVER_SERVICES_STARTED               = BASE + 2;
+    public static final int STOP_DISCOVERY_FAILED                   = 7;
     /** @hide */
-    public static final int DISCOVER_SERVICES_FAILED                = BASE + 3;
-    /** @hide */
-    public static final int SERVICE_FOUND                           = BASE + 4;
-    /** @hide */
-    public static final int SERVICE_LOST                            = BASE + 5;
+    public static final int STOP_DISCOVERY_SUCCEEDED                = 8;
 
     /** @hide */
-    public static final int STOP_DISCOVERY                          = BASE + 6;
+    public static final int REGISTER_SERVICE                        = 9;
     /** @hide */
-    public static final int STOP_DISCOVERY_FAILED                   = BASE + 7;
+    public static final int REGISTER_SERVICE_FAILED                 = 10;
     /** @hide */
-    public static final int STOP_DISCOVERY_SUCCEEDED                = BASE + 8;
+    public static final int REGISTER_SERVICE_SUCCEEDED              = 11;
 
     /** @hide */
-    public static final int REGISTER_SERVICE                        = BASE + 9;
+    public static final int UNREGISTER_SERVICE                      = 12;
     /** @hide */
-    public static final int REGISTER_SERVICE_FAILED                 = BASE + 10;
+    public static final int UNREGISTER_SERVICE_FAILED               = 13;
     /** @hide */
-    public static final int REGISTER_SERVICE_SUCCEEDED              = BASE + 11;
+    public static final int UNREGISTER_SERVICE_SUCCEEDED            = 14;
 
     /** @hide */
-    public static final int UNREGISTER_SERVICE                      = BASE + 12;
+    public static final int RESOLVE_SERVICE                         = 15;
     /** @hide */
-    public static final int UNREGISTER_SERVICE_FAILED               = BASE + 13;
+    public static final int RESOLVE_SERVICE_FAILED                  = 16;
     /** @hide */
-    public static final int UNREGISTER_SERVICE_SUCCEEDED            = BASE + 14;
+    public static final int RESOLVE_SERVICE_SUCCEEDED               = 17;
 
     /** @hide */
-    public static final int RESOLVE_SERVICE                         = BASE + 18;
-    /** @hide */
-    public static final int RESOLVE_SERVICE_FAILED                  = BASE + 19;
-    /** @hide */
-    public static final int RESOLVE_SERVICE_SUCCEEDED               = BASE + 20;
+    public static final int DAEMON_CLEANUP                          = 18;
 
     /** @hide */
-    public static final int DAEMON_CLEANUP                          = BASE + 21;
+    public static final int DAEMON_STARTUP                          = 19;
 
     /** @hide */
-    public static final int DAEMON_STARTUP                          = BASE + 22;
+    public static final int ENABLE                                  = 20;
+    /** @hide */
+    public static final int DISABLE                                 = 21;
 
     /** @hide */
-    public static final int ENABLE                                  = BASE + 24;
-    /** @hide */
-    public static final int DISABLE                                 = BASE + 25;
+    public static final int NATIVE_DAEMON_EVENT                     = 22;
 
     /** @hide */
-    public static final int NATIVE_DAEMON_EVENT                     = BASE + 26;
-
+    public static final int REGISTER_CLIENT                         = 23;
     /** @hide */
-    public static final int REGISTER_CLIENT                         = BASE + 27;
-    /** @hide */
-    public static final int UNREGISTER_CLIENT                       = BASE + 28;
+    public static final int UNREGISTER_CLIENT                       = 24;
 
     /** Dns based service discovery protocol */
     public static final int PROTOCOL_DNS_SD = 0x0001;
@@ -550,7 +546,9 @@
         final int key;
         synchronized (mMapLock) {
             int valueIndex = mListenerMap.indexOfValue(listener);
-            checkArgument(valueIndex == -1, "listener already in use");
+            if (valueIndex != -1) {
+                throw new IllegalArgumentException("listener already in use");
+            }
             key = nextListenerKey();
             mListenerMap.put(key, listener);
             mServiceMap.put(key, s);
@@ -569,7 +567,9 @@
         checkListener(listener);
         synchronized (mMapLock) {
             int valueIndex = mListenerMap.indexOfValue(listener);
-            checkArgument(valueIndex != -1, "listener not registered");
+            if (valueIndex == -1) {
+                throw new IllegalArgumentException("listener not registered");
+            }
             return mListenerMap.keyAt(valueIndex);
         }
     }
@@ -598,7 +598,9 @@
      */
     public void registerService(NsdServiceInfo serviceInfo, int protocolType,
             RegistrationListener listener) {
-        checkArgument(serviceInfo.getPort() > 0, "Invalid port number");
+        if (serviceInfo.getPort() <= 0) {
+            throw new IllegalArgumentException("Invalid port number");
+        }
         checkServiceInfo(serviceInfo);
         checkProtocol(protocolType);
         int key = putListener(listener, serviceInfo);
@@ -660,7 +662,9 @@
      * Cannot be null. Cannot be in use for an active service discovery.
      */
     public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) {
-        checkStringNotEmpty(serviceType, "Service type cannot be empty");
+        if (TextUtils.isEmpty(serviceType)) {
+            throw new IllegalArgumentException("Service type cannot be empty");
+        }
         checkProtocol(protocolType);
 
         NsdServiceInfo s = new NsdServiceInfo();
@@ -719,16 +723,22 @@
     }
 
     private static void checkListener(Object listener) {
-        checkNotNull(listener, "listener cannot be null");
+        Objects.requireNonNull(listener, "listener cannot be null");
     }
 
     private static void checkProtocol(int protocolType) {
-        checkArgument(protocolType == PROTOCOL_DNS_SD, "Unsupported protocol");
+        if (protocolType != PROTOCOL_DNS_SD) {
+            throw new IllegalArgumentException("Unsupported protocol");
+        }
     }
 
     private static void checkServiceInfo(NsdServiceInfo serviceInfo) {
-        checkNotNull(serviceInfo, "NsdServiceInfo cannot be null");
-        checkStringNotEmpty(serviceInfo.getServiceName(), "Service name cannot be empty");
-        checkStringNotEmpty(serviceInfo.getServiceType(), "Service type cannot be empty");
+        Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
+        if (TextUtils.isEmpty(serviceInfo.getServiceName())) {
+            throw new IllegalArgumentException("Service name cannot be empty");
+        }
+        if (TextUtils.isEmpty(serviceInfo.getServiceType())) {
+            throw new IllegalArgumentException("Service type cannot be empty");
+        }
     }
 }
diff --git a/packages/ConnectivityT/service/Android.bp b/packages/ConnectivityT/service/Android.bp
index 529f58d..7b88176 100644
--- a/packages/ConnectivityT/service/Android.bp
+++ b/packages/ConnectivityT/service/Android.bp
@@ -19,13 +19,57 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+// NetworkStats related libraries.
+
 filegroup {
-    name: "services.connectivity-nsd-sources",
+    name: "services.connectivity-netstats-sources",
     srcs: [
-        "src/**/*.java",
+        "src/com/android/server/net/NetworkIdentity*.java",
+        "src/com/android/server/net/NetworkStats*.java",
     ],
     path: "src",
     visibility: [
-        "//frameworks/base/services/core",
+        "//visibility:private",
     ],
 }
+
+// Nsd related libraries.
+
+filegroup {
+    name: "services.connectivity-nsd-sources",
+    srcs: [
+        "src/com/android/server/INativeDaemon*.java",
+        "src/com/android/server/NativeDaemon*.java",
+        "src/com/android/server/Nsd*.java",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+// IpSec related libraries.
+
+filegroup {
+    name: "services.connectivity-ipsec-sources",
+    srcs: [
+        "src/com/android/server/IpSecService.java",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+// Connectivity-T common libraries.
+
+filegroup {
+    name: "services.connectivity-tiramisu-sources",
+    srcs: [
+        ":services.connectivity-ipsec-sources",
+        ":services.connectivity-netstats-sources",
+        ":services.connectivity-nsd-sources",
+    ],
+    path: "src",
+    visibility: ["//frameworks/base/services/core"],
+}
diff --git a/services/core/java/com/android/server/IpSecService.java b/packages/ConnectivityT/service/src/com/android/server/IpSecService.java
similarity index 100%
rename from services/core/java/com/android/server/IpSecService.java
rename to packages/ConnectivityT/service/src/com/android/server/IpSecService.java
diff --git a/services/core/java/com/android/server/net/NetworkStatsFactory.java b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsFactory.java
similarity index 100%
rename from services/core/java/com/android/server/net/NetworkStatsFactory.java
rename to packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsFactory.java
diff --git a/services/core/java/com/android/server/net/NetworkStatsManagerInternal.java b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsManagerInternal.java
similarity index 100%
rename from services/core/java/com/android/server/net/NetworkStatsManagerInternal.java
rename to packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsManagerInternal.java
diff --git a/services/core/java/com/android/server/net/NetworkStatsObservers.java b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsObservers.java
similarity index 98%
rename from services/core/java/com/android/server/net/NetworkStatsObservers.java
rename to packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsObservers.java
index 2564dae..1a0866d 100644
--- a/services/core/java/com/android/server/net/NetworkStatsObservers.java
+++ b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsObservers.java
@@ -22,7 +22,10 @@
 
 import android.app.usage.NetworkStatsManager;
 import android.net.DataUsageRequest;
+import android.net.NetworkIdentitySet;
 import android.net.NetworkStats;
+import android.net.NetworkStatsAccess;
+import android.net.NetworkStatsCollection;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.os.Bundle;
diff --git a/services/core/java/com/android/server/net/NetworkStatsRecorder.java b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsRecorder.java
similarity index 98%
rename from services/core/java/com/android/server/net/NetworkStatsRecorder.java
rename to packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsRecorder.java
index 978ae87..5e27c77 100644
--- a/services/core/java/com/android/server/net/NetworkStatsRecorder.java
+++ b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsRecorder.java
@@ -21,8 +21,11 @@
 import static android.net.TrafficStats.MB_IN_BYTES;
 import static android.text.format.DateUtils.YEAR_IN_MILLIS;
 
+import android.net.NetworkIdentitySet;
 import android.net.NetworkStats;
 import android.net.NetworkStats.NonMonotonicObserver;
+import android.net.NetworkStatsAccess;
+import android.net.NetworkStatsCollection;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.net.TrafficStats;
diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsService.java
similarity index 99%
rename from services/core/java/com/android/server/net/NetworkStatsService.java
rename to packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsService.java
index c876d41..2beca73 100644
--- a/services/core/java/com/android/server/net/NetworkStatsService.java
+++ b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsService.java
@@ -96,11 +96,14 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkIdentity;
+import android.net.NetworkIdentitySet;
 import android.net.NetworkSpecifier;
 import android.net.NetworkStack;
 import android.net.NetworkStateSnapshot;
 import android.net.NetworkStats;
 import android.net.NetworkStats.NonMonotonicObserver;
+import android.net.NetworkStatsAccess;
+import android.net.NetworkStatsCollection;
 import android.net.NetworkStatsHistory;
 import android.net.NetworkTemplate;
 import android.net.TelephonyNetworkSpecifier;
diff --git a/services/core/java/com/android/server/net/NetworkStatsSubscriptionsMonitor.java b/packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java
similarity index 100%
rename from services/core/java/com/android/server/net/NetworkStatsSubscriptionsMonitor.java
rename to packages/ConnectivityT/service/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index e5b5285..f60bc97 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -165,6 +165,7 @@
     <uses-permission android:name="com.android.permission.USE_INSTALLER_V2" />
     <uses-permission android:name="android.permission.INSTALL_TEST_ONLY_PACKAGE" />
     <uses-permission android:name="com.android.permission.USE_SYSTEM_DATA_LOADERS" />
+    <uses-permission android:name="android.permission.PACKAGE_VERIFICATION_AGENT" />
     <uses-permission android:name="android.permission.MOVE_PACKAGE" />
     <uses-permission android:name="android.permission.KEEP_UNINSTALLED_PACKAGES" />
     <uses-permission android:name="android.permission.CLEAR_APP_USER_DATA" />
@@ -200,6 +201,7 @@
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.CREATE_USERS" />
+    <uses-permission android:name="android.permission.QUERY_USERS" />
     <uses-permission android:name="android.permission.CREATE_VIRTUAL_DEVICE" />
     <uses-permission android:name="android.permission.MANAGE_CREDENTIAL_MANAGEMENT_APP" />
     <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index a1b3df8..1e9a41e 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -218,7 +218,6 @@
 
     <!-- notifications & DND access -->
     <uses-permission android:name="android.permission.MANAGE_NOTIFICATIONS" />
-    <uses-permission android:name="android.permission.REVOKE_RUNTIME_PERMISSIONS" />
     <uses-permission android:name="android.permission.GET_RUNTIME_PERMISSIONS" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
diff --git a/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml b/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml
index 67a70bb..b3987f1 100644
--- a/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml
+++ b/packages/SystemUI/res-keyguard/drawable/super_lock_icon.xml
@@ -94,4 +94,9 @@
         android:fromId="@id/unlocked"
         android:toId="@id/unlocked_aod"
         android:drawable="@drawable/unlocked_ls_to_aod" />
+
+    <transition
+        android:fromId="@id/unlocked"
+        android:toId="@id/locked_aod"
+        android:drawable="@drawable/unlocked_to_aod_lock" />
 </animated-selector>
diff --git a/packages/SystemUI/res-keyguard/drawable/unlocked_to_aod_lock.xml b/packages/SystemUI/res-keyguard/drawable/unlocked_to_aod_lock.xml
new file mode 100644
index 0000000..b6d76e0
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/unlocked_to_aod_lock.xml
@@ -0,0 +1,169 @@
+<?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_T_1" android:translateX="22.75" android:translateY="22.25" android:scaleX="1.02" android:scaleY="1.02">
+                    <group android:name="_R_G_L_2_G" android:translateX="-8.75" android:translateY="-8.75">
+                        <path android:name="_R_G_L_2_G_D_0_P_0" android:strokeColor="#FF000000"
+                              android:strokeLineJoin="round"
+                              android:strokeWidth="2.5"
+                              android:strokeAlpha="1"
+                              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>
+                <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: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: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>
+            <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="strokeWidth"
+                                android:duration="333" android:startOffset="0" android:valueFrom="2.5" android:valueTo="2" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <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="83" android:startOffset="0" android:valueFrom="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 " android:valueTo="M27.13 10.19 C27.13,10.19 27.13,3.67 27.13,3.67 C27.13,0.3 24.38,-1.75 21.13,-1.87 C17.68,-2.01 14.94,0.11 14.94,3.49 C14.94,3.49 15,15 15,15 " android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.456,0 0.464,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="133" android:startOffset="83" android:valueFrom="M27.13 10.19 C27.13,10.19 27.13,3.67 27.13,3.67 C27.13,0.3 24.38,-1.75 21.13,-1.87 C17.68,-2.01 14.94,0.11 14.94,3.49 C14.94,3.49 15,15 15,15 " android:valueTo="M2.5 10.38 C2.5,10.38 2.5,3.99 2.5,3.99 C2.5,0.61 5.3,-2.12 8.75,-2.12 C12.2,-2.12 15,0.61 15,3.99 C15,3.99 15,15 15,15 " android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.606,0 0.035,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="pathData"
+                                android:duration="117" android:startOffset="217" android:valueFrom="M2.5 10.38 C2.5,10.38 2.5,3.99 2.5,3.99 C2.5,0.61 5.3,-2.12 8.75,-2.12 C12.2,-2.12 15,0.61 15,3.99 C15,3.99 15,15 15,15 " android:valueTo="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 " android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.511,0 0.409,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="translateX"
+                                android:duration="333" android:startOffset="0" android:valueFrom="22.75" android:valueTo="23" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="translateY" android:duration="333" android:startOffset="0" android:valueFrom="22.25" android:valueTo="25.5" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_2_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="scaleX"
+                                android:duration="333" android:startOffset="0" android:valueFrom="1.02" android:valueTo="0.72" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator android:propertyName="scaleY"
+                                android:duration="333" android:startOffset="0" android:valueFrom="1.02" android:valueTo="0.72" android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,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="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="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="translateX"
+                                android:duration="850" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
index a2b8bf6..4f0925f 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
@@ -20,19 +20,18 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/keyguard_bouncer_user_switcher"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
+    android:layout_height="wrap_content"
     android:clipChildren="false"
     android:clipToPadding="false"
     android:orientation="vertical"
     android:gravity="center"
-    android:paddingTop="12dp"
     android:importantForAccessibility="yes"> <!-- Needed because TYPE_WINDOW_STATE_CHANGED is sent
                                                   from this view when bouncer is shown -->
 
-  <ImageView
-      android:id="@+id/user_icon"
-      android:layout_width="@dimen/keyguard_user_switcher_icon_size"
-      android:layout_height="@dimen/keyguard_user_switcher_icon_size" />
+    <ImageView
+        android:id="@+id/user_icon"
+        android:layout_width="@dimen/keyguard_user_switcher_icon_size"
+        android:layout_height="@dimen/keyguard_user_switcher_icon_size" />
 
     <!-- need to keep this outer view in order to have a correctly sized anchor
          for the dropdown menu, as well as dropdown background in the right place -->
@@ -41,7 +40,7 @@
         android:orientation="horizontal"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
-        android:layout_marginTop="32dp"
+        android:layout_marginTop="30dp"
         android:minHeight="48dp">
       <TextView
           style="@style/Keyguard.UserSwitcher.Spinner.Header"
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 9533040..2819dc9 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -108,10 +108,10 @@
     <dimen name="one_handed_bouncer_move_animation_translation">120dp</dimen>
 
 
-    <dimen name="keyguard_user_switcher_header_text_size">24sp</dimen>
-    <dimen name="keyguard_user_switcher_item_text_size">18sp</dimen>
-    <dimen name="keyguard_user_switcher_width">300dp</dimen>
-    <dimen name="keyguard_user_switcher_icon_size">250dp</dimen>
+    <dimen name="keyguard_user_switcher_header_text_size">32sp</dimen>
+    <dimen name="keyguard_user_switcher_item_text_size">32sp</dimen>
+    <dimen name="keyguard_user_switcher_width">320dp</dimen>
+    <dimen name="keyguard_user_switcher_icon_size">310dp</dimen>
     <dimen name="keyguard_user_switcher_corner">32dp</dimen>
     <dimen name="keyguard_user_switcher_popup_corner">24dp</dimen>
     <dimen name="keyguard_user_switcher_item_padding_vertical">15dp</dimen>
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml b/packages/SystemUI/res/drawable/ic_signal_wifi_off.xml
similarity index 62%
copy from libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml
copy to packages/SystemUI/res/drawable/ic_signal_wifi_off.xml
index af9063a..a93abb7 100644
--- a/libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml
+++ b/packages/SystemUI/res/drawable/ic_signal_wifi_off.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2021 The Android Open Source Project
   ~
@@ -15,11 +14,11 @@
   ~ limitations under the License.
   -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="@dimen/size_compat_hint_point_width"
-        android:height="8dp"
-        android:viewportWidth="10"
-        android:viewportHeight="8">
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
     <path
-        android:fillColor="@color/size_compat_background"
-        android:pathData="M10,0 l-4.1875,6.6875 a1,1 0 0,1 -1.625,0 l-4.1875,-6.6875z"/>
-</vector>
+        android:fillColor="@android:color/white"
+        android:pathData="M18.575,15.2L7.55,4.175q1.1,-0.325 2.212,-0.462 1.113,-0.138 2.238,-0.138 3.45,0 6.55,1.45Q21.65,6.475 24,9zM20.225,23.575l-4.75,-4.75 -3.475,4.1L0,9q0.675,-0.725 1.438,-1.4 0.762,-0.675 1.612,-1.225l-2.625,-2.6L2.1,2.1l19.8,19.8z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml
index 3c9e44e..89690e8 100644
--- a/packages/SystemUI/res/layout/auth_biometric_contents.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml
@@ -73,6 +73,9 @@
         android:accessibilityLiveRegion="polite"
         android:singleLine="true"
         android:ellipsize="marquee"
+        android:marqueeRepeatLimit="marquee_forever"
+        android:scrollHorizontally="true"
+        android:fadingEdge="horizontal"
         android:textColor="@color/biometric_dialog_gray"/>
 
     <LinearLayout
diff --git a/packages/SystemUI/res/layout/dream_overlay_container.xml b/packages/SystemUI/res/layout/dream_overlay_container.xml
new file mode 100644
index 0000000..b611ffa
--- /dev/null
+++ b/packages/SystemUI/res/layout/dream_overlay_container.xml
@@ -0,0 +1,61 @@
+<?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.
+-->
+<com.android.systemui.dreams.DreamOverlayContainerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/dream_overlay_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/dream_overlay_content"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent" />
+
+    <com.android.systemui.dreams.DreamOverlayStatusBarView
+        android:id="@+id/dream_overlay_status_bar"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/dream_overlay_status_bar_height"
+        android:layout_marginEnd="@dimen/dream_overlay_status_bar_margin"
+        android:layout_marginStart="@dimen/dream_overlay_status_bar_margin"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/dream_overlay_system_status"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            app:layout_constraintEnd_toEndOf="parent">
+
+            <com.android.systemui.statusbar.AlphaOptimizedImageView
+                android:id="@+id/dream_overlay_wifi_status"
+                android:layout_width="@dimen/status_bar_wifi_signal_size"
+                android:layout_height="match_parent"
+                android:layout_marginEnd="@dimen/dream_overlay_status_icon_margin"
+                android:visibility="gone"
+                app:layout_constraintEnd_toStartOf="@id/dream_overlay_battery" />
+
+            <com.android.systemui.battery.BatteryMeterView
+                android:id="@+id/dream_overlay_battery"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                app:layout_constraintEnd_toEndOf="parent" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+    </com.android.systemui.dreams.DreamOverlayStatusBarView>
+</com.android.systemui.dreams.DreamOverlayContainerView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-h800dp/dimens.xml b/packages/SystemUI/res/values-h800dp/dimens.xml
index f057603..1d6f279 100644
--- a/packages/SystemUI/res/values-h800dp/dimens.xml
+++ b/packages/SystemUI/res/values-h800dp/dimens.xml
@@ -22,5 +22,5 @@
     <dimen name="large_clock_text_size">200dp</dimen>
 
     <!-- With the large clock, move up slightly from the center -->
-    <dimen name="keyguard_large_clock_top_margin">-104dp</dimen>
+    <dimen name="keyguard_large_clock_top_margin">-112dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index 9d4c2c3..3412722 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -78,7 +78,7 @@
     <color name="biometric_dialog_error">#fff28b82</color> <!-- red 300 -->
 
     <!-- UDFPS colors -->
-    <color name="udfps_enroll_icon">#ffffff</color> <!-- 100% white -->
+    <color name="udfps_enroll_icon">#7DA7F1</color>
 
     <color name="GM2_green_500">#FF41Af6A</color>
     <color name="GM2_blue_500">#5195EA</color>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 8cad5b3..fc28f09 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -134,10 +134,10 @@
     <color name="biometric_dialog_error">#ffd93025</color>                  <!-- red 600 -->
 
     <!-- UDFPS colors -->
-    <color name="udfps_enroll_icon">#000000</color>                         <!-- 100% black -->
-    <color name="udfps_moving_target_fill">#cc4285f4</color>                <!-- 80% blue -->
-    <color name="udfps_enroll_progress">#ff669DF6</color>                   <!-- blue 400 -->
-    <color name="udfps_enroll_progress_help">#ffEE675C</color>              <!-- red 400 -->
+    <color name="udfps_enroll_icon">#7DA7F1</color>
+    <color name="udfps_moving_target_fill">#475670</color>
+    <color name="udfps_enroll_progress">#7DA7F1</color>
+    <color name="udfps_enroll_progress_help">#ffEE675C</color>
 
     <!-- Global screenshot actions -->
     <color name="screenshot_button_ripple">#1f000000</color>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index df8fc22..3695286 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -598,17 +598,6 @@
         280
     </integer>
 
-    <!-- Haptic feedback intensity for ticks used for the udfps dwell time -->
-    <item name="config_udfpsTickIntensity" translatable="false" format="float"
-          type="dimen">.5</item>
-
-    <!-- Haptic feedback delay between ticks used for udfps dwell time -->
-    <integer name="config_udfpsTickDelay" translatable="false">25</integer>
-
-    <!-- Haptic feedback tick type - if true, uses VibrationEffect.Composition.PRIMITIVE_LOW_TICK
-         else uses VibrationEffect.Composition.PRIMITIVE_TICK -->
-    <bool name="config_udfpsUseLowTick">true</bool>
-
     <!-- package name of a built-in camera app to use to restrict implicit intent resolution
          when the double-press power gesture is used. Ignored if empty. -->
     <string translatable="false" name="config_cameraGesturePackage"></string>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index c7350a1..d4398a8 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -30,10 +30,10 @@
     <dimen name="navigation_bar_deadzone_size_max">32dp</dimen>
 
     <!-- dimensions for the navigation bar handle -->
-    <dimen name="navigation_handle_radius">1dp</dimen>
-    <dimen name="navigation_handle_bottom">6dp</dimen>
+    <dimen name="navigation_handle_radius">2dp</dimen>
+    <dimen name="navigation_handle_bottom">10dp</dimen>
     <dimen name="navigation_handle_sample_horizontal_margin">10dp</dimen>
-    <dimen name="navigation_home_handle_width">72dp</dimen>
+    <dimen name="navigation_home_handle_width">108dp</dimen>
 
     <!-- Size of the nav bar edge panels, should be greater to the
          edge sensitivity + the drag threshold -->
@@ -602,7 +602,7 @@
     <!-- When large clock is showing, offset the smartspace by this amount -->
     <dimen name="keyguard_smartspace_top_offset">12dp</dimen>
     <!-- With the large clock, move up slightly from the center -->
-    <dimen name="keyguard_large_clock_top_margin">-52dp</dimen>
+    <dimen name="keyguard_large_clock_top_margin">-60dp</dimen>
 
     <!-- Default line spacing multiplier between hours and minutes of the keyguard clock -->
     <item name="keyguard_clock_line_spacing_scale" type="dimen" format="float">.7</item>
@@ -1303,4 +1303,9 @@
     <dimen name="keyguard_unfold_translation_x">16dp</dimen>
 
     <dimen name="fgs_manager_min_width_minor">100%</dimen>
+
+    <!-- Dream overlay related dimensions -->
+    <dimen name="dream_overlay_status_bar_height">80dp</dimen>
+    <dimen name="dream_overlay_status_bar_margin">40dp</dimen>
+    <dimen name="dream_overlay_status_icon_margin">8dp</dimen>
 </resources>
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 30db136..c1d9d0d 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
@@ -65,6 +65,7 @@
     public final Rect localBounds;
     public final Rect sourceContainerBounds;
     public final Rect screenSpaceBounds;
+    public final Rect startScreenSpaceBounds;
     public final boolean isNotInRecents;
     public final Rect contentInsets;
     public final ActivityManager.RunningTaskInfo taskInfo;
@@ -88,6 +89,7 @@
         localBounds = app.localBounds;
         sourceContainerBounds = app.sourceContainerBounds;
         screenSpaceBounds = app.screenSpaceBounds;
+        startScreenSpaceBounds = screenSpaceBounds;
         prefixOrderIndex = app.prefixOrderIndex;
         isNotInRecents = app.isNotInRecents;
         contentInsets = app.contentInsets;
@@ -219,6 +221,8 @@
         localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
         sourceContainerBounds = null;
         screenSpaceBounds = new Rect(change.getEndAbsBounds());
+        startScreenSpaceBounds = new Rect(change.getStartAbsBounds());
+
         prefixOrderIndex = order;
         // TODO(shell-transitions): I guess we need to send content insets? evaluate how its used.
         contentInsets = new Rect(0, 0, 0, 0);
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
index ac463eb..1571913 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
@@ -168,6 +168,12 @@
         if (!mIsDozing) mView.animateAppearOnLockscreen();
     }
 
+    /** Animate the clock appearance when a foldable device goes from fully-open/half-open state to
+     * fully folded state and it goes to sleep (always on display screen) */
+    public void animateFoldAppear() {
+        mView.animateFoldAppear();
+    }
+
     /**
      * Updates the time for the view.
      */
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java
deleted file mode 100644
index 2a0c285..0000000
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java
+++ /dev/null
@@ -1,315 +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.keyguard;
-
-import android.annotation.FloatRange;
-import android.annotation.IntRange;
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.text.format.DateFormat;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-import com.android.systemui.R;
-
-import java.util.Calendar;
-import java.util.Locale;
-import java.util.TimeZone;
-
-import kotlin.Unit;
-
-/**
- * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
- * The time's text color is a gradient that changes its colors based on its controller.
- */
-public class AnimatableClockView extends TextView {
-    private static final CharSequence DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm";
-    private static final CharSequence DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm";
-    private static final long DOZE_ANIM_DURATION = 300;
-    private static final long APPEAR_ANIM_DURATION = 350;
-    private static final long CHARGE_ANIM_DURATION_PHASE_0 = 500;
-    private static final long CHARGE_ANIM_DURATION_PHASE_1 = 1000;
-
-    private final Calendar mTime = Calendar.getInstance();
-
-    private final int mDozingWeight;
-    private final int mLockScreenWeight;
-    private CharSequence mFormat;
-    private CharSequence mDescFormat;
-    private int mDozingColor;
-    private int mLockScreenColor;
-    private float mLineSpacingScale = 1f;
-    private int mChargeAnimationDelay = 0;
-
-    private TextAnimator mTextAnimator = null;
-    private Runnable mOnTextAnimatorInitialized;
-
-    private boolean mIsSingleLine;
-
-    public AnimatableClockView(Context context) {
-        this(context, null, 0, 0);
-    }
-
-    public AnimatableClockView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0, 0);
-    }
-
-    public AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        TypedArray ta = context.obtainStyledAttributes(
-                attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes);
-        try {
-            mDozingWeight = ta.getInt(R.styleable.AnimatableClockView_dozeWeight, 100);
-            mLockScreenWeight = ta.getInt(R.styleable.AnimatableClockView_lockScreenWeight, 300);
-            mChargeAnimationDelay = ta.getInt(
-                    R.styleable.AnimatableClockView_chargeAnimationDelay, 200);
-        } finally {
-            ta.recycle();
-        }
-
-        ta = context.obtainStyledAttributes(
-                attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes);
-        try {
-            mIsSingleLine = ta.getBoolean(android.R.styleable.TextView_singleLine, false);
-        } finally {
-            ta.recycle();
-        }
-
-        refreshFormat();
-    }
-
-    @Override
-    public void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        refreshFormat();
-    }
-
-    @Override
-    public void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-    }
-
-    int getDozingWeight() {
-        if (useBoldedVersion()) {
-            return mDozingWeight + 100;
-        }
-        return mDozingWeight;
-    }
-
-    int getLockScreenWeight() {
-        if (useBoldedVersion()) {
-            return mLockScreenWeight + 100;
-        }
-        return mLockScreenWeight;
-    }
-
-    /**
-     * Whether to use a bolded version based on the user specified fontWeightAdjustment.
-     */
-    boolean useBoldedVersion() {
-        // "Bold text" fontWeightAdjustment is 300.
-        return getResources().getConfiguration().fontWeightAdjustment > 100;
-    }
-
-    void refreshTime() {
-        mTime.setTimeInMillis(System.currentTimeMillis());
-        setText(DateFormat.format(mFormat, mTime));
-        setContentDescription(DateFormat.format(mDescFormat, mTime));
-    }
-
-    void onTimeZoneChanged(TimeZone timeZone) {
-        mTime.setTimeZone(timeZone);
-        refreshFormat();
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        if (mTextAnimator == null) {
-            mTextAnimator = new TextAnimator(
-                    getLayout(),
-                    () -> {
-                        invalidate();
-                        return Unit.INSTANCE;
-                    });
-            if (mOnTextAnimatorInitialized != null) {
-                mOnTextAnimatorInitialized.run();
-                mOnTextAnimatorInitialized = null;
-            }
-        } else {
-            mTextAnimator.updateLayout(getLayout());
-        }
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        mTextAnimator.draw(canvas);
-    }
-
-    void setLineSpacingScale(float scale) {
-        mLineSpacingScale = scale;
-        setLineSpacing(0, mLineSpacingScale);
-    }
-
-    void setColors(int dozingColor, int lockScreenColor) {
-        mDozingColor = dozingColor;
-        mLockScreenColor = lockScreenColor;
-    }
-
-    void animateAppearOnLockscreen() {
-        if (mTextAnimator == null) {
-            return;
-        }
-
-        setTextStyle(
-                getDozingWeight(),
-                -1 /* text size, no update */,
-                mLockScreenColor,
-                false /* animate */,
-                0 /* duration */,
-                0 /* delay */,
-                null /* onAnimationEnd */);
-
-        setTextStyle(
-                getLockScreenWeight(),
-                -1 /* text size, no update */,
-                mLockScreenColor,
-                true, /* animate */
-                APPEAR_ANIM_DURATION,
-                0 /* delay */,
-                null /* onAnimationEnd */);
-    }
-
-    void animateCharge(DozeStateGetter dozeStateGetter) {
-        if (mTextAnimator == null || mTextAnimator.isRunning()) {
-            // Skip charge animation if dozing animation is already playing.
-            return;
-        }
-        Runnable startAnimPhase2 = () -> setTextStyle(
-                dozeStateGetter.isDozing() ? getDozingWeight() : getLockScreenWeight() /* weight */,
-                -1,
-                null,
-                true /* animate */,
-                CHARGE_ANIM_DURATION_PHASE_1,
-                0 /* delay */,
-                null /* onAnimationEnd */);
-        setTextStyle(dozeStateGetter.isDozing()
-                        ? getLockScreenWeight()
-                        : getDozingWeight()/* weight */,
-                -1,
-                null,
-                true /* animate */,
-                CHARGE_ANIM_DURATION_PHASE_0,
-                mChargeAnimationDelay,
-                startAnimPhase2);
-    }
-
-    void animateDoze(boolean isDozing, boolean animate) {
-        setTextStyle(isDozing ? getDozingWeight() : getLockScreenWeight() /* weight */,
-                -1,
-                isDozing ? mDozingColor : mLockScreenColor,
-                animate,
-                DOZE_ANIM_DURATION,
-                0 /* delay */,
-                null /* onAnimationEnd */);
-    }
-
-    /**
-     * Set text style with an optional animation.
-     *
-     * By passing -1 to weight, the view preserves its current weight.
-     * By passing -1 to textSize, the view preserves its current text size.
-     *
-     * @param weight text weight.
-     * @param textSize font size.
-     * @param animate true to animate the text style change, otherwise false.
-     */
-    private void setTextStyle(
-            @IntRange(from = 0, to = 1000) int weight,
-            @FloatRange(from = 0) float textSize,
-            Integer color,
-            boolean animate,
-            long duration,
-            long delay,
-            Runnable onAnimationEnd) {
-        if (mTextAnimator != null) {
-            mTextAnimator.setTextStyle(weight, textSize, color, animate, duration, null,
-                    delay, onAnimationEnd);
-        } else {
-            // when the text animator is set, update its start values
-            mOnTextAnimatorInitialized =
-                    () -> mTextAnimator.setTextStyle(
-                            weight, textSize, color, false, duration, null,
-                            delay, onAnimationEnd);
-        }
-    }
-
-    void refreshFormat() {
-        Patterns.update(mContext);
-
-        final boolean use24HourFormat = DateFormat.is24HourFormat(getContext());
-        if (mIsSingleLine && use24HourFormat) {
-            mFormat = Patterns.sClockView24;
-        } else if (!mIsSingleLine && use24HourFormat) {
-            mFormat = DOUBLE_LINE_FORMAT_24_HOUR;
-        } else if (mIsSingleLine && !use24HourFormat) {
-            mFormat = Patterns.sClockView12;
-        } else {
-            mFormat = DOUBLE_LINE_FORMAT_12_HOUR;
-        }
-
-        mDescFormat = use24HourFormat ? Patterns.sClockView24 : Patterns.sClockView12;
-        refreshTime();
-    }
-
-    // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
-    // This is an optimization to ensure we only recompute the patterns when the inputs change.
-    private static final class Patterns {
-        static String sClockView12;
-        static String sClockView24;
-        static String sCacheKey;
-
-        static void update(Context context) {
-            final Locale locale = Locale.getDefault();
-            final Resources res = context.getResources();
-            final String clockView12Skel = res.getString(R.string.clock_12hr_format);
-            final String clockView24Skel = res.getString(R.string.clock_24hr_format);
-            final String key = locale.toString() + clockView12Skel + clockView24Skel;
-            if (key.equals(sCacheKey)) return;
-            sClockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel);
-
-            // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
-            // format.  The following code removes the AM/PM indicator if we didn't want it.
-            if (!clockView12Skel.contains("a")) {
-                sClockView12 = sClockView12.replaceAll("a", "").trim();
-            }
-            sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel);
-            sCacheKey = key;
-        }
-    }
-
-    interface DozeStateGetter {
-        boolean isDozing();
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt
new file mode 100644
index 0000000..357be25
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.keyguard
+
+import android.animation.TimeInterpolator
+import android.annotation.ColorInt
+import android.annotation.FloatRange
+import android.annotation.IntRange
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.text.format.DateFormat
+import android.util.AttributeSet
+import android.widget.TextView
+import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
+ * The time's text color is a gradient that changes its colors based on its controller.
+ */
+class AnimatableClockView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : TextView(context, attrs, defStyleAttr, defStyleRes) {
+
+    private val time = Calendar.getInstance()
+
+    private val dozingWeightInternal: Int
+    private val lockScreenWeightInternal: Int
+    private val isSingleLineInternal: Boolean
+
+    private var format: CharSequence? = null
+    private var descFormat: CharSequence? = null
+
+    @ColorInt
+    private var dozingColor = 0
+
+    @ColorInt
+    private var lockScreenColor = 0
+
+    private var lineSpacingScale = 1f
+    private val chargeAnimationDelay: Int
+    private var textAnimator: TextAnimator? = null
+    private var onTextAnimatorInitialized: Runnable? = null
+
+    val dozingWeight: Int
+        get() = if (useBoldedVersion()) dozingWeightInternal + 100 else dozingWeightInternal
+
+    val lockScreenWeight: Int
+        get() = if (useBoldedVersion()) lockScreenWeightInternal + 100 else lockScreenWeightInternal
+
+    init {
+        val animatableClockViewAttributes = context.obtainStyledAttributes(
+            attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes
+        )
+
+        try {
+            dozingWeightInternal = animatableClockViewAttributes.getInt(
+                R.styleable.AnimatableClockView_dozeWeight,
+                100
+            )
+            lockScreenWeightInternal = animatableClockViewAttributes.getInt(
+                R.styleable.AnimatableClockView_lockScreenWeight,
+                300
+            )
+            chargeAnimationDelay = animatableClockViewAttributes.getInt(
+                R.styleable.AnimatableClockView_chargeAnimationDelay, 200
+            )
+        } finally {
+            animatableClockViewAttributes.recycle()
+        }
+
+        val textViewAttributes = context.obtainStyledAttributes(
+            attrs, android.R.styleable.TextView,
+            defStyleAttr, defStyleRes
+        )
+
+        isSingleLineInternal =
+            try {
+                textViewAttributes.getBoolean(android.R.styleable.TextView_singleLine, false)
+            } finally {
+                textViewAttributes.recycle()
+            }
+
+        refreshFormat()
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        refreshFormat()
+    }
+
+    /**
+     * Whether to use a bolded version based on the user specified fontWeightAdjustment.
+     */
+    fun useBoldedVersion(): Boolean {
+        // "Bold text" fontWeightAdjustment is 300.
+        return resources.configuration.fontWeightAdjustment > 100
+    }
+
+    fun refreshTime() {
+        time.timeInMillis = System.currentTimeMillis()
+        text = DateFormat.format(format, time)
+        contentDescription = DateFormat.format(descFormat, time)
+    }
+
+    fun onTimeZoneChanged(timeZone: TimeZone?) {
+        time.timeZone = timeZone
+        refreshFormat()
+    }
+
+    @SuppressLint("DrawAllocation")
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        val animator = textAnimator
+        if (animator == null) {
+            textAnimator = TextAnimator(layout) { invalidate() }
+            onTextAnimatorInitialized?.run()
+            onTextAnimatorInitialized = null
+        } else {
+            animator.updateLayout(layout)
+        }
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        textAnimator?.draw(canvas)
+    }
+
+    fun setLineSpacingScale(scale: Float) {
+        lineSpacingScale = scale
+        setLineSpacing(0f, lineSpacingScale)
+    }
+
+    fun setColors(@ColorInt dozingColor: Int, lockScreenColor: Int) {
+        this.dozingColor = dozingColor
+        this.lockScreenColor = lockScreenColor
+    }
+
+    fun animateAppearOnLockscreen() {
+        if (textAnimator == null) {
+            return
+        }
+        setTextStyle(
+            weight = dozingWeight,
+            textSize = -1f,
+            color = lockScreenColor,
+            animate = false,
+            duration = 0,
+            delay = 0,
+            onAnimationEnd = null
+        )
+        setTextStyle(
+            weight = lockScreenWeight,
+            textSize = -1f,
+            color = lockScreenColor,
+            animate = true,
+            duration = APPEAR_ANIM_DURATION,
+            delay = 0,
+            onAnimationEnd = null
+        )
+    }
+
+    fun animateFoldAppear() {
+        if (textAnimator == null) {
+            return
+        }
+        setTextStyle(
+            weight = lockScreenWeightInternal,
+            textSize = -1f,
+            color = lockScreenColor,
+            animate = false,
+            duration = 0,
+            delay = 0,
+            onAnimationEnd = null
+        )
+        setTextStyle(
+            weight = dozingWeightInternal,
+            textSize = -1f,
+            color = dozingColor,
+            animate = true,
+            interpolator = Interpolators.EMPHASIZED_DECELERATE,
+            duration = StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD.toLong(),
+            delay = 0,
+            onAnimationEnd = null
+        )
+    }
+
+    fun animateCharge(dozeStateGetter: DozeStateGetter) {
+        if (textAnimator == null || textAnimator!!.isRunning()) {
+            // Skip charge animation if dozing animation is already playing.
+            return
+        }
+        val startAnimPhase2 = Runnable {
+            setTextStyle(
+                weight = if (dozeStateGetter.isDozing) dozingWeight else lockScreenWeight,
+                textSize = -1f,
+                color = null,
+                animate = true,
+                duration = CHARGE_ANIM_DURATION_PHASE_1,
+                delay = 0,
+                onAnimationEnd = null
+            )
+        }
+        setTextStyle(
+            weight = if (dozeStateGetter.isDozing) lockScreenWeight else dozingWeight,
+            textSize = -1f,
+            color = null,
+            animate = true,
+            duration = CHARGE_ANIM_DURATION_PHASE_0,
+            delay = chargeAnimationDelay.toLong(),
+            onAnimationEnd = startAnimPhase2
+        )
+    }
+
+    fun animateDoze(isDozing: Boolean, animate: Boolean) {
+        setTextStyle(
+            weight = if (isDozing) dozingWeight else lockScreenWeight,
+            textSize = -1f,
+            color = if (isDozing) dozingColor else lockScreenColor,
+            animate = animate,
+            duration = DOZE_ANIM_DURATION,
+            delay = 0,
+            onAnimationEnd = null
+        )
+    }
+
+    /**
+     * Set text style with an optional animation.
+     *
+     * By passing -1 to weight, the view preserves its current weight.
+     * By passing -1 to textSize, the view preserves its current text size.
+     *
+     * @param weight text weight.
+     * @param textSize font size.
+     * @param animate true to animate the text style change, otherwise false.
+     */
+    private fun setTextStyle(
+        @IntRange(from = 0, to = 1000) weight: Int,
+        @FloatRange(from = 0.0) textSize: Float,
+        color: Int?,
+        animate: Boolean,
+        interpolator: TimeInterpolator?,
+        duration: Long,
+        delay: Long,
+        onAnimationEnd: Runnable?
+    ) {
+        if (textAnimator != null) {
+            textAnimator?.setTextStyle(
+                weight = weight,
+                textSize = textSize,
+                color = color,
+                animate = animate,
+                duration = duration,
+                interpolator = interpolator,
+                delay = delay,
+                onAnimationEnd = onAnimationEnd
+            )
+        } else {
+            // when the text animator is set, update its start values
+            onTextAnimatorInitialized = Runnable {
+                textAnimator?.setTextStyle(
+                    weight = weight,
+                    textSize = textSize,
+                    color = color,
+                    animate = false,
+                    duration = duration,
+                    interpolator = interpolator,
+                    delay = delay,
+                    onAnimationEnd = onAnimationEnd
+                )
+            }
+        }
+    }
+
+    private fun setTextStyle(
+        @IntRange(from = 0, to = 1000) weight: Int,
+        @FloatRange(from = 0.0) textSize: Float,
+        color: Int?,
+        animate: Boolean,
+        duration: Long,
+        delay: Long,
+        onAnimationEnd: Runnable?
+    ) {
+        setTextStyle(
+            weight = weight,
+            textSize = textSize,
+            color = color,
+            animate = animate,
+            interpolator = null,
+            duration = duration,
+            delay = delay,
+            onAnimationEnd = onAnimationEnd
+        )
+    }
+
+    fun refreshFormat() {
+        Patterns.update(context)
+        val use24HourFormat = DateFormat.is24HourFormat(context)
+
+        format = when {
+            isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
+            !isSingleLineInternal && use24HourFormat -> DOUBLE_LINE_FORMAT_24_HOUR
+            isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
+            else -> DOUBLE_LINE_FORMAT_12_HOUR
+        }
+
+        descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
+
+        refreshTime()
+    }
+
+    // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
+    // This is an optimization to ensure we only recompute the patterns when the inputs change.
+    private object Patterns {
+        var sClockView12: String? = null
+        var sClockView24: String? = null
+        var sCacheKey: String? = null
+
+        fun update(context: Context) {
+            val locale = Locale.getDefault()
+            val res = context.resources
+            val clockView12Skel = res.getString(R.string.clock_12hr_format)
+            val clockView24Skel = res.getString(R.string.clock_24hr_format)
+            val key = locale.toString() + clockView12Skel + clockView24Skel
+            if (key == sCacheKey) return
+
+            val clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel)
+            sClockView12 = clockView12
+
+            // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
+            // format.  The following code removes the AM/PM indicator if we didn't want it.
+            if (!clockView12Skel.contains("a")) {
+                sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
+            }
+            sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
+            sCacheKey = key
+        }
+    }
+
+    interface DozeStateGetter {
+        val isDozing: Boolean
+    }
+}
+
+private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
+private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"
+private const val DOZE_ANIM_DURATION: Long = 300
+private const val APPEAR_ANIM_DURATION: Long = 350
+private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
+private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 9238b82..25dcdf9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -190,11 +190,15 @@
         }
     }
 
-    private void animateClockChange(boolean useLargeClock) {
+    private void updateClockViews(boolean useLargeClock, boolean animate) {
         if (mClockInAnim != null) mClockInAnim.cancel();
         if (mClockOutAnim != null) mClockOutAnim.cancel();
         if (mStatusAreaAnim != null) mStatusAreaAnim.cancel();
 
+        mClockInAnim = null;
+        mClockOutAnim = null;
+        mStatusAreaAnim = null;
+
         View in, out;
         int direction = 1;
         float statusAreaYTranslation;
@@ -214,6 +218,14 @@
             removeView(out);
         }
 
+        if (!animate) {
+            out.setAlpha(0f);
+            in.setAlpha(1f);
+            in.setVisibility(VISIBLE);
+            mStatusArea.setTranslationY(statusAreaYTranslation);
+            return;
+        }
+
         mClockOutAnim = new AnimatorSet();
         mClockOutAnim.setDuration(CLOCK_OUT_MILLIS);
         mClockOutAnim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
@@ -273,7 +285,7 @@
      *
      * @return true if desired clock appeared and false if it was already visible
      */
-    boolean switchToClock(@ClockSize int clockSize) {
+    boolean switchToClock(@ClockSize int clockSize, boolean animate) {
         if (mDisplayedClockSize != null && clockSize == mDisplayedClockSize) {
             return false;
         }
@@ -281,7 +293,7 @@
         // 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);
+            updateClockViews(clockSize == LARGE, animate);
         }
 
         mDisplayedClockSize = clockSize;
@@ -293,7 +305,7 @@
         super.onLayout(changed, l, t, r, b);
 
         if (mDisplayedClockSize != null && !mChildrenAreLaidOut) {
-            animateClockChange(mDisplayedClockSize == LARGE);
+            updateClockViews(mDisplayedClockSize == LARGE, /* animate */ true);
         }
 
         mChildrenAreLaidOut = true;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index c628d44..b7a5aae 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -89,7 +89,6 @@
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final KeyguardBypassController mBypassController;
 
-    private int mLargeClockTopMargin = 0;
     private int mKeyguardClockTopMargin = 0;
 
     /**
@@ -276,33 +275,37 @@
     }
 
     private void updateClockLayout() {
-        if (mSmartspaceController.isEnabled()) {
-            RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT,
-                    MATCH_PARENT);
-            mLargeClockTopMargin = getContext().getResources().getDimensionPixelSize(
-                    R.dimen.keyguard_large_clock_top_margin);
-            lp.topMargin = mLargeClockTopMargin;
-            mLargeClockFrame.setLayoutParams(lp);
-        } else {
-            mLargeClockTopMargin = 0;
-        }
+        int largeClockTopMargin = getContext().getResources().getDimensionPixelSize(
+                R.dimen.keyguard_large_clock_top_margin);
+
+        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT,
+                MATCH_PARENT);
+        lp.topMargin = largeClockTopMargin;
+        mLargeClockFrame.setLayoutParams(lp);
     }
 
     /**
      * Set which clock should be displayed on the keyguard. The other one will be automatically
      * hidden.
      */
-    public void displayClock(@KeyguardClockSwitch.ClockSize int clockSize) {
+    public void displayClock(@KeyguardClockSwitch.ClockSize int clockSize, boolean animate) {
         if (!mCanShowDoubleLineClock && clockSize == KeyguardClockSwitch.LARGE) {
             return;
         }
 
-        boolean appeared = mView.switchToClock(clockSize);
-        if (appeared && clockSize == LARGE) {
+        boolean appeared = mView.switchToClock(clockSize, animate);
+        if (animate && appeared && clockSize == LARGE) {
             mLargeClockViewController.animateAppear();
         }
     }
 
+    public void animateFoldToAod() {
+        if (mClockViewController != null) {
+            mClockViewController.animateFoldAppear();
+            mLargeClockViewController.animateFoldAppear();
+        }
+    }
+
     /**
      * If we're presenting a custom clock of just the default one.
      */
@@ -445,7 +448,7 @@
             Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1) != 0;
 
         if (!mCanShowDoubleLineClock) {
-            mUiExecutor.execute(() -> displayClock(KeyguardClockSwitch.SMALL));
+            mUiExecutor.execute(() -> displayClock(KeyguardClockSwitch.SMALL, /* animate */ true));
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 67ef02a..b84cb19 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -866,7 +866,6 @@
                 ((LayerDrawable) mUserSwitcher.getBackground()).getDrawable(1).setAlpha(255);
             }
 
-            anchor.setClickable(true);
             anchor.setOnClickListener((v) -> {
                 if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
 
@@ -877,8 +876,7 @@
                         public void onItemClick(AdapterView parent, View view, int pos, long id) {
                             if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
 
-                            // - 1 to account for the header view
-                            UserRecord user = adapter.getItem(pos - 1);
+                            UserRecord user = adapter.getItem(pos);
                             if (!user.isCurrent) {
                                 adapter.onUserListItemClicked(user);
                             }
@@ -907,9 +905,16 @@
                     == Configuration.ORIENTATION_PORTRAIT) {
                 updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL);
                 updateViewGravity(mUserSwitcherViewGroup, Gravity.CENTER_HORIZONTAL);
+                mUserSwitcherViewGroup.setTranslationY(0);
             } else {
                 updateViewGravity(mViewFlipper, Gravity.RIGHT | Gravity.BOTTOM);
-                updateViewGravity(mUserSwitcherViewGroup, Gravity.LEFT | Gravity.TOP);
+                updateViewGravity(mUserSwitcherViewGroup, Gravity.LEFT | Gravity.CENTER_VERTICAL);
+
+                // Attempt to reposition a bit higher to make up for this frame being a bit lower
+                // on the device
+                int yTrans = mView.getContext().getResources().getDimensionPixelSize(
+                        R.dimen.status_bar_height);
+                mUserSwitcherViewGroup.setTranslationY(-yTrans);
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 986d0de..8bf890d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -123,8 +123,17 @@
      * Set which clock should be displayed on the keyguard. The other one will be automatically
      * hidden.
      */
-    public void displayClock(@ClockSize int clockSize) {
-        mKeyguardClockSwitchController.displayClock(clockSize);
+    public void displayClock(@ClockSize int clockSize, boolean animate) {
+        mKeyguardClockSwitchController.displayClock(clockSize, animate);
+    }
+
+    /**
+     * Performs fold to aod animation of the clocks (changes font weight from bold to thin).
+     * This animation is played when AOD is enabled and foldable device is fully folded, it is
+     * displayed on the outer screen
+     */
+    public void animateFoldToAod() {
+        mKeyguardClockSwitchController.animateFoldToAod();
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
index dfb4667..7b6ce3e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
@@ -18,12 +18,10 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.content.res.Resources;
-import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.ListPopupWindow;
 import android.widget.ListView;
-import android.widget.TextView;
 
 import com.android.systemui.R;
 import com.android.systemui.plugins.FalsingManager;
@@ -68,12 +66,6 @@
         // This will force the popupwindow to show upward instead of drop down
         listView.addOnLayoutChangeListener(mLayoutListener);
 
-        TextView header = (TextView) LayoutInflater.from(mContext).inflate(
-                R.layout.keyguard_bouncer_user_switcher_item, listView, false);
-        header.setText(mContext.getResources().getString(
-                R.string.accessibility_multi_user_switch_switcher));
-        listView.addHeaderView(header);
-
         listView.setOnTouchListener((v, ev) -> {
             if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
                 return mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY);
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index fe68137..6626f59 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -35,7 +35,6 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Process;
 import android.os.VibrationAttributes;
-import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -111,7 +110,6 @@
     private VelocityTracker mVelocityTracker;
     // The ID of the pointer for which ACTION_DOWN has occurred. -1 means no pointer is active.
     private int mActivePointerId = -1;
-    private VibrationEffect mTick;
 
     private boolean mIsDozing;
     private boolean mIsBouncerShowing;
@@ -280,10 +278,6 @@
             mView.setContentDescription(mUnlockedLabel);
             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);
@@ -567,15 +561,11 @@
             case MotionEvent.ACTION_DOWN:
             case MotionEvent.ACTION_HOVER_ENTER:
                 if (mVibrator != null && !mDownDetected) {
-                    if (mTick == null) {
-                        mTick = UdfpsController.lowTick(getContext(), true,
-                                LONG_PRESS_TIMEOUT);
-                    }
                     mVibrator.vibrate(
                             Process.myUid(),
                             getContext().getOpPackageName(),
-                            mTick,
-                            "lock-icon-tick",
+                            UdfpsController.EFFECT_CLICK,
+                            "lock-icon-down",
                             TOUCH_VIBRATION_ATTRIBUTES);
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 8c405ca..63962fa 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui;
 
+import android.app.ActivityManager;
 import android.app.ActivityThread;
 import android.app.Application;
 import android.app.Notification;
@@ -27,6 +28,7 @@
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.Process;
+import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -111,6 +113,13 @@
                         ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG);
             }
 
+            // Enable binder tracing on system server for calls originating from SysUI
+            try {
+                ActivityManager.getService().enableBinderTracing();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Unable to enable binder tracing", e);
+            }
+
             registerReceiver(new BroadcastReceiver() {
                 @Override
                 public void onReceive(Context context, Intent intent) {
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
index 251c1e6..2767904 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
@@ -121,7 +121,7 @@
                     .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
                     .setTaskSurfaceHelper(mWMComponent.getTaskSurfaceHelper())
                     .setRecentTasks(mWMComponent.getRecentTasks())
-                    .setSizeCompatUI(Optional.of(mWMComponent.getSizeCompatUI()))
+                    .setCompatUI(Optional.of(mWMComponent.getCompatUI()))
                     .setDragAndDrop(Optional.of(mWMComponent.getDragAndDrop()));
         } else {
             // TODO: Call on prepareSysUIComponentBuilder but not with real components. Other option
@@ -141,7 +141,7 @@
                     .setStartingSurface(Optional.ofNullable(null))
                     .setTaskSurfaceHelper(Optional.ofNullable(null))
                     .setRecentTasks(Optional.ofNullable(null))
-                    .setSizeCompatUI(Optional.ofNullable(null))
+                    .setCompatUI(Optional.ofNullable(null))
                     .setDragAndDrop(Optional.ofNullable(null));
         }
         mSysUIComponent = builder.build();
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index f11dc93..e35b558 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -636,6 +636,7 @@
         mIndicatorView.setText(message);
         mIndicatorView.setTextColor(mTextColorError);
         mIndicatorView.setVisibility(View.VISIBLE);
+        mIndicatorView.setSelected(true);
         mHandler.postDelayed(resetMessageRunnable, mInjector.getDelayAfterError());
 
         Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 15374e5..1da9f21 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -47,7 +47,6 @@
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
-import android.provider.Settings;
 import android.util.Log;
 import android.view.Gravity;
 import android.view.LayoutInflater;
@@ -167,9 +166,6 @@
     private boolean mAttemptedToDismissKeyguard;
     private Set<Callback> mCallbacks = new HashSet<>();
 
-    private static final int DEFAULT_TICK = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;
-    private final VibrationEffect mTick;
-
     @VisibleForTesting
     public static final VibrationAttributes VIBRATION_ATTRIBUTES =
             new VibrationAttributes.Builder()
@@ -283,9 +279,6 @@
                     return;
                 }
                 mGoodCaptureReceived = true;
-                if (mVibrator != null) {
-                    mVibrator.cancel();
-                }
                 mView.stopIllumination();
                 if (mServerRequest != null) {
                     mServerRequest.onAcquiredGood();
@@ -589,7 +582,6 @@
         mConfigurationController = configurationController;
         mSystemClock = systemClock;
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
-        mTick = lowTick(context, false /* useShortRampup */, DEFAULT_VIBRATION_DURATION);
 
         mSensorProps = findFirstUdfps();
         // At least one UDFPS sensor exists
@@ -625,47 +617,6 @@
     }
 
     /**
-     * Returns the continuous low tick effect that starts playing on the udfps finger-down event.
-     */
-    public static VibrationEffect lowTick(
-            Context context,
-            boolean useShortRampUp,
-            long duration
-    ) {
-        boolean useLowTickDefault = context.getResources()
-                .getBoolean(R.bool.config_udfpsUseLowTick);
-        int primitiveTick = DEFAULT_TICK;
-        if (Settings.Global.getFloat(
-                context.getContentResolver(),
-                "tick-low", useLowTickDefault ? 1 : 0) == 0) {
-            primitiveTick = VibrationEffect.Composition.PRIMITIVE_TICK;
-        }
-        float tickIntensity = Settings.Global.getFloat(
-                context.getContentResolver(),
-                "tick-intensity",
-                context.getResources().getFloat(R.dimen.config_udfpsTickIntensity));
-        int tickDelay = Settings.Global.getInt(
-                context.getContentResolver(),
-                "tick-delay",
-                context.getResources().getInteger(R.integer.config_udfpsTickDelay));
-
-        VibrationEffect.Composition composition = VibrationEffect.startComposition();
-        composition.addPrimitive(primitiveTick, tickIntensity, 0);
-        int primitives = (int) (duration / tickDelay);
-        float[] rampUp = new float[]{.48f, .58f, .69f, .83f};
-        if (useShortRampUp) {
-            rampUp = new float[]{.5f, .7f};
-        }
-        for (int i = 0; i < rampUp.length; i++) {
-            composition.addPrimitive(primitiveTick, tickIntensity * rampUp[i], tickDelay);
-        }
-        for (int i = rampUp.length; i < primitives; i++) {
-            composition.addPrimitive(primitiveTick, tickIntensity, tickDelay);
-        }
-        return composition.compose();
-    }
-
-    /**
      * Play haptic to signal udfps scanning started.
      */
     @VisibleForTesting
@@ -674,8 +625,8 @@
             mVibrator.vibrate(
                     Process.myUid(),
                     mContext.getOpPackageName(),
-                    mTick,
-                    "udfps-onStart-tick",
+                    EFFECT_CLICK,
+                    "udfps-onStart-click",
                     VIBRATION_ATTRIBUTES);
         }
     }
@@ -1066,7 +1017,6 @@
             }
         }
         mOnFingerDown = false;
-        mVibrator.cancel();
         if (mView.isIlluminationRequested()) {
             mView.stopIllumination();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
index 1f01fc5..ac38b13 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
@@ -20,9 +20,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.content.Context;
-import android.content.res.TypedArray;
 import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -30,7 +28,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.Looper;
-import android.util.TypedValue;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.LinearInterpolator;
 
@@ -38,7 +35,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.internal.graphics.ColorUtils;
 import com.android.systemui.R;
 
 /**
@@ -106,9 +102,8 @@
 
         mSensorOutlinePaint = new Paint(0 /* flags */);
         mSensorOutlinePaint.setAntiAlias(true);
-        mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_enroll_icon));
-        mSensorOutlinePaint.setStyle(Paint.Style.STROKE);
-        mSensorOutlinePaint.setStrokeWidth(2.f);
+        mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_moving_target_fill));
+        mSensorOutlinePaint.setStyle(Paint.Style.FILL);
 
         mBlueFill = new Paint(0 /* flags */);
         mBlueFill.setAntiAlias(true);
@@ -117,12 +112,12 @@
 
         mMovingTargetFpIcon = context.getResources()
                 .getDrawable(R.drawable.ic_kg_fingerprint, null);
-        mMovingTargetFpIcon.setTint(Color.WHITE);
+        mMovingTargetFpIcon.setTint(mContext.getColor(R.color.udfps_enroll_icon));
         mMovingTargetFpIcon.mutate();
 
         mFingerprintDrawable.setTint(mContext.getColor(R.color.udfps_enroll_icon));
 
-        mHintColorFaded = getHintColorFaded(context);
+        mHintColorFaded = context.getColor(R.color.udfps_moving_target_fill);
         mHintColorHighlight = context.getColor(R.color.udfps_enroll_progress);
         mHintMaxWidthPx = Utils.dpToPixels(context, HINT_MAX_WIDTH_DP);
         mHintPaddingPx = Utils.dpToPixels(context, HINT_PADDING_DP);
@@ -218,22 +213,6 @@
         };
     }
 
-    @ColorInt
-    private static int getHintColorFaded(@NonNull Context context) {
-        final TypedValue tv = new TypedValue();
-        context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, tv, true);
-        final int alpha = (int) (tv.getFloat() * 255f);
-
-        final int[] attrs = new int[] {android.R.attr.colorControlNormal};
-        final TypedArray ta = context.obtainStyledAttributes(attrs);
-        try {
-            @ColorInt final int color = ta.getColor(0, context.getColor(R.color.white_disabled));
-            return ColorUtils.setAlphaComponent(color, alpha);
-        } finally {
-            ta.recycle();
-        }
-    }
-
     void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) {
         mEnrollHelper = helper;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
index 79c7e66..631a461 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
@@ -18,12 +18,10 @@
 
 import android.animation.ValueAnimator;
 import android.content.Context;
-import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.ColorFilter;
 import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
-import android.util.TypedValue;
 import android.view.animation.Interpolator;
 import android.view.animation.OvershootInterpolator;
 
@@ -80,24 +78,11 @@
 
         mBackgroundPaint = new Paint();
         mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
-        mBackgroundPaint.setColor(context.getColor(R.color.white_disabled));
+        mBackgroundPaint.setColor(context.getColor(R.color.udfps_moving_target_fill));
         mBackgroundPaint.setAntiAlias(true);
         mBackgroundPaint.setStyle(Paint.Style.STROKE);
         mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
 
-        // Set background paint color and alpha.
-        final int[] attrs = new int[] {android.R.attr.colorControlNormal};
-        final TypedArray typedArray = context.obtainStyledAttributes(attrs);
-        try {
-            @ColorInt final int tintColor = typedArray.getColor(0, mBackgroundPaint.getColor());
-            mBackgroundPaint.setColor(tintColor);
-        } finally {
-            typedArray.recycle();
-        }
-        TypedValue alpha = new TypedValue();
-        context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
-        mBackgroundPaint.setAlpha((int) (alpha.getFloat() * 255f));
-
         // Progress fill should *not* use the extracted system color.
         mFillPaint = new Paint();
         mFillPaint.setStrokeWidth(mStrokeWidthPx);
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSourceMonitor.java b/packages/SystemUI/src/com/android/systemui/communal/CommunalSourceMonitor.java
index 4aa46f18..58cf35f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSourceMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSourceMonitor.java
@@ -16,12 +16,14 @@
 
 package com.android.systemui.communal;
 
+import static com.android.systemui.communal.dagger.CommunalModule.COMMUNAL_CONDITIONS;
+
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.communal.conditions.CommunalConditionsMonitor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.util.condition.Monitor;
 
 import com.google.android.collect.Lists;
 
@@ -31,6 +33,7 @@
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 
 /**
  * A Monitor for reporting a {@link CommunalSource} presence.
@@ -42,7 +45,7 @@
 
     // A list of {@link Callback} that have registered to receive updates.
     private final ArrayList<WeakReference<Callback>> mCallbacks = Lists.newArrayList();
-    private final CommunalConditionsMonitor mConditionsMonitor;
+    private final Monitor mConditionsMonitor;
     private final Executor mExecutor;
 
     private CommunalSource mCurrentSource;
@@ -53,7 +56,7 @@
     // Whether the class is currently listening for condition changes.
     private boolean mListeningForConditions = false;
 
-    private final CommunalConditionsMonitor.Callback mConditionsCallback =
+    private final Monitor.Callback mConditionsCallback =
             allConditionsMet -> {
                 if (mAllCommunalConditionsMet != allConditionsMet) {
                     if (DEBUG) Log.d(TAG, "communal conditions changed: " + allConditionsMet);
@@ -66,7 +69,7 @@
     @VisibleForTesting
     @Inject
     public CommunalSourceMonitor(@Main Executor executor,
-            CommunalConditionsMonitor communalConditionsMonitor) {
+            @Named(COMMUNAL_CONDITIONS) Monitor communalConditionsMonitor) {
         mExecutor = executor;
         mConditionsMonitor = communalConditionsMonitor;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalConditionsMonitor.java b/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalConditionsMonitor.java
deleted file mode 100644
index 1197816..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/conditions/CommunalConditionsMonitor.java
+++ /dev/null
@@ -1,42 +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.communal.conditions;
-
-
-import static com.android.systemui.communal.dagger.CommunalModule.COMMUNAL_CONDITIONS;
-
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.util.condition.Condition;
-import com.android.systemui.util.condition.Monitor;
-
-import java.util.Set;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-/**
- * A concrete implementation of {@Monitor} with conditions for monitoring when communal mode should
- * be enabled.
- */
-@SysUISingleton
-public class CommunalConditionsMonitor extends Monitor {
-    @Inject
-    public CommunalConditionsMonitor(
-            @Named(COMMUNAL_CONDITIONS) Set<Condition> communalConditions) {
-        super(communalConditions);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java
index f27ae34..e1f1ac4 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java
@@ -34,6 +34,8 @@
 import com.android.systemui.idle.LightSensorEventsDebounceAlgorithm;
 import com.android.systemui.idle.dagger.IdleViewComponent;
 import com.android.systemui.util.condition.Condition;
+import com.android.systemui.util.condition.Monitor;
+import com.android.systemui.util.condition.dagger.MonitorComponent;
 
 import java.util.Collections;
 import java.util.HashSet;
@@ -135,4 +137,14 @@
             return Optional.empty();
         }
     }
+
+    /** */
+    @Provides
+    @Named(COMMUNAL_CONDITIONS)
+    static Monitor provideCommunalSourceMonitor(
+            @Named(COMMUNAL_CONDITIONS) Set<Condition> communalConditions,
+            MonitorComponent.Factory factory) {
+        final MonitorComponent component = factory.create(communalConditions, new HashSet<>());
+        return component.getMonitor();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 2e0c0f4..e337678 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -33,6 +33,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.compatui.CompatUI;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
 import com.android.wm.shell.draganddrop.DragAndDrop;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
@@ -40,7 +41,6 @@
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.recents.RecentTasks;
-import com.android.wm.shell.sizecompatui.SizeCompatUI;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.startingsurface.StartingSurface;
 import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelper;
@@ -112,7 +112,7 @@
         Builder setRecentTasks(Optional<RecentTasks> r);
 
         @BindsInstance
-        Builder setSizeCompatUI(Optional<SizeCompatUI> s);
+        Builder setCompatUI(Optional<CompatUI> s);
 
         @BindsInstance
         Builder setDragAndDrop(Optional<DragAndDrop> d);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
index 90a3ad2..b815d4e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
@@ -25,6 +25,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.compatui.CompatUI;
 import com.android.wm.shell.dagger.TvWMShellModule;
 import com.android.wm.shell.dagger.WMShellModule;
 import com.android.wm.shell.dagger.WMSingleton;
@@ -35,7 +36,6 @@
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.recents.RecentTasks;
-import com.android.wm.shell.sizecompatui.SizeCompatUI;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.startingsurface.StartingSurface;
 import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelper;
@@ -119,7 +119,7 @@
     Optional<RecentTasks> getRecentTasks();
 
     @WMSingleton
-    SizeCompatUI getSizeCompatUI();
+    CompatUI getCompatUI();
 
     @WMSingleton
     DragAndDrop getDragAndDrop();
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
index 471a327..b2fe3bb 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
@@ -37,10 +37,14 @@
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.tuner.TunerService;
+import com.android.systemui.unfold.FoldAodAnimationController;
+import com.android.systemui.unfold.FoldAodAnimationController.FoldAodAnimationStatus;
+import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.util.AlarmTimeout;
 import com.android.systemui.util.wakelock.WakeLock;
 
 import java.util.Calendar;
+import java.util.Optional;
 
 import javax.inject.Inject;
 
@@ -49,7 +53,8 @@
  */
 @DozeScope
 public class DozeUi implements DozeMachine.Part, TunerService.Tunable,
-        ConfigurationController.ConfigurationListener, StatusBarStateController.StateListener {
+        ConfigurationController.ConfigurationListener, FoldAodAnimationStatus,
+        StatusBarStateController.StateListener {
     // if enabled, calls dozeTimeTick() whenever the time changes:
     private static final boolean BURN_IN_TESTING_ENABLED = false;
     private static final long TIME_TICK_DEADLINE_MILLIS = 90 * 1000; // 1.5min
@@ -57,6 +62,7 @@
     private final DozeHost mHost;
     private final Handler mHandler;
     private final WakeLock mWakeLock;
+    private final FoldAodAnimationController mFoldAodAnimationController;
     private DozeMachine mMachine;
     private final AlarmTimeout mTimeTicker;
     private final boolean mCanAnimateTransition;
@@ -100,6 +106,7 @@
             DozeParameters params, KeyguardUpdateMonitor keyguardUpdateMonitor,
             DozeLog dozeLog, TunerService tunerService,
             StatusBarStateController statusBarStateController,
+            Optional<SysUIUnfoldComponent> sysUiUnfoldComponent,
             ConfigurationController configurationController) {
         mContext = context;
         mWakeLock = wakeLock;
@@ -118,12 +125,23 @@
 
         mConfigurationController = configurationController;
         mConfigurationController.addCallback(this);
+
+        mFoldAodAnimationController = sysUiUnfoldComponent
+                .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
+
+        if (mFoldAodAnimationController != null) {
+            mFoldAodAnimationController.addCallback(this);
+        }
     }
 
     @Override
     public void destroy() {
         mTunerService.removeTunable(this);
         mConfigurationController.removeCallback(this);
+
+        if (mFoldAodAnimationController != null) {
+            mFoldAodAnimationController.removeCallback(this);
+        }
     }
 
     @Override
@@ -142,7 +160,8 @@
                     && (mKeyguardShowing || mDozeParameters.shouldControlUnlockedScreenOff())
                     && !mHost.isPowerSaveActive();
             mDozeParameters.setControlScreenOffAnimation(controlScreenOff);
-            mHost.setAnimateScreenOff(controlScreenOff);
+            mHost.setAnimateScreenOff(controlScreenOff
+                    && mDozeParameters.shouldAnimateDozingChange());
         }
     }
 
@@ -299,4 +318,9 @@
     public void onStatePostChange() {
         updateAnimateScreenOff();
     }
+
+    @Override
+    public void onFoldToAodAnimationChanged() {
+        updateAnimateScreenOff();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerView.java
new file mode 100644
index 0000000..bc1f772
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerView.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.systemui.dreams;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+/**
+ * {@link DreamOverlayContainerView} contains a dream overlay and its status bar.
+ */
+public class DreamOverlayContainerView extends ConstraintLayout {
+    public DreamOverlayContainerView(Context context) {
+        this(context, null);
+    }
+
+    public DreamOverlayContainerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DreamOverlayContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public DreamOverlayContainerView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 8f0ea2f..393f039 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -29,11 +29,11 @@
 import android.view.WindowManager;
 
 import androidx.annotation.NonNull;
-import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.PhoneWindow;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dreams.dagger.DreamOverlayComponent;
 
 import java.util.concurrent.Executor;
 
@@ -54,57 +54,75 @@
     private final Executor mExecutor;
     // The state controller informs the service of updates to the overlays present.
     private final DreamOverlayStateController mStateController;
+    // The component used to resolve dream overlay dependencies.
+    private final DreamOverlayComponent mDreamOverlayComponent;
 
-    // The window is populated once the dream informs the service it has begun dreaming.
-    private Window mWindow;
-    private ConstraintLayout mLayout;
+    // The dream overlay's content view, which is located below the status bar (in z-order) and is
+    // the space into which widgets are placed.
+    private ViewGroup mDreamOverlayContentView;
 
     private final DreamOverlayStateController.Callback mOverlayStateCallback =
             new DreamOverlayStateController.Callback() {
-        @Override
-        public void onOverlayChanged() {
-            mExecutor.execute(() -> reloadOverlaysLocked());
-        }
-    };
+                @Override
+                public void onOverlayChanged() {
+                    mExecutor.execute(() -> reloadOverlaysLocked());
+                }
+            };
 
     // The service listens to view changes in order to declare that input occurring in areas outside
     // the overlay should be passed through to the dream underneath.
-    private View.OnAttachStateChangeListener mRootViewAttachListener =
+    private final View.OnAttachStateChangeListener mRootViewAttachListener =
             new View.OnAttachStateChangeListener() {
-        @Override
-        public void onViewAttachedToWindow(View v) {
-            v.getViewTreeObserver()
-                    .addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
-        }
+                @Override
+                public void onViewAttachedToWindow(View v) {
+                    v.getViewTreeObserver()
+                            .addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
+                }
 
-        @Override
-        public void onViewDetachedFromWindow(View v) {
-            v.getViewTreeObserver()
-                    .removeOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
-        }
-    };
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                    v.getViewTreeObserver()
+                            .removeOnComputeInternalInsetsListener(
+                                    mOnComputeInternalInsetsListener);
+                }
+            };
 
     // A hook into the internal inset calculation where we declare the overlays as the only
     // touchable regions.
-    private ViewTreeObserver.OnComputeInternalInsetsListener mOnComputeInternalInsetsListener  =
+    private final ViewTreeObserver.OnComputeInternalInsetsListener
+            mOnComputeInternalInsetsListener =
             new ViewTreeObserver.OnComputeInternalInsetsListener() {
-        @Override
-        public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
-            if (mLayout != null) {
-                inoutInfo.setTouchableInsets(
-                        ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
-                final Region region = new Region();
-                for (int i = 0; i < mLayout.getChildCount(); i++) {
-                    View child = mLayout.getChildAt(i);
-                    final Rect rect = new Rect();
-                    child.getGlobalVisibleRect(rect);
-                    region.op(rect, Region.Op.UNION);
-                }
+                @Override
+                public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+                    if (mDreamOverlayContentView != null) {
+                        inoutInfo.setTouchableInsets(
+                                ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+                        final Region region = new Region();
+                        for (int i = 0; i < mDreamOverlayContentView.getChildCount(); i++) {
+                            View child = mDreamOverlayContentView.getChildAt(i);
+                            final Rect rect = new Rect();
+                            child.getGlobalVisibleRect(rect);
+                            region.op(rect, Region.Op.UNION);
+                        }
 
-                inoutInfo.touchableRegion.set(region);
-            }
-        }
-    };
+                        inoutInfo.touchableRegion.set(region);
+                    }
+                }
+            };
+
+    @Inject
+    public DreamOverlayService(
+            Context context,
+            @Main Executor executor,
+            DreamOverlayStateController overlayStateController,
+            DreamOverlayComponent.Factory dreamOverlayComponentFactory) {
+        mContext = context;
+        mExecutor = executor;
+        mStateController = overlayStateController;
+        mDreamOverlayComponent = dreamOverlayComponentFactory.create();
+
+        mStateController.addCallback(mOverlayStateCallback);
+    }
 
     @Override
     public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
@@ -112,10 +130,10 @@
     }
 
     private void reloadOverlaysLocked() {
-        if (mLayout == null) {
+        if (mDreamOverlayContentView == null) {
             return;
         }
-        mLayout.removeAllViews();
+        mDreamOverlayContentView.removeAllViews();
         for (OverlayProvider overlayProvider : mStateController.getOverlays()) {
             addOverlay(overlayProvider);
         }
@@ -129,31 +147,32 @@
      *                     into the dream window.
      */
     private void addOverlayWindowLocked(WindowManager.LayoutParams layoutParams) {
-        mWindow = new PhoneWindow(mContext);
-        mWindow.setAttributes(layoutParams);
-        mWindow.setWindowManager(null, layoutParams.token, "DreamOverlay", true);
+        final PhoneWindow window = new PhoneWindow(mContext);
+        window.setAttributes(layoutParams);
+        window.setWindowManager(null, layoutParams.token, "DreamOverlay", true);
 
-        mWindow.setBackgroundDrawable(new ColorDrawable(0));
+        window.setBackgroundDrawable(new ColorDrawable(0));
 
-        mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
-        mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
-        mWindow.requestFeature(Window.FEATURE_NO_TITLE);
+        window.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+        window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+        window.requestFeature(Window.FEATURE_NO_TITLE);
         // Hide all insets when the dream is showing
-        mWindow.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
-        mWindow.setDecorFitsSystemWindows(false);
+        window.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
+        window.setDecorFitsSystemWindows(false);
 
         if (DEBUG) {
             Log.d(TAG, "adding overlay window to dream");
         }
 
-        mLayout = new ConstraintLayout(mContext);
-        mLayout.setLayoutParams(new ViewGroup.LayoutParams(
-                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
-        mLayout.addOnAttachStateChangeListener(mRootViewAttachListener);
-        mWindow.setContentView(mLayout);
+        window.setContentView(mDreamOverlayComponent.getDreamOverlayContainerView());
+
+        mDreamOverlayContentView = mDreamOverlayComponent.getDreamOverlayContentView();
+        mDreamOverlayContentView.addOnAttachStateChangeListener(mRootViewAttachListener);
+
+        mDreamOverlayComponent.getDreamOverlayStatusBarViewController().init();
 
         final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
-        windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
+        windowManager.addView(window.getDecorView(), window.getAttributes());
         mExecutor.execute(this::reloadOverlaysLocked);
     }
 
@@ -163,11 +182,11 @@
                 (view, layoutParams) -> {
                     // Always move UI related work to the main thread.
                     mExecutor.execute(() -> {
-                        if (mLayout == null) {
+                        if (mDreamOverlayContentView == null) {
                             return;
                         }
 
-                        mLayout.addView(view, layoutParams);
+                        mDreamOverlayContentView.addView(view, layoutParams);
                     });
                 },
                 () -> {
@@ -178,15 +197,6 @@
                 });
     }
 
-    @Inject
-    public DreamOverlayService(Context context, @Main Executor executor,
-            DreamOverlayStateController overlayStateController) {
-        mContext = context;
-        mExecutor = executor;
-        mStateController = overlayStateController;
-        mStateController.addCallback(mOverlayStateCallback);
-    }
-
     @Override
     public void onDestroy() {
         mStateController.removeCallback(mOverlayStateCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
new file mode 100644
index 0000000..9847ef6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.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 com.android.systemui.dreams;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.android.internal.util.Preconditions;
+import com.android.systemui.R;
+import com.android.systemui.battery.BatteryMeterView;
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
+
+/**
+ * {@link DreamOverlayStatusBarView} is the view responsible for displaying the status bar in a
+ * dream. The status bar includes status icons such as battery and wifi.
+ */
+public class DreamOverlayStatusBarView extends ConstraintLayout implements
+        BatteryStateChangeCallback {
+
+    private BatteryMeterView mBatteryView;
+    private ImageView mWifiStatusView;
+
+    public DreamOverlayStatusBarView(Context context) {
+        this(context, null);
+    }
+
+    public DreamOverlayStatusBarView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DreamOverlayStatusBarView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public DreamOverlayStatusBarView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mBatteryView = Preconditions.checkNotNull(findViewById(R.id.dream_overlay_battery),
+                "R.id.dream_overlay_battery must not be null");
+        mWifiStatusView = Preconditions.checkNotNull(findViewById(R.id.dream_overlay_wifi_status),
+                "R.id.dream_overlay_wifi_status must not be null");
+
+        mWifiStatusView.setImageDrawable(getContext().getDrawable(R.drawable.ic_signal_wifi_off));
+    }
+
+    /**
+     * Whether to show the battery percent text next to the battery status icons.
+     * @param show True if the battery percent text should be shown.
+     */
+    void showBatteryPercentText(boolean show) {
+        mBatteryView.setForceShowPercent(show);
+    }
+
+    /**
+     * Whether to show the wifi status icon.
+     * @param show True if the wifi status icon should be shown.
+     */
+    void showWifiStatus(boolean show) {
+        // Only show the wifi status icon when wifi isn't available.
+        mWifiStatusView.setVisibility(show ? View.VISIBLE : View.GONE);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
new file mode 100644
index 0000000..5674b9f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.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.dreams;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+
+import com.android.systemui.battery.BatteryMeterViewController;
+import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.dreams.dagger.DreamOverlayModule;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.util.ViewController;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * View controller for {@link DreamOverlayStatusBarView}.
+ */
+@DreamOverlayComponent.DreamOverlayScope
+public class DreamOverlayStatusBarViewController extends ViewController<DreamOverlayStatusBarView> {
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "WIFI_STATUS_" }, value = {
+            WIFI_STATUS_UNKNOWN,
+            WIFI_STATUS_UNAVAILABLE,
+            WIFI_STATUS_AVAILABLE
+    })
+    private @interface WifiStatus {}
+    private static final int WIFI_STATUS_UNKNOWN = 0;
+    private static final int WIFI_STATUS_UNAVAILABLE = 1;
+    private static final int WIFI_STATUS_AVAILABLE = 2;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "BATTERY_STATUS_" }, value = {
+            BATTERY_STATUS_UNKNOWN,
+            BATTERY_STATUS_NOT_CHARGING,
+            BATTERY_STATUS_CHARGING
+    })
+    private @interface BatteryStatus {}
+    private static final int BATTERY_STATUS_UNKNOWN = 0;
+    private static final int BATTERY_STATUS_NOT_CHARGING = 1;
+    private static final int BATTERY_STATUS_CHARGING = 2;
+
+    private final BatteryController mBatteryController;
+    private final BatteryMeterViewController mBatteryMeterViewController;
+    private final ConnectivityManager mConnectivityManager;
+    private final boolean mShowPercentAvailable;
+
+    private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder()
+            .clearCapabilities()
+            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
+
+    private final NetworkCallback mNetworkCallback = new NetworkCallback() {
+        @Override
+        public void onCapabilitiesChanged(
+                Network network, NetworkCapabilities networkCapabilities) {
+            onWifiAvailabilityChanged(
+                    networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI));
+        }
+
+        @Override
+        public void onAvailable(Network network) {
+            onWifiAvailabilityChanged(true);
+        }
+
+        @Override
+        public void onLost(Network network) {
+            onWifiAvailabilityChanged(false);
+        }
+    };
+
+    private final BatteryController.BatteryStateChangeCallback mBatteryStateChangeCallback =
+            new BatteryController.BatteryStateChangeCallback() {
+                @Override
+                public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+                    DreamOverlayStatusBarViewController.this.onBatteryLevelChanged(charging);
+                }
+            };
+
+    private @WifiStatus int mWifiStatus = WIFI_STATUS_UNKNOWN;
+    private @BatteryStatus int mBatteryStatus = BATTERY_STATUS_UNKNOWN;
+
+    @Inject
+    public DreamOverlayStatusBarViewController(
+            Context context,
+            DreamOverlayStatusBarView view,
+            BatteryController batteryController,
+            @Named(DreamOverlayModule.DREAM_OVERLAY_BATTERY_CONTROLLER)
+                    BatteryMeterViewController batteryMeterViewController,
+            ConnectivityManager connectivityManager) {
+        super(view);
+        mBatteryController = batteryController;
+        mBatteryMeterViewController = batteryMeterViewController;
+        mConnectivityManager = connectivityManager;
+
+        mShowPercentAvailable = context.getResources().getBoolean(
+                com.android.internal.R.bool.config_battery_percentage_setting_available);
+    }
+
+    @Override
+    protected void onInit() {
+        super.onInit();
+        mBatteryMeterViewController.init();
+    }
+
+    @Override
+    protected void onViewAttached() {
+        mBatteryController.addCallback(mBatteryStateChangeCallback);
+        mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback);
+
+        NetworkCapabilities capabilities =
+                mConnectivityManager.getNetworkCapabilities(
+                        mConnectivityManager.getActiveNetwork());
+        onWifiAvailabilityChanged(
+                capabilities != null
+                        && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI));
+    }
+
+    @Override
+    protected void onViewDetached() {
+        mBatteryController.removeCallback(mBatteryStateChangeCallback);
+        mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+    }
+
+    /**
+     * Wifi availability has changed. Update the wifi status icon as appropriate.
+     * @param available Whether wifi is available.
+     */
+    private void onWifiAvailabilityChanged(boolean available) {
+        final int newWifiStatus = available ? WIFI_STATUS_AVAILABLE : WIFI_STATUS_UNAVAILABLE;
+        if (mWifiStatus != newWifiStatus) {
+            mWifiStatus = newWifiStatus;
+            mView.showWifiStatus(mWifiStatus == WIFI_STATUS_UNAVAILABLE);
+        }
+    }
+
+    /**
+     * The battery level has changed. Update the battery status icon as appropriate.
+     * @param charging Whether the battery is currently charging.
+     */
+    private void onBatteryLevelChanged(boolean charging) {
+        final int newBatteryStatus =
+                charging ? BATTERY_STATUS_CHARGING : BATTERY_STATUS_NOT_CHARGING;
+        if (mBatteryStatus != newBatteryStatus) {
+            mBatteryStatus = newBatteryStatus;
+            mView.showBatteryPercentText(
+                    mBatteryStatus == BATTERY_STATUS_CHARGING && mShowPercentAvailable);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 7bf2361..ff5beb5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -21,8 +21,6 @@
 /**
  * Dagger Module providing Communal-related functionality.
  */
-@Module(subcomponents = {
-        AppWidgetOverlayComponent.class,
-})
+@Module(subcomponents = {AppWidgetOverlayComponent.class, DreamOverlayComponent.class})
 public interface DreamModule {
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
new file mode 100644
index 0000000..a3a446a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.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.systemui.dreams.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import android.view.ViewGroup;
+
+import com.android.systemui.dreams.DreamOverlayContainerView;
+import com.android.systemui.dreams.DreamOverlayStatusBarViewController;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Scope;
+
+import dagger.Subcomponent;
+
+/**
+ * Dagger subcomponent for {@link DreamOverlayModule}.
+ */
+@Subcomponent(modules = {DreamOverlayModule.class})
+@DreamOverlayComponent.DreamOverlayScope
+public interface DreamOverlayComponent {
+    /** Simple factory for {@link DreamOverlayComponent}. */
+    @Subcomponent.Factory
+    interface Factory {
+        DreamOverlayComponent create();
+    }
+
+    /** Scope annotation for singleton items within the {@link DreamOverlayComponent}. */
+    @Documented
+    @Retention(RUNTIME)
+    @Scope
+    @interface DreamOverlayScope {}
+
+    /** Builds a {@link DreamOverlayContainerView} */
+    @DreamOverlayScope
+    DreamOverlayContainerView getDreamOverlayContainerView();
+
+    /** Builds a content view for dream overlays */
+    @DreamOverlayScope
+    ViewGroup getDreamOverlayContentView();
+
+    /** Builds a {@link DreamOverlayStatusBarViewController}. */
+    @DreamOverlayScope
+    DreamOverlayStatusBarViewController getDreamOverlayStatusBarViewController();
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
new file mode 100644
index 0000000..d0a8fad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.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.systemui.dreams.dagger;
+
+import android.content.ContentResolver;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import com.android.internal.util.Preconditions;
+import com.android.systemui.R;
+import com.android.systemui.battery.BatteryMeterView;
+import com.android.systemui.battery.BatteryMeterViewController;
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dreams.DreamOverlayContainerView;
+import com.android.systemui.dreams.DreamOverlayStatusBarView;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.tuner.TunerService;
+
+import javax.inject.Named;
+
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger module for {@link DreamOverlayComponent}. */
+@Module
+public abstract class DreamOverlayModule {
+    private static final String DREAM_OVERLAY_BATTERY_VIEW = "dream_overlay_battery_view";
+    public static final String DREAM_OVERLAY_BATTERY_CONTROLLER =
+            "dream_overlay_battery_controller";
+
+    /** */
+    @Provides
+    @DreamOverlayComponent.DreamOverlayScope
+    public static DreamOverlayContainerView providesDreamOverlayContainerView(
+            LayoutInflater layoutInflater) {
+        return Preconditions.checkNotNull((DreamOverlayContainerView)
+                layoutInflater.inflate(R.layout.dream_overlay_container, null),
+                "R.layout.dream_layout_container could not be properly inflated");
+    }
+
+    /** */
+    @Provides
+    @DreamOverlayComponent.DreamOverlayScope
+    public static ViewGroup providesDreamOverlayContentView(DreamOverlayContainerView view) {
+        return Preconditions.checkNotNull(view.findViewById(R.id.dream_overlay_content),
+                "R.id.dream_overlay_content must not be null");
+    }
+
+    /** */
+    @Provides
+    @DreamOverlayComponent.DreamOverlayScope
+    public static DreamOverlayStatusBarView providesDreamOverlayStatusBarView(
+            DreamOverlayContainerView view) {
+        return Preconditions.checkNotNull(view.findViewById(R.id.dream_overlay_status_bar),
+                "R.id.status_bar must not be null");
+    }
+
+    /** */
+    @Provides
+    @DreamOverlayComponent.DreamOverlayScope
+    @Named(DREAM_OVERLAY_BATTERY_VIEW)
+    static BatteryMeterView providesBatteryMeterView(DreamOverlayContainerView view) {
+        return Preconditions.checkNotNull(view.findViewById(R.id.dream_overlay_battery),
+                "R.id.battery must not be null");
+    }
+
+    /** */
+    @Provides
+    @DreamOverlayComponent.DreamOverlayScope
+    @Named(DREAM_OVERLAY_BATTERY_CONTROLLER)
+    static BatteryMeterViewController providesBatteryMeterViewController(
+            @Named(DREAM_OVERLAY_BATTERY_VIEW) BatteryMeterView batteryMeterView,
+            ConfigurationController configurationController,
+            TunerService tunerService,
+            BroadcastDispatcher broadcastDispatcher,
+            @Main Handler mainHandler,
+            ContentResolver contentResolver,
+            BatteryController batteryController) {
+        return new BatteryMeterViewController(
+                batteryMeterView,
+                configurationController,
+                tunerService,
+                broadcastDispatcher,
+                mainHandler,
+                contentResolver,
+                batteryController);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index c14d32e..0c9e315 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -122,6 +122,7 @@
 import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.unfold.FoldAodAnimationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
 import com.android.systemui.util.DeviceConfigProxy;
@@ -131,7 +132,6 @@
 import java.util.ArrayList;
 import java.util.Optional;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicInteger;
 
 import dagger.Lazy;
 
@@ -439,7 +439,7 @@
     private boolean mInGestureNavigationMode;
 
     private boolean mWakeAndUnlocking;
-    private IKeyguardDrawnCallback mDrawnCallback;
+    private Runnable mWakeAndUnlockingDrawnCallback;
     private CharSequence mCustomMessage;
 
     /**
@@ -817,7 +817,8 @@
     private DozeParameters mDozeParameters;
 
     private final Optional<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealAnimation;
-    private final AtomicInteger mPendingDrawnTasks = new AtomicInteger();
+    private final Optional<FoldAodAnimationController> mFoldAodAnimationController;
+    private final PendingDrawnTasksContainer mPendingDrawnTasks = new PendingDrawnTasksContainer();
 
     private final KeyguardStateController mKeyguardStateController;
     private final Lazy<KeyguardUnlockAnimationController> mKeyguardUnlockAnimationControllerLazy;
@@ -877,8 +878,12 @@
                     mInGestureNavigationMode = QuickStepContract.isGesturalMode(mode);
                 }));
         mDozeParameters = dozeParameters;
-        mUnfoldLightRevealAnimation = unfoldComponent.map(
-                c -> c.getUnfoldLightRevealOverlayAnimation());
+
+        mUnfoldLightRevealAnimation = unfoldComponent
+                .map(SysUIUnfoldComponent::getUnfoldLightRevealOverlayAnimation);
+        mFoldAodAnimationController = unfoldComponent
+                .map(SysUIUnfoldComponent::getFoldAodAnimationController);
+
         mStatusBarStateController = statusBarStateController;
         statusBarStateController.addCallback(this);
 
@@ -1069,7 +1074,7 @@
             mDeviceInteractive = false;
             mGoingToSleep = false;
             mWakeAndUnlocking = false;
-            mAnimatingScreenOff = mDozeParameters.shouldControlUnlockedScreenOff();
+            mAnimatingScreenOff = mDozeParameters.shouldAnimateDozingChange();
 
             resetKeyguardDonePendingLocked();
             mHideAnimationRun = false;
@@ -2099,6 +2104,15 @@
     private final Runnable mKeyguardGoingAwayRunnable = new Runnable() {
         @Override
         public void run() {
+            // If the keyguard is already going away, or it's about to because we are going to
+            // trigger the going-away remote animation to show the surface behind, don't do it
+            // again. That will cause the current animation to be cancelled unnecessarily.
+            if (mKeyguardStateController.isKeyguardGoingAway()
+                    || mSurfaceBehindRemoteAnimationRequested
+                    || mSurfaceBehindRemoteAnimationRunning) {
+                return;
+            }
+
             Trace.beginSection("KeyguardViewMediator.mKeyGuardGoingAwayRunnable");
             if (DEBUG) Log.d(TAG, "keyguardGoingAway");
             mKeyguardViewControllerLazy.get().keyguardGoingAway();
@@ -2221,14 +2235,14 @@
             IRemoteAnimationRunner runner = mKeyguardExitAnimationRunner;
             mKeyguardExitAnimationRunner = null;
 
-            if (mWakeAndUnlocking && mDrawnCallback != null) {
+            if (mWakeAndUnlocking && mWakeAndUnlockingDrawnCallback != null) {
 
                 // Hack level over 9000: To speed up wake-and-unlock sequence, force it to report
                 // the next draw from here so we don't have to wait for window manager to signal
                 // this to our ViewRootImpl.
                 mKeyguardViewControllerLazy.get().getViewRootImpl().setReportNextDraw();
-                notifyDrawn(mDrawnCallback);
-                mDrawnCallback = null;
+                mWakeAndUnlockingDrawnCallback.run();
+                mWakeAndUnlockingDrawnCallback = null;
             }
 
             LatencyTracker.getInstance(mContext)
@@ -2457,9 +2471,7 @@
 
         if (mSurfaceBehindRemoteAnimationFinishedCallback != null) {
             try {
-                if (!cancelled) {
-                    mSurfaceBehindRemoteAnimationFinishedCallback.onAnimationFinished();
-                }
+                mSurfaceBehindRemoteAnimationFinishedCallback.onAnimationFinished();
                 mSurfaceBehindRemoteAnimationFinishedCallback = null;
             } catch (RemoteException e) {
                 e.printStackTrace();
@@ -2566,31 +2578,27 @@
         synchronized (KeyguardViewMediator.this) {
             if (DEBUG) Log.d(TAG, "handleNotifyScreenTurningOn");
 
-            if (mUnfoldLightRevealAnimation.isPresent()) {
-                mPendingDrawnTasks.set(2); // unfold overlay and keyguard drawn
+            mPendingDrawnTasks.reset();
 
+            if (mUnfoldLightRevealAnimation.isPresent()) {
                 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
+                        .onScreenTurningOn(mPendingDrawnTasks.registerTask("unfold-reveal"));
+            }
+
+            if (mFoldAodAnimationController.isPresent()) {
+                mFoldAodAnimationController.get()
+                        .onScreenTurningOn(mPendingDrawnTasks.registerTask("fold-to-aod"));
             }
 
             mKeyguardViewControllerLazy.get().onScreenTurningOn();
             if (callback != null) {
                 if (mWakeAndUnlocking) {
-                    mDrawnCallback = callback;
-                } else {
-                    notifyDrawn(callback);
+                    mWakeAndUnlockingDrawnCallback =
+                            mPendingDrawnTasks.registerTask("wake-and-unlocking");
                 }
             }
+
+            mPendingDrawnTasks.onTasksComplete(() -> notifyDrawn(callback));
         }
         Trace.endSection();
     }
@@ -2599,6 +2607,8 @@
         Trace.beginSection("KeyguardViewMediator#handleNotifyScreenTurnedOn");
         synchronized (this) {
             if (DEBUG) Log.d(TAG, "handleNotifyScreenTurnedOn");
+
+            mPendingDrawnTasks.reset();
             mKeyguardViewControllerLazy.get().onScreenTurnedOn();
         }
         Trace.endSection();
@@ -2607,18 +2617,18 @@
     private void handleNotifyScreenTurnedOff() {
         synchronized (this) {
             if (DEBUG) Log.d(TAG, "handleNotifyScreenTurnedOff");
-            mDrawnCallback = null;
+            mWakeAndUnlockingDrawnCallback = null;
         }
     }
 
     private void notifyDrawn(final IKeyguardDrawnCallback callback) {
         Trace.beginSection("KeyguardViewMediator#notifyDrawn");
-        if (mPendingDrawnTasks.decrementAndGet() == 0) {
-            try {
+        try {
+            if (callback != null) {
                 callback.onDrawn();
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Exception calling onDrawn():", e);
             }
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Exception calling onDrawn():", e);
         }
         Trace.endSection();
     }
@@ -2777,9 +2787,9 @@
         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("  mPendingDrawnTasks: "); pw.println(mPendingDrawnTasks.getPendingCount());
         pw.print("  mWakeAndUnlocking: "); pw.println(mWakeAndUnlocking);
-        pw.print("  mDrawnCallback: "); pw.println(mDrawnCallback);
+        pw.print("  mDrawnCallback: "); pw.println(mWakeAndUnlockingDrawnCallback);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/PendingDrawnTasksContainer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/PendingDrawnTasksContainer.kt
new file mode 100644
index 0000000..bccd106
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/PendingDrawnTasksContainer.kt
@@ -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.systemui.keyguard
+
+import android.os.Trace
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * Allows to wait for multiple callbacks and notify when the last one is executed
+ */
+class PendingDrawnTasksContainer {
+
+    private lateinit var pendingDrawnTasksCount: AtomicInteger
+    private var completionCallback: AtomicReference<Runnable> = AtomicReference()
+
+    /**
+     * Registers a task that we should wait for
+     * @return a runnable that should be invoked when the task is finished
+     */
+    fun registerTask(name: String): Runnable {
+        pendingDrawnTasksCount.incrementAndGet()
+
+        if (ENABLE_TRACE) {
+            Trace.beginAsyncSection("PendingDrawnTasksContainer#$name", 0)
+        }
+
+        return Runnable {
+            if (pendingDrawnTasksCount.decrementAndGet() == 0) {
+                val onComplete = completionCallback.getAndSet(null)
+                onComplete?.run()
+
+                if (ENABLE_TRACE) {
+                    Trace.endAsyncSection("PendingDrawnTasksContainer#$name", 0)
+                }
+            }
+        }
+    }
+
+    /**
+     * Clears state and initializes the container
+     */
+    fun reset() {
+        // Create new objects in case if there are pending callbacks from the previous invocations
+        completionCallback = AtomicReference()
+        pendingDrawnTasksCount = AtomicInteger(0)
+    }
+
+    /**
+     * Starts waiting for all tasks to be completed
+     * When all registered tasks complete it will invoke the [onComplete] callback
+     */
+    fun onTasksComplete(onComplete: Runnable) {
+        completionCallback.set(onComplete)
+
+        if (pendingDrawnTasksCount.get() == 0) {
+            val currentOnComplete = completionCallback.getAndSet(null)
+            currentOnComplete?.run()
+        }
+    }
+
+    /**
+     * Returns current pending tasks count
+     */
+    fun getPendingCount(): Int = pendingDrawnTasksCount.get()
+}
+
+private const val ENABLE_TRACE = false
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 49a63c3..d926e7d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -728,9 +728,9 @@
                     PlaybackState.ACTION_SKIP_TO_NEXT)
 
             // Then, check for custom actions
-            val customActions = MutableList<MediaAction?>(4) { null }
+            val customActions = MutableList<MediaAction?>(MAX_CUSTOM_ACTIONS) { null }
             var customCount = 0
-            for (i in 0..MAX_CUSTOM_ACTIONS) {
+            for (i in 0..(MAX_CUSTOM_ACTIONS - 1)) {
                 getCustomAction(state, packageName, controller, customCount)?.let {
                     customActions[customCount++] = it
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 8026df7..cc91384 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -1251,6 +1251,7 @@
     private void onImeSwitcherClick(View v) {
         mInputMethodManager.showInputMethodPickerFromSystem(
                 true /* showAuxiliarySubtypes */, mDisplayId);
+        mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
     };
 
     private boolean onLongPressBackHome(View v) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
index debd2eb..d27b716 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
@@ -96,6 +96,9 @@
         @UiEvent(doc = "The overview button was pressed in the navigation bar.")
         NAVBAR_OVERVIEW_BUTTON_TAP(535),
 
+        @UiEvent(doc = "The ime switcher button was pressed in the navigation bar.")
+        NAVBAR_IME_SWITCHER_BUTTON_TAP(923),
+
         @UiEvent(doc = "The home button was long-pressed in the navigation bar.")
         NAVBAR_HOME_BUTTON_LONGPRESS(536),
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
index 5bd02cc..10efec3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
@@ -48,6 +48,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.Dependency;
@@ -562,7 +563,8 @@
             return this;
         }
 
-        CustomTile build() {
+        @VisibleForTesting
+        public CustomTile build() {
             if (mUserContext == null) {
                 throw new NullPointerException("UserContext cannot be null");
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index ac95bf5..c2a9e3a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -169,7 +169,7 @@
         mFgsManagerTileProvider = fgsManagerTileProvider;
     }
 
-    public QSTile createTile(String tileSpec) {
+    public final QSTile createTile(String tileSpec) {
         QSTileImpl tile = createTileInternal(tileSpec);
         if (tile != null) {
             tile.initialize();
@@ -178,7 +178,7 @@
         return tile;
     }
 
-    private QSTileImpl createTileInternal(String tileSpec) {
+    protected QSTileImpl createTileInternal(String tileSpec) {
         // Stock tiles.
         switch (tileSpec) {
             case "wifi":
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FgsManagerTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/FgsManagerTile.kt
index 75cf4d1..939a297 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FgsManagerTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FgsManagerTile.kt
@@ -54,7 +54,7 @@
     qsLogger: QSLogger?,
     private val fgsManagerDialogFactory: FgsManagerDialogFactory,
     private val runningFgsController: RunningFgsController
-) : QSTileImpl<QSTile.State?>(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
+) : QSTileImpl<QSTile.State>(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
         statusBarStateController, activityStarter, qsLogger), RunningFgsController.Callback {
 
     override fun handleInitialize() {
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 7dd24b4..dd742b8 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
@@ -90,6 +90,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicReference;
@@ -485,6 +486,11 @@
 
     private Map<Integer, CharSequence> getUniqueSubscriptionDisplayNames(Context context) {
         class DisplayInfo {
+            DisplayInfo(SubscriptionInfo subscriptionInfo, CharSequence originalName) {
+                this.subscriptionInfo = subscriptionInfo;
+                this.originalName = originalName;
+            }
+
             public SubscriptionInfo subscriptionInfo;
             public CharSequence originalName;
             public CharSequence uniqueName;
@@ -498,12 +504,7 @@
                             // Filter out null values.
                             return (i != null && i.getDisplayName() != null);
                         })
-                        .map(i -> {
-                            DisplayInfo info = new DisplayInfo();
-                            info.subscriptionInfo = i;
-                            info.originalName = i.getDisplayName().toString().trim();
-                            return info;
-                        });
+                        .map(i -> new DisplayInfo(i, i.getDisplayName().toString().trim()));
 
         // A Unique set of display names
         Set<CharSequence> uniqueNames = new HashSet<>();
@@ -582,7 +583,7 @@
             return "";
         }
 
-        int resId = mapIconSets(config).get(iconKey).dataContentDescription;
+        int resId = Objects.requireNonNull(mapIconSets(config).get(iconKey)).dataContentDescription;
         if (isCarrierNetworkActive()) {
             SignalIcon.MobileIconGroup carrierMergedWifiIconGroup =
                     TelephonyIcons.CARRIER_MERGED_WIFI;
@@ -878,10 +879,8 @@
         if (accessPoints == null || accessPoints.size() == 0) {
             mConnectedEntry = null;
             mWifiEntriesCount = 0;
-            if (mCallback != null) {
-                mCallback.onAccessPointsChanged(null /* wifiEntries */, null /* connectedEntry */,
-                        false /* hasMoreEntry */);
-            }
+            mCallback.onAccessPointsChanged(null /* wifiEntries */, null /* connectedEntry */,
+                    false /* hasMoreEntry */);
             return;
         }
 
@@ -913,9 +912,7 @@
         mConnectedEntry = connectedEntry;
         mWifiEntriesCount = wifiEntries.size();
 
-        if (mCallback != null) {
-            mCallback.onAccessPointsChanged(wifiEntries, mConnectedEntry, hasMoreEntry);
-        }
+        mCallback.onAccessPointsChanged(wifiEntries, mConnectedEntry, hasMoreEntry);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 3ed7e84..e7cd1e2 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -76,6 +76,7 @@
 
 import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.util.ScreenshotHelper;
 import com.android.systemui.Dumpable;
@@ -88,6 +89,7 @@
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.navigationbar.buttons.KeyButtonView;
 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
 import com.android.systemui.settings.CurrentUserTracker;
 import com.android.systemui.shared.recents.IOverviewProxy;
@@ -161,6 +163,7 @@
     private final Optional<StartingSurface> mStartingSurface;
     private final SmartspaceTransitionController mSmartspaceTransitionController;
     private final Optional<RecentTasks> mRecentTasks;
+    private final UiEventLogger mUiEventLogger;
 
     private Region mActiveNavBarRegion;
 
@@ -248,6 +251,7 @@
             mContext.getSystemService(InputMethodManager.class)
                     .showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */,
                             DEFAULT_DISPLAY);
+            mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
         }
 
         @Override
@@ -560,6 +564,7 @@
             ShellTransitions shellTransitions,
             ScreenLifecycle screenLifecycle,
             SmartspaceTransitionController smartspaceTransitionController,
+            UiEventLogger uiEventLogger,
             DumpManager dumpManager) {
         super(broadcastDispatcher);
         mContext = context;
@@ -581,6 +586,7 @@
         mOneHandedOptional = oneHandedOptional;
         mShellTransitions = shellTransitions;
         mRecentTasks = recentTasks;
+        mUiEventLogger = uiEventLogger;
 
         // Assumes device always starts with back button until launcher tells it that it does not
         mNavBarButtonAlpha = 1.0f;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
index 55d549d..d65fa3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
@@ -18,6 +18,7 @@
 import android.app.Fragment
 import com.android.systemui.R
 import com.android.systemui.fragments.FragmentHostManager
+import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions
 import com.android.systemui.statusbar.phone.PhoneStatusBarView
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent
@@ -50,12 +51,11 @@
                     override fun onFragmentViewCreated(tag: String, fragment: Fragment) {
                         val statusBarFragmentComponent = (fragment as CollapsedStatusBarFragment)
                                 .statusBarFragmentComponent ?: throw IllegalStateException()
-                        val statusBarView = statusBarFragmentComponent.phoneStatusBarView
-                        val sbViewController =
-                                statusBarFragmentComponent.phoneStatusBarViewController
-
-                        statusBarViewUpdatedListener
-                                ?.onStatusBarViewUpdated(statusBarView, sbViewController)
+                        statusBarViewUpdatedListener?.onStatusBarViewUpdated(
+                            statusBarFragmentComponent.phoneStatusBarView,
+                            statusBarFragmentComponent.phoneStatusBarViewController,
+                            statusBarFragmentComponent.phoneStatusBarTransitions
+                        )
                     }
 
                     override fun onFragmentViewDestroyed(tag: String?, fragment: Fragment?) {
@@ -72,7 +72,8 @@
     interface OnStatusBarViewUpdatedListener {
         fun onStatusBarViewUpdated(
             statusBarView: PhoneStatusBarView,
-            statusBarViewController: PhoneStatusBarViewController
+            statusBarViewController: PhoneStatusBarViewController,
+            statusBarTransitions: PhoneStatusBarTransitions
         )
     }
 }
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 ce86953..962c7fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -102,12 +102,17 @@
 
         configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
             override fun onLayoutDirectionChanged(isRtl: Boolean) {
-                synchronized(this) {
-                    val corner = selectDesignatedCorner(nextViewState.rotation, isRtl)
-                    nextViewState = nextViewState.copy(
-                            layoutRtl = isRtl,
-                            designatedCorner = corner
-                    )
+                uiExecutor?.execute {
+                    // If rtl changed, hide all dotes until the next state resolves
+                    setCornerVisibilities(View.INVISIBLE)
+
+                    synchronized(this) {
+                        val corner = selectDesignatedCorner(nextViewState.rotation, isRtl)
+                        nextViewState = nextViewState.copy(
+                                layoutRtl = isRtl,
+                                designatedCorner = corner
+                        )
+                    }
                 }
             }
         })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt
index f8d6c6d..b2e15f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt
@@ -25,8 +25,8 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.NotificationMediaManager
 import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.NotificationEntryManager
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.tuner.TunerService
@@ -44,7 +44,7 @@
     private val headsUpManager: HeadsUpManagerPhone,
     private val notificationLockscreenUserManager: NotificationLockscreenUserManager,
     private val mediaManager: NotificationMediaManager,
-    private val entryManager: NotificationEntryManager,
+    private val commonNotifCollection: CommonNotifCollection,
     tunerService: TunerService
 ) : StatusBarStateController.StateListener, NotificationMediaManager.MediaListener {
 
@@ -77,8 +77,7 @@
 
     override fun onPrimaryMetadataOrStateChanged(metadata: MediaMetadata?, state: Int) {
         val previous = currentMediaEntry
-        var newEntry = entryManager
-                .getActiveNotificationUnfiltered(mediaManager.mediaNotificationKey)
+        var newEntry = commonNotifCollection.getEntry(mediaManager.mediaNotificationKey)
         if (!NotificationMediaManager.isPlayingState(state)) {
             newEntry = null
         }
@@ -112,7 +111,7 @@
             // filter notifications invisible on Keyguard
             return false
         }
-        if (entryManager.getActiveNotificationUnfiltered(entry.key) != null) {
+        if (commonNotifCollection.getEntry(entry.key) != null) {
             // filter notifications not the active list currently
             return false
         }
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 e1dbf4e..518788b 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
@@ -669,6 +669,7 @@
 
     public void setIsRemoteInputActive(boolean isActive) {
         mIsRemoteInputActive = isActive;
+        updateFooter();
     }
 
     @VisibleForTesting
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 e3a4bf0..2dc9276 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
@@ -51,6 +51,7 @@
     public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 400;
     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 400;
+    public static final int ANIMATION_DURATION_FOLD_TO_AOD = 600;
     public static final int ANIMATION_DURATION_PULSE_APPEAR =
             KeyguardSliceView.DEFAULT_ANIM_DURATION;
     public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240;
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 4b8b580..d87a024 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -243,6 +243,10 @@
         return mScreenOffAnimationController.shouldShowLightRevealScrim();
     }
 
+    public boolean shouldAnimateDozingChange() {
+        return mScreenOffAnimationController.shouldAnimateDozingChange();
+    }
+
     /**
      * Whether we're capable of controlling the screen off animation if we want to. This isn't
      * possible if AOD isn't even enabled or if the flag is disabled.
@@ -324,6 +328,7 @@
         for (Callback callback : mCallbacks) {
             callback.onAlwaysOnChange();
         }
+        mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn());
     }
 
     @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 097c0d1..3b7063e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -26,6 +26,7 @@
 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.animation.Interpolators.EMPHASIZED_DECELERATE;
 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;
@@ -34,6 +35,7 @@
 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.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
 import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED;
 import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN;
 import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING;
@@ -1423,11 +1425,12 @@
                 .getVisibleNotificationCount() != 0 || mMediaDataManager.hasActiveMedia();
         boolean splitShadeWithActiveMedia =
                 mShouldUseSplitNotificationShade && mMediaDataManager.hasActiveMedia();
+        boolean shouldAnimateClockChange = mScreenOffAnimationController.shouldAnimateClockChange();
         if ((hasVisibleNotifications && !mShouldUseSplitNotificationShade)
                 || (splitShadeWithActiveMedia && !mDozing)) {
-            mKeyguardStatusViewController.displayClock(SMALL);
+            mKeyguardStatusViewController.displayClock(SMALL, shouldAnimateClockChange);
         } else {
-            mKeyguardStatusViewController.displayClock(LARGE);
+            mKeyguardStatusViewController.displayClock(LARGE, shouldAnimateClockChange);
         }
         updateKeyguardStatusViewAlignment(true /* animate */);
         int userIconHeight = mKeyguardQsUserSwitchController != null
@@ -1463,7 +1466,7 @@
                 mKeyguardStatusViewController.isClockTopAligned());
         mClockPositionAlgorithm.run(mClockPositionResult);
         boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
-        boolean animateClock = animate || mAnimateNextPositionUpdate;
+        boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange;
         mKeyguardStatusViewController.updatePosition(
                 mClockPositionResult.clockX, mClockPositionResult.clockY,
                 mClockPositionResult.clockScale, animateClock);
@@ -3821,6 +3824,45 @@
         }
     }
 
+    /**
+     * Updates the views to the initial state for the fold to AOD animation
+     */
+    public void prepareFoldToAodAnimation() {
+        // Force show AOD UI even if we are not locked
+        showAodUi();
+
+        // Move the content of the AOD all the way to the left
+        // so we can animate to the initial position
+        final int translationAmount = mView.getResources().getDimensionPixelSize(
+                R.dimen.below_clock_padding_start);
+        mView.setTranslationX(-translationAmount);
+        mView.setAlpha(0);
+    }
+
+    /**
+     * Starts fold to AOD animation
+     */
+    public void startFoldToAodAnimation(Runnable endAction) {
+        mView.animate()
+            .translationX(0)
+            .alpha(1f)
+            .setDuration(ANIMATION_DURATION_FOLD_TO_AOD)
+            .setInterpolator(EMPHASIZED_DECELERATE)
+            .setListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    endAction.run();
+                }
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    endAction.run();
+                }
+            })
+            .start();
+
+        mKeyguardStatusViewController.animateFoldToAod();
+    }
+
     /** */
     public void setImportantForAccessibility(int mode) {
         mView.setImportantForAccessibility(mode);
@@ -3935,6 +3977,10 @@
         mView.setAlpha(alpha);
     }
 
+    public void resetTranslation() {
+        mView.setTranslationX(0f);
+    }
+
     public ViewPropertyAnimator fadeOut(long startDelayMs, long durationMs, Runnable endAction) {
         return mView.animate().alpha(0).setStartDelay(startDelayMs).setDuration(
                 durationMs).setInterpolator(Interpolators.ALPHA_OUT).withLayer().withEndAction(
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 55f1450..81d4bbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -98,7 +98,6 @@
     private boolean mExpandAnimationRunning;
     private NotificationStackScrollLayout mStackScrollLayout;
     private PhoneStatusBarView mStatusBarView;
-    private PhoneStatusBarTransitions mBarTransitions;
     private StatusBar mService;
     private NotificationShadeWindowController mNotificationShadeWindowController;
     private DragDownHelper mDragDownHelper;
@@ -497,17 +496,8 @@
         }
     }
 
-    public PhoneStatusBarTransitions getBarTransitions() {
-        return mBarTransitions;
-    }
-
     public void setStatusBarView(PhoneStatusBarView statusBarView) {
         mStatusBarView = statusBarView;
-        if (statusBarView != null) {
-            mBarTransitions = new PhoneStatusBarTransitions(
-                    statusBarView,
-                    mStatusBarWindowController.getBackgroundView());
-        }
     }
 
     public void setService(StatusBar statusBar, NotificationShadeWindowController controller) {
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 f67d181..1e71ceb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -38,7 +38,6 @@
 import com.android.systemui.R;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.statusbar.window.StatusBarWindowView;
 import com.android.systemui.util.leak.RotationUtils;
 
 import java.util.Objects;
@@ -76,6 +75,7 @@
 
     @Override
     public void onFinishInflate() {
+        super.onFinishInflate();
         mBattery = findViewById(R.id.battery);
         mClock = findViewById(R.id.clock);
         mCutoutSpace = findViewById(R.id.cutout_space_view);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
index 497e7d7..e806ca0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
@@ -19,16 +19,23 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.unfold.FoldAodAnimationController
+import com.android.systemui.unfold.SysUIUnfoldComponent
+import java.util.Optional
 import javax.inject.Inject
 
 @SysUISingleton
 class ScreenOffAnimationController @Inject constructor(
+    sysUiUnfoldComponent: Optional<SysUIUnfoldComponent>,
     unlockedScreenOffAnimation: UnlockedScreenOffAnimationController,
     private val wakefulnessLifecycle: WakefulnessLifecycle,
 ) : WakefulnessLifecycle.Observer {
 
-    // TODO(b/202844967) add fold to aod animation here
-    private val animations: List<ScreenOffAnimation> = listOf(unlockedScreenOffAnimation)
+    private val foldToAodAnimation: FoldAodAnimationController? = sysUiUnfoldComponent
+        .orElse(null)?.getFoldAodAnimationController()
+
+    private val animations: List<ScreenOffAnimation> =
+        listOfNotNull(foldToAodAnimation, unlockedScreenOffAnimation)
 
     fun initialize(statusBar: StatusBar, lightRevealScrim: LightRevealScrim) {
         animations.forEach { it.initialize(statusBar, lightRevealScrim) }
@@ -43,6 +50,19 @@
     }
 
     /**
+     * Called when opaqueness of the light reveal scrim has change
+     * When [isOpaque] is true then scrim is visible and covers the screen
+     */
+    fun onScrimOpaqueChanged(isOpaque: Boolean) =
+        animations.forEach { it.onScrimOpaqueChanged(isOpaque) }
+
+    /**
+     * Called when always on display setting changed
+     */
+    fun onAlwaysOnChanged(alwaysOn: Boolean) =
+        animations.forEach { it.onAlwaysOnChanged(alwaysOn) }
+
+    /**
      * If returns true we are taking over the screen off animation from display manager to SysUI.
      * We can play our custom animation instead of default fade out animation.
      */
@@ -103,6 +123,12 @@
         animations.any { it.isAnimationPlaying() }
 
     /**
+     * Return true to ignore requests to hide keyguard
+     */
+    fun isKeyguardHideDelayed(): Boolean =
+        animations.any { it.isKeyguardHideDelayed() }
+
+    /**
      * Return true to make the StatusBar expanded so we can animate [LightRevealScrim]
      */
     fun shouldShowLightRevealScrim(): Boolean =
@@ -145,6 +171,19 @@
      */
     fun shouldAnimateAodIcons(): Boolean =
         animations.all { it.shouldAnimateAodIcons() }
+
+    /**
+     * Return true to animate doze state change, if returns false dozing will be applied without
+     * animation (sends only 0.0f or 1.0f dozing progress)
+     */
+    fun shouldAnimateDozingChange(): Boolean =
+        animations.all { it.shouldAnimateDozingChange() }
+
+    /**
+     * Return true to animate large <-> small clock transition
+     */
+    fun shouldAnimateClockChange(): Boolean =
+        animations.all { it.shouldAnimateClockChange() }
 }
 
 interface ScreenOffAnimation {
@@ -158,11 +197,17 @@
     fun shouldPlayAnimation(): Boolean = false
     fun isAnimationPlaying(): Boolean = false
 
+    fun onScrimOpaqueChanged(isOpaque: Boolean) {}
+    fun onAlwaysOnChanged(alwaysOn: Boolean) {}
+
     fun shouldAnimateInKeyguard(): Boolean = false
     fun animateInKeyguard(keyguardView: View, after: Runnable) = after.run()
 
+    fun isKeyguardHideDelayed(): Boolean = false
     fun shouldHideScrimOnWakeUp(): Boolean = false
     fun overrideNotificationsDozeAmount(): Boolean = false
     fun shouldShowAodIconsWhenShade(): Boolean = false
     fun shouldAnimateAodIcons(): Boolean = true
+    fun shouldAnimateDozingChange(): Boolean = true
+    fun shouldAnimateClockChange(): Boolean = true
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SplitShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SplitShadeHeaderController.kt
index f6e19bf..8cf7288 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SplitShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SplitShadeHeaderController.kt
@@ -49,8 +49,9 @@
     }
 
     private val combinedHeaders = featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)
-    // TODO(b/194178072) Handle RSSI hiding when multi carrier
     private val iconManager: StatusBarIconController.TintedIconManager
+    private val iconContainer: StatusIconContainer
+    private val carrierIconSlots: List<String>
     private val qsCarrierGroupController: QSCarrierGroupController
     private var visible = false
         set(value) {
@@ -117,10 +118,19 @@
         batteryMeterViewController.ignoreTunerUpdates()
         batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
 
-        val iconContainer: StatusIconContainer = statusBar.findViewById(R.id.statusIcons)
+        iconContainer = statusBar.findViewById(R.id.statusIcons)
         iconManager = StatusBarIconController.TintedIconManager(iconContainer, featureFlags)
         iconManager.setTint(Utils.getColorAttrDefaultColor(statusBar.context,
                 android.R.attr.textColorPrimary))
+
+        carrierIconSlots = if (featureFlags.isEnabled(Flags.COMBINED_STATUS_BAR_SIGNAL_ICONS)) {
+            listOf(
+                statusBar.context.getString(com.android.internal.R.string.status_bar_no_calling),
+                statusBar.context.getString(com.android.internal.R.string.status_bar_call_strength)
+            )
+        } else {
+            listOf(statusBar.context.getString(com.android.internal.R.string.status_bar_mobile))
+        }
         qsCarrierGroupController = qsCarrierGroupControllerBuilder
                 .setQSCarrierGroup(statusBar.findViewById(R.id.carrier_group))
                 .build()
@@ -185,9 +195,20 @@
     private fun updateListeners() {
         qsCarrierGroupController.setListening(visible)
         if (visible) {
+            updateSingleCarrier(qsCarrierGroupController.isSingleCarrier)
+            qsCarrierGroupController.setOnSingleCarrierChangedListener { updateSingleCarrier(it) }
             statusBarIconController.addIconGroup(iconManager)
         } else {
+            qsCarrierGroupController.setOnSingleCarrierChangedListener(null)
             statusBarIconController.removeIconGroup(iconManager)
         }
     }
+
+    private fun updateSingleCarrier(singleCarrier: Boolean) {
+        if (singleCarrier) {
+            iconContainer.removeIgnoredSlots(carrierIconSlots)
+        } else {
+            iconContainer.addIgnoredSlots(carrierIconSlots)
+        }
+    }
 }
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 6c0b717..1ca297a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -341,7 +341,7 @@
         mStatusBarWindowState =  state;
         mStatusBarWindowHidden = state == WINDOW_STATE_HIDDEN;
         mStatusBarHideIconsForBouncerManager.setStatusBarWindowHidden(mStatusBarWindowHidden);
-        if (getStatusBarView() != null) {
+        if (mStatusBarView != null) {
             // Should #updateHideIconsForBouncer always be called, regardless of whether we have a
             //   status bar view? If so, we can make #updateHideIconsForBouncer private.
             mStatusBarHideIconsForBouncerManager.updateHideIconsForBouncer(/* animate= */ false);
@@ -458,6 +458,7 @@
     protected NotificationShadeWindowView mNotificationShadeWindowView;
     protected PhoneStatusBarView mStatusBarView;
     private PhoneStatusBarViewController mPhoneStatusBarViewController;
+    private PhoneStatusBarTransitions mStatusBarTransitions;
     private AuthRippleController mAuthRippleController;
     private int mStatusBarWindowState = WINDOW_STATE_SHOWING;
     protected NotificationShadeWindowController mNotificationShadeWindowController;
@@ -1124,23 +1125,19 @@
         // Set up CollapsedStatusBarFragment and PhoneStatusBarView
         StatusBarInitializer initializer = mStatusBarComponent.getStatusBarInitializer();
         initializer.setStatusBarViewUpdatedListener(
-                new StatusBarInitializer.OnStatusBarViewUpdatedListener() {
-                    @Override
-                    public void onStatusBarViewUpdated(
-                            @NonNull PhoneStatusBarView statusBarView,
-                            @NonNull PhoneStatusBarViewController statusBarViewController) {
-                        mStatusBarView = statusBarView;
-                        mPhoneStatusBarViewController = statusBarViewController;
-                        mNotificationShadeWindowViewController.setStatusBarView(mStatusBarView);
-                        // 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);
-                        checkBarModes();
-                    }
+                (statusBarView, statusBarViewController, statusBarTransitions) -> {
+                    mStatusBarView = statusBarView;
+                    mPhoneStatusBarViewController = statusBarViewController;
+                    mStatusBarTransitions = statusBarTransitions;
+                    mNotificationShadeWindowViewController.setStatusBarView(mStatusBarView);
+                    // 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);
+                    checkBarModes();
                 });
         initializer.initializeStatusBar(mStatusBarComponent);
 
@@ -1199,6 +1196,8 @@
             Runnable updateOpaqueness = () -> {
                 mNotificationShadeWindowController.setLightRevealScrimOpaque(
                         mLightRevealScrim.isScrimOpaque());
+                mScreenOffAnimationController
+                        .onScrimOpaqueChanged(mLightRevealScrim.isScrimOpaque());
             };
             if (opaque) {
                 // Delay making the view opaque for a frame, because it needs some time to render
@@ -1578,10 +1577,6 @@
         Trace.endSection();
     }
 
-    protected PhoneStatusBarView getStatusBarView() {
-        return mStatusBarView;
-    }
-
     public NotificationShadeWindowView getNotificationShadeWindowView() {
         return mNotificationShadeWindowView;
     }
@@ -2192,14 +2187,10 @@
                 }, false, sUiEventLogger).show(animationDelay);
     }
 
-    protected BarTransitions getStatusBarTransitions() {
-        return mNotificationShadeWindowViewController.getBarTransitions();
-    }
-
     public void checkBarModes() {
         if (mDemoModeController.isInDemoMode()) return;
-        if (mNotificationShadeWindowViewController != null && getStatusBarTransitions() != null) {
-            checkBarMode(mStatusBarMode, mStatusBarWindowState, getStatusBarTransitions());
+        if (mStatusBarTransitions != null) {
+            checkBarMode(mStatusBarMode, mStatusBarWindowState, mStatusBarTransitions);
         }
         mNavigationBarController.checkNavBarModes(mDisplayId);
         mNoAnimationOnNextBarModeChange = false;
@@ -2226,9 +2217,8 @@
     }
 
     private void finishBarAnimations() {
-        if (mNotificationShadeWindowController != null
-                && mNotificationShadeWindowViewController.getBarTransitions() != null) {
-            mNotificationShadeWindowViewController.getBarTransitions().finishAnimations();
+        if (mStatusBarTransitions != null) {
+            mStatusBarTransitions.finishAnimations();
         }
         mNavigationBarController.finishBarAnimations(mDisplayId);
     }
@@ -2282,8 +2272,7 @@
         pw.println("  ShadeWindowView: ");
         if (mNotificationShadeWindowViewController != null) {
             mNotificationShadeWindowViewController.dump(fd, pw, args);
-            dumpBarTransitions(pw, "PhoneStatusBarTransitions",
-                    mNotificationShadeWindowViewController.getBarTransitions());
+            dumpBarTransitions(pw, "PhoneStatusBarTransitions", mStatusBarTransitions);
         }
 
         pw.println("  mMediaManager: ");
@@ -2915,7 +2904,17 @@
                 showKeyguardImpl();
             }
         } else {
-            return hideKeyguardImpl(force);
+            // During folding a foldable device this might be called as a result of
+            // 'onScreenTurnedOff' call for the inner display.
+            // In this case:
+            //  * When phone is locked on folding: it doesn't make sense to hide keyguard as it
+            //    will be immediately locked again
+            //  * When phone is unlocked: we still don't want to execute hiding of the keyguard
+            //    as the animation could prepare 'fake AOD' interface (without actually
+            //    transitioning to keyguard state) and this might reset the view states
+            if (!mScreenOffAnimationController.isKeyguardHideDelayed()) {
+                return hideKeyguardImpl(force);
+            }
         }
         return false;
     }
@@ -3078,6 +3077,7 @@
         mNotificationPanelViewController.onAffordanceLaunchEnded();
         mNotificationPanelViewController.cancelAnimation();
         mNotificationPanelViewController.setAlpha(1f);
+        mNotificationPanelViewController.resetTranslation();
         mNotificationPanelViewController.resetViewGroupFade();
         updateDozingState();
         updateScrimController();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java
index 3c09b78..29c1372 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java
@@ -32,7 +32,6 @@
 import com.android.systemui.demomode.DemoModeCommandReceiver;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.navigationbar.NavigationBarController;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentScope;
 import com.android.systemui.statusbar.policy.Clock;
 import com.android.systemui.util.ViewController;
@@ -54,8 +53,7 @@
     private final Clock mClockView;
     private final View mOperatorNameView;
     private final DemoModeController mDemoModeController;
-    private final NotificationShadeWindowController mNotificationShadeWindowController;
-    private final NotificationShadeWindowViewController mNotificationShadeWindowViewController;
+    private final PhoneStatusBarTransitions mPhoneStatusBarTransitions;
     private final NavigationBarController mNavigationBarController;
     private final int mDisplayId;
 
@@ -64,16 +62,14 @@
             Clock clockView,
             @Named(OPERATOR_NAME_VIEW) View operatorNameView,
             DemoModeController demoModeController,
-            NotificationShadeWindowController notificationShadeWindowController,
-            NotificationShadeWindowViewController notificationShadeWindowViewController,
+            PhoneStatusBarTransitions phoneStatusBarTransitions,
             NavigationBarController navigationBarController,
             @DisplayId int displayId) {
         super(clockView);
         mClockView = clockView;
         mOperatorNameView = operatorNameView;
         mDemoModeController = demoModeController;
-        mNotificationShadeWindowController = notificationShadeWindowController;
-        mNotificationShadeWindowViewController = notificationShadeWindowViewController;
+        mPhoneStatusBarTransitions = phoneStatusBarTransitions;
         mNavigationBarController = navigationBarController;
         mDisplayId = displayId;
     }
@@ -128,11 +124,7 @@
                                                     -1;
             if (barMode != -1) {
                 boolean animate = true;
-                if (mNotificationShadeWindowController != null
-                        && mNotificationShadeWindowViewController.getBarTransitions() != null) {
-                    mNotificationShadeWindowViewController.getBarTransitions().transitionTo(
-                            barMode, animate);
-                }
+                mPhoneStatusBarTransitions.transitionTo(barMode, animate);
                 mNavigationBarController.transitionTo(mDisplayId, barMode, animate);
             }
         }
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 b84e6e6..875b7e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -111,8 +111,6 @@
     private final NavigationModeController mNavigationModeController;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final KeyguardBouncer.Factory mKeyguardBouncerFactory;
-    private final WakefulnessLifecycle mWakefulnessLifecycle;
-    private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
     private final KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
     private KeyguardMessageAreaController mKeyguardMessageAreaController;
     private final Lazy<ShadeController> mShadeController;
@@ -244,8 +242,6 @@
             KeyguardStateController keyguardStateController,
             NotificationMediaManager notificationMediaManager,
             KeyguardBouncer.Factory keyguardBouncerFactory,
-            WakefulnessLifecycle wakefulnessLifecycle,
-            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             KeyguardMessageAreaController.Factory keyguardMessageAreaFactory,
             Lazy<ShadeController> shadeController) {
         mContext = context;
@@ -260,8 +256,6 @@
         mStatusBarStateController = sysuiStatusBarStateController;
         mDockManager = dockManager;
         mKeyguardBouncerFactory = keyguardBouncerFactory;
-        mWakefulnessLifecycle = wakefulnessLifecycle;
-        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
         mKeyguardMessageAreaFactory = keyguardMessageAreaFactory;
         mShadeController = shadeController;
     }
@@ -416,7 +410,7 @@
      * dragging it and translation should be deferred {@see KeyguardBouncer#show(boolean, boolean)}
      */
     public void showGenericBouncer(boolean scrimmed) {
-        if (mAlternateAuthInterceptor != null) {
+        if (shouldShowAltAuth()) {
             updateAlternateAuthShowing(mAlternateAuthInterceptor.showAlternateAuthBouncer());
             return;
         }
@@ -424,6 +418,11 @@
         showBouncer(scrimmed);
     }
 
+    private boolean shouldShowAltAuth() {
+        return mAlternateAuthInterceptor != null
+                && mKeyguardUpdateManager.isUnlockingWithBiometricAllowed(true);
+    }
+
     /**
      * Hides the input bouncer (pin/password/pattern).
      */
@@ -479,7 +478,7 @@
 
             // If there is an an alternate auth interceptor (like the UDFPS), show that one instead
             // of the bouncer.
-            if (mAlternateAuthInterceptor != null) {
+            if (shouldShowAltAuth()) {
                 if (!afterKeyguardGone) {
                     mBouncer.setDismissAction(mAfterKeyguardGoneAction, mKeyguardGoneCancelAction);
                     mAfterKeyguardGoneAction = null;
@@ -1155,7 +1154,7 @@
 
     @Override
     public ViewRootImpl getViewRootImpl() {
-        return mStatusBar.getStatusBarView().getViewRootImpl();
+        return mNotificationShadeWindowController.getNotificationShadeView().getViewRootImpl();
     }
 
     public void launchPendingWakeupAction() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentComponent.java
index a4ebab9..22b7f64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentComponent.java
@@ -20,6 +20,7 @@
 import com.android.systemui.dagger.qualifiers.RootView;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.LightsOutNotifController;
+import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions;
 import com.android.systemui.statusbar.phone.PhoneStatusBarView;
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
 import com.android.systemui.statusbar.phone.StatusBarDemoMode;
@@ -36,6 +37,9 @@
  * controllers need access to that view, so those controllers will be re-created whenever the
  * fragment is recreated.
  *
+ * Anything that depends on {@link CollapsedStatusBarFragment} or {@link PhoneStatusBarView}
+ * should be included here or in {@link StatusBarFragmentModule}.
+ *
  * Note that this is completely separate from
  * {@link com.android.systemui.statusbar.phone.dagger.StatusBarComponent}. This component gets
  * re-created on each new fragment creation, whereas
@@ -90,4 +94,8 @@
     /** */
     @StatusBarFragmentScope
     StatusBarDemoMode getStatusBarDemoMode();
+
+    /** */
+    @StatusBarFragmentScope
+    PhoneStatusBarTransitions getPhoneStatusBarTransitions();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
index 0cbd401..dea1b43 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
@@ -22,10 +22,12 @@
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.dagger.qualifiers.RootView;
 import com.android.systemui.statusbar.phone.NotificationPanelViewController;
+import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions;
 import com.android.systemui.statusbar.phone.PhoneStatusBarView;
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
 import com.android.systemui.statusbar.policy.Clock;
+import com.android.systemui.statusbar.window.StatusBarWindowController;
 
 import javax.inject.Named;
 
@@ -89,4 +91,14 @@
                 phoneStatusBarView,
                 notificationPanelViewController.getStatusBarTouchEventHandler());
     }
+
+    /** */
+    @Provides
+    @StatusBarFragmentScope
+    static PhoneStatusBarTransitions providePhoneStatusBarTransitions(
+            @RootView PhoneStatusBarView view,
+            StatusBarWindowController statusBarWindowController
+    ) {
+        return new PhoneStatusBarTransitions(view, statusBarWindowController.getBackgroundView());
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
new file mode 100644
index 0000000..fb9df01
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.os.PowerManager
+import android.provider.Settings
+import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.ScreenLifecycle
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.statusbar.phone.ScreenOffAnimation
+import com.android.systemui.statusbar.phone.StatusBar
+import com.android.systemui.statusbar.policy.CallbackController
+import com.android.systemui.unfold.FoldAodAnimationController.FoldAodAnimationStatus
+import com.android.systemui.util.settings.GlobalSettings
+import dagger.Lazy
+import javax.inject.Inject
+
+/**
+ * Controls folding to AOD animation: when AOD is enabled and foldable device is folded
+ * we play a special AOD animation on the outer screen
+ */
+@SysUIUnfoldScope
+class FoldAodAnimationController @Inject constructor(
+    private val screenLifecycle: ScreenLifecycle,
+    private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>,
+    private val wakefulnessLifecycle: WakefulnessLifecycle,
+    private val globalSettings: GlobalSettings
+) : ScreenLifecycle.Observer,
+    CallbackController<FoldAodAnimationStatus>,
+    ScreenOffAnimation,
+    WakefulnessLifecycle.Observer {
+
+    private var alwaysOnEnabled: Boolean = false
+    private var isScrimOpaque: Boolean = false
+    private lateinit var statusBar: StatusBar
+    private var pendingScrimReadyCallback: Runnable? = null
+
+    private var shouldPlayAnimation = false
+    private val statusListeners = arrayListOf<FoldAodAnimationStatus>()
+
+    private var isAnimationPlaying = false
+
+    override fun initialize(statusBar: StatusBar, lightRevealScrim: LightRevealScrim) {
+        this.statusBar = statusBar
+
+        screenLifecycle.addObserver(this)
+        wakefulnessLifecycle.addObserver(this)
+    }
+
+    /**
+     * Returns true if we should run fold to AOD animation
+     */
+    override fun shouldPlayAnimation(): Boolean =
+        shouldPlayAnimation
+
+    override fun startAnimation(): Boolean =
+        if (alwaysOnEnabled &&
+            wakefulnessLifecycle.lastSleepReason == PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD &&
+            globalSettings.getString(Settings.Global.ANIMATOR_DURATION_SCALE) != "0"
+        ) {
+            shouldPlayAnimation = true
+
+            isAnimationPlaying = true
+            statusBar.notificationPanelViewController.prepareFoldToAodAnimation()
+
+            statusListeners.forEach(FoldAodAnimationStatus::onFoldToAodAnimationChanged)
+
+            true
+        } else {
+            shouldPlayAnimation = false
+            false
+        }
+
+    override fun onStartedWakingUp() {
+        shouldPlayAnimation = false
+        isAnimationPlaying = false
+    }
+
+    /**
+     * Called when screen starts turning on, the contents of the screen might not be visible yet.
+     * This method reports back that the animation is ready in [onReady] callback.
+     *
+     * @param onReady callback when the animation is ready
+     * @see [com.android.systemui.keyguard.KeyguardViewMediator]
+     */
+    fun onScreenTurningOn(onReady: Runnable) {
+        if (shouldPlayAnimation) {
+            if (isScrimOpaque) {
+                onReady.run()
+            } else {
+                pendingScrimReadyCallback = onReady
+            }
+        } else {
+            // No animation, call ready callback immediately
+            onReady.run()
+        }
+    }
+
+    /**
+     * Called when keyguard scrim opaque changed
+     */
+    override fun onScrimOpaqueChanged(isOpaque: Boolean) {
+        isScrimOpaque = isOpaque
+
+        if (isOpaque) {
+            pendingScrimReadyCallback?.run()
+            pendingScrimReadyCallback = null
+        }
+    }
+
+    override fun onScreenTurnedOn() {
+        if (shouldPlayAnimation) {
+            statusBar.notificationPanelViewController.startFoldToAodAnimation {
+                // End action
+                isAnimationPlaying = false
+                keyguardViewMediatorLazy.get().maybeHandlePendingLock()
+            }
+            shouldPlayAnimation = false
+        }
+    }
+
+    override fun isAnimationPlaying(): Boolean =
+        isAnimationPlaying
+
+    override fun isKeyguardHideDelayed(): Boolean =
+        isAnimationPlaying()
+
+    override fun shouldShowAodIconsWhenShade(): Boolean =
+        shouldPlayAnimation()
+
+    override fun shouldAnimateAodIcons(): Boolean =
+        !shouldPlayAnimation()
+
+    override fun shouldAnimateDozingChange(): Boolean =
+        !shouldPlayAnimation()
+
+    override fun shouldAnimateClockChange(): Boolean =
+        !isAnimationPlaying()
+
+    /**
+     * Called when AOD status is changed
+     */
+    override fun onAlwaysOnChanged(alwaysOn: Boolean) {
+        alwaysOnEnabled = alwaysOn
+    }
+
+    override fun addCallback(listener: FoldAodAnimationStatus) {
+        statusListeners += listener
+    }
+
+    override fun removeCallback(listener: FoldAodAnimationStatus) {
+        statusListeners.remove(listener)
+    }
+
+    interface FoldAodAnimationStatus {
+        fun onFoldToAodAnimationChanged()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
index b53ab21..ccde316 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
@@ -78,6 +78,8 @@
 
     fun getStatusBarMoveFromCenterAnimationController(): StatusBarMoveFromCenterAnimationController
 
+    fun getFoldAodAnimationController(): FoldAodAnimationController
+
     fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController
 
     fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
index a7e9cdb..8b6e982 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
@@ -23,7 +23,6 @@
 
 import org.jetbrains.annotations.NotNull;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -38,7 +37,7 @@
 public class Monitor implements CallbackController<Monitor.Callback> {
     private final String mTag = getClass().getSimpleName();
 
-    private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
+    private final ArrayList<Callback> mCallbacks = new ArrayList<>();
 
     // Set of all conditions that need to be monitored.
     private final Set<Condition> mConditions;
@@ -66,9 +65,9 @@
         mAllConditionsMet = newAllConditionsMet;
 
         // Updates all callbacks.
-        final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
+        final Iterator<Callback> iterator = mCallbacks.iterator();
         while (iterator.hasNext()) {
-            final Callback callback = iterator.next().get();
+            final Callback callback = iterator.next();
             if (callback == null) {
                 iterator.remove();
             } else {
@@ -78,7 +77,7 @@
     };
 
     @Inject
-    public Monitor(Set<Condition> conditions) {
+    public Monitor(Set<Condition> conditions, Set<Callback> callbacks) {
         mConditions = conditions;
 
         // If there is no condition, give green pass.
@@ -89,12 +88,20 @@
 
         // Initializes the conditions map and registers a callback for each condition.
         mConditions.forEach((condition -> mConditionsMap.put(condition, false)));
+
+        if (callbacks == null) {
+            return;
+        }
+
+        for (Callback callback : callbacks) {
+            addCallback(callback);
+        }
     }
 
     @Override
     public void addCallback(@NotNull Callback callback) {
         if (shouldLog()) Log.d(mTag, "adding callback");
-        mCallbacks.add(new WeakReference<>(callback));
+        mCallbacks.add(callback);
 
         // Updates the callback immediately.
         callback.onConditionsChanged(mAllConditionsMet);
@@ -109,9 +116,9 @@
     @Override
     public void removeCallback(@NotNull Callback callback) {
         if (shouldLog()) Log.d(mTag, "removing callback");
-        final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
+        final Iterator<Callback> iterator = mCallbacks.iterator();
         while (iterator.hasNext()) {
-            final Callback cb = iterator.next().get();
+            final Callback cb = iterator.next();
             if (cb == null || cb == callback) {
                 iterator.remove();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/dagger/MonitorComponent.java b/packages/SystemUI/src/com/android/systemui/util/condition/dagger/MonitorComponent.java
new file mode 100644
index 0000000..fc67973
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/dagger/MonitorComponent.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.util.condition.dagger;
+
+import com.android.systemui.util.condition.Condition;
+import com.android.systemui.util.condition.Monitor;
+
+import java.util.Set;
+
+import dagger.BindsInstance;
+import dagger.Subcomponent;
+
+/**
+ * Component for {@link Monitor}.
+ */
+@Subcomponent
+public interface MonitorComponent {
+    /**
+     * Factory for {@link MonitorComponent}.
+     */
+    @Subcomponent.Factory
+    interface Factory {
+        MonitorComponent create(@BindsInstance Set<Condition> conditions,
+                @BindsInstance Set<Monitor.Callback> callbacks);
+    }
+
+    /**
+     * Provides {@link Monitor}.
+     * @return
+     */
+    Monitor getMonitor();
+}
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 981bf01..7892d6e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
@@ -18,6 +18,7 @@
 
 import com.android.systemui.util.RingerModeTracker;
 import com.android.systemui.util.RingerModeTrackerImpl;
+import com.android.systemui.util.condition.dagger.MonitorComponent;
 import com.android.systemui.util.wrapper.UtilWrapperModule;
 
 import dagger.Binds;
@@ -26,6 +27,9 @@
 /** Dagger Module for code in the util package. */
 @Module(includes = {
                 UtilWrapperModule.class
+        },
+        subcomponents = {
+                MonitorComponent.class,
         })
 public interface UtilModule {
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 6dd6d6d..4600bc7 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -56,6 +56,7 @@
 import com.android.systemui.tracing.ProtoTracer;
 import com.android.systemui.tracing.nano.SystemUiTraceProto;
 import com.android.wm.shell.ShellCommandHandler;
+import com.android.wm.shell.compatui.CompatUI;
 import com.android.wm.shell.draganddrop.DragAndDrop;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
 import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
@@ -66,7 +67,6 @@
 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.sizecompatui.SizeCompatUI;
 import com.android.wm.shell.splitscreen.SplitScreen;
 
 import java.io.FileDescriptor;
@@ -114,7 +114,7 @@
     private final Optional<OneHanded> mOneHandedOptional;
     private final Optional<HideDisplayCutout> mHideDisplayCutoutOptional;
     private final Optional<ShellCommandHandler> mShellCommandHandler;
-    private final Optional<SizeCompatUI> mSizeCompatUIOptional;
+    private final Optional<CompatUI> mCompatUIOptional;
     private final Optional<DragAndDrop> mDragAndDropOptional;
 
     private final CommandQueue mCommandQueue;
@@ -132,7 +132,7 @@
     private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback;
     private KeyguardUpdateMonitorCallback mPipKeyguardCallback;
     private KeyguardUpdateMonitorCallback mOneHandedKeyguardCallback;
-    private KeyguardUpdateMonitorCallback mSizeCompatUIKeyguardCallback;
+    private KeyguardUpdateMonitorCallback mCompatUIKeyguardCallback;
     private WakefulnessLifecycle.Observer mWakefulnessObserver;
 
     @Inject
@@ -143,7 +143,7 @@
             Optional<OneHanded> oneHandedOptional,
             Optional<HideDisplayCutout> hideDisplayCutoutOptional,
             Optional<ShellCommandHandler> shellCommandHandler,
-            Optional<SizeCompatUI> sizeCompatUIOptional,
+            Optional<CompatUI> sizeCompatUIOptional,
             Optional<DragAndDrop> dragAndDropOptional,
             CommandQueue commandQueue,
             ConfigurationController configurationController,
@@ -169,7 +169,7 @@
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mProtoTracer = protoTracer;
         mShellCommandHandler = shellCommandHandler;
-        mSizeCompatUIOptional = sizeCompatUIOptional;
+        mCompatUIOptional = sizeCompatUIOptional;
         mDragAndDropOptional = dragAndDropOptional;
         mSysUiMainExecutor = sysUiMainExecutor;
     }
@@ -185,7 +185,7 @@
         mSplitScreenOptional.ifPresent(this::initSplitScreen);
         mOneHandedOptional.ifPresent(this::initOneHanded);
         mHideDisplayCutoutOptional.ifPresent(this::initHideDisplayCutout);
-        mSizeCompatUIOptional.ifPresent(this::initSizeCompatUi);
+        mCompatUIOptional.ifPresent(this::initCompatUi);
         mDragAndDropOptional.ifPresent(this::initDragAndDrop);
     }
 
@@ -391,14 +391,14 @@
     }
 
     @VisibleForTesting
-    void initSizeCompatUi(SizeCompatUI sizeCompatUI) {
-        mSizeCompatUIKeyguardCallback = new KeyguardUpdateMonitorCallback() {
+    void initCompatUi(CompatUI sizeCompatUI) {
+        mCompatUIKeyguardCallback = new KeyguardUpdateMonitorCallback() {
             @Override
             public void onKeyguardOccludedChanged(boolean occluded) {
                 sizeCompatUI.onKeyguardOccludedChanged(occluded);
             }
         };
-        mKeyguardUpdateMonitor.registerCallback(mSizeCompatUIKeyguardCallback);
+        mKeyguardUpdateMonitor.registerCallback(mCompatUIKeyguardCallback);
     }
 
     void initDragAndDrop(DragAndDrop dragAndDrop) {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index e967033..74e0f40 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -264,7 +264,7 @@
         reset(mView);
         observer.onChange(true);
         mExecutor.runAllReady();
-        verify(mView).switchToClock(KeyguardClockSwitch.SMALL);
+        verify(mView).switchToClock(KeyguardClockSwitch.SMALL, /* animate */ true);
     }
 
     private void verifyAttachment(VerificationMode times) {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
index e4336fe..8717a0e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
@@ -253,8 +253,8 @@
     }
 
     @Test
-    public void switchingToBigClock_makesSmallClockDisappear() {
-        mKeyguardClockSwitch.switchToClock(LARGE);
+    public void switchingToBigClockWithAnimation_makesSmallClockDisappear() {
+        mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true);
 
         mKeyguardClockSwitch.mClockInAnim.end();
         mKeyguardClockSwitch.mClockOutAnim.end();
@@ -265,8 +265,17 @@
     }
 
     @Test
-    public void switchingToSmallClock_makesBigClockDisappear() {
-        mKeyguardClockSwitch.switchToClock(SMALL);
+    public void switchingToBigClockNoAnimation_makesSmallClockDisappear() {
+        mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ false);
+
+        assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
+        assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
+        assertThat(mClockFrame.getAlpha()).isEqualTo(0);
+    }
+
+    @Test
+    public void switchingToSmallClockWithAnimation_makesBigClockDisappear() {
+        mKeyguardClockSwitch.switchToClock(SMALL, /* animate */ true);
 
         mKeyguardClockSwitch.mClockInAnim.end();
         mKeyguardClockSwitch.mClockOutAnim.end();
@@ -279,8 +288,19 @@
     }
 
     @Test
+    public void switchingToSmallClockNoAnimation_makesBigClockDisappear() {
+        mKeyguardClockSwitch.switchToClock(SMALL, false);
+
+        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();
+        assertThat(mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true)).isTrue();
+        assertThat(mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true)).isFalse();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 7776285..24b01e0 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -244,7 +244,7 @@
         ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById(
                 R.id.keyguard_bouncer_user_switcher);
         assertThat(((FrameLayout.LayoutParams) userSwitcher.getLayoutParams()).gravity)
-                .isEqualTo(Gravity.LEFT | Gravity.TOP);
+                .isEqualTo(Gravity.LEFT | Gravity.CENTER_VERTICAL);
     }
 
     @Test
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 b5f4c82..1dea678 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -643,12 +643,12 @@
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
         moveEvent.recycle();
 
-        // THEN low-tick haptic is played
+        // THEN tick haptic is played
         verify(mVibrator).vibrate(
                 anyInt(),
                 anyString(),
                 any(),
-                eq("udfps-onStart-tick"),
+                eq("udfps-onStart-click"),
                 eq(UdfpsController.VIBRATION_ATTRIBUTES));
 
         // THEN make sure vibration attributes has so that it always will play the haptic,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalManagerUpdaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalManagerUpdaterTest.java
index 9a9b7c4..4a29ada 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalManagerUpdaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalManagerUpdaterTest.java
@@ -28,8 +28,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.communal.conditions.CommunalConditionsMonitor;
 import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.condition.Monitor;
 import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
@@ -45,7 +45,7 @@
     @Mock
     private CommunalManager mCommunalManager;
     @Mock
-    private CommunalConditionsMonitor mCommunalConditionsMonitor;
+    private Monitor mCommunalConditionsMonitor;
 
     private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
 
@@ -55,7 +55,7 @@
         mContext.addMockSystemService(CommunalManager.class, mCommunalManager);
 
         doAnswer(invocation -> {
-            final CommunalConditionsMonitor.Callback callback = invocation.getArgument(0);
+            final Monitor.Callback callback = invocation.getArgument(0);
             callback.onConditionsChanged(true);
             return null;
         }).when(mCommunalConditionsMonitor).addCallback(any());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSourceMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSourceMonitorTest.java
index 409dd94..df1cc76 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSourceMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalSourceMonitorTest.java
@@ -31,8 +31,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.communal.conditions.CommunalConditionsMonitor;
 import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.condition.Monitor;
 import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
@@ -49,9 +49,9 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class CommunalSourceMonitorTest extends SysuiTestCase {
-    @Mock private CommunalConditionsMonitor mCommunalConditionsMonitor;
+    @Mock private Monitor mCommunalConditionsMonitor;
 
-    @Captor private ArgumentCaptor<CommunalConditionsMonitor.Callback> mConditionsCallbackCaptor;
+    @Captor private ArgumentCaptor<Monitor.Callback> mConditionsCallbackCaptor;
 
     private CommunalSourceMonitor mCommunalSourceMonitor;
     private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
@@ -156,7 +156,7 @@
     private void setConditionsMet(boolean value) {
         mExecutor.runAllReady();
         verify(mCommunalConditionsMonitor).addCallback(mConditionsCallbackCaptor.capture());
-        final CommunalConditionsMonitor.Callback conditionsCallback =
+        final Monitor.Callback conditionsCallback =
                 mConditionsCallbackCaptor.getValue();
         conditionsCallback.onConditionsChanged(value);
         mExecutor.runAllReady();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java
index 873f7a4..55af51d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeUiTest.java
@@ -45,6 +45,8 @@
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.tuner.TunerService;
+import com.android.systemui.unfold.FoldAodAnimationController;
+import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.util.wakelock.WakeLockFake;
 
 import org.junit.After;
@@ -54,6 +56,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class DozeUiTest extends SysuiTestCase {
@@ -79,6 +83,10 @@
     @Mock
     private StatusBarStateController mStatusBarStateController;
     @Mock
+    private FoldAodAnimationController mFoldAodAnimationController;
+    @Mock
+    private SysUIUnfoldComponent mSysUIUnfoldComponent;
+    @Mock
     private ConfigurationController mConfigurationController;
 
     @Before
@@ -90,9 +98,13 @@
         mWakeLock = new WakeLockFake();
         mHandler = mHandlerThread.getThreadHandler();
 
+        when(mSysUIUnfoldComponent.getFoldAodAnimationController())
+                .thenReturn(mFoldAodAnimationController);
+
         mDozeUi = new DozeUi(mContext, mAlarmManager, mWakeLock, mHost, mHandler,
                 mDozeParameters, mKeyguardUpdateMonitor, mDozeLog, mTunerService,
-                mStatusBarStateController, mConfigurationController);
+                mStatusBarStateController, Optional.of(mSysUIUnfoldComponent),
+                mConfigurationController);
         mDozeUi.setDozeMachine(mMachine);
     }
 
@@ -121,6 +133,7 @@
         reset(mHost);
         when(mDozeParameters.getAlwaysOn()).thenReturn(false);
         when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false);
+        when(mDozeParameters.shouldAnimateDozingChange()).thenReturn(true);
 
         mDozeUi.getKeyguardCallback().onKeyguardVisibilityChanged(false);
         verify(mHost).setAnimateScreenOff(eq(false));
@@ -131,6 +144,7 @@
         reset(mHost);
         when(mDozeParameters.getAlwaysOn()).thenReturn(true);
         when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false);
+        when(mDozeParameters.shouldAnimateDozingChange()).thenReturn(true);
 
         // Take over when the keyguard is visible.
         mDozeUi.getKeyguardCallback().onKeyguardVisibilityChanged(true);
@@ -142,6 +156,18 @@
     }
 
     @Test
+    public void propagatesAnimateScreenOff_alwaysOn_shouldAnimateDozingChangeIsFalse() {
+        reset(mHost);
+        when(mDozeParameters.getAlwaysOn()).thenReturn(true);
+        when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false);
+        when(mDozeParameters.shouldAnimateDozingChange()).thenReturn(false);
+
+        // Take over when the keyguard is visible.
+        mDozeUi.getKeyguardCallback().onKeyguardVisibilityChanged(true);
+        verify(mHost).setAnimateScreenOff(eq(false));
+    }
+
+    @Test
     public void neverAnimateScreenOff_whenNotSupported() {
         // Re-initialize DozeParameters saying that the display requires blanking.
         reset(mDozeParameters);
@@ -149,7 +175,8 @@
         when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(true);
         mDozeUi = new DozeUi(mContext, mAlarmManager, mWakeLock, mHost, mHandler,
                 mDozeParameters, mKeyguardUpdateMonitor, mDozeLog, mTunerService,
-                mStatusBarStateController, mConfigurationController);
+                mStatusBarStateController, Optional.of(mSysUIUnfoldComponent),
+                mConfigurationController);
         mDozeUi.setDozeMachine(mMachine);
 
         // Never animate if display doesn't support it.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index 53bfeee..904ee91 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -37,6 +37,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.SysuiTestableContext;
+import com.android.systemui.dreams.dagger.DreamOverlayComponent;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
@@ -78,16 +79,41 @@
     @Mock
     DreamOverlayStateController mDreamOverlayStateController;
 
+    @Mock
+    DreamOverlayComponent.Factory mDreamOverlayStatusBarViewComponentFactory;
+
+    @Mock
+    DreamOverlayComponent mDreamOverlayComponent;
+
+    @Mock
+    DreamOverlayStatusBarViewController mDreamOverlayStatusBarViewController;
+
+    @Mock
+    DreamOverlayContainerView mDreamOverlayContainerView;
+
+    @Mock
+    ViewGroup mDreamOverlayContentView;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mContext.addMockSystemService(WindowManager.class, mWindowManager);
+
+        when(mDreamOverlayComponent.getDreamOverlayContentView())
+                .thenReturn(mDreamOverlayContentView);
+        when(mDreamOverlayComponent.getDreamOverlayContainerView())
+                .thenReturn(mDreamOverlayContainerView);
+        when(mDreamOverlayComponent.getDreamOverlayStatusBarViewController())
+                .thenReturn(mDreamOverlayStatusBarViewController);
+        when(mDreamOverlayStatusBarViewComponentFactory.create())
+                .thenReturn(mDreamOverlayComponent);
+
     }
 
     @Test
     public void testInteraction() throws Exception {
         final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor,
-                mDreamOverlayStateController);
+                mDreamOverlayStateController, mDreamOverlayStatusBarViewComponentFactory);
         final IBinder proxy = service.onBind(new Intent());
         final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
         clearInvocations(mWindowManager);
@@ -128,7 +154,7 @@
     @Test
     public void testListening() throws Exception {
         final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor,
-                mDreamOverlayStateController);
+                mDreamOverlayStateController, mDreamOverlayStatusBarViewComponentFactory);
 
         final IBinder proxy = service.onBind(new Intent());
         final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
@@ -150,4 +176,35 @@
         // Verify provider is asked to create overlay.
         verify(mProvider).onCreateOverlay(any(), any(), any());
     }
+
+    @Test
+    public void testDreamOverlayStatusBarViewControllerInitialized() throws Exception {
+        final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor,
+                mDreamOverlayStateController, mDreamOverlayStatusBarViewComponentFactory);
+
+        final IBinder proxy = service.onBind(new Intent());
+        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+
+        // Inform the overlay service of dream starting.
+        overlay.startDream(mWindowParams, mDreamOverlayCallback);
+        mMainExecutor.runAllReady();
+
+        verify(mDreamOverlayStatusBarViewController).init();
+    }
+
+    @Test
+    public void testRootViewAttachListenerIsAddedToDreamOverlayContentView() throws Exception {
+        final DreamOverlayService service = new DreamOverlayService(mContext, mMainExecutor,
+                mDreamOverlayStateController, mDreamOverlayStatusBarViewComponentFactory);
+
+        final IBinder proxy = service.onBind(new Intent());
+        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+
+        // Inform the overlay service of dream starting.
+        overlay.startDream(mWindowParams, mDreamOverlayCallback);
+        mMainExecutor.runAllReady();
+
+        verify(mDreamOverlayContentView).addOnAttachStateChangeListener(
+                any(View.OnAttachStateChangeListener.class));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
new file mode 100644
index 0000000..7f72dda
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.battery.BatteryMeterViewController;
+import com.android.systemui.statusbar.policy.BatteryController;
+
+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)
+public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
+
+    @Mock
+    DreamOverlayStatusBarView mView;
+    @Mock
+    BatteryController mBatteryController;
+    @Mock
+    BatteryMeterViewController mBatteryMeterViewController;
+    @Mock
+    ConnectivityManager mConnectivityManager;
+    @Mock
+    NetworkCapabilities mNetworkCapabilities;
+    @Mock
+    Network mNetwork;
+
+    DreamOverlayStatusBarViewController mController;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mController = new DreamOverlayStatusBarViewController(
+                mContext, mView, mBatteryController, mBatteryMeterViewController,
+                mConnectivityManager);
+    }
+
+    @Test
+    public void testOnInitInitializesControllers() {
+        mController.onInit();
+        verify(mBatteryMeterViewController).init();
+    }
+
+    @Test
+    public void testOnViewAttachedAddsBatteryControllerCallback() {
+        mController.onViewAttached();
+        verify(mBatteryController)
+                .addCallback(any(BatteryController.BatteryStateChangeCallback.class));
+    }
+
+    @Test
+    public void testOnViewAttachedRegistersNetworkCallback() {
+        mController.onViewAttached();
+        verify(mConnectivityManager)
+                .registerNetworkCallback(any(NetworkRequest.class), any(
+                        ConnectivityManager.NetworkCallback.class));
+    }
+
+    @Test
+    public void testOnViewAttachedShowsWifiStatusWhenWifiUnavailable() {
+        when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+                .thenReturn(false);
+        when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(mNetworkCapabilities);
+        mController.onViewAttached();
+        verify(mView).showWifiStatus(true);
+    }
+
+    @Test
+    public void testOnViewAttachedHidesWifiStatusWhenWifiAvailable() {
+        when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+                .thenReturn(true);
+        when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(mNetworkCapabilities);
+        mController.onViewAttached();
+        verify(mView).showWifiStatus(false);
+    }
+
+    @Test
+    public void testOnViewAttachedShowsWifiStatusWhenNetworkCapabilitiesUnavailable() {
+        when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(null);
+        mController.onViewAttached();
+        verify(mView).showWifiStatus(true);
+    }
+
+    @Test
+    public void testOnViewDetachedRemovesBatteryControllerCallback() {
+        mController.onViewDetached();
+        verify(mBatteryController)
+                .removeCallback(any(BatteryController.BatteryStateChangeCallback.class));
+    }
+
+    @Test
+    public void testOnViewDetachedUnregistersNetworkCallback() {
+        mController.onViewDetached();
+        verify(mConnectivityManager)
+                .unregisterNetworkCallback(any(ConnectivityManager.NetworkCallback.class));
+    }
+
+    @Test
+    public void testBatteryPercentTextShownWhenBatteryLevelChangesWhileCharging() {
+        final ArgumentCaptor<BatteryController.BatteryStateChangeCallback> callbackCapture =
+                ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback.class);
+        mController.onViewAttached();
+        verify(mBatteryController).addCallback(callbackCapture.capture());
+        callbackCapture.getValue().onBatteryLevelChanged(1, true, true);
+        verify(mView).showBatteryPercentText(true);
+    }
+
+    @Test
+    public void testBatteryPercentTextHiddenWhenBatteryLevelChangesWhileNotCharging() {
+        final ArgumentCaptor<BatteryController.BatteryStateChangeCallback> callbackCapture =
+                ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback.class);
+        mController.onViewAttached();
+        verify(mBatteryController).addCallback(callbackCapture.capture());
+        callbackCapture.getValue().onBatteryLevelChanged(1, true, false);
+        verify(mView).showBatteryPercentText(false);
+    }
+
+    @Test
+    public void testWifiStatusHiddenWhenWifiBecomesAvailable() {
+        // Make sure wifi starts out unavailable when onViewAttached is called.
+        when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+                .thenReturn(false);
+        when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(mNetworkCapabilities);
+        mController.onViewAttached();
+
+        final ArgumentCaptor<ConnectivityManager.NetworkCallback> callbackCapture =
+                ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
+        verify(mConnectivityManager).registerNetworkCallback(any(), callbackCapture.capture());
+        callbackCapture.getValue().onAvailable(mNetwork);
+        verify(mView).showWifiStatus(false);
+    }
+
+    @Test
+    public void testWifiStatusShownWhenWifiBecomesUnavailable() {
+        // Make sure wifi starts out available when onViewAttached is called.
+        when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+                .thenReturn(true);
+        when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(mNetworkCapabilities);
+        mController.onViewAttached();
+
+        final ArgumentCaptor<ConnectivityManager.NetworkCallback> callbackCapture =
+                ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
+        verify(mConnectivityManager).registerNetworkCallback(any(), callbackCapture.capture());
+        callbackCapture.getValue().onLost(mNetwork);
+        verify(mView).showWifiStatus(true);
+    }
+
+    @Test
+    public void testWifiStatusHiddenWhenCapabilitiesChange() {
+        // Make sure wifi starts out unavailable when onViewAttached is called.
+        when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+                .thenReturn(false);
+        when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(mNetworkCapabilities);
+        mController.onViewAttached();
+
+        final ArgumentCaptor<ConnectivityManager.NetworkCallback> callbackCapture =
+                ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
+        verify(mConnectivityManager).registerNetworkCallback(any(), callbackCapture.capture());
+        when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+                .thenReturn(true);
+        callbackCapture.getValue().onCapabilitiesChanged(mNetwork, mNetworkCapabilities);
+        verify(mView).showWifiStatus(false);
+    }
+}
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 8d329c5..27fcb11 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -58,6 +58,7 @@
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.unfold.FoldAodAnimationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
 import com.android.systemui.util.DeviceConfigProxy;
@@ -69,7 +70,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -95,62 +95,40 @@
     private @Mock NavigationModeController mNavigationModeController;
     private @Mock KeyguardDisplayManager mKeyguardDisplayManager;
     private @Mock DozeParameters mDozeParameters;
-    private @Mock Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent;
-    private @Mock Optional<UnfoldLightRevealOverlayAnimation> mUnfoldAnimationOptional;
+    private @Mock SysUIUnfoldComponent mSysUIUnfoldComponent;
     private @Mock UnfoldLightRevealOverlayAnimation mUnfoldAnimation;
     private @Mock SysuiStatusBarStateController mStatusBarStateController;
     private @Mock KeyguardStateController mKeyguardStateController;
     private @Mock NotificationShadeDepthController mNotificationShadeDepthController;
     private @Mock KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     private @Mock ScreenOffAnimationController mScreenOffAnimationController;
+    private @Mock FoldAodAnimationController mFoldAodAnimationController;
     private @Mock IKeyguardDrawnCallback mKeyguardDrawnCallback;
     private @Mock InteractionJankMonitor mInteractionJankMonitor;
     private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake();
     private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
 
+    private Optional<SysUIUnfoldComponent> mSysUiUnfoldComponentOptional;
+
     private FalsingCollectorFake mFalsingCollector;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mFalsingCollector = new FalsingCollectorFake();
+        mSysUiUnfoldComponentOptional = Optional.of(mSysUIUnfoldComponent);
 
         when(mLockPatternUtils.getDevicePolicyManager()).thenReturn(mDevicePolicyManager);
         when(mPowerManager.newWakeLock(anyInt(), any())).thenReturn(mock(WakeLock.class));
-        when(mSysUIUnfoldComponent.map(
-                ArgumentMatchers.<Function<SysUIUnfoldComponent, UnfoldLightRevealOverlayAnimation>>
-                        any()))
-            .thenReturn(mUnfoldAnimationOptional);
-        when(mUnfoldAnimationOptional.isPresent()).thenReturn(true);
-        when(mUnfoldAnimationOptional.get()).thenReturn(mUnfoldAnimation);
+        when(mSysUIUnfoldComponent.getUnfoldLightRevealOverlayAnimation())
+                .thenReturn(mUnfoldAnimation);
+        when(mSysUIUnfoldComponent.getFoldAodAnimationController())
+                .thenReturn(mFoldAodAnimationController);
+
         when(mInteractionJankMonitor.begin(any(), anyInt())).thenReturn(true);
         when(mInteractionJankMonitor.end(anyInt())).thenReturn(true);
 
-        mViewMediator = new KeyguardViewMediator(
-                mContext,
-                mFalsingCollector,
-                mLockPatternUtils,
-                mBroadcastDispatcher,
-                () -> mStatusBarKeyguardViewManager,
-                mDismissCallbackRegistry,
-                mUpdateMonitor,
-                mDumpManager,
-                mUiBgExecutor,
-                mPowerManager,
-                mTrustManager,
-                mUserSwitcherController,
-                mDeviceConfig,
-                mNavigationModeController,
-                mKeyguardDisplayManager,
-                mDozeParameters,
-                mSysUIUnfoldComponent,
-                mStatusBarStateController,
-                mKeyguardStateController,
-                () -> mKeyguardUnlockAnimationController,
-                mScreenOffAnimationController,
-                () -> mNotificationShadeDepthController,
-                mInteractionJankMonitor);
-        mViewMediator.start();
+        createAndStartViewMediator();
     }
 
     @Test
@@ -179,6 +157,7 @@
         mViewMediator.onScreenTurningOn(mKeyguardDrawnCallback);
         TestableLooper.get(this).processAllMessages();
         onUnfoldOverlayReady();
+        onFoldAodReady();
 
         // Should be called when both unfold overlay and keyguard drawn ready
         verify(mKeyguardDrawnCallback).onDrawn();
@@ -188,7 +167,8 @@
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
     public void testUnfoldTransitionDisabledDrawnTasksReady_onScreenTurningOn_callsDrawnCallback()
             throws RemoteException {
-        when(mUnfoldAnimationOptional.isPresent()).thenReturn(false);
+        mSysUiUnfoldComponentOptional = Optional.empty();
+        createAndStartViewMediator();
 
         mViewMediator.onScreenTurningOn(mKeyguardDrawnCallback);
         TestableLooper.get(this).processAllMessages();
@@ -200,6 +180,7 @@
     @Test
     public void testIsAnimatingScreenOff() {
         when(mDozeParameters.shouldControlUnlockedScreenOff()).thenReturn(true);
+        when(mDozeParameters.shouldAnimateDozingChange()).thenReturn(true);
 
         mViewMediator.onFinishedGoingToSleep(OFF_BECAUSE_OF_USER, false);
         mViewMediator.setDozing(true);
@@ -244,4 +225,39 @@
         overlayReadyCaptor.getValue().run();
         TestableLooper.get(this).processAllMessages();
     }
+
+    private void onFoldAodReady() {
+        ArgumentCaptor<Runnable> ready = ArgumentCaptor.forClass(Runnable.class);
+        verify(mFoldAodAnimationController).onScreenTurningOn(ready.capture());
+        ready.getValue().run();
+        TestableLooper.get(this).processAllMessages();
+    }
+
+    private void createAndStartViewMediator() {
+        mViewMediator = new KeyguardViewMediator(
+                mContext,
+                mFalsingCollector,
+                mLockPatternUtils,
+                mBroadcastDispatcher,
+                () -> mStatusBarKeyguardViewManager,
+                mDismissCallbackRegistry,
+                mUpdateMonitor,
+                mDumpManager,
+                mUiBgExecutor,
+                mPowerManager,
+                mTrustManager,
+                mUserSwitcherController,
+                mDeviceConfig,
+                mNavigationModeController,
+                mKeyguardDisplayManager,
+                mDozeParameters,
+                mSysUiUnfoldComponentOptional,
+                mStatusBarStateController,
+                mKeyguardStateController,
+                () -> mKeyguardUnlockAnimationController,
+                mScreenOffAnimationController,
+                () -> mNotificationShadeDepthController,
+                mInteractionJankMonitor);
+        mViewMediator.start();
+    }
 }
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 d0b957c..649ee87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -669,7 +669,7 @@
 
     @Test
     fun testPlaybackActions_noPrevNext_usesCustom() {
-        val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
+        val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5")
         whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
         val stateActions = PlaybackState.ACTION_PLAY
         val stateBuilder = PlaybackState.Builder()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
new file mode 100644
index 0000000..af33daf
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
@@ -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 com.android.systemui.qs.tileimpl
+
+import android.content.ComponentName
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.external.CustomTile
+import com.android.systemui.qs.tiles.AirplaneModeTile
+import com.android.systemui.qs.tiles.AlarmTile
+import com.android.systemui.qs.tiles.BatterySaverTile
+import com.android.systemui.qs.tiles.BluetoothTile
+import com.android.systemui.qs.tiles.CameraToggleTile
+import com.android.systemui.qs.tiles.CastTile
+import com.android.systemui.qs.tiles.CellularTile
+import com.android.systemui.qs.tiles.ColorInversionTile
+import com.android.systemui.qs.tiles.DataSaverTile
+import com.android.systemui.qs.tiles.DeviceControlsTile
+import com.android.systemui.qs.tiles.DndTile
+import com.android.systemui.qs.tiles.FgsManagerTile
+import com.android.systemui.qs.tiles.FlashlightTile
+import com.android.systemui.qs.tiles.HotspotTile
+import com.android.systemui.qs.tiles.InternetTile
+import com.android.systemui.qs.tiles.LocationTile
+import com.android.systemui.qs.tiles.MicrophoneToggleTile
+import com.android.systemui.qs.tiles.NfcTile
+import com.android.systemui.qs.tiles.NightDisplayTile
+import com.android.systemui.qs.tiles.OneHandedModeTile
+import com.android.systemui.qs.tiles.QRCodeScannerTile
+import com.android.systemui.qs.tiles.QuickAccessWalletTile
+import com.android.systemui.qs.tiles.ReduceBrightColorsTile
+import com.android.systemui.qs.tiles.RotationLockTile
+import com.android.systemui.qs.tiles.ScreenRecordTile
+import com.android.systemui.qs.tiles.UiModeNightTile
+import com.android.systemui.qs.tiles.UserTile
+import com.android.systemui.qs.tiles.WifiTile
+import com.android.systemui.qs.tiles.WorkModeTile
+import com.android.systemui.util.leak.GarbageMonitor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Answers
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
+
+private val specMap = mapOf(
+        "wifi" to WifiTile::class.java,
+        "internet" to InternetTile::class.java,
+        "bt" to BluetoothTile::class.java,
+        "cell" to CellularTile::class.java,
+        "dnd" to DndTile::class.java,
+        "inversion" to ColorInversionTile::class.java,
+        "airplane" to AirplaneModeTile::class.java,
+        "work" to WorkModeTile::class.java,
+        "rotation" to RotationLockTile::class.java,
+        "flashlight" to FlashlightTile::class.java,
+        "location" to LocationTile::class.java,
+        "cast" to CastTile::class.java,
+        "hotspot" to HotspotTile::class.java,
+        "user" to UserTile::class.java,
+        "battery" to BatterySaverTile::class.java,
+        "saver" to DataSaverTile::class.java,
+        "night" to NightDisplayTile::class.java,
+        "nfc" to NfcTile::class.java,
+        "dark" to UiModeNightTile::class.java,
+        "screenrecord" to ScreenRecordTile::class.java,
+        "reduce_brightness" to ReduceBrightColorsTile::class.java,
+        "cameratoggle" to CameraToggleTile::class.java,
+        "mictoggle" to MicrophoneToggleTile::class.java,
+        "controls" to DeviceControlsTile::class.java,
+        "alarm" to AlarmTile::class.java,
+        "wallet" to QuickAccessWalletTile::class.java,
+        "qr_code_scanner" to QRCodeScannerTile::class.java,
+        "onehanded" to OneHandedModeTile::class.java,
+        "fgsmanager" to FgsManagerTile::class.java
+)
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class QSFactoryImplTest : SysuiTestCase() {
+
+    @Mock private lateinit var qsHost: QSHost
+    @Mock(answer = Answers.RETURNS_SELF) private lateinit var customTileBuilder: CustomTile.Builder
+    @Mock private lateinit var customTile: CustomTile
+
+    @Mock private lateinit var wifiTile: WifiTile
+    @Mock private lateinit var internetTile: InternetTile
+    @Mock private lateinit var bluetoothTile: BluetoothTile
+    @Mock private lateinit var cellularTile: CellularTile
+    @Mock private lateinit var dndTile: DndTile
+    @Mock private lateinit var colorInversionTile: ColorInversionTile
+    @Mock private lateinit var airplaneTile: AirplaneModeTile
+    @Mock private lateinit var workTile: WorkModeTile
+    @Mock private lateinit var rotationTile: RotationLockTile
+    @Mock private lateinit var flashlightTile: FlashlightTile
+    @Mock private lateinit var locationTile: LocationTile
+    @Mock private lateinit var castTile: CastTile
+    @Mock private lateinit var hotspotTile: HotspotTile
+    @Mock private lateinit var userTile: UserTile
+    @Mock private lateinit var batterySaverTile: BatterySaverTile
+    @Mock private lateinit var dataSaverTile: DataSaverTile
+    @Mock private lateinit var nightDisplayTile: NightDisplayTile
+    @Mock private lateinit var nfcTile: NfcTile
+    @Mock private lateinit var memoryTile: GarbageMonitor.MemoryTile
+    @Mock private lateinit var darkModeTile: UiModeNightTile
+    @Mock private lateinit var screenRecordTile: ScreenRecordTile
+    @Mock private lateinit var reduceBrightColorsTile: ReduceBrightColorsTile
+    @Mock private lateinit var cameraToggleTile: CameraToggleTile
+    @Mock private lateinit var microphoneToggleTile: MicrophoneToggleTile
+    @Mock private lateinit var deviceControlsTile: DeviceControlsTile
+    @Mock private lateinit var alarmTile: AlarmTile
+    @Mock private lateinit var quickAccessWalletTile: QuickAccessWalletTile
+    @Mock private lateinit var qrCodeScannerTile: QRCodeScannerTile
+    @Mock private lateinit var oneHandedModeTile: OneHandedModeTile
+    @Mock private lateinit var fgsManagerTile: FgsManagerTile
+
+    private lateinit var factory: QSFactoryImpl
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(qsHost.context).thenReturn(mContext)
+        whenever(qsHost.userContext).thenReturn(mContext)
+        whenever(customTileBuilder.build()).thenReturn(customTile)
+
+        factory = QSFactoryImpl(
+                { qsHost },
+                { customTileBuilder },
+                { wifiTile },
+                { internetTile },
+                { bluetoothTile },
+                { cellularTile },
+                { dndTile },
+                { colorInversionTile },
+                { airplaneTile },
+                { workTile },
+                { rotationTile },
+                { flashlightTile },
+                { locationTile },
+                { castTile },
+                { hotspotTile },
+                { userTile },
+                { batterySaverTile },
+                { dataSaverTile },
+                { nightDisplayTile },
+                { nfcTile },
+                { memoryTile },
+                { darkModeTile },
+                { screenRecordTile },
+                { reduceBrightColorsTile },
+                { cameraToggleTile },
+                { microphoneToggleTile },
+                { deviceControlsTile },
+                { alarmTile },
+                { quickAccessWalletTile },
+                { qrCodeScannerTile },
+                { oneHandedModeTile },
+                { fgsManagerTile }
+        )
+        // When adding/removing tiles, fix also [specMap]
+    }
+
+    @Test
+    fun testCorrectTileClassStock() {
+        specMap.forEach { spec, klazz ->
+            assertThat(factory.createTile(spec)).isInstanceOf(klazz)
+        }
+    }
+
+    @Test
+    fun testCustomTileClass() {
+        val customSpec = CustomTile.toSpec(ComponentName("test", "test"))
+        assertThat(factory.createTile(customSpec)).isInstanceOf(CustomTile::class.java)
+    }
+
+    @Test
+    fun testBadTileNull() {
+        assertThat(factory.createTile("-432~")).isNull()
+    }
+
+    @Test
+    fun testTileInitializedAndStale() {
+        specMap.forEach { spec, _ ->
+            val tile = factory.createTile(spec) as QSTileImpl<*>
+            val inOrder = inOrder(tile)
+            inOrder.verify(tile).initialize()
+            inOrder.verify(tile).postStale()
+        }
+
+        val customSpec = CustomTile.toSpec(ComponentName("test", "test"))
+        val tile = factory.createTile(customSpec) as QSTileImpl<*>
+        val inOrder = inOrder(tile)
+        inOrder.verify(tile).initialize()
+        inOrder.verify(tile).postStale()
+    }
+}
\ No newline at end of file
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 9bcdcc9..1cd9b9e 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
@@ -425,6 +425,7 @@
                 .thenReturn(mKeyguardUserSwitcherComponent);
         when(mKeyguardUserSwitcherComponent.getKeyguardUserSwitcherController())
                 .thenReturn(mKeyguardUserSwitcherController);
+        when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(true);
 
         doAnswer((Answer<Void>) invocation -> {
             mTouchHandler = invocation.getArgument(0);
@@ -880,11 +881,11 @@
 
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
         triggerPositionClockAndNotifications();
-        verify(mKeyguardStatusViewController).displayClock(LARGE);
+        verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
 
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
         mNotificationPanelViewController.closeQs();
-        verify(mKeyguardStatusViewController).displayClock(SMALL);
+        verify(mKeyguardStatusViewController).displayClock(SMALL, /* animate */ true);
     }
 
     @Test
@@ -894,12 +895,14 @@
 
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
         triggerPositionClockAndNotifications();
-        verify(mKeyguardStatusViewController).displayClock(LARGE);
+        verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
 
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
         triggerPositionClockAndNotifications();
-        verify(mKeyguardStatusViewController, times(2)).displayClock(LARGE);
-        verify(mKeyguardStatusViewController, never()).displayClock(SMALL);
+        verify(mKeyguardStatusViewController, times(2))
+                .displayClock(LARGE, /* animate */ true);
+        verify(mKeyguardStatusViewController, never())
+                .displayClock(SMALL, /* animate */ true);
     }
 
     @Test
@@ -911,7 +914,20 @@
 
         mNotificationPanelViewController.setDozing(true, false, null);
 
-        verify(mKeyguardStatusViewController).displayClock(LARGE);
+        verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
+    }
+
+    @Test
+    public void testSwitchesToBigClockInSplitShadeOnAodAnimateDisabled() {
+        when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false);
+        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, /* animate */ false);
     }
 
     @Test
@@ -923,13 +939,13 @@
         // one notification + media player visible
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
         triggerPositionClockAndNotifications();
-        verify(mKeyguardStatusViewController).displayClock(SMALL);
+        verify(mKeyguardStatusViewController).displayClock(SMALL, /* animate */ true);
 
         // only media player visible
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
         triggerPositionClockAndNotifications();
-        verify(mKeyguardStatusViewController, times(2)).displayClock(SMALL);
-        verify(mKeyguardStatusViewController, never()).displayClock(LARGE);
+        verify(mKeyguardStatusViewController, times(2)).displayClock(SMALL, true);
+        verify(mKeyguardStatusViewController, never()).displayClock(LARGE, /* animate */ true);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SplitShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SplitShadeHeaderControllerTest.kt
index 2d548e9..a8544a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SplitShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SplitShadeHeaderControllerTest.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.phone
 
-import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
 import android.view.View
+import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ShadeInterpolation
@@ -42,6 +42,7 @@
     var viewVisibility = View.GONE
 
     private lateinit var splitShadeHeaderController: SplitShadeHeaderController
+    private lateinit var carrierIconSlots: List<String>
 
     @Before
     fun setup() {
@@ -67,12 +68,13 @@
                 featureFlags,
                 batteryMeterViewController
         )
+        carrierIconSlots = listOf(
+                context.getString(com.android.internal.R.string.status_bar_mobile))
     }
 
     @Test
     fun setVisible_onlyInSplitShade() {
-        splitShadeHeaderController.splitShadeMode = true
-        splitShadeHeaderController.shadeExpanded = true
+        makeShadeVisible()
         assertThat(viewVisibility).isEqualTo(View.VISIBLE)
 
         splitShadeHeaderController.splitShadeMode = false
@@ -81,17 +83,38 @@
 
     @Test
     fun updateListeners_registersWhenVisible() {
-        splitShadeHeaderController.splitShadeMode = true
-        splitShadeHeaderController.shadeExpanded = true
+        makeShadeVisible()
         verify(qsCarrierGroupController).setListening(true)
         verify(statusBarIconController).addIconGroup(any())
     }
 
     @Test
     fun shadeExpandedFraction_updatesAlpha() {
-        splitShadeHeaderController.splitShadeMode = true
-        splitShadeHeaderController.shadeExpanded = true
+        makeShadeVisible()
         splitShadeHeaderController.shadeExpandedFraction = 0.5f
         verify(view).setAlpha(ShadeInterpolation.getContentAlpha(0.5f))
     }
-}
\ No newline at end of file
+
+    @Test
+    fun singleCarrier_enablesCarrierIconsInStatusIcons() {
+        whenever(qsCarrierGroupController.isSingleCarrier).thenReturn(true)
+
+        makeShadeVisible()
+
+        verify(statusIcons).removeIgnoredSlots(carrierIconSlots)
+    }
+
+    @Test
+    fun dualCarrier_disablesCarrierIconsInStatusIcons() {
+        whenever(qsCarrierGroupController.isSingleCarrier).thenReturn(false)
+
+        makeShadeVisible()
+
+        verify(statusIcons).addIgnoredSlots(carrierIconSlots)
+    }
+
+    private fun makeShadeVisible() {
+        splitShadeHeaderController.splitShadeMode = true
+        splitShadeHeaderController.shadeExpanded = true
+    }
+}
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 c5bdfed..5d80bca 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
@@ -43,9 +43,6 @@
 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.WakefulnessLifecycle;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.statusbar.NotificationMediaManager;
@@ -79,9 +76,7 @@
     @Mock
     private NotificationPanelViewController mNotificationPanelView;
     @Mock
-    private BiometricUnlockController mBiometrucUnlockController;
-    @Mock
-    private DismissCallbackRegistry mDismissCallbackRegistry;
+    private BiometricUnlockController mBiometricUnlockController;
     @Mock
     private SysuiStatusBarStateController mStatusBarStateController;
     @Mock
@@ -97,15 +92,12 @@
     @Mock
     private KeyguardBouncer mBouncer;
     @Mock
-    private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
-    @Mock
     private StatusBarKeyguardViewManager.AlternateAuthInterceptor mAlternateAuthInterceptor;
     @Mock
     private KeyguardMessageArea mKeyguardMessageArea;
     @Mock
     private ShadeController mShadeController;
 
-    private WakefulnessLifecycle mWakefulnessLifecycle;
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
 
     @Before
@@ -117,10 +109,6 @@
                 .thenReturn(mBouncer);
         when(mStatusBar.getBouncerContainer()).thenReturn(mContainer);
         when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea);
-        mWakefulnessLifecycle = new WakefulnessLifecycle(
-                getContext(),
-                null,
-                mock(DumpManager.class));
         mStatusBarKeyguardViewManager = new StatusBarKeyguardViewManager(
                 getContext(),
                 mViewMediatorCallback,
@@ -134,15 +122,13 @@
                 mKeyguardStateController,
                 mock(NotificationMediaManager.class),
                 mKeyguardBouncerFactory,
-                mWakefulnessLifecycle,
-                mUnlockedScreenOffAnimationController,
                 mKeyguardMessageAreaFactory,
                 () -> mShadeController);
         mStatusBarKeyguardViewManager.registerStatusBar(
                 mStatusBar,
                 mNotificationPanelView,
                 new PanelExpansionStateManager(),
-                mBiometrucUnlockController,
+                mBiometricUnlockController,
                 mNotificationContainer,
                 mBypassController);
         mStatusBarKeyguardViewManager.show(null);
@@ -261,7 +247,7 @@
 
     @Test
     public void onPanelExpansionChanged_neverTranslatesBouncerWhenWakeAndUnlock() {
-        when(mBiometrucUnlockController.getMode())
+        when(mBiometricUnlockController.getMode())
                 .thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(
                 /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
@@ -389,6 +375,39 @@
     }
 
     @Test
+    public void testShowAltAuth_unlockingWithBiometricNotAllowed() {
+        // GIVEN alt auth exists, unlocking with biometric isn't allowed
+        mStatusBarKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
+        when(mBouncer.isShowing()).thenReturn(false);
+        when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(false);
+
+        // WHEN showGenericBouncer is called
+        final boolean scrimmed = true;
+        mStatusBarKeyguardViewManager.showGenericBouncer(scrimmed);
+
+        // THEN regular bouncer is shown
+        verify(mBouncer).show(anyBoolean(), eq(scrimmed));
+        verify(mAlternateAuthInterceptor, never()).showAlternateAuthBouncer();
+    }
+
+    @Test
+    public void testShowAltAuth_unlockingWithBiometricAllowed() {
+        // GIVEN alt auth exists, unlocking with biometric is allowed
+        mStatusBarKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
+        when(mBouncer.isShowing()).thenReturn(false);
+        when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(true);
+
+        // WHEN showGenericBouncer is called
+        mStatusBarKeyguardViewManager.showGenericBouncer(true);
+
+        // THEN alt auth bouncer is shown
+        verify(mAlternateAuthInterceptor).showAlternateAuthBouncer();
+        verify(mBouncer, never()).show(anyBoolean(), anyBoolean());
+    }
+
+    @Test
     public void testUpdateResources_delegatesToBouncer() {
         mStatusBarKeyguardViewManager.updateResources();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
index 878bdea..d645449 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
@@ -58,7 +58,7 @@
         mCondition3 = spy(new FakeCondition());
         mConditions = new HashSet<>(Arrays.asList(mCondition1, mCondition2, mCondition3));
 
-        mConditionMonitor = new Monitor(mConditions);
+        mConditionMonitor = new Monitor(mConditions, null /*callbacks*/);
     }
 
     @Test
@@ -98,7 +98,7 @@
 
     @Test
     public void addCallback_noConditions_reportAllConditionsMet() {
-        final Monitor monitor = new Monitor(new HashSet<>());
+        final Monitor monitor = new Monitor(new HashSet<>(), null /*callbacks*/);
         final Monitor.Callback callback = mock(Monitor.Callback.class);
 
         monitor.addCallback(callback);
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 71ae5eb..c7943c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -116,7 +116,6 @@
 import com.android.wm.shell.bubbles.BubbleEntry;
 import com.android.wm.shell.bubbles.BubbleIconFactory;
 import com.android.wm.shell.bubbles.BubbleLogger;
-import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubbleStackView;
 import com.android.wm.shell.bubbles.BubbleViewInfoTask;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -631,7 +630,7 @@
     }
 
     @Test
-    public void testRemoveLastExpanded_selectsOverflow() {
+    public void testRemoveLastExpanded_collapses() {
         // Mark it as a bubble and add it explicitly
         mEntryListener.onPendingEntryAdded(mRow);
         mEntryListener.onPendingEntryAdded(mRow2);
@@ -670,11 +669,10 @@
                         stackView.getExpandedBubble().getKey()).getKey(),
                 Bubbles.DISMISS_USER_GESTURE);
 
-        // Overflow should be selected
-        assertEquals(mBubbleData.getSelectedBubble().getKey(), BubbleOverflow.KEY);
-        verify(mBubbleExpandListener).onBubbleExpandChanged(true, BubbleOverflow.KEY);
-        assertTrue(mBubbleController.hasBubbles());
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        // We should be collapsed
+        verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
+        assertFalse(mBubbleController.hasBubbles());
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
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 7b9e6a9..65c219c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
@@ -19,8 +19,11 @@
 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;
+
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -97,7 +100,6 @@
 import com.android.wm.shell.bubbles.BubbleDataRepository;
 import com.android.wm.shell.bubbles.BubbleEntry;
 import com.android.wm.shell.bubbles.BubbleLogger;
-import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubbleStackView;
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.common.DisplayController;
@@ -572,7 +574,7 @@
     }
 
     @Test
-    public void testRemoveLastExpanded_selectsOverflow() {
+    public void testRemoveLastExpanded_collapses() {
         // Mark it as a bubble and add it explicitly
         mEntryListener.onEntryAdded(mRow);
         mEntryListener.onEntryAdded(mRow2);
@@ -611,11 +613,10 @@
                         stackView.getExpandedBubble().getKey()).getKey(),
                 Bubbles.DISMISS_USER_GESTURE);
 
-        // Overflow should be selected
-        assertEquals(mBubbleData.getSelectedBubble().getKey(), BubbleOverflow.KEY);
-        verify(mBubbleExpandListener).onBubbleExpandChanged(true, BubbleOverflow.KEY);
-        assertTrue(mBubbleController.hasBubbles());
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        // We should be collapsed
+        verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
+        assertFalse(mBubbleController.hasBubbles());
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
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 1e15d2a..2f2e536 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -35,6 +35,7 @@
 import com.android.systemui.tracing.ProtoTracer;
 import com.android.wm.shell.ShellCommandHandler;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.compatui.CompatUI;
 import com.android.wm.shell.draganddrop.DragAndDrop;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
 import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
@@ -42,7 +43,6 @@
 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.sizecompatui.SizeCompatUI;
 import com.android.wm.shell.splitscreen.SplitScreen;
 
 import org.junit.Before;
@@ -78,7 +78,7 @@
     @Mock WakefulnessLifecycle mWakefulnessLifecycle;
     @Mock ProtoTracer mProtoTracer;
     @Mock ShellCommandHandler mShellCommandHandler;
-    @Mock SizeCompatUI mSizeCompatUI;
+    @Mock CompatUI mCompatUI;
     @Mock ShellExecutor mSysUiMainExecutor;
     @Mock DragAndDrop mDragAndDrop;
 
@@ -88,7 +88,7 @@
 
         mWMShell = new WMShell(mContext, Optional.of(mPip), Optional.of(mLegacySplitScreen),
                 Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mHideDisplayCutout),
-                Optional.of(mShellCommandHandler), Optional.of(mSizeCompatUI),
+                Optional.of(mShellCommandHandler), Optional.of(mCompatUI),
                 Optional.of(mDragAndDrop),
                 mCommandQueue, mConfigurationController, mKeyguardUpdateMonitor,
                 mNavigationModeController, mScreenLifecycle, mSysUiState, mProtoTracer,
@@ -136,8 +136,8 @@
     }
 
     @Test
-    public void initSizeCompatUI_registersCallbacks() {
-        mWMShell.initSizeCompatUi(mSizeCompatUI);
+    public void initCompatUI_registersCallbacks() {
+        mWMShell.initCompatUi(mCompatUI);
 
         verify(mKeyguardUpdateMonitor).registerCallback(any(KeyguardUpdateMonitorCallback.class));
     }
diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values-land/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-land/dimens.xml
index 1f5834d..a03dcbd 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlay/res/values-land/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-land/dimens.xml
@@ -18,5 +18,5 @@
 -->
 <resources>
     <!-- The height of the bottom navigation gesture area. -->
-    <dimen name="navigation_bar_gesture_height">32dp</dimen>
+    <dimen name="navigation_bar_gesture_height">24dp</dimen>
 </resources>
diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
index ac1f022..c5d0c9e 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
@@ -18,13 +18,13 @@
 -->
 <resources>
     <!-- Height of the bottom navigation / system bar. -->
-    <dimen name="navigation_bar_height">16dp</dimen>
+    <dimen name="navigation_bar_height">24dp</dimen>
     <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
-    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <dimen name="navigation_bar_height_landscape">24dp</dimen>
     <!-- Width of the navigation bar when it is placed vertically on the screen -->
-    <dimen name="navigation_bar_width">16dp</dimen>
+    <dimen name="navigation_bar_width">24dp</dimen>
     <!-- Height of the bottom navigation / system bar. -->
     <dimen name="navigation_bar_frame_height">48dp</dimen>
     <!-- The height of the bottom navigation gesture area. -->
-    <dimen name="navigation_bar_gesture_height">32dp</dimen>
+    <dimen name="navigation_bar_gesture_height">24dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml
index ac1f022..c5d0c9e 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml
@@ -18,13 +18,13 @@
 -->
 <resources>
     <!-- Height of the bottom navigation / system bar. -->
-    <dimen name="navigation_bar_height">16dp</dimen>
+    <dimen name="navigation_bar_height">24dp</dimen>
     <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
-    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <dimen name="navigation_bar_height_landscape">24dp</dimen>
     <!-- Width of the navigation bar when it is placed vertically on the screen -->
-    <dimen name="navigation_bar_width">16dp</dimen>
+    <dimen name="navigation_bar_width">24dp</dimen>
     <!-- Height of the bottom navigation / system bar. -->
     <dimen name="navigation_bar_frame_height">48dp</dimen>
     <!-- The height of the bottom navigation gesture area. -->
-    <dimen name="navigation_bar_gesture_height">32dp</dimen>
+    <dimen name="navigation_bar_gesture_height">24dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml
index ac1f022..c5d0c9e 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml
@@ -18,13 +18,13 @@
 -->
 <resources>
     <!-- Height of the bottom navigation / system bar. -->
-    <dimen name="navigation_bar_height">16dp</dimen>
+    <dimen name="navigation_bar_height">24dp</dimen>
     <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
-    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <dimen name="navigation_bar_height_landscape">24dp</dimen>
     <!-- Width of the navigation bar when it is placed vertically on the screen -->
-    <dimen name="navigation_bar_width">16dp</dimen>
+    <dimen name="navigation_bar_width">24dp</dimen>
     <!-- Height of the bottom navigation / system bar. -->
     <dimen name="navigation_bar_frame_height">48dp</dimen>
     <!-- The height of the bottom navigation gesture area. -->
-    <dimen name="navigation_bar_gesture_height">32dp</dimen>
+    <dimen name="navigation_bar_gesture_height">24dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml
index ac1f022..c5d0c9e 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml
@@ -18,13 +18,13 @@
 -->
 <resources>
     <!-- Height of the bottom navigation / system bar. -->
-    <dimen name="navigation_bar_height">16dp</dimen>
+    <dimen name="navigation_bar_height">24dp</dimen>
     <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
-    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <dimen name="navigation_bar_height_landscape">24dp</dimen>
     <!-- Width of the navigation bar when it is placed vertically on the screen -->
-    <dimen name="navigation_bar_width">16dp</dimen>
+    <dimen name="navigation_bar_width">24dp</dimen>
     <!-- Height of the bottom navigation / system bar. -->
     <dimen name="navigation_bar_frame_height">48dp</dimen>
     <!-- The height of the bottom navigation gesture area. -->
-    <dimen name="navigation_bar_gesture_height">32dp</dimen>
+    <dimen name="navigation_bar_gesture_height">24dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 881c910..f050b66 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -1027,8 +1027,8 @@
                     mSystemSupport.getMagnificationProcessor();
             final long identity = Binder.clearCallingIdentity();
             try {
-                magnificationProcessor.getMagnificationRegion(displayId, region,
-                        mSecurityPolicy.canControlMagnification(this));
+                magnificationProcessor.getFullscreenMagnificationRegion(displayId,
+                        region, mSecurityPolicy.canControlMagnification(this));
                 return region;
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -1095,7 +1095,7 @@
         try {
             MagnificationProcessor magnificationProcessor =
                     mSystemSupport.getMagnificationProcessor();
-            return (magnificationProcessor.reset(displayId, animate)
+            return (magnificationProcessor.resetFullscreenMagnification(displayId, animate)
                     || !magnificationProcessor.isMagnifying(displayId));
         } finally {
             Binder.restoreCallingIdentity(identity);
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
index d0b9895..7a525ee 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
@@ -119,6 +119,18 @@
         return false;
     }
 
+    private boolean setScaleAndCenterForFullScreenMagnification(int displayId, float scale,
+            float centerX, float centerY,
+            boolean animate, int id) {
+        if (!isRegistered(displayId)) {
+            register(displayId);
+        }
+        return mController.getFullScreenMagnificationController().setScaleAndCenter(
+                displayId,
+                scale,
+                centerX, centerY, animate, id);
+    }
+
     /**
      * Returns {@code true} if transition magnification mode needed. And it is no need to transition
      * mode when the controlling mode is unchanged or the controlling magnifier is not activated.
@@ -135,24 +147,18 @@
     }
 
     /**
-     * Returns the magnification scale. If an animation is in progress,
-     * this reflects the end state of the animation.
+     * Returns the magnification scale of full-screen magnification on the display.
+     * If an animation is in progress, this reflects the end state of the animation.
      *
      * @param displayId The logical display id.
      * @return the scale
      */
     public float getScale(int displayId) {
-        int mode = getControllingMode(displayId);
-        if (mode == MAGNIFICATION_MODE_FULLSCREEN) {
-            return mController.getFullScreenMagnificationController().getScale(displayId);
-        } else if (mode == MAGNIFICATION_MODE_WINDOW) {
-            return mController.getWindowMagnificationMgr().getScale(displayId);
-        }
-        return 0;
+        return mController.getFullScreenMagnificationController().getScale(displayId);
     }
 
     /**
-     * Returns the magnification center in X coordinate of the controlling magnification mode.
+     * Returns the magnification center in X coordinate of full-screen magnification.
      * If the service can control magnification but fullscreen magnifier is not registered, it will
      * register the magnifier for this call then unregister the magnifier finally to make the
      * magnification center correct.
@@ -162,25 +168,19 @@
      * @return the X coordinate
      */
     public float getCenterX(int displayId, boolean canControlMagnification) {
-        int mode = getControllingMode(displayId);
-        if (mode == MAGNIFICATION_MODE_FULLSCREEN) {
-            boolean registeredJustForThisCall = registerDisplayMagnificationIfNeeded(displayId,
-                    canControlMagnification);
-            try {
-                return mController.getFullScreenMagnificationController().getCenterX(displayId);
-            } finally {
-                if (registeredJustForThisCall) {
-                    unregister(displayId);
-                }
+        boolean registeredJustForThisCall = registerDisplayMagnificationIfNeeded(displayId,
+                canControlMagnification);
+        try {
+            return mController.getFullScreenMagnificationController().getCenterX(displayId);
+        } finally {
+            if (registeredJustForThisCall) {
+                unregister(displayId);
             }
-        } else if (mode == MAGNIFICATION_MODE_WINDOW) {
-            return mController.getWindowMagnificationMgr().getCenterX(displayId);
         }
-        return 0;
     }
 
     /**
-     * Returns the magnification center in Y coordinate of the controlling magnification mode.
+     * Returns the magnification center in Y coordinate of full-screen magnification.
      * If the service can control magnification but fullscreen magnifier is not registered, it will
      * register the magnifier for this call then unregister the magnifier finally to make the
      * magnification center correct.
@@ -190,49 +190,25 @@
      * @return the Y coordinate
      */
     public float getCenterY(int displayId, boolean canControlMagnification) {
-        int mode = getControllingMode(displayId);
-        if (mode == MAGNIFICATION_MODE_FULLSCREEN) {
-            boolean registeredJustForThisCall = registerDisplayMagnificationIfNeeded(displayId,
-                    canControlMagnification);
-            try {
-                return mController.getFullScreenMagnificationController().getCenterY(displayId);
-            } finally {
-                if (registeredJustForThisCall) {
-                    unregister(displayId);
-                }
+        boolean registeredJustForThisCall = registerDisplayMagnificationIfNeeded(displayId,
+                canControlMagnification);
+        try {
+            return mController.getFullScreenMagnificationController().getCenterY(displayId);
+        } finally {
+            if (registeredJustForThisCall) {
+                unregister(displayId);
             }
-        } else if (mode == MAGNIFICATION_MODE_WINDOW) {
-            return mController.getWindowMagnificationMgr().getCenterY(displayId);
         }
-        return 0;
     }
 
     /**
-     * Return the magnification bounds of the current controlling magnification on the given
-     * display. If the magnifier is not enabled, it returns an empty region.
-     * If the service can control magnification but fullscreen magnifier is not registered, it will
-     * register the magnifier for this call then unregister the magnifier finally to make
-     * the magnification region correct.
+     * Returns the magnification bounds of full-screen magnification on the given display.
      *
      * @param displayId The logical display id
      * @param outRegion the region to populate
      * @param canControlMagnification Whether the service can control magnification
-     * @return outRegion the magnification bounds of full-screen magnifier or the magnification
-     * source bounds of window magnifier
      */
-    public Region getMagnificationRegion(int displayId, @NonNull Region outRegion,
-            boolean canControlMagnification) {
-        int mode = getControllingMode(displayId);
-        if (mode == MAGNIFICATION_MODE_FULLSCREEN) {
-            getFullscreenMagnificationRegion(displayId, outRegion, canControlMagnification);
-        } else if (mode == MAGNIFICATION_MODE_WINDOW) {
-            mController.getWindowMagnificationMgr().getMagnificationSourceBounds(displayId,
-                    outRegion);
-        }
-        return outRegion;
-    }
-
-    private void getFullscreenMagnificationRegion(int displayId, @NonNull Region outRegion,
+    public void getFullscreenMagnificationRegion(int displayId, @NonNull Region outRegion,
             boolean canControlMagnification) {
         boolean registeredJustForThisCall = registerDisplayMagnificationIfNeeded(displayId,
                 canControlMagnification);
@@ -246,21 +222,9 @@
         }
     }
 
-    private boolean setScaleAndCenterForFullScreenMagnification(int displayId, float scale,
-            float centerX, float centerY,
-            boolean animate, int id) {
-        if (!isRegistered(displayId)) {
-            register(displayId);
-        }
-        return mController.getFullScreenMagnificationController().setScaleAndCenter(
-                displayId,
-                scale,
-                centerX, centerY, animate, id);
-    }
-
     /**
-     * Resets the magnification on the given display. The reset mode could be full-screen or
-     * window if it is activated.
+     * Resets the current magnification on the given display. The reset mode could be
+     * full-screen or window if it is activated.
      *
      * @param displayId The logical display id.
      * @param animate   {@code true} to animate the transition, {@code false}
@@ -268,7 +232,7 @@
      * @return {@code true} if the magnification spec changed, {@code false} if
      * the spec did not change
      */
-    public boolean reset(int displayId, boolean animate) {
+    public boolean resetCurrentMagnification(int displayId, boolean animate) {
         int mode = getControllingMode(displayId);
         if (mode == MAGNIFICATION_MODE_FULLSCREEN) {
             return mController.getFullScreenMagnificationController().reset(displayId, animate);
@@ -279,6 +243,19 @@
     }
 
     /**
+     * Resets the full-screen magnification on the given display.
+     *
+     * @param displayId The logical display id.
+     * @param animate   {@code true} to animate the transition, {@code false}
+     *                  to transition immediately
+     * @return {@code true} if the magnification spec changed, {@code false} if
+     * the spec did not change
+     */
+    public boolean resetFullscreenMagnification(int displayId, boolean animate) {
+        return mController.getFullScreenMagnificationController().reset(displayId, animate);
+    }
+
+    /**
      * {@link FullScreenMagnificationController#resetIfNeeded(int, boolean)}
      */
     // TODO: support window magnification
diff --git a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
index 0855b9d..0fe90b1 100644
--- a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
@@ -6,7 +6,6 @@
 import static android.os.ParcelFileDescriptor.MODE_TRUNCATE;
 
 import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
-import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP_WAIT;
 
 import android.app.ApplicationThreadConstants;
 import android.app.IBackupAgent;
@@ -22,6 +21,7 @@
 import android.os.SELinux;
 import android.util.Slog;
 
+import com.android.server.backup.OperationStorage.OpType;
 import com.android.server.backup.fullbackup.AppMetadataBackupWriter;
 import com.android.server.backup.remote.ServiceBackupCallback;
 import com.android.server.backup.utils.FullBackupUtils;
@@ -162,7 +162,7 @@
         long kvBackupAgentTimeoutMillis = mAgentTimeoutParameters.getKvBackupAgentTimeoutMillis();
         try {
             mBackupManagerService.prepareOperationTimeout(token, kvBackupAgentTimeoutMillis, null,
-                    OP_TYPE_BACKUP_WAIT);
+                    OpType.BACKUP_WAIT);
 
             IBackupCallback callback =
                     new ServiceBackupCallback(
@@ -262,7 +262,7 @@
             pipes = ParcelFileDescriptor.createPipe();
 
             mBackupManagerService.prepareOperationTimeout(token, kvBackupAgentTimeoutMillis, null,
-                    OP_TYPE_BACKUP_WAIT);
+                    OpType.BACKUP_WAIT);
 
             // We will have to create a runnable that will read the manifest and backup data we
             // created, such that we can pipe the data into mOutput. The reason we do this is that
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 98ea03e..81d6381 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -107,12 +107,14 @@
 import com.android.server.AppWidgetBackupBridge;
 import com.android.server.EventLogTags;
 import com.android.server.LocalServices;
+import com.android.server.backup.OperationStorage.OpState;
+import com.android.server.backup.OperationStorage.OpType;
 import com.android.server.backup.fullbackup.FullBackupEntry;
 import com.android.server.backup.fullbackup.PerformFullTransportBackupTask;
 import com.android.server.backup.internal.BackupHandler;
 import com.android.server.backup.internal.ClearDataObserver;
+import com.android.server.backup.internal.LifecycleOperationStorage;
 import com.android.server.backup.internal.OnTaskFinishedListener;
-import com.android.server.backup.internal.Operation;
 import com.android.server.backup.internal.PerformInitializeTask;
 import com.android.server.backup.internal.RunInitializeReceiver;
 import com.android.server.backup.internal.SetupObserver;
@@ -287,21 +289,6 @@
     private static final String BACKUP_FINISHED_ACTION = "android.intent.action.BACKUP_FINISHED";
     private static final String BACKUP_FINISHED_PACKAGE_EXTRA = "packageName";
 
-    // Bookkeeping of in-flight operations. The operation token is the index of the entry in the
-    // pending operations list.
-    public static final int OP_PENDING = 0;
-    private static final int OP_ACKNOWLEDGED = 1;
-    private static final int OP_TIMEOUT = -1;
-
-    // Waiting for backup agent to respond during backup operation.
-    public static final int OP_TYPE_BACKUP_WAIT = 0;
-
-    // Waiting for backup agent to respond during restore operation.
-    public static final int OP_TYPE_RESTORE_WAIT = 1;
-
-    // An entire backup operation spanning multiple packages.
-    public static final int OP_TYPE_BACKUP = 2;
-
     // Time delay for initialization operations that can be delayed so as not to consume too much
     // CPU on bring-up and increase time-to-UI.
     private static final long INITIALIZATION_DELAY_MILLIS = 3000;
@@ -400,30 +387,8 @@
 
     private ActiveRestoreSession mActiveRestoreSession;
 
-    /**
-     * mCurrentOperations contains the list of currently active operations.
-     *
-     * If type of operation is OP_TYPE_WAIT, it are waiting for an ack or timeout.
-     * An operation wraps a BackupRestoreTask within it.
-     * It's the responsibility of this task to remove the operation from this array.
-     *
-     * A BackupRestore task gets notified of ack/timeout for the operation via
-     * BackupRestoreTask#handleCancel, BackupRestoreTask#operationComplete and notifyAll called
-     * on the mCurrentOpLock.
-     * {@link UserBackupManagerService#waitUntilOperationComplete(int)} is
-     * used in various places to 'wait' for notifyAll and detect change of pending state of an
-     * operation. So typically, an operation will be removed from this array by:
-     * - BackupRestoreTask#handleCancel and
-     * - BackupRestoreTask#operationComplete OR waitUntilOperationComplete. Do not remove at both
-     * these places because waitUntilOperationComplete relies on the operation being present to
-     * determine its completion status.
-     *
-     * If type of operation is OP_BACKUP, it is a task running backups. It provides a handle to
-     * cancel backup tasks.
-     */
-    @GuardedBy("mCurrentOpLock")
-    private final SparseArray<Operation> mCurrentOperations = new SparseArray<>();
-    private final Object mCurrentOpLock = new Object();
+    private final LifecycleOperationStorage mOperationStorage;
+
     private final Random mTokenGenerator = new Random();
     private final AtomicInteger mNextToken = new AtomicInteger();
 
@@ -542,12 +507,14 @@
     }
 
     @VisibleForTesting
-    UserBackupManagerService(Context context, PackageManager packageManager) {
+    UserBackupManagerService(Context context, PackageManager packageManager,
+            LifecycleOperationStorage operationStorage) {
         mContext = context;
 
         mUserId = 0;
         mRegisterTransportsRequestedTime = 0;
         mPackageManager = packageManager;
+        mOperationStorage = operationStorage;
 
         mBaseStateDir = null;
         mDataDir = null;
@@ -600,8 +567,10 @@
                 BackupAgentTimeoutParameters(Handler.getMain(), mContext.getContentResolver());
         mAgentTimeoutParameters.start();
 
+        mOperationStorage = new LifecycleOperationStorage(mUserId);
+
         Objects.requireNonNull(userBackupThread, "userBackupThread cannot be null");
-        mBackupHandler = new BackupHandler(this, userBackupThread);
+        mBackupHandler = new BackupHandler(this, mOperationStorage, userBackupThread);
 
         // Set up our bookkeeping
         final ContentResolver resolver = context.getContentResolver();
@@ -756,6 +725,10 @@
         return mTransportManager;
     }
 
+    public OperationStorage getOperationStorage() {
+        return mOperationStorage;
+    }
+
     public boolean isEnabled() {
         return mEnabled;
     }
@@ -838,14 +811,6 @@
         return mActiveRestoreSession;
     }
 
-    public SparseArray<Operation> getCurrentOperations() {
-        return mCurrentOperations;
-    }
-
-    public Object getCurrentOpLock() {
-        return mCurrentOpLock;
-    }
-
     public SparseArray<AdbParams> getAdbBackupRestoreConfirmations() {
         return mAdbBackupRestoreConfirmations;
     }
@@ -1987,18 +1952,12 @@
         }
         final long oldToken = Binder.clearCallingIdentity();
         try {
-            List<Integer> operationsToCancel = new ArrayList<>();
-            synchronized (mCurrentOpLock) {
-                for (int i = 0; i < mCurrentOperations.size(); i++) {
-                    Operation op = mCurrentOperations.valueAt(i);
-                    int token = mCurrentOperations.keyAt(i);
-                    if (op.type == OP_TYPE_BACKUP) {
-                        operationsToCancel.add(token);
-                    }
-                }
-            }
+            Set<Integer> operationsToCancel =
+                    mOperationStorage.operationTokensForOpType(OpType.BACKUP);
+
             for (Integer token : operationsToCancel) {
-                handleCancel(token, true /* cancelAll */);
+                mOperationStorage.cancelOperation(token, /* cancelAll */ true,
+                        operationType -> { /* no callback needed here */ });
             }
             // We don't want the backup jobs to kick in any time soon.
             // Reschedules them to run in the distant future.
@@ -2012,7 +1971,7 @@
     /** Schedule a timeout message for the operation identified by {@code token}. */
     public void prepareOperationTimeout(int token, long interval, BackupRestoreTask callback,
             int operationType) {
-        if (operationType != OP_TYPE_BACKUP_WAIT && operationType != OP_TYPE_RESTORE_WAIT) {
+        if (operationType != OpType.BACKUP_WAIT && operationType != OpType.RESTORE_WAIT) {
             Slog.wtf(
                     TAG,
                     addUserIdToLogMessage(
@@ -2036,19 +1995,17 @@
                                     + callback));
         }
 
-        synchronized (mCurrentOpLock) {
-            mCurrentOperations.put(token, new Operation(OP_PENDING, callback, operationType));
-            Message msg = mBackupHandler.obtainMessage(getMessageIdForOperationType(operationType),
-                    token, 0, callback);
-            mBackupHandler.sendMessageDelayed(msg, interval);
-        }
+        mOperationStorage.registerOperation(token, OpState.PENDING, callback, operationType);
+        Message msg = mBackupHandler.obtainMessage(getMessageIdForOperationType(operationType),
+                token, 0, callback);
+        mBackupHandler.sendMessageDelayed(msg, interval);
     }
 
     private int getMessageIdForOperationType(int operationType) {
         switch (operationType) {
-            case OP_TYPE_BACKUP_WAIT:
+            case OpType.BACKUP_WAIT:
                 return MSG_BACKUP_OPERATION_TIMEOUT;
-            case OP_TYPE_RESTORE_WAIT:
+            case OpType.RESTORE_WAIT:
                 return MSG_RESTORE_OPERATION_TIMEOUT;
             default:
                 Slog.wtf(
@@ -2061,162 +2018,28 @@
         }
     }
 
-    /**
-     * Add an operation to the list of currently running operations. Used for cancellation,
-     * completion and timeout callbacks that act on the operation via the {@code token}.
-     */
-    public void putOperation(int token, Operation operation) {
-        if (MORE_DEBUG) {
-            Slog.d(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId,
-                            "Adding operation token="
-                                    + Integer.toHexString(token)
-                                    + ", operation type="
-                                    + operation.type));
-        }
-        synchronized (mCurrentOpLock) {
-            mCurrentOperations.put(token, operation);
-        }
-    }
-
-    /**
-     * Remove an operation from the list of currently running operations. An operation is removed
-     * when it is completed, cancelled, or timed out, and thus no longer running.
-     */
-    public void removeOperation(int token) {
-        if (MORE_DEBUG) {
-            Slog.d(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId, "Removing operation token=" + Integer.toHexString(token)));
-        }
-        synchronized (mCurrentOpLock) {
-            if (mCurrentOperations.get(token) == null) {
-                Slog.w(TAG, addUserIdToLogMessage(mUserId, "Duplicate remove for operation. token="
-                        + Integer.toHexString(token)));
-            }
-            mCurrentOperations.remove(token);
-        }
-    }
-
     /** Block until we received an operation complete message (from the agent or cancellation). */
     public boolean waitUntilOperationComplete(int token) {
-        if (MORE_DEBUG) {
-            Slog.i(TAG, addUserIdToLogMessage(mUserId, "Blocking until operation complete for "
-                    + Integer.toHexString(token)));
-        }
-        int finalState = OP_PENDING;
-        Operation op = null;
-        synchronized (mCurrentOpLock) {
-            while (true) {
-                op = mCurrentOperations.get(token);
-                if (op == null) {
-                    // mysterious disappearance: treat as success with no callback
-                    break;
-                } else {
-                    if (op.state == OP_PENDING) {
-                        try {
-                            mCurrentOpLock.wait();
-                        } catch (InterruptedException e) {
-                        }
-                        // When the wait is notified we loop around and recheck the current state
-                    } else {
-                        if (MORE_DEBUG) {
-                            Slog.d(
-                                    TAG,
-                                    addUserIdToLogMessage(
-                                            mUserId,
-                                            "Unblocked waiting for operation token="
-                                                    + Integer.toHexString(token)));
-                        }
-                        // No longer pending; we're done
-                        finalState = op.state;
-                        break;
-                    }
-                }
-            }
-        }
-
-        removeOperation(token);
-        if (op != null) {
-            mBackupHandler.removeMessages(getMessageIdForOperationType(op.type));
-        }
-        if (MORE_DEBUG) {
-            Slog.v(TAG, addUserIdToLogMessage(mUserId, "operation " + Integer.toHexString(token)
-                    + " complete: finalState=" + finalState));
-        }
-        return finalState == OP_ACKNOWLEDGED;
+        return mOperationStorage.waitUntilOperationComplete(token, operationType -> {
+            mBackupHandler.removeMessages(getMessageIdForOperationType(operationType));
+        });
     }
 
     /** Cancel the operation associated with {@code token}. */
     public void handleCancel(int token, boolean cancelAll) {
-        // Notify any synchronous waiters
-        Operation op = null;
-        synchronized (mCurrentOpLock) {
-            op = mCurrentOperations.get(token);
-            if (MORE_DEBUG) {
-                if (op == null) {
-                    Slog.w(
-                            TAG,
-                            addUserIdToLogMessage(
-                                    mUserId,
-                                    "Cancel of token "
-                                            + Integer.toHexString(token)
-                                            + " but no op found"));
-                }
+        // Remove all pending timeout messages of types OpType.BACKUP_WAIT and
+        // OpType.RESTORE_WAIT. On the other hand, OP_TYPE_BACKUP cannot time out and
+        // doesn't require cancellation.
+        mOperationStorage.cancelOperation(token, cancelAll, operationType -> {
+            if (operationType == OpType.BACKUP_WAIT || operationType == OpType.RESTORE_WAIT) {
+                mBackupHandler.removeMessages(getMessageIdForOperationType(operationType));
             }
-            int state = (op != null) ? op.state : OP_TIMEOUT;
-            if (state == OP_ACKNOWLEDGED) {
-                // The operation finished cleanly, so we have nothing more to do.
-                if (DEBUG) {
-                    Slog.w(TAG, addUserIdToLogMessage(mUserId, "Operation already got an ack."
-                            + "Should have been removed from mCurrentOperations."));
-                }
-                op = null;
-                mCurrentOperations.delete(token);
-            } else if (state == OP_PENDING) {
-                if (DEBUG) {
-                    Slog.v(
-                            TAG,
-                            addUserIdToLogMessage(
-                                    mUserId, "Cancel: token=" + Integer.toHexString(token)));
-                }
-                op.state = OP_TIMEOUT;
-                // Can't delete op from mCurrentOperations here. waitUntilOperationComplete may be
-                // called after we receive cancel here. We need this op's state there.
-
-                // Remove all pending timeout messages of types OP_TYPE_BACKUP_WAIT and
-                // OP_TYPE_RESTORE_WAIT. On the other hand, OP_TYPE_BACKUP cannot time out and
-                // doesn't require cancellation.
-                if (op.type == OP_TYPE_BACKUP_WAIT || op.type == OP_TYPE_RESTORE_WAIT) {
-                    mBackupHandler.removeMessages(getMessageIdForOperationType(op.type));
-                }
-            }
-            mCurrentOpLock.notifyAll();
-        }
-
-        // If there's a TimeoutHandler for this event, call it
-        if (op != null && op.callback != null) {
-            if (MORE_DEBUG) {
-                Slog.v(TAG, addUserIdToLogMessage(mUserId, "   Invoking cancel on " + op.callback));
-            }
-            op.callback.handleCancel(cancelAll);
-        }
+        });
     }
 
     /** Returns {@code true} if a backup is currently running, else returns {@code false}. */
     public boolean isBackupOperationInProgress() {
-        synchronized (mCurrentOpLock) {
-            for (int i = 0; i < mCurrentOperations.size(); i++) {
-                Operation op = mCurrentOperations.valueAt(i);
-                if (op.type == OP_TYPE_BACKUP && op.state == OP_PENDING) {
-                    return true;
-                }
-            }
-        }
-        return false;
+        return mOperationStorage.isBackupOperationInProgress();
     }
 
     /** Unbind the backup agent and kill the app if it's a non-system app. */
@@ -2578,6 +2401,7 @@
             String[] pkg = new String[]{entry.packageName};
             mRunningFullBackupTask = PerformFullTransportBackupTask.newWithCurrentTransport(
                     this,
+                    mOperationStorage,
                     /* observer */ null,
                     pkg,
                     /* updateSchedule */ true,
@@ -3107,6 +2931,7 @@
                 CountDownLatch latch = new CountDownLatch(1);
                 Runnable task = PerformFullTransportBackupTask.newWithCurrentTransport(
                         this,
+                        mOperationStorage,
                         /* observer */ null,
                         pkgNames,
                         /* updateSchedule */ false,
@@ -4126,48 +3951,11 @@
      * outstanding asynchronous backup/restore operation.
      */
     public void opComplete(int token, long result) {
-        if (MORE_DEBUG) {
-            Slog.v(
-                    TAG,
-                    addUserIdToLogMessage(
-                            mUserId,
-                            "opComplete: " + Integer.toHexString(token) + " result=" + result));
-        }
-        Operation op = null;
-        synchronized (mCurrentOpLock) {
-            op = mCurrentOperations.get(token);
-            if (op != null) {
-                if (op.state == OP_TIMEOUT) {
-                    // The operation already timed out, and this is a late response.  Tidy up
-                    // and ignore it; we've already dealt with the timeout.
-                    op = null;
-                    mCurrentOperations.delete(token);
-                } else if (op.state == OP_ACKNOWLEDGED) {
-                    if (DEBUG) {
-                        Slog.w(
-                                TAG,
-                                addUserIdToLogMessage(
-                                        mUserId,
-                                        "Received duplicate ack for token="
-                                                + Integer.toHexString(token)));
-                    }
-                    op = null;
-                    mCurrentOperations.remove(token);
-                } else if (op.state == OP_PENDING) {
-                    // Can't delete op from mCurrentOperations. waitUntilOperationComplete can be
-                    // called after we we receive this call.
-                    op.state = OP_ACKNOWLEDGED;
-                }
-            }
-            mCurrentOpLock.notifyAll();
-        }
-
-        // The completion callback, if any, is invoked on the handler
-        if (op != null && op.callback != null) {
-            Pair<BackupRestoreTask, Long> callbackAndResult = Pair.create(op.callback, result);
+        mOperationStorage.onOperationComplete(token, result, callback -> {
+            Pair<BackupRestoreTask, Long> callbackAndResult = Pair.create(callback, result);
             Message msg = mBackupHandler.obtainMessage(MSG_OP_COMPLETE, callbackAndResult);
             mBackupHandler.sendMessage(msg);
-        }
+        });
     }
 
     /** Checks if the package is eligible for backup. */
diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
index fe5497f..1e1ca95 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
@@ -21,7 +21,6 @@
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
-import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP_WAIT;
 import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
 
 import android.annotation.UserIdInt;
@@ -39,6 +38,7 @@
 import com.android.server.AppWidgetBackupBridge;
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.OperationStorage.OpType;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.remote.RemoteCall;
 import com.android.server.backup.utils.BackupEligibilityRules;
@@ -147,7 +147,7 @@
                         mToken,
                         timeout,
                         mTimeoutMonitor /* in parent class */,
-                        OP_TYPE_BACKUP_WAIT);
+                        OpType.BACKUP_WAIT);
                 mAgent.doFullBackup(
                         mPipe,
                         mQuota,
diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java
index aaf1f0a..be6ac26 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java
@@ -18,7 +18,6 @@
 
 import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
-import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP_WAIT;
 
 import android.app.backup.IBackupManager;
 import android.content.ComponentName;
@@ -33,6 +32,7 @@
 
 import com.android.internal.backup.IObbBackupService;
 import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.OperationStorage.OpType;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.utils.FullBackupUtils;
 
@@ -83,7 +83,7 @@
             long fullBackupAgentTimeoutMillis =
                     mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
             backupManagerService.prepareOperationTimeout(
-                    token, fullBackupAgentTimeoutMillis, null, OP_TYPE_BACKUP_WAIT);
+                    token, fullBackupAgentTimeoutMillis, null, OpType.BACKUP_WAIT);
             mService.backupObbs(pkg.packageName, pipes[1], token,
                     backupManagerService.getBackupManagerBinder());
             FullBackupUtils.routeSocketDataToOutput(pipes[0], out);
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
index 448e086..7ee307e 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
@@ -37,6 +37,7 @@
 import com.android.server.AppWidgetBackupBridge;
 import com.android.server.backup.BackupRestoreTask;
 import com.android.server.backup.KeyValueAdbBackupEngine;
+import com.android.server.backup.OperationStorage;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.utils.BackupEligibilityRules;
 import com.android.server.backup.utils.PasswordUtils;
@@ -67,6 +68,7 @@
 public class PerformAdbBackupTask extends FullBackupTask implements BackupRestoreTask {
 
     private final UserBackupManagerService mUserBackupManagerService;
+    private final OperationStorage mOperationStorage;
     private final AtomicBoolean mLatch;
 
     private final ParcelFileDescriptor mOutputFile;
@@ -85,7 +87,8 @@
     private final int mCurrentOpToken;
     private final BackupEligibilityRules mBackupEligibilityRules;
 
-    public PerformAdbBackupTask(UserBackupManagerService backupManagerService,
+    public PerformAdbBackupTask(
+            UserBackupManagerService backupManagerService, OperationStorage operationStorage,
             ParcelFileDescriptor fd, IFullBackupRestoreObserver observer,
             boolean includeApks, boolean includeObbs, boolean includeShared, boolean doWidgets,
             String curPassword, String encryptPassword, boolean doAllApps, boolean doSystem,
@@ -93,6 +96,7 @@
             BackupEligibilityRules backupEligibilityRules) {
         super(observer);
         mUserBackupManagerService = backupManagerService;
+        mOperationStorage = operationStorage;
         mCurrentOpToken = backupManagerService.generateRandomIntegerToken();
         mLatch = latch;
 
@@ -505,6 +509,6 @@
         if (target != null) {
             mUserBackupManagerService.tearDownAgentAndKill(mCurrentTarget.applicationInfo);
         }
-        mUserBackupManagerService.removeOperation(mCurrentOpToken);
+        mOperationStorage.removeOperation(mCurrentOpToken);
     }
 }
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index 9ce4eab..0ca77d1 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -19,9 +19,6 @@
 import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
 import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
-import static com.android.server.backup.UserBackupManagerService.OP_PENDING;
-import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP;
-import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP_WAIT;
 
 import android.annotation.Nullable;
 import android.app.IBackupAgent;
@@ -45,10 +42,12 @@
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupRestoreTask;
 import com.android.server.backup.FullBackupJob;
+import com.android.server.backup.OperationStorage;
+import com.android.server.backup.OperationStorage.OpState;
+import com.android.server.backup.OperationStorage.OpType;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.internal.OnTaskFinishedListener;
-import com.android.server.backup.internal.Operation;
 import com.android.server.backup.remote.RemoteCall;
 import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
@@ -99,6 +98,7 @@
 public class PerformFullTransportBackupTask extends FullBackupTask implements BackupRestoreTask {
     public static PerformFullTransportBackupTask newWithCurrentTransport(
             UserBackupManagerService backupManagerService,
+            OperationStorage operationStorage,
             IFullBackupRestoreObserver observer,
             String[] whichPackages,
             boolean updateSchedule,
@@ -118,6 +118,7 @@
                                 listenerCaller);
         return new PerformFullTransportBackupTask(
                 backupManagerService,
+                operationStorage,
                 transportConnection,
                 observer,
                 whichPackages,
@@ -136,6 +137,7 @@
     private UserBackupManagerService mUserBackupManagerService;
     private final Object mCancelLock = new Object();
 
+    OperationStorage mOperationStorage;
     List<PackageInfo> mPackages;
     PackageInfo mCurrentPackage;
     boolean mUpdateSchedule;
@@ -158,6 +160,7 @@
     private final BackupEligibilityRules mBackupEligibilityRules;
 
     public PerformFullTransportBackupTask(UserBackupManagerService backupManagerService,
+            OperationStorage operationStorage,
             TransportConnection transportConnection,
             IFullBackupRestoreObserver observer,
             String[] whichPackages, boolean updateSchedule,
@@ -165,7 +168,8 @@
             @Nullable IBackupManagerMonitor monitor, @Nullable OnTaskFinishedListener listener,
             boolean userInitiated, BackupEligibilityRules backupEligibilityRules) {
         super(observer);
-        this.mUserBackupManagerService = backupManagerService;
+        mUserBackupManagerService = backupManagerService;
+        mOperationStorage = operationStorage;
         mTransportConnection = transportConnection;
         mUpdateSchedule = updateSchedule;
         mLatch = latch;
@@ -261,16 +265,13 @@
     }
 
     private void registerTask() {
-        synchronized (mUserBackupManagerService.getCurrentOpLock()) {
-            Slog.d(TAG, "backupmanager pftbt token=" + Integer.toHexString(mCurrentOpToken));
-            mUserBackupManagerService.getCurrentOperations().put(
-                    mCurrentOpToken,
-                    new Operation(OP_PENDING, this, OP_TYPE_BACKUP));
-        }
+        Slog.d(TAG, "backupmanager pftbt token=" + Integer.toHexString(mCurrentOpToken));
+        mOperationStorage.registerOperation(mCurrentOpToken, OpState.PENDING, this, OpType.BACKUP);
     }
 
+    // public, because called from KeyValueBackupTask.finishTask.
     public void unregisterTask() {
-        mUserBackupManagerService.removeOperation(mCurrentOpToken);
+        mOperationStorage.removeOperation(mCurrentOpToken);
     }
 
     @Override
@@ -722,7 +723,7 @@
                     mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
             try {
                 mUserBackupManagerService.prepareOperationTimeout(
-                        mCurrentOpToken, fullBackupAgentTimeoutMillis, this, OP_TYPE_BACKUP_WAIT);
+                        mCurrentOpToken, fullBackupAgentTimeoutMillis, this, OpType.BACKUP_WAIT);
                 if (MORE_DEBUG) {
                     Slog.d(TAG, "Preflighting full payload of " + pkg.packageName);
                 }
@@ -777,7 +778,7 @@
             }
             mResult.set(result);
             mLatch.countDown();
-            mUserBackupManagerService.removeOperation(mCurrentOpToken);
+            mOperationStorage.removeOperation(mCurrentOpToken);
         }
 
         @Override
@@ -787,7 +788,7 @@
             }
             mResult.set(BackupTransport.AGENT_ERROR);
             mLatch.countDown();
-            mUserBackupManagerService.removeOperation(mCurrentOpToken);
+            mOperationStorage.removeOperation(mCurrentOpToken);
         }
 
         @Override
@@ -837,16 +838,12 @@
         }
 
         void registerTask() {
-            synchronized (mUserBackupManagerService.getCurrentOpLock()) {
-                mUserBackupManagerService.getCurrentOperations().put(
-                        mCurrentOpToken, new Operation(OP_PENDING, this, OP_TYPE_BACKUP_WAIT));
-            }
+            mOperationStorage.registerOperation(mCurrentOpToken,
+                    OpState.PENDING, this, OpType.BACKUP_WAIT);
         }
 
         void unregisterTask() {
-            synchronized (mUserBackupManagerService.getCurrentOpLock()) {
-                mUserBackupManagerService.getCurrentOperations().remove(mCurrentOpToken);
-            }
+            mOperationStorage.removeOperation(mCurrentOpToken);
         }
 
         @Override
@@ -956,7 +953,7 @@
             mPreflightLatch.countDown();
             mBackupLatch.countDown();
             // We are done with this operation.
-            mUserBackupManagerService.removeOperation(mCurrentOpToken);
+            mOperationStorage.removeOperation(mCurrentOpToken);
         }
     }
 }
diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
index 5c24859..03796ea 100644
--- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java
+++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
@@ -35,6 +35,7 @@
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupRestoreTask;
 import com.android.server.backup.DataChangedJournal;
+import com.android.server.backup.OperationStorage;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.fullbackup.PerformAdbBackupTask;
@@ -84,6 +85,7 @@
     public static final int MSG_STOP = 22;
 
     private final UserBackupManagerService backupManagerService;
+    private final OperationStorage mOperationStorage;
     private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
 
     private final HandlerThread mBackupThread;
@@ -92,10 +94,12 @@
     volatile boolean mIsStopping = false;
 
     public BackupHandler(
-            UserBackupManagerService backupManagerService, HandlerThread backupThread) {
+            UserBackupManagerService backupManagerService, OperationStorage operationStorage,
+            HandlerThread backupThread) {
         super(backupThread.getLooper());
         mBackupThread = backupThread;
         this.backupManagerService = backupManagerService;
+        mOperationStorage = operationStorage;
         mAgentTimeoutParameters = Objects.requireNonNull(
                 backupManagerService.getAgentTimeoutParameters(),
                 "Timeout parameters cannot be null");
@@ -215,6 +219,7 @@
                                                         caller);
                         KeyValueBackupTask.start(
                                 backupManagerService,
+                                mOperationStorage,
                                 transportConnection,
                                 transport.transportDirName(),
                                 queue,
@@ -278,8 +283,8 @@
                 // TODO: refactor full backup to be a looper-based state machine
                 // similar to normal backup/restore.
                 AdbBackupParams params = (AdbBackupParams) msg.obj;
-                PerformAdbBackupTask task = new PerformAdbBackupTask(backupManagerService,
-                        params.fd,
+                PerformAdbBackupTask task = new PerformAdbBackupTask(
+                        backupManagerService, mOperationStorage, params.fd,
                         params.observer, params.includeApks, params.includeObbs,
                         params.includeShared, params.doWidgets, params.curPassword,
                         params.encryptPassword, params.allApps, params.includeSystem,
@@ -296,6 +301,7 @@
                 PerformUnifiedRestoreTask task =
                         new PerformUnifiedRestoreTask(
                                 backupManagerService,
+                                mOperationStorage,
                                 params.mTransportConnection,
                                 params.observer,
                                 params.monitor,
@@ -332,7 +338,7 @@
                 // similar to normal backup/restore.
                 AdbRestoreParams params = (AdbRestoreParams) msg.obj;
                 PerformAdbRestoreTask task = new PerformAdbRestoreTask(backupManagerService,
-                        params.fd,
+                        mOperationStorage, params.fd,
                         params.curPassword, params.encryptPassword,
                         params.observer, params.latch);
                 (new Thread(task, "adb-restore")).start();
@@ -459,6 +465,7 @@
 
                 KeyValueBackupTask.start(
                         backupManagerService,
+                        mOperationStorage,
                         params.mTransportConnection,
                         params.dirName,
                         params.kvPackages,
diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
index 30da8c1..16aa4eb 100644
--- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
+++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
@@ -23,8 +23,6 @@
 import static android.os.ParcelFileDescriptor.MODE_TRUNCATE;
 
 import static com.android.server.backup.UserBackupManagerService.KEY_WIDGET_STATE;
-import static com.android.server.backup.UserBackupManagerService.OP_PENDING;
-import static com.android.server.backup.UserBackupManagerService.OP_TYPE_BACKUP;
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
@@ -57,10 +55,12 @@
 import com.android.server.backup.BackupRestoreTask;
 import com.android.server.backup.DataChangedJournal;
 import com.android.server.backup.KeyValueBackupJob;
+import com.android.server.backup.OperationStorage;
+import com.android.server.backup.OperationStorage.OpState;
+import com.android.server.backup.OperationStorage.OpType;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.fullbackup.PerformFullTransportBackupTask;
 import com.android.server.backup.internal.OnTaskFinishedListener;
-import com.android.server.backup.internal.Operation;
 import com.android.server.backup.remote.RemoteCall;
 import com.android.server.backup.remote.RemoteCallable;
 import com.android.server.backup.remote.RemoteResult;
@@ -211,6 +211,7 @@
      */
     public static KeyValueBackupTask start(
             UserBackupManagerService backupManagerService,
+            OperationStorage operationStorage,
             TransportConnection transportConnection,
             String transportDirName,
             List<String> queue,
@@ -227,6 +228,7 @@
         KeyValueBackupTask task =
                 new KeyValueBackupTask(
                         backupManagerService,
+                        operationStorage,
                         transportConnection,
                         transportDirName,
                         queue,
@@ -244,6 +246,7 @@
     }
 
     private final UserBackupManagerService mBackupManagerService;
+    private final OperationStorage mOperationStorage;
     private final PackageManager mPackageManager;
     private final TransportConnection mTransportConnection;
     private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
@@ -302,6 +305,7 @@
     @VisibleForTesting
     public KeyValueBackupTask(
             UserBackupManagerService backupManagerService,
+            OperationStorage operationStorage,
             TransportConnection transportConnection,
             String transportDirName,
             List<String> queue,
@@ -313,6 +317,7 @@
             boolean nonIncremental,
             BackupEligibilityRules backupEligibilityRules) {
         mBackupManagerService = backupManagerService;
+        mOperationStorage = operationStorage;
         mPackageManager = backupManagerService.getPackageManager();
         mTransportConnection = transportConnection;
         mOriginalQueue = queue;
@@ -338,12 +343,11 @@
     }
 
     private void registerTask() {
-        mBackupManagerService.putOperation(
-                mCurrentOpToken, new Operation(OP_PENDING, this, OP_TYPE_BACKUP));
+        mOperationStorage.registerOperation(mCurrentOpToken, OpState.PENDING, this, OpType.BACKUP);
     }
 
     private void unregisterTask() {
-        mBackupManagerService.removeOperation(mCurrentOpToken);
+        mOperationStorage.removeOperation(mCurrentOpToken);
     }
 
     @Override
@@ -639,6 +643,7 @@
     private PerformFullTransportBackupTask createFullBackupTask(List<String> packages) {
         return new PerformFullTransportBackupTask(
                 mBackupManagerService,
+                mOperationStorage,
                 mTransportConnection,
                 /* fullBackupRestoreObserver */ null,
                 packages.toArray(new String[packages.size()]),
diff --git a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
index 376b618..cfc0f20 100644
--- a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
+++ b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
@@ -23,6 +23,7 @@
 
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.OperationStorage;
 import com.android.server.backup.UserBackupManagerService;
 
 import java.util.Objects;
@@ -36,13 +37,16 @@
 
     private static final String TAG = "AdbRestoreFinishedLatch";
     private UserBackupManagerService backupManagerService;
+    private final OperationStorage mOperationStorage;
     final CountDownLatch mLatch;
     private final int mCurrentOpToken;
     private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
 
     public AdbRestoreFinishedLatch(UserBackupManagerService backupManagerService,
+            OperationStorage operationStorage,
             int currentOpToken) {
         this.backupManagerService = backupManagerService;
+        mOperationStorage = operationStorage;
         mLatch = new CountDownLatch(1);
         mCurrentOpToken = currentOpToken;
         mAgentTimeoutParameters = Objects.requireNonNull(
@@ -72,7 +76,7 @@
             Slog.w(TAG, "adb onRestoreFinished() complete");
         }
         mLatch.countDown();
-        backupManagerService.removeOperation(mCurrentOpToken);
+        mOperationStorage.removeOperation(mCurrentOpToken);
     }
 
     @Override
@@ -81,6 +85,6 @@
             Slog.w(TAG, "adb onRestoreFinished() timed out");
         }
         mLatch.countDown();
-        backupManagerService.removeOperation(mCurrentOpToken);
+        mOperationStorage.removeOperation(mCurrentOpToken);
     }
 }
diff --git a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
index 5718bdf..76df8b9 100644
--- a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
+++ b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
@@ -21,7 +21,6 @@
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
 import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
-import static com.android.server.backup.UserBackupManagerService.OP_TYPE_RESTORE_WAIT;
 import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
 import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERATION_TIMEOUT;
 
@@ -48,6 +47,8 @@
 import com.android.server.backup.BackupRestoreTask;
 import com.android.server.backup.FileMetadata;
 import com.android.server.backup.KeyValueAdbRestoreEngine;
+import com.android.server.backup.OperationStorage;
+import com.android.server.backup.OperationStorage.OpType;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.fullbackup.FullBackupObbConnection;
 import com.android.server.backup.utils.BackupEligibilityRules;
@@ -71,6 +72,7 @@
 public class FullRestoreEngine extends RestoreEngine {
 
     private final UserBackupManagerService mBackupManagerService;
+    private final OperationStorage mOperationStorage;
     private final int mUserId;
 
     // Task in charge of monitoring timeouts
@@ -133,12 +135,14 @@
     private boolean mPipesClosed;
     private final BackupEligibilityRules mBackupEligibilityRules;
 
-    public FullRestoreEngine(UserBackupManagerService backupManagerService,
+    public FullRestoreEngine(
+            UserBackupManagerService backupManagerService, OperationStorage operationStorage,
             BackupRestoreTask monitorTask, IFullBackupRestoreObserver observer,
             IBackupManagerMonitor monitor, PackageInfo onlyPackage, boolean allowApks,
             int ephemeralOpToken, boolean isAdbRestore,
             BackupEligibilityRules backupEligibilityRules) {
         mBackupManagerService = backupManagerService;
+        mOperationStorage = operationStorage;
         mEphemeralOpToken = ephemeralOpToken;
         mMonitorTask = monitorTask;
         mObserver = observer;
@@ -409,7 +413,7 @@
                             mBackupManagerService.prepareOperationTimeout(token,
                                     timeout,
                                     mMonitorTask,
-                                    OP_TYPE_RESTORE_WAIT);
+                                    OpType.RESTORE_WAIT);
 
                             if (FullBackup.OBB_TREE_TOKEN.equals(info.domain)) {
                                 if (DEBUG) {
@@ -603,9 +607,9 @@
                     long fullBackupAgentTimeoutMillis =
                             mAgentTimeoutParameters.getFullBackupAgentTimeoutMillis();
                     final AdbRestoreFinishedLatch latch = new AdbRestoreFinishedLatch(
-                            mBackupManagerService, token);
+                            mBackupManagerService, mOperationStorage, token);
                     mBackupManagerService.prepareOperationTimeout(
-                            token, fullBackupAgentTimeoutMillis, latch, OP_TYPE_RESTORE_WAIT);
+                            token, fullBackupAgentTimeoutMillis, latch, OpType.RESTORE_WAIT);
                     if (mTargetApp.processName.equals("system")) {
                         if (MORE_DEBUG) {
                             Slog.d(TAG, "system agent - restoreFinished on thread");
diff --git a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
index e03150e..22af19e 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
@@ -32,6 +32,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.LocalServices;
+import com.android.server.backup.OperationStorage;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.fullbackup.FullBackupObbConnection;
 import com.android.server.backup.utils.BackupEligibilityRules;
@@ -60,6 +61,7 @@
 public class PerformAdbRestoreTask implements Runnable {
 
     private final UserBackupManagerService mBackupManagerService;
+    private final OperationStorage mOperationStorage;
     private final ParcelFileDescriptor mInputFile;
     private final String mCurrentPassword;
     private final String mDecryptPassword;
@@ -68,10 +70,12 @@
 
     private IFullBackupRestoreObserver mObserver;
 
-    public PerformAdbRestoreTask(UserBackupManagerService backupManagerService,
+    public PerformAdbRestoreTask(
+            UserBackupManagerService backupManagerService, OperationStorage operationStorage,
             ParcelFileDescriptor fd, String curPassword, String decryptPassword,
             IFullBackupRestoreObserver observer, AtomicBoolean latch) {
         this.mBackupManagerService = backupManagerService;
+        mOperationStorage = operationStorage;
         mInputFile = fd;
         mCurrentPassword = curPassword;
         mDecryptPassword = decryptPassword;
@@ -109,9 +113,9 @@
                     mBackupManagerService.getPackageManager(),
                     LocalServices.getService(PackageManagerInternal.class),
                     mBackupManagerService.getUserId(), BackupManager.OperationType.ADB_BACKUP);
-            FullRestoreEngine mEngine = new FullRestoreEngine(mBackupManagerService, null,
-                    mObserver, null, null, true, 0 /*unused*/, true,
-                    eligibilityRules);
+            FullRestoreEngine mEngine = new FullRestoreEngine(mBackupManagerService,
+                    mOperationStorage, null, mObserver, null, null,
+                    true, 0 /*unused*/, true, eligibilityRules);
             FullRestoreEngineThread mEngineThread = new FullRestoreEngineThread(mEngine,
                     tarInputStream);
             mEngineThread.run();
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index ac831af..b48367d 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -20,7 +20,6 @@
 import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 import static com.android.server.backup.UserBackupManagerService.KEY_WIDGET_STATE;
-import static com.android.server.backup.UserBackupManagerService.OP_TYPE_RESTORE_WAIT;
 import static com.android.server.backup.UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
 import static com.android.server.backup.UserBackupManagerService.SETTINGS_PACKAGE;
 import static com.android.server.backup.internal.BackupHandler.MSG_BACKUP_RESTORE_STEP;
@@ -60,6 +59,8 @@
 import com.android.server.backup.BackupAgentTimeoutParameters;
 import com.android.server.backup.BackupRestoreTask;
 import com.android.server.backup.BackupUtils;
+import com.android.server.backup.OperationStorage;
+import com.android.server.backup.OperationStorage.OpType;
 import com.android.server.backup.PackageManagerBackupAgent;
 import com.android.server.backup.PackageManagerBackupAgent.Metadata;
 import com.android.server.backup.TransportManager;
@@ -84,6 +85,7 @@
 public class PerformUnifiedRestoreTask implements BackupRestoreTask {
 
     private UserBackupManagerService backupManagerService;
+    private final OperationStorage mOperationStorage;
     private final int mUserId;
     private final TransportManager mTransportManager;
     // Transport client we're working with to do the restore
@@ -169,6 +171,7 @@
     PerformUnifiedRestoreTask(UserBackupManagerService backupManagerService) {
         mListener = null;
         mAgentTimeoutParameters = null;
+        mOperationStorage = null;
         mTransportConnection = null;
         mTransportManager = null;
         mEphemeralOpToken = 0;
@@ -181,6 +184,7 @@
     // about releasing it.
     public PerformUnifiedRestoreTask(
             UserBackupManagerService backupManagerService,
+            OperationStorage operationStorage,
             TransportConnection transportConnection,
             IRestoreObserver observer,
             IBackupManagerMonitor monitor,
@@ -192,6 +196,7 @@
             OnTaskFinishedListener listener,
             BackupEligibilityRules backupEligibilityRules) {
         this.backupManagerService = backupManagerService;
+        mOperationStorage = operationStorage;
         mUserId = backupManagerService.getUserId();
         mTransportManager = backupManagerService.getTransportManager();
         mEphemeralOpToken = backupManagerService.generateRandomIntegerToken();
@@ -767,7 +772,7 @@
             long restoreAgentTimeoutMillis = mAgentTimeoutParameters.getRestoreAgentTimeoutMillis(
                     app.applicationInfo.uid);
             backupManagerService.prepareOperationTimeout(
-                    mEphemeralOpToken, restoreAgentTimeoutMillis, this, OP_TYPE_RESTORE_WAIT);
+                    mEphemeralOpToken, restoreAgentTimeoutMillis, this, OpType.RESTORE_WAIT);
             startedAgentRestore = true;
             mAgent.doRestoreWithExcludedKeys(mBackupData, appVersionCode, mNewState,
                     mEphemeralOpToken, backupManagerService.getBackupManagerBinder(),
@@ -877,7 +882,7 @@
             backupManagerService
                     .prepareOperationTimeout(mEphemeralOpToken,
                             restoreAgentFinishedTimeoutMillis, this,
-                            OP_TYPE_RESTORE_WAIT);
+                            OpType.RESTORE_WAIT);
             mAgent.doRestoreFinished(mEphemeralOpToken,
                     backupManagerService.getBackupManagerBinder());
             // If we get this far, the callback or timeout will schedule the
@@ -921,7 +926,7 @@
             EventLog.writeEvent(EventLogTags.FULL_RESTORE_PACKAGE,
                     mCurrentPackage.packageName);
 
-            mEngine = new FullRestoreEngine(backupManagerService, this, null,
+            mEngine = new FullRestoreEngine(backupManagerService, mOperationStorage, this, null,
                     mMonitor, mCurrentPackage, false, mEphemeralOpToken, false,
                     mBackupEligibilityRules);
             mEngineThread = new FullRestoreEngineThread(mEngine, mEnginePipes[0]);
@@ -1071,7 +1076,7 @@
         // The app has timed out handling a restoring file
         @Override
         public void handleCancel(boolean cancelAll) {
-            backupManagerService.removeOperation(mEphemeralOpToken);
+            mOperationStorage.removeOperation(mEphemeralOpToken);
             if (DEBUG) {
                 Slog.w(TAG, "Full-data restore target timed out; shutting down");
             }
@@ -1268,7 +1273,7 @@
 
     @Override
     public void operationComplete(long unusedResult) {
-        backupManagerService.removeOperation(mEphemeralOpToken);
+        mOperationStorage.removeOperation(mEphemeralOpToken);
         if (MORE_DEBUG) {
             Slog.i(TAG, "operationComplete() during restore: target="
                     + mCurrentPackage.packageName
@@ -1331,7 +1336,7 @@
     // A call to agent.doRestore() or agent.doRestoreFinished() has timed out
     @Override
     public void handleCancel(boolean cancelAll) {
-        backupManagerService.removeOperation(mEphemeralOpToken);
+        mOperationStorage.removeOperation(mEphemeralOpToken);
         Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName);
         mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
                 BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_RESTORE_TIMEOUT,
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index bcc345f..637994f 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -16,8 +16,13 @@
 
 package com.android.server.companion;
 
+import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+import static android.app.PendingIntent.FLAG_ONE_SHOT;
+import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME;
+import static android.content.ComponentName.createRelative;
+
 import static com.android.internal.util.CollectionUtils.filter;
-import static com.android.internal.util.FunctionalUtils.uncheckExceptions;
 import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
 import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG;
 import static com.android.server.companion.PermissionsUtils.enforcePermissionsForAssociation;
@@ -28,73 +33,114 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.app.PendingIntent;
 import android.companion.AssociationInfo;
 import android.companion.AssociationRequest;
-import android.companion.CompanionDeviceManager;
 import android.companion.IAssociationRequestCallback;
-import android.companion.ICompanionDeviceDiscoveryService;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.Signature;
+import android.net.MacAddress;
 import android.os.Binder;
-import android.os.IBinder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.util.PackageUtils;
 import android.util.Slog;
 
-import com.android.internal.infra.AndroidFuture;
-import com.android.internal.infra.PerUser;
-import com.android.internal.infra.ServiceConnector;
 import com.android.internal.util.ArrayUtils;
-import com.android.server.FgThread;
 
-import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.HashSet;
-import java.util.Objects;
 import java.util.Set;
 
+/**
+ * Class responsible for handling incoming {@link AssociationRequest}s.
+ * The main responsibilities of an {@link AssociationRequestsProcessor} are:
+ * <ul>
+ * <li> Requests validation and checking if the package that would own the association holds all
+ * necessary permissions.
+ * <li> Communication with the requester via a provided
+ * {@link android.companion.CompanionDeviceManager.Callback}.
+ * <li> Constructing an {@link Intent} for collecting user's approval (if needed), and handling the
+ * approval.
+ * <li> Calling to {@link CompanionDeviceManagerService} to create an association when/if the
+ * request was found valid and was approved by user.
+ * </ul>
+ *
+ * The class supports two variants of the "Association Flow": the full variant, and the shortened
+ * (a.k.a. No-UI) variant.
+ * Both flows start similarly: in
+ * {@link #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)}
+ * invoked from
+ * {@link CompanionDeviceManagerService.CompanionDeviceManagerImpl#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
+ * method call.
+ * Then an {@link AssociationRequestsProcessor} makes a decision whether user's confirmation is
+ * required.
+ *
+ * If the user's approval is NOT required: an {@link AssociationRequestsProcessor} invokes
+ * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}
+ * which after calling to  {@link CompanionDeviceManagerService} to create an association, notifies
+ * the requester via
+ * {@link android.companion.CompanionDeviceManager.Callback#onAssociationCreated(AssociationInfo)}.
+ *
+ * If the user's approval is required: an {@link AssociationRequestsProcessor} constructs a
+ * {@link PendingIntent} for the approval UI and sends it back to the requester via
+ * {@link android.companion.CompanionDeviceManager.Callback#onAssociationPending(IntentSender)}.
+ * When/if user approves the request,  {@link AssociationRequestsProcessor} receives a "callback"
+ * from the Approval UI in via {@link #mOnRequestConfirmationReceiver} and invokes
+ * {@link #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback, ResultReceiver, MacAddress)}
+ * which one more time checks that the packages holds all necessary permissions before proceeding to
+ * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}.
+ *
+ * @see #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)
+ * @see #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback,
+ * ResultReceiver, MacAddress)
+ */
 class AssociationRequestsProcessor {
     private static final String TAG = LOG_TAG + ".AssociationRequestsProcessor";
 
-    private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
-            CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
-            ".CompanionDeviceDiscoveryService");
+    private static final ComponentName ASSOCIATION_REQUEST_APPROVAL_ACTIVITY =
+            createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, ".CompanionDeviceActivity");
+
+    // AssociationRequestsProcessor <-> UI
+    private static final String EXTRA_APPLICATION_CALLBACK = "application_callback";
+    private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
+    private static final String EXTRA_RESULT_RECEIVER = "result_receiver";
+
+    // AssociationRequestsProcessor -> UI
+    private static final int RESULT_CODE_ASSOCIATION_CREATED = 0;
+    private static final String EXTRA_ASSOCIATION = "association";
+
+    // UI -> AssociationRequestsProcessor
+    private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0;
+    private static final String EXTRA_MAC_ADDRESS = "mac_address";
 
     private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
     private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;
 
     private final Context mContext;
     private final CompanionDeviceManagerService mService;
-    private final PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>> mServiceConnectors;
-
-    private AssociationRequest mRequest;
-    private IAssociationRequestCallback mAppCallback;
-    private AndroidFuture<?> mOngoingDeviceDiscovery;
+    private final PackageManagerInternal mPackageManager;
 
     AssociationRequestsProcessor(CompanionDeviceManagerService service) {
         mContext = service.getContext();
         mService = service;
-
-        final Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO);
-        mServiceConnectors = new PerUser<>() {
-            @Override
-            protected ServiceConnector<ICompanionDeviceDiscoveryService> create(int userId) {
-                return new ServiceConnector.Impl<>(
-                    mContext,
-                    serviceIntent, 0/* bindingFlags */, userId,
-                    ICompanionDeviceDiscoveryService.Stub::asInterface);
-            }
-        };
+        mPackageManager = service.mPackageManagerInternal;
     }
 
     /**
      * Handle incoming {@link AssociationRequest}s, sent via
      * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
      */
-    void process(@NonNull AssociationRequest request, @NonNull String packageName,
-            @UserIdInt int userId, @NonNull IAssociationRequestCallback callback) {
+    void processNewAssociationRequest(@NonNull AssociationRequest request,
+            @NonNull String packageName, @UserIdInt int userId,
+            @NonNull IAssociationRequestCallback callback) {
         requireNonNull(request, "Request MUST NOT be null");
         if (request.isSelfManaged()) {
             requireNonNull(request.getDisplayName(), "AssociationRequest.displayName "
@@ -103,14 +149,15 @@
         requireNonNull(packageName, "Package name MUST NOT be null");
         requireNonNull(callback, "Callback MUST NOT be null");
 
+        final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
         if (DEBUG) {
-            Slog.d(TAG, "process() "
+            Slog.d(TAG, "processNewAssociationRequest() "
                     + "request=" + request + ", "
-                    + "package=u" + userId + "/" + packageName);
+                    + "package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")");
         }
 
         // 1. Enforce permissions and other requirements.
-        enforcePermissionsForAssociation(mContext, request, packageName, userId);
+        enforcePermissionsForAssociation(mContext, request, packageUid);
         mService.checkUsesFeature(packageName, userId);
 
         // 2. Check if association can be created without launching UI (i.e. CDM needs NEITHER
@@ -118,71 +165,99 @@
         if (request.isSelfManaged() && !request.isForceConfirmation()
                 && !willAddRoleHolder(request, packageName, userId)) {
             // 2a. Create association right away.
-            final AssociationInfo association = mService.createAssociation(userId, packageName,
-                /* macAddress */ null, request.getDisplayName(), request.getDeviceProfile(),
-                /* selfManaged */true);
-            withCatchingRemoteException(() -> callback.onAssociationCreated(association));
+            createAssociationAndNotifyApplication(request, packageName, userId,
+                    /*macAddress*/ null, callback);
             return;
         }
 
-        // 2b. Launch the UI.
-        synchronized (mService.mLock) {
-            if (mRequest != null) {
-                Slog.w(TAG, "CDM is already processing another AssociationRequest.");
+        // 2b. Build a PendingIntent for launching the confirmation UI, and send it back to the app:
 
-                withCatchingRemoteException(() -> callback.onFailure("Busy."));
-            }
+        // 2b.1. Populate the request with required info.
+        request.setPackageName(packageName);
+        request.setUserId(userId);
+        request.setSkipPrompt(mayAssociateWithoutPrompt(request, packageName, userId));
 
-            final boolean linked = withCatchingRemoteException(
-                    () -> callback.asBinder().linkToDeath(mBinderDeathRecipient, 0));
-            if (!linked) {
-                // The process has died by now: do not proceed.
-                return;
-            }
+        // 2b.2. Prepare extras and create an Intent.
+        final Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_ASSOCIATION_REQUEST, request);
+        extras.putBinder(EXTRA_APPLICATION_CALLBACK, callback.asBinder());
+        extras.putParcelable(EXTRA_RESULT_RECEIVER, prepareForIpc(mOnRequestConfirmationReceiver));
 
-            mRequest = request;
+        final Intent intent = new Intent();
+        intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY);
+        intent.putExtras(extras);
+
+        // 2b.3. Create a PendingIntent.
+        final PendingIntent pendingIntent;
+        final long token = Binder.clearCallingIdentity();
+        try {
+            // Using uid of the application that will own the association (usually the same
+            // application that sent the request) allows us to have multiple "pending" association
+            // requests at the same time.
+            // If the application already has a pending association request, that PendingIntent
+            // will be cancelled.
+            pendingIntent = PendingIntent.getActivity(mContext, /*requestCode */ packageUid, intent,
+                    FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE);
+        } finally {
+            Binder.restoreCallingIdentity(token);
         }
 
-        mAppCallback = callback;
-        request.setCallingPackage(packageName);
+        // 2b.4. Send the PendingIntent back to the app.
+        try {
+            callback.onAssociationPending(pendingIntent);
+        } catch (RemoteException ignore) { }
+    }
 
-        if (mayAssociateWithoutPrompt(packageName, userId)) {
-            Slog.i(TAG, "setSkipPrompt(true)");
-            request.setSkipPrompt(true);
+    private void processAssociationRequestApproval(@NonNull AssociationRequest request,
+            @NonNull IAssociationRequestCallback callback,
+            @NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress) {
+        final String packageName = request.getPackageName();
+        final int userId = request.getUserId();
+        final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
+
+        if (DEBUG) {
+            Slog.d(TAG, "processAssociationRequestApproval()\n"
+                    + "   package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")\n"
+                    + "   request=" + request + "\n"
+                    + "   macAddress=" + macAddress + "\n");
         }
 
-        final String deviceProfile = request.getDeviceProfile();
-        mOngoingDeviceDiscovery = getDeviceProfilePermissionDescription(deviceProfile)
-                .thenComposeAsync(description -> {
-                    if (DEBUG) {
-                        Slog.d(TAG, "fetchProfileDescription done: " + description);
-                    }
+        // 1. Need to check permissions again in case something changed, since we first received
+        // this request.
+        try {
+            enforcePermissionsForAssociation(mContext, request, packageUid);
+        } catch (SecurityException e) {
+            // Since, at this point the caller is our own UI, we need to catch the exception on
+            // forward it back to the application via the callback.
+            try {
+                callback.onFailure(e.getMessage());
+            } catch (RemoteException ignore) { }
+            return;
+        }
 
-                    request.setDeviceProfilePrivilegesDescription(description);
+        // 2. Create association and notify the application.
+        final AssociationInfo association = createAssociationAndNotifyApplication(
+                request, packageName, userId, macAddress, callback);
 
-                    return mServiceConnectors.forUser(userId).postAsync(service -> {
-                        if (DEBUG) {
-                            Slog.d(TAG, "Connected to CDM service -> "
-                                    + "Starting discovery for " + request);
-                        }
+        // 3. Send the association back the Approval Activity, so that it can report back to the app
+        // via Activity.setResult().
+        final Bundle data = new Bundle();
+        data.putParcelable(EXTRA_ASSOCIATION, association);
+        resultReceiver.send(RESULT_CODE_ASSOCIATION_CREATED, data);
+    }
 
-                        AndroidFuture<String> future = new AndroidFuture<>();
-                        service.startDiscovery(request, packageName, callback, future);
-                        return future;
-                    }).cancelTimeout();
+    private AssociationInfo createAssociationAndNotifyApplication(
+            @NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId,
+            @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback) {
+        final AssociationInfo association = mService.createAssociation(userId, packageName,
+                macAddress, request.getDisplayName(), request.getDeviceProfile(),
+                request.isSelfManaged());
 
-                }, FgThread.getExecutor()).whenComplete(uncheckExceptions((deviceAddress, err) -> {
-                    if (err == null) {
-                        mService.legacyCreateAssociation(
-                                userId, deviceAddress, packageName, deviceProfile);
-                        mServiceConnectors.forUser(userId).post(
-                                ICompanionDeviceDiscoveryService::onAssociationCreated);
-                    } else {
-                        Slog.e(TAG, "Failed to discover device(s)", err);
-                        callback.onFailure("No devices found: " + err.getMessage());
-                    }
-                    cleanup();
-                }));
+        try {
+            callback.onAssociationCreated(association);
+        } catch (RemoteException ignore) { }
+
+        return association;
     }
 
     private boolean willAddRoleHolder(@NonNull AssociationRequest request,
@@ -197,26 +272,44 @@
         return !isRoleHolder;
     }
 
-    private void cleanup() {
-        if (DEBUG) {
-            Slog.d(TAG, "cleanup(); discovery = "
-                    + mOngoingDeviceDiscovery + ", request = " + mRequest);
-        }
-        synchronized (mService.mLock) {
-            AndroidFuture<?> ongoingDeviceDiscovery = mOngoingDeviceDiscovery;
-            if (ongoingDeviceDiscovery != null && !ongoingDeviceDiscovery.isDone()) {
-                ongoingDeviceDiscovery.cancel(true);
+    private final ResultReceiver mOnRequestConfirmationReceiver =
+            new ResultReceiver(Handler.getMain()) {
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle data) {
+            if (DEBUG) {
+                Slog.d(TAG, "mOnRequestConfirmationReceiver.onReceiveResult() "
+                        + "code=" + resultCode + ", " + "data=" + data);
             }
-            if (mAppCallback != null) {
-                mAppCallback.asBinder().unlinkToDeath(mBinderDeathRecipient, 0);
-                mAppCallback = null;
-            }
-            mRequest = null;
-        }
-    }
 
-    private boolean mayAssociateWithoutPrompt(String packageName, int userId) {
-        final String deviceProfile = mRequest.getDeviceProfile();
+            if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) {
+                Slog.w(TAG, "Unknown result code:" + resultCode);
+                return;
+            }
+
+            final AssociationRequest request = data.getParcelable(EXTRA_ASSOCIATION_REQUEST);
+            final IAssociationRequestCallback callback = IAssociationRequestCallback.Stub
+                    .asInterface(data.getBinder(EXTRA_APPLICATION_CALLBACK));
+            final ResultReceiver resultReceiver = data.getParcelable(EXTRA_RESULT_RECEIVER);
+
+            requireNonNull(request);
+            requireNonNull(callback);
+            requireNonNull(resultReceiver);
+
+            final MacAddress macAddress;
+            if (request.isSelfManaged()) {
+                macAddress = null;
+            } else {
+                macAddress = data.getParcelable(EXTRA_MAC_ADDRESS);
+                requireNonNull(macAddress);
+            }
+
+            processAssociationRequestApproval(request, callback, resultReceiver, macAddress);
+        }
+    };
+
+    private boolean mayAssociateWithoutPrompt(@NonNull AssociationRequest request,
+            @NonNull String packageName, @UserIdInt int userId) {
+        final String deviceProfile = request.getDeviceProfile();
         if (deviceProfile != null) {
             final boolean isRoleHolder = Binder.withCleanCallingIdentity(
                     () -> isRoleHolder(mContext, userId, packageName, deviceProfile));
@@ -252,8 +345,8 @@
         String[] sameOemCerts = mContext.getResources()
                 .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
 
-        Signature[] signatures = mService.mPackageManagerInternal
-                .getPackage(packageName).getSigningDetails().getSignatures();
+        Signature[] signatures = mPackageManager.getPackage(packageName).getSigningDetails()
+                .getSignatures();
         String[] apkCerts = PackageUtils.computeSignaturesSha256Digests(signatures);
 
         Set<String> sameOemPackageCerts =
@@ -274,47 +367,6 @@
         return false;
     }
 
-    @NonNull
-    private AndroidFuture<String> getDeviceProfilePermissionDescription(
-            @Nullable String deviceProfile) {
-        if (deviceProfile == null) {
-            return AndroidFuture.completedFuture(null);
-        }
-
-        final AndroidFuture<String> result = new AndroidFuture<>();
-        mService.mPermissionControllerManager.getPrivilegesDescriptionStringForProfile(
-                deviceProfile, FgThread.getExecutor(), desc -> {
-                    try {
-                        result.complete(String.valueOf(desc));
-                    } catch (Exception e) {
-                        result.completeExceptionally(e);
-                    }
-                });
-        return result;
-    }
-
-
-    void dump(@NonNull PrintWriter pw) {
-        pw.append("Discovery Service State:").append('\n');
-        for (int i = 0, size = mServiceConnectors.size(); i < size; i++) {
-            int userId = mServiceConnectors.keyAt(i);
-            pw.append("  ")
-                    .append("u").append(Integer.toString(userId)).append(": ")
-                    .append(Objects.toString(mServiceConnectors.valueAt(i)))
-                    .append('\n');
-        }
-    }
-
-    private final IBinder.DeathRecipient mBinderDeathRecipient = new IBinder.DeathRecipient() {
-        @Override
-        public void binderDied() {
-            if (DEBUG) {
-                Slog.d(TAG, "binderDied()");
-            }
-            mService.mMainHandler.post(AssociationRequestsProcessor.this::cleanup);
-        }
-    };
-
     private static Set<String> getSameOemPackageCerts(
             String packageName, String[] oemPackages, String[] sameOemCerts) {
         Set<String> sameOemPackageCerts = new HashSet<>();
@@ -330,16 +382,19 @@
         return sameOemPackageCerts;
     }
 
-    private static boolean withCatchingRemoteException(ThrowingRunnable runnable) {
-        try {
-            runnable.run();
-        } catch (RemoteException e) {
-            return false;
-        }
-        return true;
-    }
+    /**
+     * Convert an instance of a "locally-defined" ResultReceiver to an instance of
+     * {@link android.os.ResultReceiver} itself, which the receiving process will be able to
+     * unmarshall.
+     */
+    private static <T extends ResultReceiver> ResultReceiver prepareForIpc(T resultReceiver) {
+        final Parcel parcel = Parcel.obtain();
+        resultReceiver.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
 
-    private interface ThrowingRunnable {
-        void run() throws RemoteException;
+        final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        return ipcFriendly;
     }
 }
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index b32d543..626128a 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -177,6 +177,7 @@
             new BluetoothDeviceConnectedListener();
     private BleStateBroadcastReceiver mBleStateBroadcastReceiver = new BleStateBroadcastReceiver();
     private List<String> mCurrentlyConnectedDevices = new ArrayList<>();
+    Set<Integer> mPresentSelfManagedDevices = new HashSet<>();
     private ArrayMap<String, Date> mDevicesLastNearby = new ArrayMap<>();
     private UnbindDeviceListenersRunnable
             mUnbindDeviceListenersRunnable = new UnbindDeviceListenersRunnable();
@@ -217,7 +218,7 @@
         mPermissionControllerManager = requireNonNull(
                 context.getSystemService(PermissionControllerManager.class));
         mUserManager = context.getSystemService(UserManager.class);
-        mCompanionDevicePresenceController = new CompanionDevicePresenceController();
+        mCompanionDevicePresenceController = new CompanionDevicePresenceController(this);
         mAssociationRequestsProcessor = new AssociationRequestsProcessor(this);
 
         registerPackageMonitor();
@@ -405,7 +406,8 @@
             enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName,
                     "create associations");
 
-            mAssociationRequestsProcessor.process(request, packageName, userId, callback);
+            mAssociationRequestsProcessor.processNewAssociationRequest(
+                    request, packageName, userId, callback);
         }
 
         @Override
@@ -555,6 +557,57 @@
             //TODO: b/199427116
         }
 
+        @Override
+        public void notifyDeviceAppeared(int associationId) {
+            final AssociationInfo association = getAssociationWithCallerChecks(associationId);
+            if (association == null) {
+                throw new IllegalArgumentException("Association with ID " + associationId + " "
+                        + "does not exist "
+                        + "or belongs to a different package "
+                        + "or belongs to a different user");
+            }
+
+            if (!association.isSelfManaged()) {
+                throw new IllegalArgumentException("Association with ID " + associationId
+                        + " is not self-managed. notifyDeviceAppeared(int) can only be called for"
+                        + " self-managed associations.");
+            }
+
+            if (!mPresentSelfManagedDevices.add(associationId)) {
+                Slog.w(LOG_TAG, "Association with ID " + associationId + " is already present");
+                return;
+            }
+
+            mCompanionDevicePresenceController.onDeviceNotifyAppeared(
+                    association, getContext(), mMainHandler);
+        }
+
+        @Override
+        public void notifyDeviceDisappeared(int associationId) {
+            final AssociationInfo association = getAssociationWithCallerChecks(associationId);
+            if (association == null) {
+                throw new IllegalArgumentException("Association with ID " + associationId + " "
+                        + "does not exist "
+                        + "or belongs to a different package "
+                        + "or belongs to a different user");
+            }
+
+            if (!association.isSelfManaged()) {
+                throw new IllegalArgumentException("Association with ID " + associationId
+                        + " is not self-managed. notifyDeviceAppeared(int) can only be called for"
+                        + " self-managed associations.");
+            }
+
+            if (!mPresentSelfManagedDevices.contains(associationId)) {
+                Slog.w(LOG_TAG, "Association with ID " + associationId + " is not connected");
+                return;
+            }
+
+            mPresentSelfManagedDevices.remove(associationId);
+            mCompanionDevicePresenceController.onDeviceNotifyDisappearedAndUnbind(
+                    association, getContext(), mMainHandler);
+        }
+
         private void registerDevicePresenceListenerActive(String packageName, String deviceAddress,
                 boolean active) throws RemoteException {
             getContext().enforceCallingOrSelfPermission(
@@ -645,11 +698,18 @@
                 }
 
             }
+
             fout.append("Currently Connected Devices:").append('\n');
             for (int i = 0, size = mCurrentlyConnectedDevices.size(); i < size; i++) {
                 fout.append("  ").append(mCurrentlyConnectedDevices.get(i)).append('\n');
             }
 
+            fout.append("Currently SelfManaged Connected Devices associationId:").append('\n');
+            for (Integer associationId : mPresentSelfManagedDevices) {
+                fout.append("  ").append("AssociationId:  ").append(
+                        String.valueOf(associationId)).append('\n');
+            }
+
             fout.append("Devices Last Nearby:").append('\n');
             for (int i = 0, size = mDevicesLastNearby.size(); i < size; i++) {
                 String device = mDevicesLastNearby.keyAt(i);
@@ -658,8 +718,6 @@
                         .append(sDateFormat.format(time)).append('\n');
             }
 
-            mAssociationRequestsProcessor.dump(fout);
-
             fout.append("Device Listener Services State:").append('\n');
             for (int i = 0, size =  mCompanionDevicePresenceController.mBoundServices.size();
                     i < size; i++) {
@@ -774,7 +832,9 @@
     }
 
     void onAssociationPreRemove(AssociationInfo association) {
-        if (association.isNotifyOnDeviceNearby()) {
+        if (association.isNotifyOnDeviceNearby()
+                || (association.isSelfManaged()
+                && mPresentSelfManagedDevices.contains(association.getId()))) {
             mCompanionDevicePresenceController.unbindDevicePresenceListener(
                     association.getPackageName(), association.getUserId());
         }
diff --git a/services/companion/java/com/android/server/companion/CompanionDevicePresenceController.java b/services/companion/java/com/android/server/companion/CompanionDevicePresenceController.java
index 3e00846..4447684 100644
--- a/services/companion/java/com/android/server/companion/CompanionDevicePresenceController.java
+++ b/services/companion/java/com/android/server/companion/CompanionDevicePresenceController.java
@@ -49,8 +49,10 @@
     private static final String LOG_TAG = "CompanionDevicePresenceController";
     PerUser<ArrayMap<String, List<BoundService>>> mBoundServices;
     private static final String META_DATA_KEY_PRIMARY = "primary";
+    private final CompanionDeviceManagerService mService;
 
-    public CompanionDevicePresenceController() {
+    public CompanionDevicePresenceController(CompanionDeviceManagerService service) {
+        mService = service;
         mBoundServices = new PerUser<ArrayMap<String, List<BoundService>>>() {
             @NonNull
             @Override
@@ -61,24 +63,45 @@
     }
 
     void onDeviceNotifyAppeared(AssociationInfo association, Context context, Handler handler) {
-        ServiceConnector<ICompanionDeviceService> primaryConnector =
-                getPrimaryServiceConnector(association, context, handler);
-        if (primaryConnector != null) {
-            Slog.i(LOG_TAG,
-                    "Sending onDeviceAppeared to " + association.getPackageName() + ")");
-            primaryConnector.run(
-                    s -> s.onDeviceAppeared(association.getDeviceMacAddressAsString()));
+        for (BoundService boundService : getDeviceListenerServiceConnector(
+                association, context,  handler)) {
+            if (boundService.mIsPrimary) {
+                Slog.i(LOG_TAG,
+                        "Sending onDeviceAppeared to " + association.getPackageName() + ")");
+                boundService.mServiceConnector.run(
+                        service -> service.onDeviceAppeared(association));
+            } else {
+                Slog.i(LOG_TAG, "Connecting to " + boundService.mComponentName);
+                boundService.mServiceConnector.connect();
+            }
         }
     }
 
     void onDeviceNotifyDisappeared(AssociationInfo association, Context context, Handler handler) {
-        ServiceConnector<ICompanionDeviceService> primaryConnector =
-                getPrimaryServiceConnector(association, context, handler);
-        if (primaryConnector != null) {
-            Slog.i(LOG_TAG,
-                    "Sending onDeviceDisappeared to " + association.getPackageName() + ")");
-            primaryConnector.run(
-                    s -> s.onDeviceDisappeared(association.getDeviceMacAddressAsString()));
+        for (BoundService boundService : getDeviceListenerServiceConnector(
+                association, context,  handler)) {
+            if (boundService.mIsPrimary) {
+                Slog.i(LOG_TAG,
+                        "Sending onDeviceDisappeared to " + association.getPackageName() + ")");
+                boundService.mServiceConnector.run(service ->
+                        service.onDeviceDisappeared(association));
+            }
+        }
+    }
+
+    void onDeviceNotifyDisappearedAndUnbind(AssociationInfo association,
+            Context context, Handler handler) {
+        for (BoundService boundService : getDeviceListenerServiceConnector(
+                association, context, handler)) {
+            if (boundService.mIsPrimary) {
+                Slog.i(LOG_TAG,
+                        "Sending onDeviceDisappeared to " + association.getPackageName() + ")");
+                boundService.mServiceConnector.post(
+                        service -> {
+                            service.onDeviceDisappeared(association);
+                        }).thenRun(() -> unbindDevicePresenceListener(
+                                        association.getPackageName(), association.getUserId()));
+            }
         }
     }
 
@@ -93,17 +116,6 @@
         }
     }
 
-    private ServiceConnector<ICompanionDeviceService> getPrimaryServiceConnector(
-            AssociationInfo association, Context context, Handler handler) {
-        for (BoundService boundService: getDeviceListenerServiceConnector(association, context,
-                handler)) {
-            if (boundService.mIsPrimary) {
-                return boundService.mServiceConnector;
-            }
-        }
-        return null;
-    }
-
     private List<BoundService> getDeviceListenerServiceConnector(AssociationInfo a, Context context,
             Handler handler) {
         return mBoundServices.forUser(a.getUserId()).computeIfAbsent(
@@ -140,18 +152,22 @@
                         protected long getAutoDisconnectTimeoutMs() {
                             // Service binding is managed manually based on corresponding device
                             // being nearby
-                            return Long.MAX_VALUE;
+                            return -1;
                         }
 
                         @Override
                         public void binderDied() {
                             super.binderDied();
-
-                            // Re-connect to the service if process gets killed
-                            handler.postDelayed(
-                                    this::connect,
-                                    CompanionDeviceManagerService
-                                            .DEVICE_LISTENER_DIED_REBIND_TIMEOUT_MS);
+                            if (a.isSelfManaged()) {
+                                mBoundServices.forUser(a.getUserId()).remove(a.getPackageName());
+                                mService.mPresentSelfManagedDevices.remove(a.getId());
+                            } else {
+                                // Re-connect to the service if process gets killed
+                                handler.postDelayed(
+                                        this::connect,
+                                        CompanionDeviceManagerService
+                                                .DEVICE_LISTENER_DIED_REBIND_TIMEOUT_MS);
+                            }
                         }
                     };
 
@@ -191,6 +207,13 @@
             }
         }
 
+        if (packageResolveInfos.size() > 1 && primaryCount == 0) {
+            Slog.e(LOG_TAG, "Must have exactly one primary CompanionDeviceService "
+                    + "to be bound when declare more than one CompanionDeviceService but "
+                    + association.getPackageName() + " has " + primaryCount);
+            return false;
+        }
+
         if (packageResolveInfos.size() == 1 && primaryCount != 0) {
             Slog.w(LOG_TAG, "Do not need the primary metadata if there's only one"
                     + " CompanionDeviceService " + "but " + association.getPackageName()
diff --git a/services/companion/java/com/android/server/companion/PermissionsUtils.java b/services/companion/java/com/android/server/companion/PermissionsUtils.java
index ea57089..3a8ee73 100644
--- a/services/companion/java/com/android/server/companion/PermissionsUtils.java
+++ b/services/companion/java/com/android/server/companion/PermissionsUtils.java
@@ -38,13 +38,11 @@
 import android.companion.AssociationRequest;
 import android.companion.CompanionDeviceManager;
 import android.content.Context;
-import android.content.pm.PackageManagerInternal;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.ArrayMap;
 
 import com.android.internal.app.IAppOpsService;
-import com.android.server.LocalServices;
 
 import java.util.Map;
 
@@ -69,9 +67,7 @@
     }
 
     static void enforcePermissionsForAssociation(@NonNull Context context,
-            @NonNull AssociationRequest request, @NonNull String packageName,
-            @UserIdInt int userId) {
-        final int packageUid = getPackageUid(userId, packageName);
+            @NonNull AssociationRequest request, int packageUid) {
         enforceRequestDeviceProfilePermissions(context, request.getDeviceProfile(), packageUid);
 
         if (request.isSelfManaged()) {
@@ -207,11 +203,6 @@
         }
     }
 
-    private static int getPackageUid(@UserIdInt int userId, @NonNull String packageName) {
-        return LocalServices.getService(PackageManagerInternal.class)
-                .getPackageUid(packageName, 0, userId);
-    }
-
     private static IAppOpsService getAppOpsService() {
         if (sAppOpsService == null) {
             synchronized (PermissionsUtils.class) {
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
new file mode 100644
index 0000000..067edcc
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.virtual;
+
+import android.annotation.NonNull;
+import android.graphics.Point;
+import android.hardware.input.VirtualKeyEvent;
+import android.hardware.input.VirtualMouseButtonEvent;
+import android.hardware.input.VirtualMouseRelativeEvent;
+import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualTouchEvent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+/** Controls virtual input devices, including device lifecycle and event dispatch. */
+class InputController {
+
+    private final Object mLock;
+
+    /* Token -> file descriptor associations. */
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    final Map<IBinder, Integer> mInputDeviceFds = new ArrayMap<>();
+
+    private final NativeWrapper mNativeWrapper;
+
+    InputController(@NonNull Object lock) {
+        this(lock, new NativeWrapper());
+    }
+
+    @VisibleForTesting
+    InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper) {
+        mLock = lock;
+        mNativeWrapper = nativeWrapper;
+    }
+
+    void close() {
+        synchronized (mLock) {
+            for (int fd : mInputDeviceFds.values()) {
+                mNativeWrapper.closeUinput(fd);
+            }
+            mInputDeviceFds.clear();
+        }
+    }
+
+    void createKeyboard(@NonNull String deviceName,
+            int vendorId,
+            int productId,
+            @NonNull IBinder deviceToken) {
+        final int fd = mNativeWrapper.openUinputKeyboard(deviceName, vendorId, productId);
+        if (fd < 0) {
+            throw new RuntimeException(
+                    "A native error occurred when creating keyboard: " + -fd);
+        }
+        synchronized (mLock) {
+            mInputDeviceFds.put(deviceToken, fd);
+        }
+        try {
+            deviceToken.linkToDeath(new BinderDeathRecipient(deviceToken), /* flags= */ 0);
+        } catch (RemoteException e) {
+            throw new RuntimeException("Could not create virtual keyboard", e);
+        }
+    }
+
+    void createMouse(@NonNull String deviceName,
+            int vendorId,
+            int productId,
+            @NonNull IBinder deviceToken) {
+        final int fd = mNativeWrapper.openUinputMouse(deviceName, vendorId, productId);
+        if (fd < 0) {
+            throw new RuntimeException(
+                    "A native error occurred when creating mouse: " + -fd);
+        }
+        synchronized (mLock) {
+            mInputDeviceFds.put(deviceToken, fd);
+        }
+        try {
+            deviceToken.linkToDeath(new BinderDeathRecipient(deviceToken), /* flags= */ 0);
+        } catch (RemoteException e) {
+            throw new RuntimeException("Could not create virtual mouse", e);
+        }
+    }
+
+    void createTouchscreen(@NonNull String deviceName,
+            int vendorId,
+            int productId,
+            @NonNull IBinder deviceToken,
+            @NonNull Point screenSize) {
+        final int fd = mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
+                screenSize.y, screenSize.x);
+        if (fd < 0) {
+            throw new RuntimeException(
+                    "A native error occurred when creating touchscreen: " + -fd);
+        }
+        synchronized (mLock) {
+            mInputDeviceFds.put(deviceToken, fd);
+        }
+        try {
+            deviceToken.linkToDeath(new BinderDeathRecipient(deviceToken), /* flags= */ 0);
+        } catch (RemoteException e) {
+            throw new RuntimeException("Could not create virtual touchscreen", e);
+        }
+    }
+
+    void unregisterInputDevice(@NonNull IBinder token) {
+        synchronized (mLock) {
+            final Integer fd = mInputDeviceFds.remove(token);
+            if (fd == null) {
+                throw new IllegalArgumentException(
+                        "Could not unregister input device for given token");
+            }
+            mNativeWrapper.closeUinput(fd);
+        }
+    }
+
+    boolean sendKeyEvent(@NonNull IBinder token, @NonNull VirtualKeyEvent event) {
+        synchronized (mLock) {
+            final Integer fd = mInputDeviceFds.get(token);
+            if (fd == null) {
+                throw new IllegalArgumentException(
+                        "Could not send key event to input device for given token");
+            }
+            return mNativeWrapper.writeKeyEvent(fd, event.getKeyCode(), event.getAction());
+        }
+    }
+
+    boolean sendButtonEvent(@NonNull IBinder token, @NonNull VirtualMouseButtonEvent event) {
+        synchronized (mLock) {
+            final Integer fd = mInputDeviceFds.get(token);
+            if (fd == null) {
+                throw new IllegalArgumentException(
+                        "Could not send button event to input device for given token");
+            }
+            return mNativeWrapper.writeButtonEvent(fd, event.getButtonCode(), event.getAction());
+        }
+    }
+
+    boolean sendTouchEvent(@NonNull IBinder token, @NonNull VirtualTouchEvent event) {
+        synchronized (mLock) {
+            final Integer fd = mInputDeviceFds.get(token);
+            if (fd == null) {
+                throw new IllegalArgumentException(
+                        "Could not send touch event to input device for given token");
+            }
+            return mNativeWrapper.writeTouchEvent(fd, event.getPointerId(), event.getToolType(),
+                    event.getAction(), event.getX(), event.getY(), event.getPressure(),
+                    event.getMajorAxisSize());
+        }
+    }
+
+    boolean sendRelativeEvent(@NonNull IBinder token, @NonNull VirtualMouseRelativeEvent event) {
+        synchronized (mLock) {
+            final Integer fd = mInputDeviceFds.get(token);
+            if (fd == null) {
+                throw new IllegalArgumentException(
+                        "Could not send relative event to input device for given token");
+            }
+            return mNativeWrapper.writeRelativeEvent(fd, event.getRelativeX(),
+                    event.getRelativeY());
+        }
+    }
+
+    boolean sendScrollEvent(@NonNull IBinder token, @NonNull VirtualMouseScrollEvent event) {
+        synchronized (mLock) {
+            final Integer fd = mInputDeviceFds.get(token);
+            if (fd == null) {
+                throw new IllegalArgumentException(
+                        "Could not send scroll event to input device for given token");
+            }
+            return mNativeWrapper.writeScrollEvent(fd, event.getXAxisMovement(),
+                    event.getYAxisMovement());
+        }
+    }
+
+    public void dump(@NonNull PrintWriter fout) {
+        fout.println("    InputController: ");
+        synchronized (mLock) {
+            fout.println("      Active file descriptors: ");
+            for (int inputDeviceFd : mInputDeviceFds.values()) {
+                fout.println(inputDeviceFd);
+            }
+        }
+    }
+
+    private static native int nativeOpenUinputKeyboard(String deviceName, int vendorId,
+            int productId);
+    private static native int nativeOpenUinputMouse(String deviceName, int vendorId,
+            int productId);
+    private static native int nativeOpenUinputTouchscreen(String deviceName, int vendorId,
+            int productId, int height, int width);
+    private static native boolean nativeCloseUinput(int fd);
+    private static native boolean nativeWriteKeyEvent(int fd, int androidKeyCode, int action);
+    private static native boolean nativeWriteButtonEvent(int fd, int buttonCode, int action);
+    private static native boolean nativeWriteTouchEvent(int fd, int pointerId, int toolType,
+            int action, float locationX, float locationY, float pressure, float majorAxisSize);
+    private static native boolean nativeWriteRelativeEvent(int fd, float relativeX,
+            float relativeY);
+    private static native boolean nativeWriteScrollEvent(int fd, float xAxisMovement,
+            float yAxisMovement);
+
+    /** Wrapper around the static native methods for tests. */
+    @VisibleForTesting
+    protected static class NativeWrapper {
+        public int openUinputKeyboard(String deviceName, int vendorId, int productId) {
+            return nativeOpenUinputKeyboard(deviceName, vendorId,
+                    productId);
+        }
+
+        public int openUinputMouse(String deviceName, int vendorId, int productId) {
+            return nativeOpenUinputMouse(deviceName, vendorId,
+                    productId);
+        }
+
+        public int openUinputTouchscreen(String deviceName, int vendorId, int productId, int height,
+                int width) {
+            return nativeOpenUinputTouchscreen(deviceName, vendorId,
+                    productId, height, width);
+        }
+
+        public boolean closeUinput(int fd) {
+            return nativeCloseUinput(fd);
+        }
+
+        public boolean writeKeyEvent(int fd, int androidKeyCode, int action) {
+            return nativeWriteKeyEvent(fd, androidKeyCode, action);
+        }
+
+        public boolean writeButtonEvent(int fd, int buttonCode, int action) {
+            return nativeWriteButtonEvent(fd, buttonCode, action);
+        }
+
+        public boolean writeTouchEvent(int fd, int pointerId, int toolType, int action,
+                float locationX, float locationY, float pressure, float majorAxisSize) {
+            return nativeWriteTouchEvent(fd, pointerId, toolType,
+                    action, locationX, locationY,
+                    pressure, majorAxisSize);
+        }
+
+        public boolean writeRelativeEvent(int fd, float relativeX, float relativeY) {
+            return nativeWriteRelativeEvent(fd, relativeX, relativeY);
+        }
+
+        public boolean writeScrollEvent(int fd, float xAxisMovement, float yAxisMovement) {
+            return nativeWriteScrollEvent(fd, xAxisMovement,
+                    yAxisMovement);
+        }
+    }
+
+    private final class BinderDeathRecipient implements IBinder.DeathRecipient {
+
+        private final IBinder mDeviceToken;
+
+        BinderDeathRecipient(IBinder deviceToken) {
+            mDeviceToken = deviceToken;
+        }
+
+        @Override
+        public void binderDied() {
+            unregisterInputDevice(mDeviceToken);
+        }
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
new file mode 100644
index 0000000..022da43
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.virtual;
+
+import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
+import android.annotation.NonNull;
+import android.companion.AssociationInfo;
+import android.companion.virtual.IVirtualDevice;
+import android.content.Context;
+import android.graphics.Point;
+import android.hardware.input.VirtualKeyEvent;
+import android.hardware.input.VirtualMouseButtonEvent;
+import android.hardware.input.VirtualMouseRelativeEvent;
+import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualTouchEvent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.window.DisplayWindowPolicyController;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+
+final class VirtualDeviceImpl extends IVirtualDevice.Stub
+        implements IBinder.DeathRecipient {
+
+    private final Object mVirtualDeviceLock = new Object();
+
+    private final Context mContext;
+    private final AssociationInfo mAssociationInfo;
+    private final int mOwnerUid;
+    private final GenericWindowPolicyController mGenericWindowPolicyController;
+    private final InputController mInputController;
+    @VisibleForTesting
+    final List<Integer> mVirtualDisplayIds = new ArrayList<>();
+    private final OnDeviceCloseListener mListener;
+
+    VirtualDeviceImpl(Context context, AssociationInfo associationInfo,
+            IBinder token, int ownerUid, OnDeviceCloseListener listener) {
+        this(context, associationInfo, token, ownerUid, /* inputController= */ null, listener);
+    }
+
+    @VisibleForTesting
+    VirtualDeviceImpl(Context context, AssociationInfo associationInfo, IBinder token,
+            int ownerUid, InputController inputController, OnDeviceCloseListener listener) {
+        mContext = context;
+        mAssociationInfo = associationInfo;
+        mGenericWindowPolicyController = new GenericWindowPolicyController(FLAG_SECURE,
+                SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+        mOwnerUid = ownerUid;
+        if (inputController == null) {
+            mInputController = new InputController(mVirtualDeviceLock);
+        } else {
+            mInputController = inputController;
+        }
+        mListener = listener;
+        try {
+            token.linkToDeath(this, 0);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    public int getAssociationId() {
+        return mAssociationInfo.getId();
+    }
+
+    @Override // Binder call
+    public void close() {
+        mListener.onClose(mAssociationInfo.getId());
+        mInputController.close();
+    }
+
+    @Override
+    public void binderDied() {
+        close();
+    }
+
+    @Override // Binder call
+    public void createVirtualKeyboard(
+            int displayId,
+            @NonNull String deviceName,
+            int vendorId,
+            int productId,
+            @NonNull IBinder deviceToken) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+                "Permission required to create a virtual keyboard");
+        synchronized (mVirtualDeviceLock) {
+            if (!mVirtualDisplayIds.contains(displayId)) {
+                throw new SecurityException(
+                        "Cannot create a virtual keyboard for a display not associated with "
+                                + "this virtual device");
+            }
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mInputController.createKeyboard(deviceName, vendorId, productId, deviceToken);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override // Binder call
+    public void createVirtualMouse(
+            int displayId,
+            @NonNull String deviceName,
+            int vendorId,
+            int productId,
+            @NonNull IBinder deviceToken) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+                "Permission required to create a virtual mouse");
+        synchronized (mVirtualDeviceLock) {
+            if (!mVirtualDisplayIds.contains(displayId)) {
+                throw new SecurityException(
+                        "Cannot create a virtual mouse for a display not associated with this "
+                                + "virtual device");
+            }
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mInputController.createMouse(deviceName, vendorId, productId, deviceToken);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override // Binder call
+    public void createVirtualTouchscreen(
+            int displayId,
+            @NonNull String deviceName,
+            int vendorId,
+            int productId,
+            @NonNull IBinder deviceToken,
+            @NonNull Point screenSize) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+                "Permission required to create a virtual touchscreen");
+        synchronized (mVirtualDeviceLock) {
+            if (!mVirtualDisplayIds.contains(displayId)) {
+                throw new SecurityException(
+                        "Cannot create a virtual touchscreen for a display not associated with "
+                                + "this virtual device");
+            }
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mInputController.createTouchscreen(deviceName, vendorId, productId,
+                    deviceToken, screenSize);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override // Binder call
+    public void unregisterInputDevice(IBinder token) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
+                "Permission required to unregister this input device");
+
+        final long binderToken = Binder.clearCallingIdentity();
+        try {
+            mInputController.unregisterInputDevice(token);
+        } finally {
+            Binder.restoreCallingIdentity(binderToken);
+        }
+    }
+
+    @Override // Binder call
+    public boolean sendKeyEvent(IBinder token, VirtualKeyEvent event) {
+        final long binderToken = Binder.clearCallingIdentity();
+        try {
+            return mInputController.sendKeyEvent(token, event);
+        } finally {
+            Binder.restoreCallingIdentity(binderToken);
+        }
+    }
+
+    @Override // Binder call
+    public boolean sendButtonEvent(IBinder token, VirtualMouseButtonEvent event) {
+        final long binderToken = Binder.clearCallingIdentity();
+        try {
+            return mInputController.sendButtonEvent(token, event);
+        } finally {
+            Binder.restoreCallingIdentity(binderToken);
+        }
+    }
+
+    @Override // Binder call
+    public boolean sendTouchEvent(IBinder token, VirtualTouchEvent event) {
+        final long binderToken = Binder.clearCallingIdentity();
+        try {
+            return mInputController.sendTouchEvent(token, event);
+        } finally {
+            Binder.restoreCallingIdentity(binderToken);
+        }
+    }
+
+    @Override // Binder call
+    public boolean sendRelativeEvent(IBinder token, VirtualMouseRelativeEvent event) {
+        final long binderToken = Binder.clearCallingIdentity();
+        try {
+            return mInputController.sendRelativeEvent(token, event);
+        } finally {
+            Binder.restoreCallingIdentity(binderToken);
+        }
+    }
+
+    @Override // Binder call
+    public boolean sendScrollEvent(IBinder token, VirtualMouseScrollEvent event) {
+        final long binderToken = Binder.clearCallingIdentity();
+        try {
+            return mInputController.sendScrollEvent(token, event);
+        } finally {
+            Binder.restoreCallingIdentity(binderToken);
+        }
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
+        fout.println("  VirtualDevice: ");
+        fout.println("    mVirtualDisplayIds: ");
+        synchronized (mVirtualDeviceLock) {
+            for (int id : mVirtualDisplayIds) {
+                fout.println("      " + id);
+            }
+        }
+        mInputController.dump(fout);
+    }
+
+    DisplayWindowPolicyController onVirtualDisplayCreatedLocked(int displayId) {
+        if (mVirtualDisplayIds.contains(displayId)) {
+            throw new IllegalStateException(
+                    "Virtual device already have a virtual display with ID " + displayId);
+        }
+        mVirtualDisplayIds.add(displayId);
+        return mGenericWindowPolicyController;
+    }
+
+    void onVirtualDisplayRemovedLocked(int displayId) {
+        if (!mVirtualDisplayIds.contains(displayId)) {
+            throw new IllegalStateException(
+                    "Virtual device doesn't have a virtual display with ID " + displayId);
+        }
+        mVirtualDisplayIds.remove(displayId);
+    }
+
+    int getOwnerUid() {
+        return mOwnerUid;
+    }
+
+    interface OnDeviceCloseListener {
+        void onClose(int associationId);
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 2742608..46e75f7 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -16,9 +16,6 @@
 
 package com.android.server.companion.virtual;
 
-import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
-import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -42,7 +39,6 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -52,6 +48,7 @@
 
     private static final boolean DEBUG = false;
     private static final String LOG_TAG = "VirtualDeviceManagerService";
+
     private final Object mVirtualDeviceManagerLock = new Object();
     private final VirtualDeviceManagerImpl mImpl;
 
@@ -130,64 +127,9 @@
         }
     }
 
-    private class VirtualDeviceImpl extends IVirtualDevice.Stub implements IBinder.DeathRecipient {
-
-        private final AssociationInfo mAssociationInfo;
-        private final int mOwnerUid;
-        private final GenericWindowPolicyController mGenericWindowPolicyController;
-        private final ArrayList<Integer> mDisplayIds = new ArrayList<>();
-
-        private VirtualDeviceImpl(int ownerUid, IBinder token, AssociationInfo associationInfo) {
-            mOwnerUid = ownerUid;
-            mAssociationInfo = associationInfo;
-            mGenericWindowPolicyController = new GenericWindowPolicyController(FLAG_SECURE,
-                    SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
-            try {
-                token.linkToDeath(this, 0);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
-            mVirtualDevices.put(associationInfo.getId(), this);
-        }
-
-        @Override
-        public int getAssociationId() {
-            return mAssociationInfo.getId();
-        }
-
-        @Override
-        public void close() {
-            synchronized (mVirtualDeviceManagerLock) {
-                mVirtualDevices.remove(mAssociationInfo.getId());
-            }
-        }
-
-        @Override
-        public void binderDied() {
-            close();
-        }
-
-        DisplayWindowPolicyController onVirtualDisplayCreatedLocked(int displayId) {
-            if (mDisplayIds.contains(displayId)) {
-                throw new IllegalStateException(
-                        "Virtual device already have a virtual display with ID " + displayId);
-            }
-            mDisplayIds.add(displayId);
-            return mGenericWindowPolicyController;
-        }
-
-        void onVirtualDisplayRemovedLocked(int displayId) {
-            if (!mDisplayIds.contains(displayId)) {
-                throw new IllegalStateException(
-                        "Virtual device doesn't have a virtual display with ID " + displayId);
-            }
-            mDisplayIds.remove(displayId);
-        }
-    }
-
     class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub {
 
-        @Override
+        @Override // Binder call
         public IVirtualDevice createVirtualDevice(
                 IBinder token, String packageName, int associationId) {
             getContext().enforceCallingOrSelfPermission(
@@ -209,7 +151,18 @@
                             "Virtual device for association ID " + associationId
                                     + " already exists");
                 }
-                return new VirtualDeviceImpl(callingUid, token, associationInfo);
+                VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(),
+                        associationInfo, token, callingUid,
+                        new VirtualDeviceImpl.OnDeviceCloseListener() {
+                            @Override
+                            public void onClose(int associationId) {
+                                synchronized (mVirtualDeviceManagerLock) {
+                                    mVirtualDevices.remove(associationId);
+                                }
+                            }
+                        });
+                mVirtualDevices.put(associationInfo.getId(), virtualDevice);
+                return virtualDevice;
             }
         }
 
@@ -254,8 +207,7 @@
             fout.println("Created virtual devices: ");
             synchronized (mVirtualDeviceManagerLock) {
                 for (int i = 0; i < mVirtualDevices.size(); i++) {
-                    VirtualDeviceImpl virtualDevice = mVirtualDevices.valueAt(i);
-                    fout.printf("%d: %s\n", mVirtualDevices.keyAt(i), virtualDevice);
+                    mVirtualDevices.valueAt(i).dump(fd, fout, args);
                 }
             }
         }
@@ -290,7 +242,7 @@
             synchronized (mVirtualDeviceManagerLock) {
                 int size = mVirtualDevices.size();
                 for (int i = 0; i < size; i++) {
-                    if (mVirtualDevices.valueAt(i).mOwnerUid == uid) {
+                    if (mVirtualDevices.valueAt(i).getOwnerUid() == uid) {
                         return true;
                     }
                 }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index ba6854b..9641eb5 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -121,7 +121,7 @@
         "java/com/android/server/am/EventLogTags.logtags",
         "java/com/android/server/wm/EventLogTags.logtags",
         "java/com/android/server/policy/EventLogTags.logtags",
-        ":services.connectivity-nsd-sources",
+        ":services.connectivity-tiramisu-sources",
     ],
 
     libs: [
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 4775127..91cd2f6 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -152,6 +152,8 @@
 
         int callerUid;
         int callerPid;
+        boolean renounceFineLocationAccess;
+        boolean renounceCoarseLocationAccess;
 
         Set<Integer> eventList;
 
@@ -995,14 +997,25 @@
     }
 
     @Override
-    public void listenWithEventList(int subId, String callingPackage, String callingFeatureId,
-            IPhoneStateListener callback, int[] events, boolean notifyNow) {
+    public void listenWithEventList(boolean renounceFineLocationAccess,
+            boolean renounceCoarseLocationAccess, int subId, String callingPackage,
+            String callingFeatureId, IPhoneStateListener callback,
+            int[] events, boolean notifyNow) {
         Set<Integer> eventList = Arrays.stream(events).boxed().collect(Collectors.toSet());
-        listen(callingPackage, callingFeatureId, callback, eventList, notifyNow, subId);
+        listen(renounceFineLocationAccess, renounceFineLocationAccess, callingPackage,
+                callingFeatureId, callback, eventList, notifyNow, subId);
     }
 
     private void listen(String callingPackage, @Nullable String callingFeatureId,
             IPhoneStateListener callback, Set<Integer> events, boolean notifyNow, int subId) {
+        listen(false, false, callingPackage,
+                callingFeatureId, callback, events, notifyNow, subId);
+    }
+
+    private void listen(boolean renounceFineLocationAccess,
+            boolean renounceCoarseLocationAccess, String callingPackage,
+            @Nullable String callingFeatureId, IPhoneStateListener callback,
+            Set<Integer> events, boolean notifyNow, int subId) {
         int callerUserId = UserHandle.getCallingUserId();
         mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
         String str = "listen: E pkg=" + pii(callingPackage) + " uid=" + Binder.getCallingUid()
@@ -1047,6 +1060,8 @@
             r.callback = callback;
             r.callingPackage = callingPackage;
             r.callingFeatureId = callingFeatureId;
+            r.renounceCoarseLocationAccess = renounceCoarseLocationAccess;
+            r.renounceFineLocationAccess = renounceFineLocationAccess;
             r.callerUid = Binder.getCallingUid();
             r.callerPid = Binder.getCallingPid();
             // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID,
@@ -3199,6 +3214,9 @@
      * If you don't need app compat logic, use {@link #checkFineLocationAccess(Record)}.
      */
     private boolean checkFineLocationAccess(Record r, int minSdk) {
+        if (r.renounceFineLocationAccess) {
+            return false;
+        }
         LocationAccessPolicy.LocationPermissionQuery query =
                 new LocationAccessPolicy.LocationPermissionQuery.Builder()
                         .setCallingPackage(r.callingPackage)
@@ -3225,6 +3243,9 @@
      * If you don't need app compat logic, use {@link #checkCoarseLocationAccess(Record)}.
      */
     private boolean checkCoarseLocationAccess(Record r, int minSdk) {
+        if (r.renounceCoarseLocationAccess) {
+            return false;
+        }
         LocationAccessPolicy.LocationPermissionQuery query =
                 new LocationAccessPolicy.LocationPermissionQuery.Builder()
                         .setCallingPackage(r.callingPackage)
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a33aa60..4727b16 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -93,6 +93,7 @@
 import static android.provider.Settings.Global.NETWORK_ACCESS_TIMEOUT_MS;
 import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALL;
@@ -307,6 +308,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.EventLog;
+import android.util.FeatureFlagUtils;
 import android.util.IntArray;
 import android.util.Log;
 import android.util.Pair;
@@ -14598,6 +14600,8 @@
     private void checkExcessivePowerUsage() {
         updateCpuStatsNow();
 
+        final boolean monitorPhantomProcs = mSystemReady && FeatureFlagUtils.isEnabled(mContext,
+                SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS);
         synchronized (mProcLock) {
             final boolean doCpuKills = mLastPowerCheckUptime != 0;
             final long curUptime = SystemClock.uptimeMillis();
@@ -14623,9 +14627,11 @@
 
                     updateAppProcessCpuTimeLPr(uptimeSince, doCpuKills, checkDur, cpuLimit, app);
 
-                    // Also check the phantom processes if there is any
-                    updatePhantomProcessCpuTimeLPr(
-                            uptimeSince, doCpuKills, checkDur, cpuLimit, app);
+                    if (monitorPhantomProcs) {
+                        // Also check the phantom processes if there is any
+                        updatePhantomProcessCpuTimeLPr(
+                                uptimeSince, doCpuKills, checkDur, cpuLimit, app);
+                    }
                 }
             });
         }
@@ -15659,6 +15665,11 @@
         }
     }
 
+    @Override
+    public void enableBinderTracing() {
+        Binder.enableTracingForUid(Binder.getCallingUid());
+    }
+
     @VisibleForTesting
     public final class LocalService extends ActivityManagerInternal
             implements ActivityManagerLocal {
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 221de8d..d6a4cf6 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT;
 import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL;
 import static android.os.Process.FIRST_APPLICATION_UID;
+import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
 
 import static com.android.internal.app.procstats.ProcessStats.ADJ_MEM_FACTOR_CRITICAL;
 import static com.android.internal.app.procstats.ProcessStats.ADJ_MEM_FACTOR_LOW;
@@ -77,6 +78,7 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.DebugUtils;
+import android.util.FeatureFlagUtils;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -1805,6 +1807,8 @@
     }
 
     void updateCpuStatsNow() {
+        final boolean monitorPhantomProcs = mService.mSystemReady && FeatureFlagUtils.isEnabled(
+                mService.mContext, SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS);
         synchronized (mProcessCpuTracker) {
             mProcessCpuMutexFree.set(false);
             final long now = SystemClock.uptimeMillis();
@@ -1843,7 +1847,7 @@
                 }
             }
 
-            if (haveNewCpuStats) {
+            if (monitorPhantomProcs && haveNewCpuStats) {
                 mService.mPhantomProcessList.updateProcessCpuStatesLocked(mProcessCpuTracker);
             }
 
diff --git a/services/core/java/com/android/server/am/PhantomProcessList.java b/services/core/java/com/android/server/am/PhantomProcessList.java
index b07684c..2ec1aed 100644
--- a/services/core/java/com/android/server/am/PhantomProcessList.java
+++ b/services/core/java/com/android/server/am/PhantomProcessList.java
@@ -18,6 +18,7 @@
 
 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
 
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESSES;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
@@ -28,6 +29,7 @@
 import android.os.Handler;
 import android.os.Process;
 import android.os.StrictMode;
+import android.util.FeatureFlagUtils;
 import android.util.Slog;
 import android.util.SparseArray;
 
@@ -419,6 +421,10 @@
      * order of the oom adjs of their parent process.
      */
     void trimPhantomProcessesIfNecessary() {
+        if (!mService.mSystemReady || !FeatureFlagUtils.isEnabled(mService.mContext,
+                SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS)) {
+            return;
+        }
         synchronized (mService.mProcLock) {
             synchronized (mLock) {
                 mTrimPhantomProcessScheduled = false;
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index 20606f0..8dce8e9 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -385,9 +385,8 @@
             }
 
             public boolean isValid() {
-                return (mGameMode == GameManager.GAME_MODE_PERFORMANCE
-                        || mGameMode == GameManager.GAME_MODE_BATTERY)
-                        && (!mAllowDownscale || getCompatChangeId() != 0);
+                return mGameMode == GameManager.GAME_MODE_PERFORMANCE
+                        || mGameMode == GameManager.GAME_MODE_BATTERY;
             }
 
             /**
@@ -839,7 +838,7 @@
             }
             long scaleId = modeConfig.getCompatChangeId();
             if (scaleId == 0) {
-                Slog.w(TAG, "Invalid downscaling change id " + scaleId + " for "
+                Slog.i(TAG, "Invalid downscaling change id " + scaleId + " for "
                         + packageName);
                 return;
             }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index e4ac7be..2fcdd61 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -327,7 +327,7 @@
         }
 
         boolean isBtScoRequested = isBluetoothScoRequested();
-        if (isBtScoRequested && !wasBtScoRequested) {
+        if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) {
             if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
                 Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for pid: "
                         + pid);
diff --git a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
new file mode 100644
index 0000000..241abaf
--- /dev/null
+++ b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.content.Context;
+import android.media.AudioManager;
+import android.os.ShellCommand;
+
+import java.io.PrintWriter;
+
+
+class AudioManagerShellCommand extends ShellCommand {
+    private static final String TAG = "AudioManagerShellCommand";
+
+    private final AudioService mService;
+
+    AudioManagerShellCommand(AudioService service) {
+        mService = service;
+    }
+
+    @Override
+    public int onCommand(String cmd) {
+        if (cmd == null) {
+            return handleDefaultCommands(cmd);
+        }
+        final PrintWriter pw = getOutPrintWriter();
+        switch(cmd) {
+            case "set-surround-format-enabled":
+                return setSurroundFormatEnabled();
+            case "get-is-surround-format-enabled":
+                return getIsSurroundFormatEnabled();
+            case "set-encoded-surround-mode":
+                return setEncodedSurroundMode();
+            case "get-encoded-surround-mode":
+                return getEncodedSurroundMode();
+        }
+        return 0;
+    }
+
+    @Override
+    public void onHelp() {
+        final PrintWriter pw = getOutPrintWriter();
+        pw.println("Audio manager commands:");
+        pw.println("  help");
+        pw.println("    Print this help text.");
+        pw.println();
+        pw.println("  set-surround-format-enabled SURROUND_FORMAT IS_ENABLED");
+        pw.println("    Enables/disabled the SURROUND_FORMAT based on IS_ENABLED");
+        pw.println("  get-is-surround-format-enabled SURROUND_FORMAT");
+        pw.println("    Returns if the SURROUND_FORMAT is enabled");
+        pw.println("  set-encoded-surround-mode SURROUND_SOUND_MODE");
+        pw.println("    Sets the encoded surround sound mode to SURROUND_SOUND_MODE");
+        pw.println("  get-encoded-surround-mode");
+        pw.println("    Returns the encoded surround sound mode");
+    }
+
+    private int setSurroundFormatEnabled() {
+        String surroundFormatText = getNextArg();
+        String isSurroundFormatEnabledText = getNextArg();
+
+        if (surroundFormatText == null) {
+            getErrPrintWriter().println("Error: no surroundFormat specified");
+            return 1;
+        }
+
+        if (isSurroundFormatEnabledText == null) {
+            getErrPrintWriter().println("Error: no enabled value for surroundFormat specified");
+            return 1;
+        }
+
+        int surroundFormat = -1;
+        boolean isSurroundFormatEnabled = false;
+        try {
+            surroundFormat = Integer.parseInt(surroundFormatText);
+            isSurroundFormatEnabled = Boolean.parseBoolean(isSurroundFormatEnabledText);
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Error: wrong format specified for surroundFormat");
+            return 1;
+        }
+        if (surroundFormat < 0) {
+            getErrPrintWriter().println("Error: invalid value of surroundFormat");
+            return 1;
+        }
+
+        final Context context = mService.mContext;
+        final AudioManager am = context.getSystemService(AudioManager.class);
+        am.setSurroundFormatEnabled(surroundFormat, isSurroundFormatEnabled);
+        return 0;
+    }
+
+    private int getIsSurroundFormatEnabled() {
+        String surroundFormatText = getNextArg();
+
+        if (surroundFormatText == null) {
+            getErrPrintWriter().println("Error: no surroundFormat specified");
+            return 1;
+        }
+
+        int surroundFormat = -1;
+        try {
+            surroundFormat = Integer.parseInt(surroundFormatText);
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Error: wrong format specified for surroundFormat");
+            return 1;
+        }
+
+        if (surroundFormat < 0) {
+            getErrPrintWriter().println("Error: invalid value of surroundFormat");
+            return 1;
+        }
+        final Context context = mService.mContext;
+        final AudioManager am = context.getSystemService(AudioManager.class);
+        getOutPrintWriter().println("Value of enabled for " + surroundFormat + " is: "
+                + am.isSurroundFormatEnabled(surroundFormat));
+        return 0;
+    }
+
+    private int setEncodedSurroundMode() {
+        String encodedSurroundModeText = getNextArg();
+
+        if (encodedSurroundModeText == null) {
+            getErrPrintWriter().println("Error: no encodedSurroundMode specified");
+            return 1;
+        }
+
+        int encodedSurroundMode = -1;
+        try {
+            encodedSurroundMode = Integer.parseInt(encodedSurroundModeText);
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Error: wrong format specified for encoded surround mode");
+            return 1;
+        }
+
+        if (encodedSurroundMode < 0) {
+            getErrPrintWriter().println("Error: invalid value of encodedSurroundMode");
+            return 1;
+        }
+
+        final Context context = mService.mContext;
+        final AudioManager am = context.getSystemService(AudioManager.class);
+        am.setEncodedSurroundMode(encodedSurroundMode);
+        return 0;
+    }
+
+    private int getEncodedSurroundMode() {
+        final Context context = mService.mContext;
+        final AudioManager am = context.getSystemService(AudioManager.class);
+        getOutPrintWriter().println("Encoded surround mode: " + am.getEncodedSurroundMode());
+        return 0;
+    }
+}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 6238198..8615393 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -130,7 +130,9 @@
 import android.os.Process;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.os.ServiceManager;
+import android.os.ShellCallback;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
@@ -1996,6 +1998,18 @@
         }
     }
 
+    @Override // Binder call
+    public void onShellCommand(FileDescriptor in, FileDescriptor out,
+            FileDescriptor err, String[] args, ShellCallback callback,
+            ResultReceiver resultReceiver) {
+        if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_AUDIO_POLICY)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("Missing MANAGE_AUDIO_POLICY permission");
+        }
+        new AudioManagerShellCommand(AudioService.this).exec(this, in, out, err,
+                args, callback, resultReceiver);
+    }
+
     /** @see AudioManager#getSurroundFormats() */
     @Override
     public Map<Integer, Boolean> getSurroundFormats() {
@@ -10142,6 +10156,27 @@
         return AudioManager.SUCCESS;
     }
 
+    /** @see AudioPolicy#getFocusStack() */
+    public List<AudioFocusInfo> getFocusStack() {
+        enforceModifyAudioRoutingPermission();
+        return mMediaFocusControl.getFocusStack();
+    }
+
+    /** @see AudioPolicy#sendFocusLoss */
+    public boolean sendFocusLoss(@NonNull AudioFocusInfo focusLoser,
+            @NonNull IAudioPolicyCallback apcb) {
+        Objects.requireNonNull(focusLoser);
+        Objects.requireNonNull(apcb);
+        enforceModifyAudioRoutingPermission();
+        if (!mAudioPolicies.containsKey(apcb.asBinder())) {
+            throw new IllegalStateException("Only registered AudioPolicy can change focus");
+        }
+        if (!mAudioPolicies.get(apcb.asBinder()).mHasFocusListener) {
+            throw new IllegalStateException("AudioPolicy must have focus listener to change focus");
+        }
+        return mMediaFocusControl.sendFocusLoss(focusLoser);
+    }
+
     /** see AudioManager.hasRegisteredDynamicPolicy */
     public boolean hasRegisteredDynamicPolicy() {
         synchronized (mAudioPolicies) {
diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java
index 66f6235..69a4c23 100644
--- a/services/core/java/com/android/server/audio/MediaFocusControl.java
+++ b/services/core/java/com/android/server/audio/MediaFocusControl.java
@@ -27,6 +27,7 @@
 import android.media.AudioSystem;
 import android.media.IAudioFocusDispatcher;
 import android.media.MediaMetrics;
+import android.media.audiopolicy.AudioPolicy;
 import android.media.audiopolicy.IAudioPolicyCallback;
 import android.os.Binder;
 import android.os.Build;
@@ -221,6 +222,51 @@
         }
     }
 
+    /**
+     * Return a copy of the focus stack for external consumption (composed of AudioFocusInfo
+     * instead of FocusRequester instances)
+     * @return a SystemApi-friendly version of the focus stack, in the same order (last entry
+     *         is top of focus stack, i.e. latest focus owner)
+     * @see AudioPolicy#getFocusStack()
+     */
+    @NonNull List<AudioFocusInfo> getFocusStack() {
+        synchronized (mAudioFocusLock) {
+            final ArrayList<AudioFocusInfo> stack = new ArrayList<>(mFocusStack.size());
+            for (FocusRequester fr : mFocusStack) {
+                stack.add(fr.toAudioFocusInfo());
+            }
+            return stack;
+        }
+    }
+
+    /**
+     * Send AUDIOFOCUS_LOSS to a specific stack entry.
+     * Note this method is supporting an external API, and is restricted to LOSS in order to
+     * prevent allowing the stack to be in an invalid state (e.g. entry inside stack has focus)
+     * @param focusLoser the stack entry that is exiting the stack through a focus loss
+     * @return false if the focusLoser wasn't found in the stack, true otherwise
+     * @see AudioPolicy#sendFocusLoss(AudioFocusInfo)
+     */
+    boolean sendFocusLoss(@NonNull AudioFocusInfo focusLoser) {
+        synchronized (mAudioFocusLock) {
+            FocusRequester loserToRemove = null;
+            for (FocusRequester fr : mFocusStack) {
+                if (fr.getClientId().equals(focusLoser.getClientId())) {
+                    fr.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null,
+                            false /*forceDuck*/);
+                    loserToRemove = fr;
+                    break;
+                }
+            }
+            if (loserToRemove != null) {
+                mFocusStack.remove(loserToRemove);
+                loserToRemove.release();
+                return true;
+            }
+        }
+        return false;
+    }
+
     @GuardedBy("mAudioFocusLock")
     private void notifyTopOfAudioFocusStack() {
         // notify the top of the stack it gained focus
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index 5ce72c2..3120dc5 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -25,7 +25,6 @@
 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;
@@ -35,6 +34,7 @@
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
 import android.content.res.Configuration;
 import android.hardware.CameraSessionStats;
 import android.hardware.CameraStreamStats;
@@ -84,7 +84,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -309,8 +308,6 @@
 
     private final DisplayWindowListener mDisplayWindowListener = new DisplayWindowListener();
 
-    private final TaskStateHandler mTaskStackListener = new TaskStateHandler();
-
     public static final class TaskInfo {
         public int frontTaskId;
         public boolean isResizeable;
@@ -320,54 +317,6 @@
         public int userId;
     }
 
-    private final class TaskStateHandler extends TaskStackListener {
-        private final Object mMapLock = new Object();
-
-        // maps the package name to its corresponding current top level task id
-        @GuardedBy("mMapLock")
-        private final ArrayMap<String, TaskInfo> mTaskInfoMap = new ArrayMap<>();
-
-        @Override
-        public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
-            synchronized (mMapLock) {
-                TaskInfo info = new TaskInfo();
-                info.frontTaskId = taskInfo.taskId;
-                info.isResizeable =
-                        (taskInfo.topActivityInfo.resizeMode != RESIZE_MODE_UNRESIZEABLE);
-                info.displayId = taskInfo.displayId;
-                info.userId = taskInfo.userId;
-                info.isFixedOrientationLandscape = ActivityInfo.isFixedOrientationLandscape(
-                        taskInfo.topActivityInfo.screenOrientation);
-                info.isFixedOrientationPortrait = ActivityInfo.isFixedOrientationPortrait(
-                        taskInfo.topActivityInfo.screenOrientation);
-                mTaskInfoMap.put(taskInfo.topActivityInfo.packageName, info);
-            }
-        }
-
-        @Override
-        public void onTaskRemoved(int taskId) {
-            synchronized (mMapLock) {
-                for (Map.Entry<String, TaskInfo> entry : mTaskInfoMap.entrySet()){
-                    if (entry.getValue().frontTaskId == taskId) {
-                        mTaskInfoMap.remove(entry.getKey());
-                        break;
-                    }
-                }
-            }
-        }
-
-        public @Nullable TaskInfo getFrontTaskInfo(String packageName) {
-            synchronized (mMapLock) {
-                if (mTaskInfoMap.containsKey(packageName)) {
-                    return mTaskInfoMap.get(packageName);
-                }
-            }
-
-            Log.e(TAG, "Top task with package name: " + packageName + " not found!");
-            return null;
-        }
-    }
-
     private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -490,18 +439,53 @@
 
     private final ICameraServiceProxy.Stub mCameraServiceProxy = new ICameraServiceProxy.Stub() {
         @Override
-        public int getRotateAndCropOverride(String packageName, int lensFacing) {
+        public int getRotateAndCropOverride(String packageName, int lensFacing, int userId) {
             if (Binder.getCallingUid() != Process.CAMERASERVER_UID) {
                 Slog.e(TAG, "Calling UID: " + Binder.getCallingUid() + " doesn't match expected " +
                         " camera service UID!");
                 return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
             }
 
+            TaskInfo taskInfo = null;
+            ParceledListSlice<ActivityManager.RecentTaskInfo> recentTasks = null;
+
+            try {
+                recentTasks = ActivityTaskManager.getService().getRecentTasks(/*maxNum*/1,
+                        /*flags*/ 0, userId);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to query recent tasks!");
+                return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
+            }
+
+            if ((recentTasks != null) && (!recentTasks.getList().isEmpty())) {
+                ActivityManager.RecentTaskInfo task = recentTasks.getList().get(0);
+                if (packageName.equals(task.topActivityInfo.packageName)) {
+                    taskInfo = new TaskInfo();
+                    taskInfo.frontTaskId = task.taskId;
+                    taskInfo.isResizeable =
+                            (task.topActivityInfo.resizeMode != RESIZE_MODE_UNRESIZEABLE);
+                    taskInfo.displayId = task.displayId;
+                    taskInfo.userId = task.userId;
+                    taskInfo.isFixedOrientationLandscape =
+                            ActivityInfo.isFixedOrientationLandscape(
+                                    task.topActivityInfo.screenOrientation);
+                    taskInfo.isFixedOrientationPortrait =
+                            ActivityInfo.isFixedOrientationPortrait(
+                                    task.topActivityInfo.screenOrientation);
+                } else {
+                    Log.e(TAG, "Recent task package name: " + task.topActivityInfo.packageName
+                            + " doesn't match with camera client package name: " + packageName);
+                    return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
+                }
+            } else {
+                Log.e(TAG, "Recent task list is empty!");
+                return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
+            }
+
             // TODO: Modify the sensor orientation in camera characteristics along with any 3A
             //  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.
-            TaskInfo taskInfo = mTaskStackListener.getFrontTaskInfo(packageName);
             if ((taskInfo != null) && (CompatChanges.isChangeEnabled(
                         OVERRIDE_CAMERA_ROTATE_AND_CROP_DEFAULTS, packageName,
                         UserHandle.getUserHandleForUid(taskInfo.userId)))) {
@@ -673,12 +657,6 @@
             CameraStatsJobService.schedule(mContext);
 
             try {
-                ActivityTaskManager.getService().registerTaskStackListener(mTaskStackListener);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to register task stack listener!");
-            }
-
-            try {
                 int[] displayIds = WindowManagerGlobal.getWindowManagerService()
                         .registerDisplayWindowListener(mDisplayWindowListener);
                 for (int i = 0; i < displayIds.length; i++) {
diff --git a/services/core/java/com/android/server/communal/OWNERS b/services/core/java/com/android/server/communal/OWNERS
index e499b160..b02883d 100644
--- a/services/core/java/com/android/server/communal/OWNERS
+++ b/services/core/java/com/android/server/communal/OWNERS
@@ -1,3 +1,4 @@
 brycelee@google.com
 justinkoh@google.com
-lusilva@google.com
\ No newline at end of file
+lusilva@google.com
+xilei@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3f55848..4c56999 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1072,6 +1072,13 @@
                         + "setUserDisabledHdrTypesInternal");
                 return;
             }
+
+            // Verify if userDisabledHdrTypes contains expected HDR types
+            if (!isSubsetOf(Display.HdrCapabilities.HDR_TYPES, userDisabledHdrTypes)) {
+                Slog.e(TAG, "userDisabledHdrTypes contains unexpected types");
+                return;
+            }
+
             Arrays.sort(userDisabledHdrTypes);
             if (Arrays.equals(mUserDisabledHdrTypes, userDisabledHdrTypes)) {
                 return;
@@ -1094,6 +1101,15 @@
         }
     }
 
+    private boolean isSubsetOf(int[] sortedSuperset, int[] subset) {
+        for (int i : subset) {
+            if (Arrays.binarySearch(sortedSuperset, i) < 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private void setAreUserDisabledHdrTypesAllowedInternal(
             boolean areUserDisabledHdrTypesAllowed) {
         synchronized (mSyncRoot) {
diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
index 9412c93..43a850c 100644
--- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
+++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
@@ -20,9 +20,11 @@
 import android.content.Intent;
 import android.hardware.display.DisplayManager;
 import android.os.ShellCommand;
+import android.util.Slog;
 import android.view.Display;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
 
 class DisplayManagerShellCommand extends ShellCommand {
     private static final String TAG = "DisplayManagerShellCommand";
@@ -60,6 +62,20 @@
                 return setAmbientColorTemperatureOverride();
             case "constrain-launcher-metrics":
                 return setConstrainLauncherMetrics();
+            case "set-user-preferred-display-mode":
+                return setUserPreferredDisplayMode();
+            case "clear-user-preferred-display-mode":
+                return clearUserPreferredDisplayMode();
+            case "get-user-preferred-display-mode":
+                return getUserPreferredDisplayMode();
+            case "set-match-content-frame-rate-pref":
+                return setMatchContentFrameRateUserPreference();
+            case "get-match-content-frame-rate-pref":
+                return getMatchContentFrameRateUserPreference();
+            case "set-user-disabled-hdr-types":
+                return setUserDisabledHdrTypes();
+            case "get-user-disabled-hdr-types":
+                return getUserDisabledHdrTypes();
             default:
                 return handleDefaultCommands(cmd);
         }
@@ -93,6 +109,21 @@
         pw.println("  constrain-launcher-metrics [true|false]");
         pw.println("    Sets if Display#getRealSize and getRealMetrics should be constrained for ");
         pw.println("    Launcher.");
+        pw.println("  set-user-preferred-display-mode WIDTH HEIGHT REFRESH-RATE");
+        pw.println("    Sets the user preferred display mode which has fields WIDTH, HEIGHT and "
+                + "REFRESH-RATE");
+        pw.println("  clear-user-preferred-display-mode");
+        pw.println("    Clears the user preferred display mode");
+        pw.println("  get-user-preferred-display-mode");
+        pw.println("    Returns the user preferred display mode or null id no mode is set by user");
+        pw.println("  set-match-content-frame-rate-pref PREFERENCE");
+        pw.println("    Sets the match content frame rate preference as PREFERENCE ");
+        pw.println("  get-match-content-frame-rate-pref");
+        pw.println("    Returns the match content frame rate preference");
+        pw.println("  set-user-disabled-hdr-types TYPES...");
+        pw.println("    Sets the user disabled HDR types as TYPES");
+        pw.println("  get-user-disabled-hdr-types");
+        pw.println("    Returns the user disabled HDR types");
         pw.println();
         Intent.printIntentArgsHelp(pw , "");
     }
@@ -166,4 +197,152 @@
         mService.setShouldConstrainMetricsForLauncher(constrain);
         return 0;
     }
+
+    private int setUserPreferredDisplayMode() {
+        final String widthText = getNextArg();
+        if (widthText == null) {
+            getErrPrintWriter().println("Error: no width specified");
+            return 1;
+        }
+
+        final String heightText = getNextArg();
+        if (heightText == null) {
+            getErrPrintWriter().println("Error: no height specified");
+            return 1;
+        }
+
+        final String refreshRateText = getNextArg();
+        if (refreshRateText == null) {
+            getErrPrintWriter().println("Error: no refresh-rate specified");
+            return 1;
+        }
+
+        final int width, height;
+        final float refreshRate;
+        try {
+            width = Integer.parseInt(widthText);
+            height = Integer.parseInt(heightText);
+            refreshRate = Float.parseFloat(refreshRateText);
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Error: invalid format of width, height or refresh rate");
+            return 1;
+        }
+        if (width < 0 || height < 0 || refreshRate <= 0.0f) {
+            getErrPrintWriter().println("Error: invalid value of width, height or refresh rate");
+            return 1;
+        }
+
+        final Context context = mService.getContext();
+        final DisplayManager dm = context.getSystemService(DisplayManager.class);
+        dm.setUserPreferredDisplayMode(new Display.Mode(width, height, refreshRate));
+        return 0;
+    }
+
+    private int clearUserPreferredDisplayMode() {
+        final Context context = mService.getContext();
+        final DisplayManager dm = context.getSystemService(DisplayManager.class);
+        dm.clearUserPreferredDisplayMode();
+        return 0;
+    }
+
+    private int getUserPreferredDisplayMode() {
+        final Context context = mService.getContext();
+        final DisplayManager dm = context.getSystemService(DisplayManager.class);
+        final Display.Mode mode =  dm.getUserPreferredDisplayMode();
+        if (mode == null) {
+            getOutPrintWriter().println("User preferred display mode: null");
+            return 0;
+        }
+
+        getOutPrintWriter().println("User preferred display mode: " + mode.getPhysicalWidth() + " "
+                + mode.getPhysicalHeight() + " " + mode.getRefreshRate());
+        return 0;
+    }
+
+    private int setMatchContentFrameRateUserPreference() {
+        final String matchContentFrameRatePrefText = getNextArg();
+        if (matchContentFrameRatePrefText == null) {
+            getErrPrintWriter().println("Error: no matchContentFrameRatePref specified");
+            return 1;
+        }
+
+        final int matchContentFrameRatePreference;
+        try {
+            matchContentFrameRatePreference = Integer.parseInt(matchContentFrameRatePrefText);
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Error: invalid format of matchContentFrameRatePreference");
+            return 1;
+        }
+        if (matchContentFrameRatePreference < 0) {
+            getErrPrintWriter().println("Error: invalid value of matchContentFrameRatePreference");
+            return 1;
+        }
+
+        final Context context = mService.getContext();
+        final DisplayManager dm = context.getSystemService(DisplayManager.class);
+
+        final int refreshRateSwitchingType =
+                toRefreshRateSwitchingType(matchContentFrameRatePreference);
+        dm.setRefreshRateSwitchingType(refreshRateSwitchingType);
+        return 0;
+    }
+
+    private int getMatchContentFrameRateUserPreference() {
+        final Context context = mService.getContext();
+        final DisplayManager dm = context.getSystemService(DisplayManager.class);
+        getOutPrintWriter().println("Match content frame rate type: "
+                + dm.getMatchContentFrameRateUserPreference());
+        return 0;
+    }
+
+    private int setUserDisabledHdrTypes() {
+        final String[] userDisabledHdrTypesText = getAllArgs();
+        if (userDisabledHdrTypesText == null) {
+            getErrPrintWriter().println("Error: no userDisabledHdrTypes specified");
+            return 1;
+        }
+
+        int[] userDisabledHdrTypes = new int[userDisabledHdrTypesText.length];
+        try {
+            int index = 0;
+            for (String userDisabledHdrType : userDisabledHdrTypesText) {
+                userDisabledHdrTypes[index++] = Integer.parseInt(userDisabledHdrType);
+            }
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Error: invalid format of userDisabledHdrTypes");
+            return 1;
+        }
+
+        final Context context = mService.getContext();
+        final DisplayManager dm = context.getSystemService(DisplayManager.class);
+        dm.setUserDisabledHdrTypes(userDisabledHdrTypes);
+        return 0;
+    }
+
+    private int getUserDisabledHdrTypes() {
+        final Context context = mService.getContext();
+        final DisplayManager dm = context.getSystemService(DisplayManager.class);
+        final int[] userDisabledHdrTypes = dm.getUserDisabledHdrTypes();
+        getOutPrintWriter().println("User disabled HDR types: "
+                + Arrays.toString(userDisabledHdrTypes));
+        return 0;
+    }
+
+    @DisplayManager.SwitchingType
+    private int toRefreshRateSwitchingType(
+            @DisplayManager.MatchContentFrameRateType int matchContentFrameRateType) {
+        switch (matchContentFrameRateType) {
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER:
+                return DisplayManager.SWITCHING_TYPE_NONE;
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY:
+                return DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS;
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS:
+                return DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS;
+            case DisplayManager.MATCH_CONTENT_FRAMERATE_UNKNOWN:
+            default:
+                Slog.e(TAG, matchContentFrameRateType + " is not a valid value of "
+                        + "matchContentFrameRate type.");
+                return -1;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index 05e1bdd..3d91fee 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -39,6 +39,7 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.ArrayMap;
+import android.util.EventLog;
 import android.util.Slog;
 import android.view.IWindowManager;
 import android.view.WindowManager;
@@ -49,6 +50,7 @@
 import com.android.internal.inputmethod.InputBindResult;
 import com.android.internal.inputmethod.UnbindReason;
 import com.android.internal.view.IInputMethod;
+import com.android.server.EventLogTags;
 import com.android.server.wm.WindowManagerInternal;
 
 /**
@@ -58,6 +60,9 @@
     static final boolean DEBUG = false;
     private static final String TAG = InputMethodBindingController.class.getSimpleName();
 
+    /** Time in milliseconds that the IME service has to bind before it is reconnected. */
+    static final long TIME_TO_RECONNECT = 3 * 1000;
+
     @NonNull private final InputMethodManagerService mService;
     @NonNull private final Context mContext;
     @NonNull private final ArrayMap<String, InputMethodInfo> mMethodMap;
@@ -239,7 +244,7 @@
     }
 
     /**
-     * Indicates whether {@link #getVisibleConnection} is currently in use.
+     * Indicates whether {@link #mVisibleConnection} is currently in use.
      */
     boolean isVisibleBound() {
         return mVisibleBound;
@@ -248,11 +253,6 @@
     /**
      * Used to bring IME service up to visible adjustment while it is being shown.
      */
-    @NonNull
-    ServiceConnection getVisibleConnection() {
-        return mVisibleConnection;
-    }
-
     private final ServiceConnection mVisibleConnection = new ServiceConnection() {
         @Override public void onBindingDied(ComponentName name) {
             synchronized (mMethodMap) {
@@ -334,7 +334,7 @@
                     // We consider this to be a new bind attempt, since the system
                     // should now try to restart the service for us.
                     mLastBindTime = SystemClock.uptimeMillis();
-                    mService.clearClientSessionsLocked();
+                    clearCurMethodAndSessionsLocked();
                     mService.clearInputShowRequestLocked();
                     mService.unbindCurrentClientLocked(UnbindReason.DISCONNECT_IME);
                 }
@@ -358,11 +358,12 @@
         }
 
         mCurId = null;
-        mService.clearClientSessionsLocked();
+        clearCurMethodAndSessionsLocked();
     }
 
     @GuardedBy("mMethodMap")
-    void clearCurMethodLocked() {
+    private void clearCurMethodAndSessionsLocked() {
+        mService.clearClientSessionsLocked();
         mCurMethod = null;
         mCurMethodUid = Process.INVALID_UID;
     }
@@ -382,7 +383,7 @@
 
     @GuardedBy("mMethodMap")
     @NonNull
-    InputBindResult bindCurrentMethodLocked(int displayIdToShowIme) {
+    InputBindResult bindCurrentMethodLocked() {
         InputMethodInfo info = mMethodMap.get(mSelectedMethodId);
         if (info == null) {
             throw new IllegalArgumentException("Unknown id: " + mSelectedMethodId);
@@ -394,7 +395,7 @@
             mCurId = info.getId();
             mLastBindTime = SystemClock.uptimeMillis();
 
-            addFreshWindowTokenLocked(displayIdToShowIme);
+            addFreshWindowTokenLocked();
             return new InputBindResult(
                     InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
                     null, null, mCurId, mCurSeq, false);
@@ -419,7 +420,8 @@
     }
 
     @GuardedBy("mMethodMap")
-    private void addFreshWindowTokenLocked(int displayIdToShowIme) {
+    private void addFreshWindowTokenLocked() {
+        int displayIdToShowIme = mService.getDisplayIdToShowIme();
         mCurToken = new Binder();
 
         mService.setCurTokenDisplayId(displayIdToShowIme);
@@ -438,7 +440,7 @@
     }
 
     @GuardedBy("mMethodMap")
-    void unbindMainConnectionLocked() {
+    private void unbindMainConnectionLocked() {
         mContext.unbindService(mMainConnection);
         mHasConnection = false;
     }
@@ -460,17 +462,61 @@
     }
 
     @GuardedBy("mMethodMap")
-    boolean bindCurrentInputMethodServiceVisibleConnectionLocked() {
+    private boolean bindCurrentInputMethodServiceVisibleConnectionLocked() {
         mVisibleBound = bindCurrentInputMethodServiceLocked(mVisibleConnection,
                 IME_VISIBLE_BIND_FLAGS);
         return mVisibleBound;
     }
 
     @GuardedBy("mMethodMap")
-    boolean bindCurrentInputMethodServiceMainConnectionLocked() {
+    private boolean bindCurrentInputMethodServiceMainConnectionLocked() {
         mHasConnection = bindCurrentInputMethodServiceLocked(mMainConnection,
                 mImeConnectionBindFlags);
         return mHasConnection;
     }
 
+    /**
+     * Bind the IME so that it can be shown.
+     *
+     * <p>
+     * Performs a rebind if no binding is achieved in {@link #TIME_TO_RECONNECT} milliseconds.
+     */
+    @GuardedBy("mMethodMap")
+    void setCurrentMethodVisibleLocked() {
+        if (mCurMethod != null) {
+            if (DEBUG) Slog.d(TAG, "setCurrentMethodVisibleLocked: mCurToken=" + mCurToken);
+            if (mHasConnection && !mVisibleBound) {
+                bindCurrentInputMethodServiceVisibleConnectionLocked();
+            }
+            return;
+        }
+
+        long bindingDuration = SystemClock.uptimeMillis() - mLastBindTime;
+        if (mHasConnection && bindingDuration >= TIME_TO_RECONNECT) {
+            // The client has asked to have the input method shown, but
+            // we have been sitting here too long with a connection to the
+            // service and no interface received, so let's disconnect/connect
+            // to try to prod things along.
+            EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME, getSelectedMethodId(),
+                    bindingDuration, 1);
+            Slog.w(TAG, "Force disconnect/connect to the IME in setCurrentMethodVisibleLocked()");
+            unbindMainConnectionLocked();
+            bindCurrentInputMethodServiceMainConnectionLocked();
+        } else {
+            if (DEBUG) {
+                Slog.d(TAG, "Can't show input: connection = " + mHasConnection + ", time = "
+                        + (TIME_TO_RECONNECT - bindingDuration));
+            }
+        }
+    }
+
+    /**
+     * Remove the binding needed for the IME to be shown.
+     */
+    @GuardedBy("mMethodMap")
+    void setCurrentMethodNotVisibleLocked() {
+        if (mVisibleBound) {
+            unbindVisibleConnectionLocked();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 3c6b096..bff4f27 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -49,6 +49,8 @@
 import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 
+import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT;
+
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.Manifest;
@@ -111,12 +113,10 @@
 import android.os.UserManager;
 import android.provider.Settings;
 import android.text.TextUtils;
-import android.text.style.SuggestionSpan;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.EventLog;
 import android.util.IndentingPrintWriter;
-import android.util.LruCache;
 import android.util.Pair;
 import android.util.PrintWriterPrinter;
 import android.util.Printer;
@@ -250,10 +250,6 @@
 
     static final int MSG_NOTIFY_IME_UID_TO_AUDIO_SERVICE = 7000;
 
-    static final long TIME_TO_RECONNECT = 3 * 1000;
-
-    static final int SECURE_SUGGESTION_SPANS_MAX_SIZE = 20;
-
     private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
     private static final String TAG_TRY_SUPPRESSING_IME_SWITCHER = "TrySuppressingImeSwitcher";
     private static final String HANDLER_THREAD_NAME = "android.imms";
@@ -301,8 +297,6 @@
     // lock for this class.
     final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>();
     final ArrayMap<String, InputMethodInfo> mMethodMap = new ArrayMap<>();
-    private final LruCache<SuggestionSpan, InputMethodInfo> mSecureSuggestionSpans =
-            new LruCache<>(SECURE_SUGGESTION_SPANS_MAX_SIZE);
     final InputMethodSubtypeSwitchingController mSwitchingController;
 
     /**
@@ -312,13 +306,16 @@
     private int mMethodMapUpdateCount = 0;
 
     /**
-     * Indicates whether {@link InputMethodBindingController#getVisibleConnection} is currently
-     * in use.
+     * The display id for which the latest startInput was called.
      */
-    private boolean isVisibleBound() {
-        return mBindingController.isVisibleBound();
+    @GuardedBy("mMethodMap")
+    int getDisplayIdToShowIme() {
+        return mDisplayIdToShowIme;
     }
 
+    @GuardedBy("mMethodMap")
+    private int mDisplayIdToShowIme = INVALID_DISPLAY;
+
     // Ongoing notification
     private NotificationManager mNotificationManager;
     KeyguardManager mKeyguardManager;
@@ -2354,10 +2351,10 @@
         }
         // Compute the final shown display ID with validated cs.selfReportedDisplayId for this
         // session & other conditions.
-        final int displayIdToShowIme = computeImeDisplayIdForTarget(cs.selfReportedDisplayId,
+        mDisplayIdToShowIme = computeImeDisplayIdForTarget(cs.selfReportedDisplayId,
                 mImeDisplayValidator);
 
-        if (displayIdToShowIme == INVALID_DISPLAY) {
+        if (mDisplayIdToShowIme == INVALID_DISPLAY) {
             mImeHiddenByDisplayPolicy = true;
             hideCurrentInputLocked(mCurFocusedWindow, 0, null,
                     SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE);
@@ -2378,7 +2375,7 @@
         // Check if the input method is changing.
         // We expect the caller has already verified that the client is allowed to access this
         // display ID.
-        if (isSelectedMethodBound(displayIdToShowIme)) {
+        if (isSelectedMethodBound()) {
             if (cs.curSession != null) {
                 // Fast case: if we are already connected to the input method,
                 // then just return it.
@@ -2394,13 +2391,13 @@
 
         mBindingController.unbindCurrentMethodLocked();
 
-        return mBindingController.bindCurrentMethodLocked(displayIdToShowIme);
+        return mBindingController.bindCurrentMethodLocked();
     }
 
-    private boolean isSelectedMethodBound(int displayIdToShowIme) {
+    private boolean isSelectedMethodBound() {
         String curId = getCurId();
         return curId != null && curId.equals(getSelectedMethodId())
-                && displayIdToShowIme == mCurTokenDisplayId;
+                && mDisplayIdToShowIme == mCurTokenDisplayId;
     }
 
     @GuardedBy("mMethodMap")
@@ -2590,7 +2587,6 @@
 
             finishSessionLocked(mEnabledSession);
             mEnabledSession = null;
-            mBindingController.clearCurMethodLocked();
             scheduleNotifyImeUidToAudioService(Process.INVALID_UID);
         }
         hideStatusBarIconLocked();
@@ -3047,42 +3043,20 @@
             return false;
         }
 
-        boolean res = false;
-        IInputMethod curMethod = getCurMethod();
-        if (curMethod != null) {
-            if (DEBUG) Slog.d(TAG, "showCurrentInputLocked: mCurToken=" + getCurToken());
+        mBindingController.setCurrentMethodVisibleLocked();
+        if (getCurMethod() != null) {
             // create a placeholder token for IMS so that IMS cannot inject windows into client app.
             Binder showInputToken = new Binder();
             mShowRequestWindowMap.put(showInputToken, windowToken);
+            IInputMethod curMethod = getCurMethod();
             executeOrSendMessage(curMethod, mCaller.obtainMessageIIOOO(MSG_SHOW_SOFT_INPUT,
                     getImeShowFlagsLocked(), reason, curMethod, resultReceiver,
                     showInputToken));
             mInputShown = true;
-            if (hasConnection() && !isVisibleBound()) {
-                mBindingController.bindCurrentInputMethodServiceVisibleConnectionLocked();
-            }
-            res = true;
-        } else {
-            long bindingDuration = SystemClock.uptimeMillis() - getLastBindTime();
-            if (hasConnection() && bindingDuration >= TIME_TO_RECONNECT) {
-                // The client has asked to have the input method shown, but
-                // we have been sitting here too long with a connection to the
-                // service and no interface received, so let's disconnect/connect
-                // to try to prod things along.
-                EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME, getSelectedMethodId(),
-                        bindingDuration, 1);
-                Slog.w(TAG, "Force disconnect/connect to the IME in showCurrentInputLocked()");
-                mBindingController.unbindMainConnectionLocked();
-                mBindingController.bindCurrentInputMethodServiceMainConnectionLocked();
-            } else {
-                if (DEBUG) {
-                    Slog.d(TAG, "Can't show input: connection = " + hasConnection() + ", time = "
-                            + (TIME_TO_RECONNECT - bindingDuration));
-                }
-            }
+            return true;
         }
 
-        return res;
+        return false;
     }
 
     @Override
@@ -3166,9 +3140,7 @@
         } else {
             res = false;
         }
-        if (hasConnection() && isVisibleBound()) {
-            mBindingController.unbindVisibleConnectionLocked();
-        }
+        mBindingController.setCurrentMethodNotVisibleLocked();
         mInputShown = false;
         mShowRequested = false;
         mShowExplicitlyRequested = false;
@@ -3522,10 +3494,6 @@
         return mWindowManagerInternal.shouldRestoreImeVisibility(windowToken);
     }
 
-    private boolean isImeVisible() {
-        return (mImeWindowVis & InputMethodService.IME_VISIBLE) != 0;
-    }
-
     @GuardedBy("mMethodMap")
     private boolean canShowInputMethodPickerLocked(IInputMethodClient client) {
         // TODO(yukawa): multi-display support.
@@ -5114,7 +5082,8 @@
                     + " client=" + mCurFocusedWindowClient);
             focusedWindowClient = mCurFocusedWindowClient;
             p.println("  mCurId=" + getCurId() + " mHaveConnection=" + hasConnection()
-                    + " mBoundToMethod=" + mBoundToMethod + " mVisibleBound=" + isVisibleBound());
+                    + " mBoundToMethod=" + mBoundToMethod + " mVisibleBound="
+                    + mBindingController.isVisibleBound());
             p.println("  mCurToken=" + getCurToken());
             p.println("  mCurTokenDisplayId=" + mCurTokenDisplayId);
             p.println("  mCurHostInputToken=" + mCurHostInputToken);
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index 2550e3a..ffef803 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -112,7 +112,6 @@
 import com.android.server.location.injector.DeviceStationaryHelper;
 import com.android.server.location.injector.EmergencyHelper;
 import com.android.server.location.injector.Injector;
-import com.android.server.location.injector.LocationAttributionHelper;
 import com.android.server.location.injector.LocationPermissionsHelper;
 import com.android.server.location.injector.LocationPowerSaveModeHelper;
 import com.android.server.location.injector.LocationUsageLogger;
@@ -1705,7 +1704,6 @@
         private final SystemScreenInteractiveHelper mScreenInteractiveHelper;
         private final SystemDeviceStationaryHelper mDeviceStationaryHelper;
         private final SystemDeviceIdleHelper mDeviceIdleHelper;
-        private final LocationAttributionHelper mLocationAttributionHelper;
         private final LocationUsageLogger mLocationUsageLogger;
 
         // lazily instantiated since they may not always be used
@@ -1731,7 +1729,6 @@
             mScreenInteractiveHelper = new SystemScreenInteractiveHelper(context);
             mDeviceStationaryHelper = new SystemDeviceStationaryHelper();
             mDeviceIdleHelper = new SystemDeviceIdleHelper(context);
-            mLocationAttributionHelper = new LocationAttributionHelper(mAppOpsHelper);
             mLocationUsageLogger = new LocationUsageLogger();
         }
 
@@ -1808,11 +1805,6 @@
         }
 
         @Override
-        public LocationAttributionHelper getLocationAttributionHelper() {
-            return mLocationAttributionHelper;
-        }
-
-        @Override
         public synchronized EmergencyHelper getEmergencyHelper() {
             if (mEmergencyCallHelper == null) {
                 mEmergencyCallHelper = new SystemEmergencyHelper(mContext);
diff --git a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
index 1781588..699f143 100644
--- a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
@@ -16,6 +16,8 @@
 
 package com.android.server.location.gnss;
 
+import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
+
 import static com.android.server.location.gnss.GnssManagerService.D;
 import static com.android.server.location.gnss.GnssManagerService.TAG;
 
@@ -32,7 +34,6 @@
 import com.android.server.location.gnss.hal.GnssNative;
 import com.android.server.location.injector.AppOpsHelper;
 import com.android.server.location.injector.Injector;
-import com.android.server.location.injector.LocationAttributionHelper;
 import com.android.server.location.injector.LocationUsageLogger;
 import com.android.server.location.injector.SettingsHelper;
 
@@ -68,25 +69,23 @@
         @Nullable
         @Override
         protected void onActive() {
-            mLocationAttributionHelper.reportHighPowerLocationStart(getIdentity());
+            mAppOpsHelper.startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION, getIdentity());
         }
 
         @Nullable
         @Override
         protected void onInactive() {
-            mLocationAttributionHelper.reportHighPowerLocationStop(getIdentity());
+            mAppOpsHelper.finishOp(OP_MONITOR_HIGH_POWER_LOCATION, getIdentity());
         }
     }
 
     private final AppOpsHelper mAppOpsHelper;
-    private final LocationAttributionHelper mLocationAttributionHelper;
     private final LocationUsageLogger mLogger;
     private final GnssNative mGnssNative;
 
     public GnssMeasurementsProvider(Injector injector, GnssNative gnssNative) {
         super(injector);
         mAppOpsHelper = injector.getAppOpsHelper();
-        mLocationAttributionHelper = injector.getLocationAttributionHelper();
         mLogger = injector.getLocationUsageLogger();
         mGnssNative = gnssNative;
 
diff --git a/services/core/java/com/android/server/location/injector/Injector.java b/services/core/java/com/android/server/location/injector/Injector.java
index 173fd13..c0ce3a6 100644
--- a/services/core/java/com/android/server/location/injector/Injector.java
+++ b/services/core/java/com/android/server/location/injector/Injector.java
@@ -58,9 +58,6 @@
     /** Returns a DeviceIdleHelper. */
     DeviceIdleHelper getDeviceIdleHelper();
 
-    /** Returns a LocationAttributionHelper. */
-    LocationAttributionHelper getLocationAttributionHelper();
-
     /** Returns an EmergencyHelper. */
     EmergencyHelper getEmergencyHelper();
 
diff --git a/services/core/java/com/android/server/location/injector/LocationAttributionHelper.java b/services/core/java/com/android/server/location/injector/LocationAttributionHelper.java
deleted file mode 100644
index 4838752..0000000
--- a/services/core/java/com/android/server/location/injector/LocationAttributionHelper.java
+++ /dev/null
@@ -1,122 +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.location.injector;
-
-import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
-import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
-
-import static com.android.server.location.LocationManagerService.D;
-import static com.android.server.location.LocationManagerService.TAG;
-
-import android.location.util.identity.CallerIdentity;
-import android.util.ArrayMap;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.Map;
-
-/**
- * Helps manage appop monitoring for multiple location clients.
- */
-public class LocationAttributionHelper {
-
-    private final AppOpsHelper mAppOpsHelper;
-
-    @GuardedBy("this")
-    private final Map<CallerIdentity, Integer> mAttributions;
-    @GuardedBy("this")
-    private final Map<CallerIdentity, Integer> mHighPowerAttributions;
-
-    public LocationAttributionHelper(AppOpsHelper appOpsHelper) {
-        mAppOpsHelper = appOpsHelper;
-
-        mAttributions = new ArrayMap<>();
-        mHighPowerAttributions = new ArrayMap<>();
-    }
-
-    /**
-     * Report normal location usage for the given caller in the given bucket, with a unique key.
-     */
-    public synchronized void reportLocationStart(CallerIdentity identity) {
-        identity = CallerIdentity.forAggregation(identity);
-
-        int count = mAttributions.getOrDefault(identity, 0);
-        if (count == 0) {
-            if (mAppOpsHelper.startOpNoThrow(OP_MONITOR_LOCATION, identity)) {
-                mAttributions.put(identity, 1);
-            }
-        } else {
-            mAttributions.put(identity, count + 1);
-        }
-    }
-
-    /**
-     * Report normal location usage has stopped for the given caller in the given bucket, with a
-     * unique key.
-     */
-    public synchronized void reportLocationStop(CallerIdentity identity) {
-        identity = CallerIdentity.forAggregation(identity);
-
-        int count = mAttributions.getOrDefault(identity, 0);
-        if (count == 1) {
-            mAttributions.remove(identity);
-            mAppOpsHelper.finishOp(OP_MONITOR_LOCATION, identity);
-        } else if (count > 1) {
-            mAttributions.put(identity, count - 1);
-        }
-    }
-
-    /**
-     * Report high power location usage for the given caller in the given bucket, with a unique
-     * key.
-     */
-    public synchronized void reportHighPowerLocationStart(CallerIdentity identity) {
-        identity = CallerIdentity.forAggregation(identity);
-
-        int count = mHighPowerAttributions.getOrDefault(identity, 0);
-        if (count == 0) {
-            if (D) {
-                Log.v(TAG, "starting high power location attribution for " + identity);
-            }
-            if (mAppOpsHelper.startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION, identity)) {
-                mHighPowerAttributions.put(identity, 1);
-            }
-        } else {
-            mHighPowerAttributions.put(identity, count + 1);
-        }
-    }
-
-    /**
-     * Report high power location usage has stopped for the given caller in the given bucket,
-     * with a unique key.
-     */
-    public synchronized void reportHighPowerLocationStop(CallerIdentity identity) {
-        identity = CallerIdentity.forAggregation(identity);
-
-        int count = mHighPowerAttributions.getOrDefault(identity, 0);
-        if (count == 1) {
-            if (D) {
-                Log.v(TAG, "stopping high power location attribution for " + identity);
-            }
-            mHighPowerAttributions.remove(identity);
-            mAppOpsHelper.finishOp(OP_MONITOR_HIGH_POWER_LOCATION, identity);
-        } else if (count > 1) {
-            mHighPowerAttributions.put(identity, count - 1);
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index 0ce24dd..c1d8e78 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -16,6 +16,8 @@
 
 package com.android.server.location.provider;
 
+import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
+import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
 import static android.app.compat.CompatChanges.isChangeEnabled;
 import static android.location.LocationManager.DELIVER_HISTORICAL_LOCATIONS;
 import static android.location.LocationManager.GPS_PROVIDER;
@@ -32,6 +34,7 @@
 import static android.os.PowerManager.LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF;
 import static android.os.PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF;
 
+import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
 import static com.android.server.location.LocationManagerService.D;
 import static com.android.server.location.LocationManagerService.TAG;
 import static com.android.server.location.LocationPermissions.PERMISSION_COARSE;
@@ -98,7 +101,6 @@
 import com.android.server.location.injector.AppForegroundHelper.AppForegroundListener;
 import com.android.server.location.injector.AppOpsHelper;
 import com.android.server.location.injector.Injector;
-import com.android.server.location.injector.LocationAttributionHelper;
 import com.android.server.location.injector.LocationPermissionsHelper;
 import com.android.server.location.injector.LocationPermissionsHelper.LocationPermissionsListener;
 import com.android.server.location.injector.LocationPowerSaveModeHelper;
@@ -177,7 +179,7 @@
     protected interface LocationTransport {
 
         void deliverOnLocationChanged(LocationResult locationResult,
-                @Nullable Runnable onCompleteCallback) throws Exception;
+                @Nullable IRemoteCallback onCompleteCallback) throws Exception;
         void deliverOnFlushComplete(int requestCode) throws Exception;
     }
 
@@ -191,15 +193,14 @@
 
         private final ILocationListener mListener;
 
-        protected LocationListenerTransport(ILocationListener listener) {
+        LocationListenerTransport(ILocationListener listener) {
             mListener = Objects.requireNonNull(listener);
         }
 
         @Override
         public void deliverOnLocationChanged(LocationResult locationResult,
-                @Nullable Runnable onCompleteCallback) throws RemoteException {
-            mListener.onLocationChanged(locationResult.asList(),
-                    SingleUseCallback.wrap(onCompleteCallback));
+                @Nullable IRemoteCallback onCompleteCallback) throws RemoteException {
+            mListener.onLocationChanged(locationResult.asList(), onCompleteCallback);
         }
 
         @Override
@@ -227,7 +228,7 @@
 
         @Override
         public void deliverOnLocationChanged(LocationResult locationResult,
-                @Nullable Runnable onCompleteCallback)
+                @Nullable IRemoteCallback onCompleteCallback)
                 throws PendingIntent.CanceledException {
             BroadcastOptions options = BroadcastOptions.makeBasic();
             options.setDontSendToRestrictedApps(true);
@@ -243,20 +244,34 @@
                 intent.putExtra(KEY_LOCATIONS, locationResult.asList().toArray(new Location[0]));
             }
 
+            PendingIntent.OnFinished onFinished = null;
+
             // send() SHOULD only run the completion callback if it completes successfully. however,
-            // b/199464864 (which could not be fixed in the S timeframe) means that it's possible
+            // b/201299281 (which could not be fixed in the S timeframe) means that it's possible
             // for send() to throw an exception AND run the completion callback. if this happens, we
             // would over-release the wakelock... we take matters into our own hands to ensure that
             // the completion callback can only be run if send() completes successfully. this means
             // the completion callback may be run inline - but as we've never specified what thread
             // the callback is run on, this is fine.
-            GatedCallback gatedCallback = new GatedCallback(onCompleteCallback);
+            GatedCallback gatedCallback;
+            if (onCompleteCallback != null) {
+                gatedCallback = new GatedCallback(() -> {
+                    try {
+                        onCompleteCallback.sendResult(null);
+                    } catch (RemoteException e) {
+                        throw e.rethrowFromSystemServer();
+                    }
+                });
+                onFinished = (pI, i, rC, rD, rE) -> gatedCallback.run();
+            } else {
+                gatedCallback = new GatedCallback(null);
+            }
 
             mPendingIntent.send(
                     mContext,
                     0,
                     intent,
-                    (pI, i, rC, rD, rE) -> gatedCallback.run(),
+                    onFinished,
                     null,
                     null,
                     options.toBundle());
@@ -287,13 +302,13 @@
 
         private final ILocationCallback mCallback;
 
-        protected GetCurrentLocationTransport(ILocationCallback callback) {
+        GetCurrentLocationTransport(ILocationCallback callback) {
             mCallback = Objects.requireNonNull(callback);
         }
 
         @Override
         public void deliverOnLocationChanged(@Nullable LocationResult locationResult,
-                @Nullable Runnable onCompleteCallback)
+                @Nullable IRemoteCallback onCompleteCallback)
                 throws RemoteException {
             // ILocationCallback doesn't currently support completion callbacks
             Preconditions.checkState(onCompleteCallback == null);
@@ -398,7 +413,7 @@
             EVENT_LOG.logProviderClientActive(mName, getIdentity());
 
             if (!getRequest().isHiddenFromAppOps()) {
-                mLocationAttributionHelper.reportLocationStart(getIdentity());
+                mAppOpsHelper.startOpNoThrow(OP_MONITOR_LOCATION, getIdentity());
             }
             onHighPowerUsageChanged();
 
@@ -413,7 +428,7 @@
 
             onHighPowerUsageChanged();
             if (!getRequest().isHiddenFromAppOps()) {
-                mLocationAttributionHelper.reportLocationStop(getIdentity());
+                mAppOpsHelper.finishOp(OP_MONITOR_LOCATION, getIdentity());
             }
 
             onProviderListenerInactive();
@@ -487,11 +502,9 @@
 
                 if (!getRequest().isHiddenFromAppOps()) {
                     if (mIsUsingHighPower) {
-                        mLocationAttributionHelper.reportHighPowerLocationStart(
-                                getIdentity());
+                        mAppOpsHelper.startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION, getIdentity());
                     } else {
-                        mLocationAttributionHelper.reportHighPowerLocationStop(
-                                getIdentity());
+                        mAppOpsHelper.finishOp(OP_MONITOR_HIGH_POWER_LOCATION, getIdentity());
                     }
                 }
             }
@@ -714,6 +727,13 @@
 
         final PowerManager.WakeLock mWakeLock;
 
+        // b/206340085 - if we allocate a new wakelock releaser object for every delivery we
+        // increase the risk of resource starvation. if a client stops processing deliveries the
+        // system server binder allocation pool will be starved as we continue to queue up
+        // deliveries, each with a new allocation. in order to mitigate this, we use a single
+        // releaser object per registration rather than per delivery.
+        final ExternalWakeLockReleaser mWakeLockReleaser;
+
         private volatile ProviderTransport mProviderTransport;
         private int mNumLocationsDelivered = 0;
         private long mExpirationRealtimeMs = Long.MAX_VALUE;
@@ -727,6 +747,7 @@
                     .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
             mWakeLock.setReferenceCounted(true);
             mWakeLock.setWorkSource(request.getWorkSource());
+            mWakeLockReleaser = new ExternalWakeLockReleaser(identity, mWakeLock);
         }
 
         @Override
@@ -943,7 +964,7 @@
                     }
 
                     listener.deliverOnLocationChanged(deliverLocationResult,
-                            mUseWakeLock ? mWakeLock::release : null);
+                            mUseWakeLock ? mWakeLockReleaser : null);
                     EVENT_LOG.logProviderDeliveredLocations(mName, locationResult.size(),
                             getIdentity());
                 }
@@ -994,7 +1015,7 @@
     protected final class LocationListenerRegistration extends LocationRegistration implements
             IBinder.DeathRecipient {
 
-        protected LocationListenerRegistration(LocationRequest request, CallerIdentity identity,
+        LocationListenerRegistration(LocationRequest request, CallerIdentity identity,
                 LocationListenerTransport transport, @PermissionLevel int permissionLevel) {
             super(request, identity, transport, permissionLevel);
         }
@@ -1059,7 +1080,7 @@
     protected final class LocationPendingIntentRegistration extends LocationRegistration implements
             PendingIntent.CancelListener {
 
-        protected LocationPendingIntentRegistration(LocationRequest request,
+        LocationPendingIntentRegistration(LocationRequest request,
                 CallerIdentity identity, LocationPendingIntentTransport transport,
                 @PermissionLevel int permissionLevel) {
             super(request, identity, transport, permissionLevel);
@@ -1068,13 +1089,15 @@
         @GuardedBy("mLock")
         @Override
         protected void onLocationListenerRegister() {
-            ((PendingIntent) getKey()).registerCancelListener(this);
+            if (!((PendingIntent) getKey()).addCancelListener(DIRECT_EXECUTOR, this)) {
+                remove();
+            }
         }
 
         @GuardedBy("mLock")
         @Override
         protected void onLocationListenerUnregister() {
-            ((PendingIntent) getKey()).unregisterCancelListener(this);
+            ((PendingIntent) getKey()).removeCancelListener(this);
         }
 
         @Override
@@ -1117,7 +1140,7 @@
 
         private long mExpirationRealtimeMs = Long.MAX_VALUE;
 
-        protected GetCurrentLocationListenerRegistration(LocationRequest request,
+        GetCurrentLocationListenerRegistration(LocationRequest request,
                 CallerIdentity identity, LocationTransport transport, int permissionLevel) {
             super(request, identity, transport, permissionLevel);
         }
@@ -1326,7 +1349,6 @@
     protected final AppForegroundHelper mAppForegroundHelper;
     protected final LocationPowerSaveModeHelper mLocationPowerSaveModeHelper;
     protected final ScreenInteractiveHelper mScreenInteractiveHelper;
-    protected final LocationAttributionHelper mLocationAttributionHelper;
     protected final LocationUsageLogger mLocationUsageLogger;
     protected final LocationFudger mLocationFudger;
 
@@ -1394,7 +1416,6 @@
         mAppForegroundHelper = injector.getAppForegroundHelper();
         mLocationPowerSaveModeHelper = injector.getLocationPowerSaveModeHelper();
         mScreenInteractiveHelper = injector.getScreenInteractiveHelper();
-        mLocationAttributionHelper = injector.getLocationAttributionHelper();
         mLocationUsageLogger = injector.getLocationUsageLogger();
         mLocationFudger = new LocationFudger(mSettingsHelper.getCoarseLocationAccuracyM());
 
@@ -2761,7 +2782,7 @@
         @GuardedBy("this")
         private boolean mRun;
 
-        GatedCallback(Runnable callback) {
+        GatedCallback(@Nullable Runnable callback) {
             mCallback = callback;
         }
 
@@ -2796,4 +2817,24 @@
             }
         }
     }
+
+    private static class ExternalWakeLockReleaser extends IRemoteCallback.Stub {
+
+        private final CallerIdentity mIdentity;
+        private final PowerManager.WakeLock mWakeLock;
+
+        ExternalWakeLockReleaser(CallerIdentity identity, PowerManager.WakeLock wakeLock) {
+            mIdentity = identity;
+            mWakeLock = Objects.requireNonNull(wakeLock);
+        }
+
+        @Override
+        public void sendResult(Bundle data) {
+            try {
+                mWakeLock.release();
+            } catch (RuntimeException e) {
+                Log.e(TAG, "wakelock over-released by " + mIdentity, e);
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java
index 99cb6f0..24008d0 100644
--- a/services/core/java/com/android/server/notification/PermissionHelper.java
+++ b/services/core/java/com/android/server/notification/PermissionHelper.java
@@ -167,6 +167,7 @@
     public void setNotificationPermission(String packageName, @UserIdInt int userId, boolean grant,
             boolean userSet) {
         assertFlag();
+        final long callingId = Binder.clearCallingIdentity();
         try {
             if (grant) {
                 mPermManager.grantRuntimePermission(packageName, NOTIFICATION_PERMISSION, userId);
@@ -180,13 +181,20 @@
             }
         } catch (RemoteException e) {
             Slog.e(TAG, "Could not reach system server", e);
+        } finally {
+            Binder.restoreCallingIdentity(callingId);
         }
     }
 
     public void setNotificationPermission(PackagePermission pkgPerm) {
         assertFlag();
-        setNotificationPermission(
-                pkgPerm.packageName, pkgPerm.userId, pkgPerm.granted, pkgPerm.userSet);
+        final long callingId = Binder.clearCallingIdentity();
+        try {
+            setNotificationPermission(
+                    pkgPerm.packageName, pkgPerm.userId, pkgPerm.granted, pkgPerm.userSet);
+        } finally {
+            Binder.restoreCallingIdentity(callingId);
+        }
     }
 
     public boolean isPermissionFixed(String packageName, @UserIdInt int userId) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 0564e85..fdcf1fc 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -18,6 +18,7 @@
 
 import static android.content.pm.PackageInstaller.LOCATION_DATA_APP;
 
+import android.Manifest;
 import android.accounts.IAccountManager;
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
@@ -170,6 +171,20 @@
 
     @Override
     public int onCommand(String cmd) {
+        switch (Binder.getCallingUid()) {
+            case Process.ROOT_UID:
+            case Process.SHELL_UID:
+                break;
+            default:
+                // This is called from a test and is allowed as non-shell with the right permission
+                if ("install-incremental".equals(cmd)) {
+                    mContext.enforceCallingPermission(Manifest.permission.USE_SYSTEM_DATA_LOADERS,
+                            "Caller missing USE_SYSTEM_DATA_LOADERS permission to use " + cmd);
+                } else {
+                    throw new IllegalArgumentException("Caller must be root or shell");
+                }
+        }
+
         if (cmd == null) {
             return handleDefaultCommands(cmd);
         }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index bc4c8b0..167ad3b 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -839,7 +839,7 @@
     @Override
     public @NonNull List<UserInfo> getUsers(boolean excludePartial, boolean excludeDying,
             boolean excludePreCreated) {
-        checkManageOrCreateUsersPermission("query users");
+        checkCreateUsersPermission("query users");
         return getUsersInternal(excludePartial, excludeDying, excludePreCreated);
     }
 
@@ -865,10 +865,10 @@
     public List<UserInfo> getProfiles(@UserIdInt int userId, boolean enabledOnly) {
         boolean returnFullInfo;
         if (userId != UserHandle.getCallingUserId()) {
-            checkManageOrCreateUsersPermission("getting profiles related to user " + userId);
+            checkQueryOrCreateUsersPermission("getting profiles related to user " + userId);
             returnFullInfo = true;
         } else {
-            returnFullInfo = hasManageOrCreateUsersPermission();
+            returnFullInfo = hasCreateUsersPermission();
         }
         final long ident = Binder.clearCallingIdentity();
         try {
@@ -898,7 +898,7 @@
     public int[] getProfileIds(@UserIdInt int userId, @Nullable String userType,
             boolean enabledOnly) {
         if (userId != UserHandle.getCallingUserId()) {
-            checkManageOrCreateUsersPermission("getting profiles related to user " + userId);
+            checkQueryOrCreateUsersPermission("getting profiles related to user " + userId);
         }
         final long ident = Binder.clearCallingIdentity();
         try {
@@ -987,7 +987,7 @@
     @Override
     public boolean isSameProfileGroup(@UserIdInt int userId, int otherUserId) {
         if (userId == otherUserId) return true;
-        checkManageUsersPermission("check if in the same profile group");
+        checkQueryUsersPermission("check if in the same profile group");
         return isSameProfileGroupNoChecks(userId, otherUserId);
     }
 
@@ -1388,7 +1388,7 @@
 
     @Override
     public UserInfo getUserInfo(@UserIdInt int userId) {
-        checkManageOrCreateUsersPermission("query user");
+        checkQueryOrCreateUsersPermission("query user");
         synchronized (mUsersLock) {
             return userWithName(getUserInfoLU(userId));
         }
@@ -1519,7 +1519,7 @@
 
     @Override
     public boolean isProfile(@UserIdInt int userId) {
-        checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "isProfile");
+        checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "isProfile");
         synchronized (mUsersLock) {
             UserInfo userInfo = getUserInfoLU(userId);
             return userInfo != null && userInfo.isProfile();
@@ -1528,7 +1528,7 @@
 
     @Override
     public boolean isManagedProfile(@UserIdInt int userId) {
-        checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "isManagedProfile");
+        checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "isManagedProfile");
         synchronized (mUsersLock) {
             UserInfo userInfo = getUserInfoLU(userId);
             return userInfo != null && userInfo.isManagedProfile();
@@ -1592,7 +1592,7 @@
     @Override
     public String getUserName() {
         final int callingUid = Binder.getCallingUid();
-        if (!hasManageOrCreateUsersPermission()
+        if (!hasQueryOrCreateUsersPermission()
                 && !hasPermissionGranted(
                         android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED, callingUid)) {
             throw new SecurityException("You need MANAGE_USERS or CREATE_USERS or "
@@ -1628,18 +1628,59 @@
         }
     }
 
+    /**
+     * Enforces that the calling user is in the same profile group as {@code userId} or that only
+     * the system UID or root's UID or apps that have the
+     * {@link android.Manifest.permission#INTERACT_ACROSS_USERS INTERACT_ACROSS_USERS}
+     * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS}
+     * can make certain calls to the UserManager.
+     *
+     * @param name used as message if SecurityException is thrown
+     * @throws SecurityException if the caller lacks the required permissions.
+     */
     private void checkManageOrInteractPermissionIfCallerInOtherProfileGroup(@UserIdInt int userId,
             String name) {
         final int callingUserId = UserHandle.getCallingUserId();
-        if (callingUserId == userId || isSameProfileGroupNoChecks(callingUserId, userId) ||
-                hasManageUsersPermission()) {
+        if (callingUserId == userId || isSameProfileGroupNoChecks(callingUserId, userId)) {
             return;
         }
-        if (!hasPermissionGranted(Manifest.permission.INTERACT_ACROSS_USERS,
-                Binder.getCallingUid())) {
-            throw new SecurityException("You need INTERACT_ACROSS_USERS or MANAGE_USERS permission "
-                    + "to: check " + name);
+        if (hasManageUsersPermission()) {
+            return;
         }
+        if (hasPermissionGranted(Manifest.permission.INTERACT_ACROSS_USERS,
+                Binder.getCallingUid())) {
+            return;
+        }
+        throw new SecurityException("You need INTERACT_ACROSS_USERS or MANAGE_USERS permission "
+                + "to: check " + name);
+    }
+
+    /**
+     * Enforces that the calling user is in the same profile group as {@code userId} or that only
+     * the system UID or root's UID or apps that have the
+     * {@link android.Manifest.permission#INTERACT_ACROSS_USERS INTERACT_ACROSS_USERS}
+     * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} or
+     * {@link android.Manifest.permission#QUERY_USERS QUERY_USERS}
+     * can make certain calls to the UserManager.
+     *
+     * @param name used as message if SecurityException is thrown
+     * @throws SecurityException if the caller lacks the required permissions.
+     */
+    private void checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(
+            @UserIdInt int userId, String name) {
+        final int callingUserId = UserHandle.getCallingUserId();
+        if (callingUserId == userId || isSameProfileGroupNoChecks(callingUserId, userId)) {
+            return;
+        }
+        if (hasQueryUsersPermission()) {
+            return;
+        }
+        if (hasPermissionGranted(
+                Manifest.permission.INTERACT_ACROSS_USERS, Binder.getCallingUid())) {
+            return;
+        }
+        throw new SecurityException("You need INTERACT_ACROSS_USERS, MANAGE_USERS, or QUERY_USERS "
+                + "permission to: check " + name);
     }
 
     @Override
@@ -1667,7 +1708,7 @@
     @Override
     public boolean isRestricted(@UserIdInt int userId) {
         if (userId != UserHandle.getCallingUserId()) {
-            checkManageOrCreateUsersPermission("query isRestricted for user " + userId);
+            checkCreateUsersPermission("query isRestricted for user " + userId);
         }
         synchronized (mUsersLock) {
             final UserInfo userInfo = getUserInfoLU(userId);
@@ -2147,7 +2188,7 @@
     @Override
     public List<EnforcingUser> getUserRestrictionSources(
             String restrictionKey, @UserIdInt int userId) {
-        checkManageUsersPermission("getUserRestrictionSource");
+        checkQueryUsersPermission("call getUserRestrictionSources.");
 
         // Shortcut for the most common case
         if (!hasUserRestriction(restrictionKey, userId)) {
@@ -2186,7 +2227,7 @@
 
     @Override
     public boolean hasBaseUserRestriction(String restrictionKey, @UserIdInt int userId) {
-        checkManageOrCreateUsersPermission("hasBaseUserRestriction");
+        checkCreateUsersPermission("hasBaseUserRestriction");
         if (!UserRestrictionsUtils.isValidRestriction(restrictionKey)) {
             return false;
         }
@@ -2403,7 +2444,7 @@
      */
     @Override
     public boolean canAddMoreUsersOfType(String userType) {
-        checkManageOrCreateUsersPermission("check if more users can be added.");
+        checkCreateUsersPermission("check if more users can be added.");
         final UserTypeDetails userTypeDetails = mUserTypes.get(userType);
         return userTypeDetails != null && canAddMoreUsersOfType(userTypeDetails);
     }
@@ -2411,7 +2452,7 @@
     /** Returns whether the creation of users of the given user type is enabled on this device. */
     @Override
     public boolean isUserTypeEnabled(String userType) {
-        checkManageOrCreateUsersPermission("check if user type is enabled.");
+        checkCreateUsersPermission("check if user type is enabled.");
         final UserTypeDetails userTypeDetails = mUserTypes.get(userType);
         return userTypeDetails != null && userTypeDetails.isEnabled();
     }
@@ -2426,7 +2467,7 @@
     @Override
     public boolean canAddMoreProfilesToUser(String userType, @UserIdInt int userId,
             boolean allowedToRemoveOne) {
-        checkManageUsersPermission("check if more profiles can be added.");
+        checkQueryUsersPermission("check if more profiles can be added.");
         final UserTypeDetails type = mUserTypes.get(userType);
         if (type == null || !type.isEnabled()) {
             return false;
@@ -2536,24 +2577,58 @@
      *
      * @param message used as message if SecurityException is thrown
      * @throws SecurityException if the caller is not system or root
-     * @see #hasManageOrCreateUsersPermission()
+     * @see #hasCreateUsersPermission()
      */
-    private static final void checkManageOrCreateUsersPermission(String message) {
-        if (!hasManageOrCreateUsersPermission()) {
+    private static final void checkCreateUsersPermission(String message) {
+        if (!hasCreateUsersPermission()) {
             throw new SecurityException(
                     "You either need MANAGE_USERS or CREATE_USERS permission to: " + message);
         }
     }
 
     /**
-     * Similar to {@link #checkManageOrCreateUsersPermission(String)} but when the caller is tries
+    * Enforces that only the system UID or root's UID or apps that have the
+     * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} or
+     * {@link android.Manifest.permission#QUERY_USERS QUERY_USERS}
+     * can make certain calls to the UserManager.
+     *
+     * @param message used as message if SecurityException is thrown
+     * @throws SecurityException if the caller lacks the required permissions.
+     */
+    private static final void checkQueryUsersPermission(String message) {
+        if (!hasQueryUsersPermission()) {
+            throw new SecurityException(
+                    "You either need MANAGE_USERS or QUERY_USERS permission to: " + message);
+        }
+    }
+
+    /**
+     * Enforces that only the system UID or root's UID or apps that have the
+     * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} or
+     * {@link android.Manifest.permission#CREATE_USERS CREATE_USERS} or
+     * {@link android.Manifest.permission#QUERY_USERS QUERY_USERS}
+     * can make certain calls to the UserManager.
+     *
+     * @param message used as message if SecurityException is thrown
+     * @throws SecurityException if the caller lacks the required permissions.
+     */
+    private static final void checkQueryOrCreateUsersPermission(String message) {
+        if (!hasQueryOrCreateUsersPermission()) {
+            throw new SecurityException(
+                    "You either need MANAGE_USERS, CREATE_USERS, or QUERY_USERS permission to: "
+                            + message);
+        }
+    }
+
+    /**
+     * Similar to {@link #checkCreateUsersPermission(String)} but when the caller is tries
      * to create user/profiles other than what is allowed for
      * {@link android.Manifest.permission#CREATE_USERS CREATE_USERS} permission, then it will only
      * allow callers with {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} permission.
      */
-    private static final void checkManageOrCreateUsersPermission(int creationFlags) {
+    private static final void checkCreateUsersPermission(int creationFlags) {
         if ((creationFlags & ~ALLOWED_FLAGS_FOR_CREATE_USERS_PERMISSION) == 0) {
-            if (!hasManageOrCreateUsersPermission()) {
+            if (!hasCreateUsersPermission()) {
                 throw new SecurityException("You either need MANAGE_USERS or CREATE_USERS "
                         + "permission to create an user with flags: " + creationFlags);
             }
@@ -2597,11 +2672,31 @@
      * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} or
      * {@link android.Manifest.permission#CREATE_USERS CREATE_USERS}.
      */
-    private static final boolean hasManageOrCreateUsersPermission() {
+    private static final boolean hasCreateUsersPermission() {
         return hasManageUsersOrPermission(android.Manifest.permission.CREATE_USERS);
     }
 
     /**
+     * @return whether the calling UID is system UID or root's UID or the calling app has the
+     * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} or
+     * {@link android.Manifest.permission#QUERY_USERS QUERY_USERS}.
+     */
+    private static final boolean hasQueryUsersPermission() {
+        return hasManageUsersOrPermission(android.Manifest.permission.QUERY_USERS);
+    }
+
+    /**
+     * @return whether the calling UID is system UID or root's UID or the calling app has
+     * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} or
+     * {@link android.Manifest.permission#CREATE_USERS CREATE_USERS} or
+     * {@link android.Manifest.permission#QUERY_USERS QUERY_USERS}.
+     */
+    private static final boolean hasQueryOrCreateUsersPermission() {
+        return hasCreateUsersPermission()
+                || hasPermissionGranted(Manifest.permission.QUERY_USERS, Binder.getCallingUid());
+    }
+
+    /**
      * Enforces that only the system UID or root's UID (on any user) can make certain calls to the
      * UserManager.
      *
@@ -3477,7 +3572,7 @@
     public UserInfo createProfileForUserWithThrow(@Nullable String name, @NonNull String userType,
             @UserInfoFlag int flags, @UserIdInt int userId, @Nullable String[] disallowedPackages)
             throws ServiceSpecificException {
-        checkManageOrCreateUsersPermission(flags);
+        checkCreateUsersPermission(flags);
         try {
             return createUserInternal(name, userType, flags, userId, disallowedPackages);
         } catch (UserManager.CheckedUserOperationException e) {
@@ -3493,7 +3588,7 @@
             @NonNull String userType,
             @UserInfoFlag int flags, @UserIdInt int userId, @Nullable String[] disallowedPackages)
             throws ServiceSpecificException {
-        checkManageOrCreateUsersPermission(flags);
+        checkCreateUsersPermission(flags);
         try {
             return createUserInternalUnchecked(name, userType, flags, userId,
                     /* preCreate= */ false, disallowedPackages, /* token= */ null);
@@ -3506,7 +3601,7 @@
     public UserInfo createUserWithThrow(String name, @NonNull String userType,
             @UserInfoFlag int flags)
             throws ServiceSpecificException {
-        checkManageOrCreateUsersPermission(flags);
+        checkCreateUsersPermission(flags);
         try {
             return createUserInternal(name, userType, flags, UserHandle.USER_NULL,
                     /* disallowedPackages= */ null);
@@ -3520,7 +3615,7 @@
         final UserTypeDetails userTypeDetails = mUserTypes.get(userType);
         final int flags = userTypeDetails != null ? userTypeDetails.getDefaultUserInfoFlags() : 0;
 
-        checkManageOrCreateUsersPermission(flags);
+        checkCreateUsersPermission(flags);
 
         Preconditions.checkArgument(isUserTypeEligibleForPreCreation(userTypeDetails),
                 "cannot pre-create user of type " + userType);
@@ -3540,7 +3635,7 @@
             String userName, String userType, @UserInfoFlag int flags,
             Bitmap userIcon,
             String accountName, String accountType, PersistableBundle accountOptions) {
-        checkManageOrCreateUsersPermission(flags);
+        checkCreateUsersPermission(flags);
 
         if (someUserHasAccountNoChecks(accountName, accountType)) {
             throw new ServiceSpecificException(
@@ -3985,7 +4080,7 @@
 
     @Override
     public String[] getPreInstallableSystemPackages(@NonNull String userType) {
-        checkManageOrCreateUsersPermission("getPreInstallableSystemPackages");
+        checkCreateUsersPermission("getPreInstallableSystemPackages");
         final Set<String> installableSystemPackages =
                 mSystemPackageInstaller.getInstallablePackagesForUserType(userType);
         if (installableSystemPackages == null) {
@@ -4110,7 +4205,7 @@
      */
     @Override
     public UserInfo createRestrictedProfileWithThrow(@Nullable String name, int parentUserId) {
-        checkManageOrCreateUsersPermission("setupRestrictedProfile");
+        checkCreateUsersPermission("setupRestrictedProfile");
         final UserInfo user = createProfileForUserWithThrow(
                 name, UserManager.USER_TYPE_FULL_RESTRICTED, 0, parentUserId, null);
         if (user == null) {
@@ -4207,7 +4302,7 @@
     @Override
     public boolean removeUser(@UserIdInt int userId) {
         Slog.i(LOG_TAG, "removeUser u" + userId);
-        checkManageOrCreateUsersPermission("Only the system can remove users");
+        checkCreateUsersPermission("Only the system can remove users");
 
         final String restriction = getUserRemovalRestriction(userId);
         if (getUserRestrictions(UserHandle.getCallingUserId()).getBoolean(restriction, false)) {
@@ -4219,7 +4314,7 @@
 
     @Override
     public boolean removeUserEvenWhenDisallowed(@UserIdInt int userId) {
-        checkManageOrCreateUsersPermission("Only the system can remove users");
+        checkCreateUsersPermission("Only the system can remove users");
         return removeUserUnchecked(userId);
     }
 
@@ -4334,7 +4429,7 @@
     @Override
     public @UserManager.RemoveResult int removeUserOrSetEphemeral(@UserIdInt int userId,
             boolean evenWhenDisallowed) {
-        checkManageOrCreateUsersPermission("Only the system can remove users");
+        checkCreateUsersPermission("Only the system can remove users");
 
         if (!evenWhenDisallowed) {
             final String restriction = getUserRemovalRestriction(userId);
@@ -5085,7 +5180,7 @@
 
     @Override
     public boolean someUserHasAccount(String accountName, String accountType) {
-        checkManageOrCreateUsersPermission("check seed account information");
+        checkCreateUsersPermission("check seed account information");
         return someUserHasAccountNoChecks(accountName, accountType);
     }
 
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
index 111087f..a3b0e3e 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
@@ -1197,6 +1197,7 @@
             @Nullable @UserIdInt Integer userId,
             @NonNull Function<String, PackageStateInternal> pkgSettingFunction)
             throws NameNotFoundException {
+        mEnforcer.assertApprovedQuerent(mConnection.getCallingUid(), mProxy);
         synchronized (mLock) {
             mDebug.printState(writer, packageName, userId, pkgSettingFunction, mAttachedPkgStates);
         }
diff --git a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java
index 122b3f3..1aa7598 100644
--- a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java
@@ -42,6 +42,7 @@
 import android.media.tv.interactive.TvIAppService;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteCallbackList;
@@ -652,6 +653,9 @@
                         userState.mServiceStateMap.put(componentName, serviceState);
                     } else if (serviceState.mService != null) {
                         serviceState.mService.prepare(type);
+                    } else {
+                        serviceState.mPendingPrepare = true;
+                        serviceState.mPendingPrepareType = type;
                     }
                 }
             } catch (RemoteException e) {
@@ -662,6 +666,40 @@
         }
 
         @Override
+        public void notifyAppLinkInfo(String tiasId, Bundle appLinkInfo, int userId) {
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
+                    Binder.getCallingUid(), userId, "notifyAppLinkInfo");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+                    TvIAppState iAppState = userState.mIAppMap.get(tiasId);
+                    if (iAppState == null) {
+                        Slogf.e(TAG, "failed to notifyAppLinkInfo - unknown TIAS id "
+                                + tiasId);
+                        return;
+                    }
+                    ComponentName componentName = iAppState.mInfo.getComponent();
+                    ServiceState serviceState = userState.mServiceStateMap.get(componentName);
+                    if (serviceState == null) {
+                        serviceState = new ServiceState(
+                                componentName, tiasId, resolvedUserId);
+                        serviceState.addPendingAppLink(appLinkInfo);
+                        userState.mServiceStateMap.put(componentName, serviceState);
+                    } else if (serviceState.mService != null) {
+                        serviceState.mService.notifyAppLinkInfo(appLinkInfo);
+                    } else {
+                        serviceState.addPendingAppLink(appLinkInfo);
+                    }
+                }
+            } catch (RemoteException e) {
+                Slogf.e(TAG, "error in notifyAppLinkInfo", e);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public void createSession(final ITvIAppClient client, final String iAppServiceId, int type,
                 int seq, int userId) {
             final int callingUid = Binder.getCallingUid();
@@ -1237,6 +1275,7 @@
         private final ServiceConnection mConnection;
         private final ComponentName mComponent;
         private final String mIAppSeriviceId;
+        private final List<Bundle> mPendingAppLinkInfo = new ArrayList<>();
 
         private boolean mPendingPrepare = false;
         private Integer mPendingPrepareType = null;
@@ -1257,6 +1296,10 @@
             mConnection = new IAppServiceConnection(component, userId);
             mIAppSeriviceId = tias;
         }
+
+        private void addPendingAppLink(Bundle info) {
+            mPendingAppLinkInfo.add(info);
+        }
     }
 
     private final class IAppServiceConnection implements ServiceConnection {
@@ -1296,6 +1339,23 @@
                     }
                 }
 
+                if (!serviceState.mPendingAppLinkInfo.isEmpty()) {
+                    for (Iterator<Bundle> it = serviceState.mPendingAppLinkInfo.iterator();
+                            it.hasNext(); ) {
+                        Bundle appLinkInfo = it.next();
+                        final long identity = Binder.clearCallingIdentity();
+                        try {
+                            serviceState.mService.notifyAppLinkInfo(appLinkInfo);
+                            it.remove();
+                        } catch (RemoteException e) {
+                            Slogf.e(TAG, "error in notifyAppLinkInfo(" + appLinkInfo
+                                    + ") when onServiceConnected", e);
+                        } finally {
+                            Binder.restoreCallingIdentity(identity);
+                        }
+                    }
+                }
+
                 List<IBinder> tokensToBeRemoved = new ArrayList<>();
 
                 // And create sessions, if any.
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index be01646..173545c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4441,6 +4441,10 @@
     }
 
     private void updateFontScaleIfNeeded(@UserIdInt int userId) {
+        if (userId != getCurrentUserId()) {
+            return;
+        }
+
         final float scaleFactor = Settings.System.getFloatForUser(mContext.getContentResolver(),
                 FONT_SCALE, 1.0f, userId);
 
@@ -4457,6 +4461,10 @@
     }
 
     private void updateFontWeightAdjustmentIfNeeded(@UserIdInt int userId) {
+        if (userId != getCurrentUserId()) {
+            return;
+        }
+
         final int fontWeightAdjustment =
                 Settings.Secure.getIntForUser(
                         mContext.getContentResolver(),
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index eb68952..44d3623 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -167,7 +167,6 @@
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.ColorSpace;
-import android.graphics.GraphicBuffer;
 import android.graphics.Insets;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -4092,8 +4091,7 @@
         // Make IME snapshot as trusted overlay
         InputMonitor.setTrustedOverlayInputInfo(imeSurface, t, getDisplayId(),
                 "IME-snapshot-surface");
-        GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(buffer);
-        t.setBuffer(imeSurface, graphicBuffer);
+        t.setBuffer(imeSurface, buffer);
         t.setColorSpace(mSurfaceControl, ColorSpace.get(ColorSpace.Named.SRGB));
         t.setRelativeLayer(imeSurface, activity.getSurfaceControl(), 1);
         t.setPosition(imeSurface, mInputMethodWindow.getDisplayFrame().left,
diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java
index 94a175c..8a2d116 100644
--- a/services/core/java/com/android/server/wm/LockTaskController.java
+++ b/services/core/java/com/android/server/wm/LockTaskController.java
@@ -251,15 +251,47 @@
      */
     boolean activityBlockedFromFinish(ActivityRecord activity) {
         final Task task = activity.getTask();
-        if (activity == task.getRootActivity()
-                && activity == task.getTopNonFinishingActivity()
-                && task.mLockTaskAuth != LOCK_TASK_AUTH_LAUNCHABLE_PRIV
-                && isRootTask(task)) {
-            Slog.i(TAG, "Not finishing task in lock task mode");
-            showLockTaskToast();
-            return true;
+        if (task.mLockTaskAuth == LOCK_TASK_AUTH_LAUNCHABLE_PRIV || !isRootTask(task)) {
+            return false;
         }
-        return false;
+
+        final ActivityRecord taskTop = task.getTopNonFinishingActivity();
+        final ActivityRecord taskRoot = task.getRootActivity();
+        // If task has more than one Activity, verify if there's only adjacent TaskFragments that
+        // should be finish together in the Task.
+        if (activity != taskRoot || activity != taskTop) {
+            final TaskFragment taskFragment = activity.getTaskFragment();
+            final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
+            if (taskFragment.asTask() != null
+                    || !taskFragment.isDelayLastActivityRemoval()
+                    || adjacentTaskFragment == null) {
+                // Don't block activity from finishing if the TaskFragment don't have any adjacent
+                // TaskFragment, or it won't finish together with its adjacent TaskFragment.
+                return false;
+            }
+
+            final boolean hasOtherActivityInTaskFragment =
+                    taskFragment.getActivity(a -> !a.finishing && a != activity) != null;
+            if (hasOtherActivityInTaskFragment) {
+                // Don't block activity from finishing if there's other Activity in the same
+                // TaskFragment.
+                return false;
+            }
+
+            final boolean hasOtherActivityInTask = task.getActivity(a -> !a.finishing
+                    && a != activity && a.getTaskFragment() != adjacentTaskFragment) != null;
+            if (hasOtherActivityInTask) {
+                // Do not block activity from finishing if there are another running activities
+                // after the current and adjacent TaskFragments are removed. Note that we don't
+                // check activities in adjacent TaskFragment because it will be finished together
+                // with TaskFragment regardless of numbers of activities.
+                return false;
+            }
+        }
+
+        Slog.i(TAG, "Not finishing task in lock task mode");
+        showLockTaskToast();
+        return true;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index c845dca..97cb512 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -240,7 +240,7 @@
     /**
      * 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.
+     * opportunity to perform animations and finishing the adjacent TaskFragment.
      */
     private boolean mDelayLastActivityRemoval;
 
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index f72f2cc..f5eb0a7 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -35,6 +35,7 @@
         "com_android_server_am_BatteryStatsService.cpp",
         "com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp",
         "com_android_server_ConsumerIrService.cpp",
+        "com_android_server_companion_virtual_InputController.cpp",
         "com_android_server_devicepolicy_CryptoTestHelper.cpp",
         "com_android_server_connectivity_Vpn.cpp",
         "com_android_server_gpu_GpuService.cpp",
diff --git a/services/core/jni/com_android_server_UsbAlsaJackDetector.cpp b/services/core/jni/com_android_server_UsbAlsaJackDetector.cpp
index ccb4f59..1c574fb 100644
--- a/services/core/jni/com_android_server_UsbAlsaJackDetector.cpp
+++ b/services/core/jni/com_android_server_UsbAlsaJackDetector.cpp
@@ -151,13 +151,8 @@
         return -1;
     }
 
-    if (!jniRegisterNativeMethods(env, "com/android/server/usb/UsbAlsaJackDetector",
-            method_table, NELEM(method_table))) {
-      ALOGE("Can't register UsbAlsaJackDetector native methods");
-      return -1;
-    }
-
-    return 0;
+    return jniRegisterNativeMethods(env, "com/android/server/usb/UsbAlsaJackDetector",
+            method_table, NELEM(method_table));
 }
 
 }
diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
new file mode 100644
index 0000000..43018a9
--- /dev/null
+++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 "InputController"
+
+#include <android-base/unique_fd.h>
+#include <android/input.h>
+#include <android/keycodes.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/uinput.h>
+#include <math.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
+#include <utils/Log.h>
+
+#include <map>
+#include <string>
+
+namespace android {
+
+enum class DeviceType {
+    KEYBOARD,
+    MOUSE,
+    TOUCHSCREEN,
+};
+
+enum class UinputAction {
+    RELEASE = 0,
+    PRESS = 1,
+    MOVE = 2,
+    CANCEL = 3,
+};
+
+static std::map<int, UinputAction> BUTTON_ACTION_MAPPING = {
+        {AMOTION_EVENT_ACTION_BUTTON_PRESS, UinputAction::PRESS},
+        {AMOTION_EVENT_ACTION_BUTTON_RELEASE, UinputAction::RELEASE},
+};
+
+static std::map<int, UinputAction> KEY_ACTION_MAPPING = {
+        {AKEY_EVENT_ACTION_DOWN, UinputAction::PRESS},
+        {AKEY_EVENT_ACTION_UP, UinputAction::RELEASE},
+};
+
+static std::map<int, UinputAction> TOUCH_ACTION_MAPPING = {
+        {AMOTION_EVENT_ACTION_DOWN, UinputAction::PRESS},
+        {AMOTION_EVENT_ACTION_UP, UinputAction::RELEASE},
+        {AMOTION_EVENT_ACTION_MOVE, UinputAction::MOVE},
+        {AMOTION_EVENT_ACTION_CANCEL, UinputAction::CANCEL},
+};
+
+// Button code mapping from https://source.android.com/devices/input/touch-devices
+static std::map<int, int> BUTTON_CODE_MAPPING = {
+        {AMOTION_EVENT_BUTTON_PRIMARY, BTN_LEFT},    {AMOTION_EVENT_BUTTON_SECONDARY, BTN_RIGHT},
+        {AMOTION_EVENT_BUTTON_TERTIARY, BTN_MIDDLE}, {AMOTION_EVENT_BUTTON_BACK, BTN_BACK},
+        {AMOTION_EVENT_BUTTON_FORWARD, BTN_FORWARD},
+};
+
+// Tool type mapping from https://source.android.com/devices/input/touch-devices
+static std::map<int, int> TOOL_TYPE_MAPPING = {
+        {AMOTION_EVENT_TOOL_TYPE_FINGER, MT_TOOL_FINGER},
+        {AMOTION_EVENT_TOOL_TYPE_PALM, MT_TOOL_PALM},
+};
+
+// Keycode mapping from https://source.android.com/devices/input/keyboard-devices
+static std::map<int, int> KEY_CODE_MAPPING = {
+        {AKEYCODE_0, KEY_0},
+        {AKEYCODE_1, KEY_1},
+        {AKEYCODE_2, KEY_2},
+        {AKEYCODE_3, KEY_3},
+        {AKEYCODE_4, KEY_4},
+        {AKEYCODE_5, KEY_5},
+        {AKEYCODE_6, KEY_6},
+        {AKEYCODE_7, KEY_7},
+        {AKEYCODE_8, KEY_8},
+        {AKEYCODE_9, KEY_9},
+        {AKEYCODE_A, KEY_A},
+        {AKEYCODE_B, KEY_B},
+        {AKEYCODE_C, KEY_C},
+        {AKEYCODE_D, KEY_D},
+        {AKEYCODE_E, KEY_E},
+        {AKEYCODE_F, KEY_F},
+        {AKEYCODE_G, KEY_G},
+        {AKEYCODE_H, KEY_H},
+        {AKEYCODE_I, KEY_I},
+        {AKEYCODE_J, KEY_J},
+        {AKEYCODE_K, KEY_K},
+        {AKEYCODE_L, KEY_L},
+        {AKEYCODE_M, KEY_M},
+        {AKEYCODE_N, KEY_N},
+        {AKEYCODE_O, KEY_O},
+        {AKEYCODE_P, KEY_P},
+        {AKEYCODE_Q, KEY_Q},
+        {AKEYCODE_R, KEY_R},
+        {AKEYCODE_S, KEY_S},
+        {AKEYCODE_T, KEY_T},
+        {AKEYCODE_U, KEY_U},
+        {AKEYCODE_V, KEY_V},
+        {AKEYCODE_W, KEY_W},
+        {AKEYCODE_X, KEY_X},
+        {AKEYCODE_Y, KEY_Y},
+        {AKEYCODE_Z, KEY_Z},
+        {AKEYCODE_GRAVE, KEY_GRAVE},
+        {AKEYCODE_MINUS, KEY_MINUS},
+        {AKEYCODE_EQUALS, KEY_EQUAL},
+        {AKEYCODE_LEFT_BRACKET, KEY_LEFTBRACE},
+        {AKEYCODE_RIGHT_BRACKET, KEY_RIGHTBRACE},
+        {AKEYCODE_BACKSLASH, KEY_BACKSLASH},
+        {AKEYCODE_SEMICOLON, KEY_SEMICOLON},
+        {AKEYCODE_APOSTROPHE, KEY_APOSTROPHE},
+        {AKEYCODE_COMMA, KEY_COMMA},
+        {AKEYCODE_PERIOD, KEY_DOT},
+        {AKEYCODE_SLASH, KEY_SLASH},
+        {AKEYCODE_ALT_LEFT, KEY_LEFTALT},
+        {AKEYCODE_ALT_RIGHT, KEY_RIGHTALT},
+        {AKEYCODE_CTRL_LEFT, KEY_LEFTCTRL},
+        {AKEYCODE_CTRL_RIGHT, KEY_RIGHTCTRL},
+        {AKEYCODE_SHIFT_LEFT, KEY_LEFTSHIFT},
+        {AKEYCODE_SHIFT_RIGHT, KEY_RIGHTSHIFT},
+        {AKEYCODE_META_LEFT, KEY_LEFTMETA},
+        {AKEYCODE_META_RIGHT, KEY_RIGHTMETA},
+        {AKEYCODE_CAPS_LOCK, KEY_CAPSLOCK},
+        {AKEYCODE_SCROLL_LOCK, KEY_SCROLLLOCK},
+        {AKEYCODE_NUM_LOCK, KEY_NUMLOCK},
+        {AKEYCODE_ENTER, KEY_ENTER},
+        {AKEYCODE_TAB, KEY_TAB},
+        {AKEYCODE_SPACE, KEY_SPACE},
+        {AKEYCODE_DPAD_DOWN, KEY_DOWN},
+        {AKEYCODE_DPAD_UP, KEY_UP},
+        {AKEYCODE_DPAD_LEFT, KEY_LEFT},
+        {AKEYCODE_DPAD_RIGHT, KEY_RIGHT},
+        {AKEYCODE_MOVE_END, KEY_END},
+        {AKEYCODE_MOVE_HOME, KEY_HOME},
+        {AKEYCODE_PAGE_DOWN, KEY_PAGEDOWN},
+        {AKEYCODE_PAGE_UP, KEY_PAGEUP},
+        {AKEYCODE_DEL, KEY_BACKSPACE},
+        {AKEYCODE_FORWARD_DEL, KEY_DELETE},
+        {AKEYCODE_INSERT, KEY_INSERT},
+        {AKEYCODE_ESCAPE, KEY_ESC},
+        {AKEYCODE_BREAK, KEY_PAUSE},
+        {AKEYCODE_F1, KEY_F1},
+        {AKEYCODE_F2, KEY_F2},
+        {AKEYCODE_F3, KEY_F3},
+        {AKEYCODE_F4, KEY_F4},
+        {AKEYCODE_F5, KEY_F5},
+        {AKEYCODE_F6, KEY_F6},
+        {AKEYCODE_F7, KEY_F7},
+        {AKEYCODE_F8, KEY_F8},
+        {AKEYCODE_F9, KEY_F9},
+        {AKEYCODE_F10, KEY_F10},
+        {AKEYCODE_F11, KEY_F11},
+        {AKEYCODE_F12, KEY_F12},
+        {AKEYCODE_BACK, KEY_BACK},
+        {AKEYCODE_FORWARD, KEY_FORWARD},
+        {AKEYCODE_NUMPAD_1, KEY_KP1},
+        {AKEYCODE_NUMPAD_2, KEY_KP2},
+        {AKEYCODE_NUMPAD_3, KEY_KP3},
+        {AKEYCODE_NUMPAD_4, KEY_KP4},
+        {AKEYCODE_NUMPAD_5, KEY_KP5},
+        {AKEYCODE_NUMPAD_6, KEY_KP6},
+        {AKEYCODE_NUMPAD_7, KEY_KP7},
+        {AKEYCODE_NUMPAD_8, KEY_KP8},
+        {AKEYCODE_NUMPAD_9, KEY_KP9},
+        {AKEYCODE_NUMPAD_0, KEY_KP0},
+        {AKEYCODE_NUMPAD_ADD, KEY_KPPLUS},
+        {AKEYCODE_NUMPAD_SUBTRACT, KEY_KPMINUS},
+        {AKEYCODE_NUMPAD_MULTIPLY, KEY_KPASTERISK},
+        {AKEYCODE_NUMPAD_DIVIDE, KEY_KPSLASH},
+        {AKEYCODE_NUMPAD_DOT, KEY_KPDOT},
+        {AKEYCODE_NUMPAD_ENTER, KEY_KPENTER},
+        {AKEYCODE_NUMPAD_EQUALS, KEY_KPEQUAL},
+        {AKEYCODE_NUMPAD_COMMA, KEY_KPCOMMA},
+};
+
+/** Creates a new uinput device and assigns a file descriptor. */
+static int openUinput(const char* readableName, jint vendorId, jint productId,
+                      DeviceType deviceType, jint screenHeight, jint screenWidth) {
+    android::base::unique_fd fd(TEMP_FAILURE_RETRY(::open("/dev/uinput", O_WRONLY | O_NONBLOCK)));
+    if (fd < 0) {
+        ALOGE("Error creating uinput device: %s", strerror(errno));
+        return -errno;
+    }
+
+    ioctl(fd, UI_SET_EVBIT, EV_KEY);
+    ioctl(fd, UI_SET_EVBIT, EV_SYN);
+    switch (deviceType) {
+        case DeviceType::KEYBOARD:
+            for (const auto& [ignored, keyCode] : KEY_CODE_MAPPING) {
+                ioctl(fd, UI_SET_KEYBIT, keyCode);
+            }
+            break;
+        case DeviceType::MOUSE:
+            ioctl(fd, UI_SET_EVBIT, EV_REL);
+            ioctl(fd, UI_SET_KEYBIT, BTN_LEFT);
+            ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT);
+            ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE);
+            ioctl(fd, UI_SET_KEYBIT, BTN_BACK);
+            ioctl(fd, UI_SET_KEYBIT, BTN_FORWARD);
+            ioctl(fd, UI_SET_RELBIT, REL_X);
+            ioctl(fd, UI_SET_RELBIT, REL_Y);
+            ioctl(fd, UI_SET_RELBIT, REL_WHEEL);
+            ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);
+            break;
+        case DeviceType::TOUCHSCREEN:
+            ioctl(fd, UI_SET_EVBIT, EV_ABS);
+            ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH);
+            ioctl(fd, UI_SET_ABSBIT, ABS_MT_SLOT);
+            ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_X);
+            ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_Y);
+            ioctl(fd, UI_SET_ABSBIT, ABS_MT_TRACKING_ID);
+            ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOOL_TYPE);
+            ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOUCH_MAJOR);
+            ioctl(fd, UI_SET_ABSBIT, ABS_MT_PRESSURE);
+            ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
+    }
+
+    int version;
+    if (ioctl(fd, UI_GET_VERSION, &version) == 0 && version >= 5) {
+        uinput_setup setup;
+        memset(&setup, 0, sizeof(setup));
+        strlcpy(setup.name, readableName, UINPUT_MAX_NAME_SIZE);
+        setup.id.version = 1;
+        setup.id.bustype = BUS_VIRTUAL;
+        setup.id.vendor = vendorId;
+        setup.id.product = productId;
+        if (deviceType == DeviceType::TOUCHSCREEN) {
+            uinput_abs_setup xAbsSetup;
+            xAbsSetup.code = ABS_MT_POSITION_X;
+            xAbsSetup.absinfo.maximum = screenWidth - 1;
+            xAbsSetup.absinfo.minimum = 0;
+            ioctl(fd, UI_ABS_SETUP, xAbsSetup);
+            uinput_abs_setup yAbsSetup;
+            yAbsSetup.code = ABS_MT_POSITION_Y;
+            yAbsSetup.absinfo.maximum = screenHeight - 1;
+            yAbsSetup.absinfo.minimum = 0;
+            ioctl(fd, UI_ABS_SETUP, yAbsSetup);
+            uinput_abs_setup majorAbsSetup;
+            majorAbsSetup.code = ABS_MT_TOUCH_MAJOR;
+            majorAbsSetup.absinfo.maximum = screenWidth - 1;
+            majorAbsSetup.absinfo.minimum = 0;
+            ioctl(fd, UI_ABS_SETUP, majorAbsSetup);
+            uinput_abs_setup pressureAbsSetup;
+            pressureAbsSetup.code = ABS_MT_PRESSURE;
+            pressureAbsSetup.absinfo.maximum = 255;
+            pressureAbsSetup.absinfo.minimum = 0;
+            ioctl(fd, UI_ABS_SETUP, pressureAbsSetup);
+        }
+        if (ioctl(fd, UI_DEV_SETUP, &setup) != 0) {
+            ALOGE("Error creating uinput device: %s", strerror(errno));
+            return -errno;
+        }
+    } else {
+        // UI_DEV_SETUP was not introduced until version 5. Try setting up manually.
+        uinput_user_dev fallback;
+        memset(&fallback, 0, sizeof(fallback));
+        strlcpy(fallback.name, readableName, UINPUT_MAX_NAME_SIZE);
+        fallback.id.version = 1;
+        fallback.id.bustype = BUS_VIRTUAL;
+        fallback.id.vendor = vendorId;
+        fallback.id.product = productId;
+        if (deviceType == DeviceType::TOUCHSCREEN) {
+            fallback.absmin[ABS_MT_POSITION_X] = 0;
+            fallback.absmax[ABS_MT_POSITION_X] = screenWidth - 1;
+            fallback.absmin[ABS_MT_POSITION_Y] = 0;
+            fallback.absmax[ABS_MT_POSITION_Y] = screenHeight - 1;
+            fallback.absmin[ABS_MT_TOUCH_MAJOR] = 0;
+            fallback.absmax[ABS_MT_TOUCH_MAJOR] = screenWidth - 1;
+            fallback.absmin[ABS_MT_PRESSURE] = 0;
+            fallback.absmax[ABS_MT_PRESSURE] = 255;
+        }
+        if (TEMP_FAILURE_RETRY(write(fd, &fallback, sizeof(fallback))) != sizeof(fallback)) {
+            ALOGE("Error creating uinput device: %s", strerror(errno));
+            return -errno;
+        }
+    }
+
+    if (ioctl(fd, UI_DEV_CREATE) != 0) {
+        ALOGE("Error creating uinput device: %s", strerror(errno));
+        return -errno;
+    }
+
+    return fd.release();
+}
+
+static int openUinputJni(JNIEnv* env, jstring name, jint vendorId, jint productId,
+                         DeviceType deviceType, int screenHeight, int screenWidth) {
+    ScopedUtfChars readableName(env, name);
+    return openUinput(readableName.c_str(), vendorId, productId, deviceType, screenHeight,
+                      screenWidth);
+}
+
+static int nativeOpenUinputKeyboard(JNIEnv* env, jobject thiz, jstring name, jint vendorId,
+                                    jint productId) {
+    return openUinputJni(env, name, vendorId, productId, DeviceType::KEYBOARD, /* screenHeight */ 0,
+                         /* screenWidth */ 0);
+}
+
+static int nativeOpenUinputMouse(JNIEnv* env, jobject thiz, jstring name, jint vendorId,
+                                 jint productId) {
+    return openUinputJni(env, name, vendorId, productId, DeviceType::MOUSE, /* screenHeight */ 0,
+                         /* screenWidth */ 0);
+}
+
+static int nativeOpenUinputTouchscreen(JNIEnv* env, jobject thiz, jstring name, jint vendorId,
+                                       jint productId, jint height, jint width) {
+    return openUinputJni(env, name, vendorId, productId, DeviceType::TOUCHSCREEN, height, width);
+}
+
+static bool nativeCloseUinput(JNIEnv* env, jobject thiz, jint fd) {
+    ioctl(fd, UI_DEV_DESTROY);
+    return close(fd);
+}
+
+static bool writeInputEvent(int fd, uint16_t type, uint16_t code, int32_t value) {
+    struct input_event ev = {.type = type, .code = code, .value = value};
+    return TEMP_FAILURE_RETRY(write(fd, &ev, sizeof(struct input_event))) == sizeof(ev);
+}
+
+static bool nativeWriteKeyEvent(JNIEnv* env, jobject thiz, jint fd, jint androidKeyCode,
+                                jint action) {
+    auto keyCodeIterator = KEY_CODE_MAPPING.find(androidKeyCode);
+    if (keyCodeIterator == KEY_CODE_MAPPING.end()) {
+        ALOGE("No supportive native keycode for androidKeyCode %d", androidKeyCode);
+        return false;
+    }
+    auto actionIterator = KEY_ACTION_MAPPING.find(action);
+    if (actionIterator == KEY_ACTION_MAPPING.end()) {
+        return false;
+    }
+    if (!writeInputEvent(fd, EV_KEY, static_cast<uint16_t>(keyCodeIterator->second),
+                         static_cast<int32_t>(actionIterator->second))) {
+        return false;
+    }
+    if (!writeInputEvent(fd, EV_SYN, SYN_REPORT, 0)) {
+        return false;
+    }
+    return true;
+}
+
+static bool nativeWriteButtonEvent(JNIEnv* env, jobject thiz, jint fd, jint buttonCode,
+                                   jint action) {
+    auto buttonCodeIterator = BUTTON_CODE_MAPPING.find(buttonCode);
+    if (buttonCodeIterator == BUTTON_CODE_MAPPING.end()) {
+        return false;
+    }
+    auto actionIterator = BUTTON_ACTION_MAPPING.find(action);
+    if (actionIterator == BUTTON_ACTION_MAPPING.end()) {
+        return false;
+    }
+    if (!writeInputEvent(fd, EV_KEY, static_cast<uint16_t>(buttonCodeIterator->second),
+                         static_cast<int32_t>(actionIterator->second))) {
+        return false;
+    }
+    if (!writeInputEvent(fd, EV_SYN, SYN_REPORT, 0)) {
+        return false;
+    }
+    return true;
+}
+
+static bool nativeWriteTouchEvent(JNIEnv* env, jobject thiz, jint fd, jint pointerId, jint toolType,
+                                  jint action, jfloat locationX, jfloat locationY, jfloat pressure,
+                                  jfloat majorAxisSize) {
+    if (!writeInputEvent(fd, EV_ABS, ABS_MT_SLOT, pointerId)) {
+        return false;
+    }
+    auto toolTypeIterator = TOOL_TYPE_MAPPING.find(toolType);
+    if (toolTypeIterator == TOOL_TYPE_MAPPING.end()) {
+        return false;
+    }
+    if (toolType != -1) {
+        if (!writeInputEvent(fd, EV_ABS, ABS_MT_TOOL_TYPE,
+                             static_cast<int32_t>(toolTypeIterator->second))) {
+            return false;
+        }
+    }
+    auto actionIterator = TOUCH_ACTION_MAPPING.find(action);
+    if (actionIterator == TOUCH_ACTION_MAPPING.end()) {
+        return false;
+    }
+    UinputAction uinputAction = actionIterator->second;
+    if (uinputAction == UinputAction::PRESS || uinputAction == UinputAction::RELEASE) {
+        if (!writeInputEvent(fd, EV_KEY, BTN_TOUCH, static_cast<int32_t>(uinputAction))) {
+            return false;
+        }
+        if (!writeInputEvent(fd, EV_ABS, ABS_MT_TRACKING_ID,
+                             static_cast<int32_t>(uinputAction == UinputAction::PRESS ? pointerId
+                                                                                      : -1))) {
+            return false;
+        }
+    }
+    if (!writeInputEvent(fd, EV_ABS, ABS_MT_POSITION_X, locationX)) {
+        return false;
+    }
+    if (!writeInputEvent(fd, EV_ABS, ABS_MT_POSITION_Y, locationY)) {
+        return false;
+    }
+    if (!isnan(pressure)) {
+        if (!writeInputEvent(fd, EV_ABS, ABS_MT_PRESSURE, pressure)) {
+            return false;
+        }
+    }
+    if (!isnan(majorAxisSize)) {
+        if (!writeInputEvent(fd, EV_ABS, ABS_MT_TOUCH_MAJOR, majorAxisSize)) {
+            return false;
+        }
+    }
+    return writeInputEvent(fd, EV_SYN, SYN_REPORT, 0);
+}
+
+static bool nativeWriteRelativeEvent(JNIEnv* env, jobject thiz, jint fd, jfloat relativeX,
+                                     jfloat relativeY) {
+    return writeInputEvent(fd, EV_REL, REL_X, relativeX) &&
+            writeInputEvent(fd, EV_REL, REL_Y, relativeY) &&
+            writeInputEvent(fd, EV_SYN, SYN_REPORT, 0);
+}
+
+static bool nativeWriteScrollEvent(JNIEnv* env, jobject thiz, jint fd, jfloat xAxisMovement,
+                                   jfloat yAxisMovement) {
+    return writeInputEvent(fd, EV_REL, REL_HWHEEL, xAxisMovement) &&
+            writeInputEvent(fd, EV_REL, REL_WHEEL, yAxisMovement) &&
+            writeInputEvent(fd, EV_SYN, SYN_REPORT, 0);
+}
+
+static JNINativeMethod methods[] = {
+        {"nativeOpenUinputKeyboard", "(Ljava/lang/String;II)I", (void*)nativeOpenUinputKeyboard},
+        {"nativeOpenUinputMouse", "(Ljava/lang/String;II)I", (void*)nativeOpenUinputMouse},
+        {"nativeOpenUinputTouchscreen", "(Ljava/lang/String;IIII)I",
+         (void*)nativeOpenUinputTouchscreen},
+        {"nativeCloseUinput", "(I)Z", (void*)nativeCloseUinput},
+        {"nativeWriteKeyEvent", "(III)Z", (void*)nativeWriteKeyEvent},
+        {"nativeWriteButtonEvent", "(III)Z", (void*)nativeWriteButtonEvent},
+        {"nativeWriteTouchEvent", "(IIIIFFFF)Z", (void*)nativeWriteTouchEvent},
+        {"nativeWriteRelativeEvent", "(IFF)Z", (void*)nativeWriteRelativeEvent},
+        {"nativeWriteScrollEvent", "(IFF)Z", (void*)nativeWriteScrollEvent},
+};
+
+int register_android_server_companion_virtual_InputController(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "com/android/server/companion/virtual/InputController",
+                                    methods, NELEM(methods));
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
index 06f5aed..f0f779d 100644
--- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
+++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
@@ -997,7 +997,7 @@
         }
     }
 
-    if (gnssHalAidl != nullptr) {
+    if (gnssHalAidl != nullptr && gnssHalAidl->getInterfaceVersion() >= 2) {
         sp<IAGnssAidl> agnssAidl;
         auto status = gnssHalAidl->getExtensionAGnss(&agnssAidl);
         if (checkAidlStatus(status, "Unable to get a handle to AGnss interface.")) {
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index ff61abc..d339ef1 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -63,6 +63,7 @@
 int register_android_server_GpuService(JNIEnv* env);
 int register_android_server_stats_pull_StatsPullAtomService(JNIEnv* env);
 int register_android_server_sensor_SensorService(JavaVM* vm, JNIEnv* env);
+int register_android_server_companion_virtual_InputController(JNIEnv* env);
 };
 
 using namespace android;
@@ -119,5 +120,6 @@
     register_android_server_GpuService(env);
     register_android_server_stats_pull_StatsPullAtomService(env);
     register_android_server_sensor_SensorService(vm, env);
+    register_android_server_companion_virtual_InputController(env);
     return JNI_VERSION_1_4;
 }
diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
index fd295c0..9e83f8e 100644
--- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
@@ -278,7 +278,7 @@
 
         assertThat(mBackupManagerService.getPendingInits()).isEmpty();
         assertThat(mBackupManagerService.isBackupRunning()).isFalse();
-        assertThat(mBackupManagerService.getCurrentOperations().size()).isEqualTo(0);
+        assertThat(mBackupManagerService.getOperationStorage().numOperations()).isEqualTo(0);
         verify(mOldJournal).delete();
     }
 
@@ -449,7 +449,7 @@
 
         assertThat(mBackupManagerService.getPendingInits()).isEmpty();
         assertThat(mBackupManagerService.isBackupRunning()).isFalse();
-        assertThat(mBackupManagerService.getCurrentOperations().size()).isEqualTo(0);
+        assertThat(mBackupManagerService.getOperationStorage().numOperations()).isEqualTo(0);
         assertThat(mBackupManagerService.getCurrentToken()).isEqualTo(1234L);
         verify(mBackupManagerService).writeRestoreTokens();
         verify(mOldJournal).delete();
@@ -2665,6 +2665,7 @@
         KeyValueBackupTask task =
                 new KeyValueBackupTask(
                         mBackupManagerService,
+                        mBackupManagerService.getOperationStorage(),
                         transportMock.mTransportConnection,
                         transportMock.transportData.transportDirName,
                         queue,
diff --git a/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java b/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
index 9eb99ae..e0812d6 100644
--- a/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
@@ -51,6 +51,7 @@
 
 import com.android.server.EventLogTags;
 import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.OperationStorage;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.internal.BackupHandler;
@@ -98,6 +99,7 @@
     @Mock private IRestoreObserver mObserver;
     @Mock private IBackupManagerMonitor mMonitor;
     @Mock private BackupEligibilityRules mBackupEligibilityRules;
+    @Mock private OperationStorage mOperationStorage;
     private ShadowLooper mShadowBackupLooper;
     private ShadowApplication mShadowApplication;
     private UserBackupManagerService.BackupWakeLock mWakeLock;
@@ -132,7 +134,9 @@
         // We need to mock BMS timeout parameters before initializing the BackupHandler since
         // the constructor of BackupHandler relies on it.
         when(mBackupManagerService.getAgentTimeoutParameters()).thenReturn(agentTimeoutParameters);
-        BackupHandler backupHandler = new BackupHandler(mBackupManagerService, handlerThread);
+
+        BackupHandler backupHandler =
+                new BackupHandler(mBackupManagerService, mOperationStorage, handlerThread);
 
         mWakeLock = createBackupWakeLock(application);
 
diff --git a/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java b/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java
index 77b5b61..fc3ec7b 100644
--- a/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java
+++ b/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java
@@ -120,7 +120,6 @@
         when(backupManagerService.getTransportManager()).thenReturn(transportManager);
         when(backupManagerService.getPackageManager()).thenReturn(packageManager);
         when(backupManagerService.getBackupHandler()).thenReturn(backupHandler);
-        when(backupManagerService.getCurrentOpLock()).thenReturn(new Object());
         when(backupManagerService.getQueueLock()).thenReturn(new Object());
         when(backupManagerService.getActivityManager()).thenReturn(mock(IActivityManager.class));
         when(backupManagerService.getWakelock()).thenReturn(wakeLock);
diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupTask.java b/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupTask.java
index 06b7fb7..6a7d031 100644
--- a/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupTask.java
+++ b/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupTask.java
@@ -19,6 +19,7 @@
 import android.annotation.Nullable;
 
 import com.android.server.backup.DataChangedJournal;
+import com.android.server.backup.OperationStorage;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.internal.OnTaskFinishedListener;
 import com.android.server.backup.keyvalue.KeyValueBackupReporter;
@@ -56,6 +57,7 @@
     @Implementation
     protected void __constructor__(
             UserBackupManagerService backupManagerService,
+            OperationStorage operationStorage,
             TransportConnection transportConnection,
             String transportDirName,
             List<String> queue,
diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowPerformUnifiedRestoreTask.java b/services/robotests/src/com/android/server/testing/shadows/ShadowPerformUnifiedRestoreTask.java
index 71010a9..d985e1b 100644
--- a/services/robotests/src/com/android/server/testing/shadows/ShadowPerformUnifiedRestoreTask.java
+++ b/services/robotests/src/com/android/server/testing/shadows/ShadowPerformUnifiedRestoreTask.java
@@ -21,6 +21,7 @@
 import android.app.backup.IRestoreObserver;
 import android.content.pm.PackageInfo;
 
+import com.android.server.backup.OperationStorage;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.internal.OnTaskFinishedListener;
 import com.android.server.backup.restore.PerformUnifiedRestoreTask;
@@ -57,6 +58,7 @@
     @Implementation
     protected void __constructor__(
             UserBackupManagerService backupManagerService,
+            OperationStorage operationStorage,
             TransportConnection transportConnection,
             IRestoreObserver observer,
             IBackupManagerMonitor monitor,
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt
index e633850..005d3e8 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt
@@ -18,6 +18,7 @@
 
 import android.content.pm.parsing.component.ParsedProcess
 import android.content.pm.parsing.component.ParsedProcessImpl
+import android.util.ArrayMap
 import kotlin.contracts.ExperimentalContracts
 
 @ExperimentalContracts
@@ -29,6 +30,8 @@
     override val excludedMethods = listOf(
         // Copying method
         "addStateFrom",
+        // Utility method
+        "putAppClassNameForPackage",
     )
 
     override val baseParams = listOf(
@@ -39,6 +42,9 @@
     )
 
     override fun extraParams() = listOf(
-        getter(ParsedProcess::getDeniedPermissions, setOf("testDeniedPermission"))
+        getter(ParsedProcess::getDeniedPermissions, setOf("testDeniedPermission")),
+        getter(ParsedProcess::getAppClassNamesByPackage, ArrayMap<String, String>().apply {
+            put("package1", "classname1");
+        }),
     )
 }
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
index d1d7cc6..d9dbf48 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
+import android.content.pm.SigningDetails
 import android.content.pm.parsing.component.ParsedActivityImpl
 import android.content.pm.parsing.component.ParsedIntentInfoImpl
 import android.content.pm.verify.domain.DomainVerificationManager
@@ -26,6 +27,7 @@
 import android.os.Build
 import android.os.Process
 import android.util.ArraySet
+import android.util.IndentingPrintWriter
 import android.util.SparseArray
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.pm.parsing.pkg.AndroidPackage
@@ -46,6 +48,7 @@
 import org.mockito.Mockito.anyLong
 import org.mockito.Mockito.anyString
 import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.verifyNoMoreInteractions
 import java.util.UUID
 import java.util.concurrent.atomic.AtomicBoolean
@@ -204,6 +207,14 @@
                 service(Type.QUERENT, "getInfo") {
                     getDomainVerificationInfo(it.targetPackageName)
                 },
+                service(Type.QUERENT, "printState") {
+                    printState(mock(IndentingPrintWriter::class.java), null, null)
+                },
+                service(Type.QUERENT, "printStateInternal") {
+                    printState(mock(IndentingPrintWriter::class.java), null, null) {
+                        mockPkgState(it, UUID.randomUUID())
+                    }
+                },
                 service(Type.VERIFIER, "setStatus") {
                     setDomainVerificationStatus(
                         it.targetDomainSetId,
@@ -311,6 +322,7 @@
                     }
                 )
             }
+            whenever(signingDetails) { SigningDetails.UNKNOWN }
         }
 
         fun mockPkgState(packageName: String, domainSetId: UUID) =
@@ -327,6 +339,7 @@
                     }
                 }
                 whenever(isSystem) { false }
+                whenever(signingDetails) { SigningDetails.UNKNOWN }
             }
     }
 
@@ -794,8 +807,12 @@
             }
 
             val valueAsInt = value as? Int
-            if (valueAsInt != null && valueAsInt == DomainVerificationManager.STATUS_OK) {
-                throw AssertionError("Expected call to return false, was $value")
+            if (valueAsInt != null) {
+                if (valueAsInt == DomainVerificationManager.STATUS_OK) {
+                    throw AssertionError("Expected call to return false, was $value")
+                }
+            } else {
+                throw AssertionError("Expected call to fail")
             }
         } catch (e: SecurityException) {
         } catch (e: PackageManager.NameNotFoundException) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeAppOpsHelper.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeAppOpsHelper.java
index d728451..189396f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeAppOpsHelper.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeAppOpsHelper.java
@@ -31,7 +31,7 @@
     private static class AppOp {
         AppOp() {}
         boolean mAllowed = true;
-        boolean mStarted = false;
+        int mStarted = 0;
         int mNoteCount = 0;
     }
 
@@ -49,7 +49,7 @@
 
     public boolean isAppOpStarted(int appOp, String packageName) {
         AppOp myAppOp = getOp(packageName, appOp);
-        return myAppOp.mStarted;
+        return myAppOp.mStarted > 0;
     }
 
     public int getAppOpNoteCount(int appOp, String packageName) {
@@ -63,16 +63,15 @@
         if (!myAppOp.mAllowed) {
             return false;
         }
-        Preconditions.checkState(!myAppOp.mStarted);
-        myAppOp.mStarted = true;
+        myAppOp.mStarted++;
         return true;
     }
 
     @Override
     public void finishOp(int appOp, CallerIdentity callerIdentity) {
         AppOp myAppOp = getOp(callerIdentity.getPackageName(), appOp);
-        Preconditions.checkState(myAppOp.mStarted);
-        myAppOp.mStarted = false;
+        Preconditions.checkState(myAppOp.mStarted > 0);
+        myAppOp.mStarted--;
     }
 
     @Override
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/LocationAttributionHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/LocationAttributionHelperTest.java
deleted file mode 100644
index 94dcdf9..0000000
--- a/services/tests/mockingservicestests/src/com/android/server/location/injector/LocationAttributionHelperTest.java
+++ /dev/null
@@ -1,143 +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.location.injector;
-
-import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
-import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
-
-import static org.mockito.ArgumentMatchers.any;
-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 static org.mockito.MockitoAnnotations.initMocks;
-
-import android.location.util.identity.CallerIdentity;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class LocationAttributionHelperTest {
-
-    @Mock private AppOpsHelper mAppOpsHelper;
-
-    private LocationAttributionHelper mHelper;
-
-    @Before
-    public void setUp() {
-        initMocks(this);
-
-        when(mAppOpsHelper.startOpNoThrow(anyInt(), any(CallerIdentity.class))).thenReturn(true);
-
-        mHelper = new LocationAttributionHelper(mAppOpsHelper);
-    }
-
-    @Test
-    public void testLocationMonitoring() {
-        CallerIdentity caller1 = CallerIdentity.forTest(1, 1, "test1", null);
-        CallerIdentity caller2 = CallerIdentity.forTest(2, 2, "test2", null);
-
-        mHelper.reportLocationStart(caller1);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportLocationStart(caller1);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportLocationStart(caller2);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-
-        mHelper.reportLocationStart(caller2);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-
-        mHelper.reportLocationStop(caller1);
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        mHelper.reportLocationStop(caller1);
-        verify(mAppOpsHelper).finishOp(OP_MONITOR_LOCATION, CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportLocationStop(caller2);
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        mHelper.reportLocationStop(caller2);
-        verify(mAppOpsHelper).finishOp(OP_MONITOR_LOCATION, CallerIdentity.forAggregation(caller2));
-    }
-
-    @Test
-    public void testHighPowerLocationMonitoring() {
-        CallerIdentity caller1 = CallerIdentity.forTest(1, 1, "test1", null);
-        CallerIdentity caller2 = CallerIdentity.forTest(2, 2, "test2", null);
-
-        mHelper.reportHighPowerLocationStart(caller1);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportHighPowerLocationStart(caller1);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportHighPowerLocationStart(caller2);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-
-        mHelper.reportHighPowerLocationStart(caller2);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-
-        mHelper.reportHighPowerLocationStop(caller1);
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        mHelper.reportHighPowerLocationStop(caller1);
-        verify(mAppOpsHelper).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportHighPowerLocationStop(caller2);
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        mHelper.reportHighPowerLocationStop(caller2);
-        verify(mAppOpsHelper).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-    }
-}
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java
index bd24cfd..02cacb7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java
@@ -33,7 +33,6 @@
     private final FakeScreenInteractiveHelper mScreenInteractiveHelper;
     private final FakeDeviceStationaryHelper mDeviceStationaryHelper;
     private final FakeDeviceIdleHelper mDeviceIdleHelper;
-    private final LocationAttributionHelper mLocationAttributionHelper;
     private final FakeEmergencyHelper mEmergencyHelper;
     private final LocationUsageLogger mLocationUsageLogger;
 
@@ -49,7 +48,6 @@
         mScreenInteractiveHelper = new FakeScreenInteractiveHelper();
         mDeviceStationaryHelper = new FakeDeviceStationaryHelper();
         mDeviceIdleHelper = new FakeDeviceIdleHelper();
-        mLocationAttributionHelper = new LocationAttributionHelper(mAppOpsHelper);
         mEmergencyHelper = new FakeEmergencyHelper();
         mLocationUsageLogger = new LocationUsageLogger();
     }
@@ -110,11 +108,6 @@
     }
 
     @Override
-    public LocationAttributionHelper getLocationAttributionHelper() {
-        return mLocationAttributionHelper;
-    }
-
-    @Override
     public EmergencyHelper getEmergencyHelper() {
         return mEmergencyHelper;
     }
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index e3c60fd..c3a364e 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -32,6 +32,7 @@
         "services.appwidget",
         "services.autofill",
         "services.backup",
+        "services.companion",
         "services.core",
         "services.devicepolicy",
         "services.net",
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 f92b872..28c1c81 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
@@ -585,8 +585,8 @@
         doAnswer((invocation) -> {
             ((Region) invocation.getArguments()[1]).set(region);
             return null;
-        }).when(mMockMagnificationProcessor).getMagnificationRegion(eq(displayId),
-                any(), anyBoolean());
+        }).when(mMockMagnificationProcessor).getFullscreenMagnificationRegion(eq(displayId), any(),
+                anyBoolean());
         when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2);
 
         final Region result = mServiceConnection.getMagnificationRegion(displayId);
@@ -620,7 +620,8 @@
     @Test
     public void resetMagnification() {
         final int displayId = 1;
-        when(mMockMagnificationProcessor.reset(displayId, true)).thenReturn(true);
+        when(mMockMagnificationProcessor.resetFullscreenMagnification(displayId, true)).thenReturn(
+                true);
 
         final boolean result = mServiceConnection.resetMagnification(displayId, true);
         assertThat(result, is(true));
@@ -629,7 +630,8 @@
     @Test
     public void resetMagnification_cantControlMagnification_returnFalse() {
         final int displayId = 1;
-        when(mMockMagnificationProcessor.reset(displayId, true)).thenReturn(true);
+        when(mMockMagnificationProcessor.resetFullscreenMagnification(displayId, true)).thenReturn(
+                true);
         when(mMockSecurityPolicy.canControlMagnification(mServiceConnection)).thenReturn(false);
 
         final boolean result = mServiceConnection.resetMagnification(displayId, true);
@@ -639,7 +641,8 @@
     @Test
     public void resetMagnification_serviceNotBelongCurrentUser_returnFalse() {
         final int displayId = 1;
-        when(mMockMagnificationProcessor.reset(displayId, true)).thenReturn(true);
+        when(mMockMagnificationProcessor.resetFullscreenMagnification(displayId, true)).thenReturn(
+                true);
         when(mMockSystemSupport.getCurrentUserIdLocked()).thenReturn(USER_ID2);
 
         final boolean result = mServiceConnection.resetMagnification(displayId, true);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MagnificationProcessorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/MagnificationProcessorTest.java
index 9ebec98..621507e 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MagnificationProcessorTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MagnificationProcessorTest.java
@@ -98,7 +98,7 @@
                 .setScale(TEST_SCALE).build();
         setMagnificationActivated(TEST_DISPLAY, config);
 
-        float scale = mMagnificationProcessor.getScale(TEST_DISPLAY);
+        float scale = mMagnificationProcessor.getMagnificationConfig(TEST_DISPLAY).getScale();
 
         assertEquals(scale, TEST_SCALE, 0);
     }
@@ -111,20 +111,19 @@
         setMagnificationActivated(TEST_DISPLAY, config);
 
         float centerX = mMagnificationProcessor.getCenterX(
-                TEST_DISPLAY,  /* canControlMagnification= */true);
+                TEST_DISPLAY, /* canControlMagnification= */true);
 
         assertEquals(centerX, TEST_CENTER_X, 0);
     }
 
     @Test
-    public void getCenterX_canControlWindowMagnification_returnCenterX() {
+    public void getCenterX_controlWindowMagnification_returnCenterX() {
         final MagnificationConfig config = new MagnificationConfig.Builder()
                 .setMode(MAGNIFICATION_MODE_WINDOW)
                 .setCenterX(TEST_CENTER_X).build();
         setMagnificationActivated(TEST_DISPLAY, config);
 
-        float centerX = mMagnificationProcessor.getCenterX(
-                TEST_DISPLAY,  /* canControlMagnification= */true);
+        float centerX = mMagnificationProcessor.getMagnificationConfig(TEST_DISPLAY).getCenterX();
 
         assertEquals(centerX, TEST_CENTER_X, 0);
     }
@@ -143,14 +142,13 @@
     }
 
     @Test
-    public void getCenterY_canControlWindowMagnification_returnCenterY() {
+    public void getCenterY_controlWindowMagnification_returnCenterY() {
         final MagnificationConfig config = new MagnificationConfig.Builder()
                 .setMode(MAGNIFICATION_MODE_WINDOW)
                 .setCenterY(TEST_CENTER_Y).build();
         setMagnificationActivated(TEST_DISPLAY, config);
 
-        float centerY = mMagnificationProcessor.getCenterY(
-                TEST_DISPLAY,  /* canControlMagnification= */false);
+        float centerY = mMagnificationProcessor.getMagnificationConfig(TEST_DISPLAY).getCenterY();
 
         assertEquals(centerY, TEST_CENTER_Y, 0);
     }
@@ -159,7 +157,7 @@
     public void getMagnificationRegion_canControlFullscreenMagnification_returnRegion() {
         final Region region = new Region(10, 20, 100, 200);
         setMagnificationActivated(TEST_DISPLAY, MAGNIFICATION_MODE_FULLSCREEN);
-        mMagnificationProcessor.getMagnificationRegion(TEST_DISPLAY,
+        mMagnificationProcessor.getFullscreenMagnificationRegion(TEST_DISPLAY,
                 region,  /* canControlMagnification= */true);
 
         verify(mMockFullScreenMagnificationController).getMagnificationRegion(eq(TEST_DISPLAY),
@@ -167,17 +165,6 @@
     }
 
     @Test
-    public void getMagnificationRegion_canControlWindowMagnification_returnRegion() {
-        final Region region = new Region(10, 20, 100, 200);
-        setMagnificationActivated(TEST_DISPLAY, MAGNIFICATION_MODE_WINDOW);
-        mMagnificationProcessor.getMagnificationRegion(TEST_DISPLAY,
-                region,  /* canControlMagnification= */true);
-
-        verify(mMockWindowMagnificationManager).getMagnificationSourceBounds(eq(TEST_DISPLAY),
-                eq(region));
-    }
-
-    @Test
     public void getMagnificationRegion_fullscreenModeNotRegistered_shouldRegisterThenUnregister() {
         final Region region = new Region(10, 20, 100, 200);
         setMagnificationActivated(TEST_DISPLAY, MAGNIFICATION_MODE_FULLSCREEN);
@@ -188,7 +175,7 @@
                 any());
 
         final Region result = new Region();
-        mMagnificationProcessor.getMagnificationRegion(TEST_DISPLAY,
+        mMagnificationProcessor.getFullscreenMagnificationRegion(TEST_DISPLAY,
                 result, /* canControlMagnification= */true);
         assertEquals(region, result);
         verify(mMockFullScreenMagnificationController).register(TEST_DISPLAY);
@@ -237,7 +224,7 @@
     public void reset_fullscreenMagnificationActivated() {
         setMagnificationActivated(TEST_DISPLAY, MAGNIFICATION_MODE_FULLSCREEN);
 
-        mMagnificationProcessor.reset(TEST_DISPLAY, /* animate= */false);
+        mMagnificationProcessor.resetFullscreenMagnification(TEST_DISPLAY, /* animate= */false);
 
         verify(mMockFullScreenMagnificationController).reset(TEST_DISPLAY, false);
     }
@@ -246,7 +233,7 @@
     public void reset_windowMagnificationActivated() {
         setMagnificationActivated(TEST_DISPLAY, MAGNIFICATION_MODE_WINDOW);
 
-        mMagnificationProcessor.reset(TEST_DISPLAY, /* animate= */false);
+        mMagnificationProcessor.resetCurrentMagnification(TEST_DISPLAY, /* animate= */false);
 
         verify(mMockWindowMagnificationManager).reset(TEST_DISPLAY);
     }
diff --git a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
index c36e1a8..bc95341 100644
--- a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -35,6 +35,7 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.backup.internal.LifecycleOperationStorage;
 import com.android.server.backup.internal.OnTaskFinishedListener;
 import com.android.server.backup.params.BackupParams;
 import com.android.server.backup.transport.BackupTransportClient;
@@ -60,7 +61,7 @@
     @Mock TransportConnection mTransportConnection;
     @Mock BackupTransportClient mBackupTransport;
     @Mock BackupEligibilityRules mBackupEligibilityRules;
-
+    @Mock LifecycleOperationStorage mOperationStorage;
 
     private TestBackupService mService;
 
@@ -68,7 +69,7 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        mService = new TestBackupService(mContext, mPackageManager);
+        mService = new TestBackupService(mContext, mPackageManager, mOperationStorage);
         mService.setEnabled(true);
         mService.setSetupComplete(true);
     }
@@ -173,8 +174,9 @@
         boolean isEnabledStatePersisted = false;
         boolean shouldUseNewBackupEligibilityRules = false;
 
-        TestBackupService(Context context, PackageManager packageManager) {
-            super(context, packageManager);
+        TestBackupService(Context context, PackageManager packageManager,
+                LifecycleOperationStorage operationStorage) {
+            super(context, packageManager, operationStorage);
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/backup/internal/BackupHandlerTest.java b/services/tests/servicestests/src/com/android/server/backup/internal/BackupHandlerTest.java
index fa35e3f..3c79d8b 100644
--- a/services/tests/servicestests/src/com/android/server/backup/internal/BackupHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/internal/BackupHandlerTest.java
@@ -18,7 +18,6 @@
 
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertThrows;
 import static org.testng.Assert.assertTrue;
 
 import android.os.HandlerThread;
@@ -28,6 +27,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.backup.BackupAgentTimeoutParameters;
+import com.android.server.backup.OperationStorage;
 import com.android.server.backup.UserBackupManagerService;
 
 import org.junit.After;
@@ -46,6 +46,7 @@
     private static final int MESSAGE_TIMEOUT_MINUTES = 1;
 
     @Mock private UserBackupManagerService mUserBackupManagerService;
+    @Mock private OperationStorage mOperationStorage;
     @Mock private BackupAgentTimeoutParameters mTimeoutParameters;
 
     private HandlerThread mHandlerThread;
@@ -114,7 +115,7 @@
         private final boolean mShouldStop;
 
         TestBackupHandler(boolean shouldStop) {
-            super(mUserBackupManagerService, mHandlerThread);
+            super(mUserBackupManagerService, mOperationStorage, mHandlerThread);
 
             mShouldStop = shouldStop;
         }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
new file mode 100644
index 0000000..c7c0756
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.virtual;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertThrows;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.graphics.Point;
+import android.hardware.display.DisplayManagerInternal;
+import android.hardware.input.VirtualKeyEvent;
+import android.hardware.input.VirtualMouseButtonEvent;
+import android.hardware.input.VirtualMouseRelativeEvent;
+import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualTouchEvent;
+import android.os.Binder;
+import android.platform.test.annotations.Presubmit;
+import android.view.KeyEvent;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class VirtualDeviceManagerServiceTest {
+
+    private static final String DEVICE_NAME = "device name";
+    private static final int DISPLAY_ID = 2;
+    private static final int PRODUCT_ID = 10;
+    private static final int VENDOR_ID = 5;
+    private static final int HEIGHT = 1800;
+    private static final int WIDTH = 900;
+    private static final Binder BINDER = new Binder("binder");
+
+    private Context mContext;
+    private VirtualDeviceImpl mDeviceImpl;
+    private InputController mInputController;
+    @Mock
+    private InputController.NativeWrapper mNativeWrapperMock;
+    @Mock
+    private DisplayManagerInternal mDisplayManagerInternalMock;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        LocalServices.removeServiceForTest(DisplayManagerInternal.class);
+        LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
+
+        mContext = Mockito.spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
+        mInputController = new InputController(new Object(), mNativeWrapperMock);
+        mDeviceImpl = new VirtualDeviceImpl(mContext,
+                /* association info */ null, new Binder(), /* uid */ 0, mInputController,
+                (int associationId) -> {});
+    }
+
+    @Test
+    public void createVirtualKeyboard_noDisplay_failsSecurityException() {
+        assertThrows(
+                SecurityException.class,
+                () -> mDeviceImpl.createVirtualKeyboard(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
+                        PRODUCT_ID, BINDER));
+    }
+
+    @Test
+    public void createVirtualMouse_noDisplay_failsSecurityException() {
+        assertThrows(
+                SecurityException.class,
+                () -> mDeviceImpl.createVirtualMouse(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
+                        PRODUCT_ID, BINDER));
+    }
+
+    @Test
+    public void createVirtualTouchscreen_noDisplay_failsSecurityException() {
+        assertThrows(
+                SecurityException.class,
+                () -> mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME,
+                        VENDOR_ID, PRODUCT_ID, BINDER, new Point(WIDTH, HEIGHT)));
+    }
+
+    @Test
+    public void createVirtualKeyboard_noPermission_failsSecurityException() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
+                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
+        assertThrows(
+                SecurityException.class,
+                () -> mDeviceImpl.createVirtualKeyboard(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
+                        PRODUCT_ID, BINDER));
+    }
+
+    @Test
+    public void createVirtualMouse_noPermission_failsSecurityException() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
+                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
+        assertThrows(
+                SecurityException.class,
+                () -> mDeviceImpl.createVirtualMouse(DISPLAY_ID, DEVICE_NAME, VENDOR_ID,
+                        PRODUCT_ID, BINDER));
+    }
+
+    @Test
+    public void createVirtualTouchscreen_noPermission_failsSecurityException() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
+                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
+        assertThrows(
+                SecurityException.class,
+                () -> mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME,
+                        VENDOR_ID, PRODUCT_ID, BINDER, new Point(WIDTH, HEIGHT)));
+    }
+
+    @Test
+    public void createVirtualKeyboard_hasDisplay_obtainFileDescriptor() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        mDeviceImpl.createVirtualKeyboard(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID,
+                BINDER);
+        assertWithMessage("Virtual keyboard should register fd when the display matches")
+                .that(mInputController.mInputDeviceFds).isNotEmpty();
+        verify(mNativeWrapperMock).openUinputKeyboard(DEVICE_NAME, VENDOR_ID, PRODUCT_ID);
+    }
+
+    @Test
+    public void createVirtualMouse_hasDisplay_obtainFileDescriptor() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        mDeviceImpl.createVirtualMouse(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID,
+                BINDER);
+        assertWithMessage("Virtual keyboard should register fd when the display matches")
+                .that(mInputController.mInputDeviceFds).isNotEmpty();
+        verify(mNativeWrapperMock).openUinputMouse(DEVICE_NAME, VENDOR_ID, PRODUCT_ID);
+    }
+
+    @Test
+    public void createVirtualTouchscreen_hasDisplay_obtainFileDescriptor() {
+        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID);
+        mDeviceImpl.createVirtualTouchscreen(DISPLAY_ID, DEVICE_NAME, VENDOR_ID, PRODUCT_ID,
+                BINDER, new Point(WIDTH, HEIGHT));
+        assertWithMessage("Virtual keyboard should register fd when the display matches")
+                .that(mInputController.mInputDeviceFds).isNotEmpty();
+        verify(mNativeWrapperMock).openUinputTouchscreen(DEVICE_NAME, VENDOR_ID, PRODUCT_ID, HEIGHT,
+                WIDTH);
+    }
+
+    @Test
+    public void sendKeyEvent_noFd() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder()
+                                .setKeyCode(KeyEvent.KEYCODE_A)
+                                .setAction(VirtualKeyEvent.ACTION_DOWN).build()));
+    }
+
+    @Test
+    public void sendKeyEvent_hasFd_writesEvent() {
+        final int fd = 1;
+        final int keyCode = KeyEvent.KEYCODE_A;
+        final int action = VirtualKeyEvent.ACTION_UP;
+        mInputController.mInputDeviceFds.put(BINDER, fd);
+        mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder().setKeyCode(keyCode)
+                .setAction(action).build());
+        verify(mNativeWrapperMock).writeKeyEvent(fd, keyCode, action);
+    }
+
+    @Test
+    public void sendButtonEvent_noFd() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mDeviceImpl.sendButtonEvent(BINDER,
+                                new VirtualMouseButtonEvent.Builder()
+                                        .setButtonCode(VirtualMouseButtonEvent.BUTTON_BACK)
+                                        .setAction(VirtualMouseButtonEvent.ACTION_BUTTON_PRESS)
+                                        .build()));
+    }
+
+    @Test
+    public void sendButtonEvent_hasFd_writesEvent() {
+        final int fd = 1;
+        final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK;
+        final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS;
+        mInputController.mInputDeviceFds.put(BINDER, fd);
+        mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder()
+                .setButtonCode(buttonCode)
+                .setAction(action).build());
+        verify(mNativeWrapperMock).writeButtonEvent(fd, buttonCode, action);
+    }
+
+    @Test
+    public void sendRelativeEvent_noFd() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mDeviceImpl.sendRelativeEvent(BINDER,
+                                new VirtualMouseRelativeEvent.Builder().setRelativeX(
+                                        0.0f).setRelativeY(0.0f).build()));
+    }
+
+    @Test
+    public void sendRelativeEvent_hasFd_writesEvent() {
+        final int fd = 1;
+        final float x = -0.2f;
+        final float y = 0.7f;
+        mInputController.mInputDeviceFds.put(BINDER, fd);
+        mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder()
+                .setRelativeX(x).setRelativeY(y).build());
+        verify(mNativeWrapperMock).writeRelativeEvent(fd, x, y);
+    }
+
+    @Test
+    public void sendScrollEvent_noFd() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mDeviceImpl.sendScrollEvent(BINDER,
+                                new VirtualMouseScrollEvent.Builder()
+                                        .setXAxisMovement(-1f)
+                                        .setYAxisMovement(1f).build()));
+    }
+
+    @Test
+    public void sendScrollEvent_hasFd_writesEvent() {
+        final int fd = 1;
+        final float x = 0.5f;
+        final float y = 1f;
+        mInputController.mInputDeviceFds.put(BINDER, fd);
+        mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder()
+                .setXAxisMovement(x)
+                .setYAxisMovement(y).build());
+        verify(mNativeWrapperMock).writeScrollEvent(fd, x, y);
+    }
+
+    @Test
+    public void sendTouchEvent_noFd() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder()
+                                .setX(0.0f)
+                                .setY(0.0f)
+                                .setAction(VirtualTouchEvent.ACTION_UP)
+                                .setPointerId(1)
+                                .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER)
+                                .build()));
+    }
+
+    @Test
+    public void sendTouchEvent_hasFd_writesEvent_withoutPressureOrMajorAxisSize() {
+        final int fd = 1;
+        final int pointerId = 5;
+        final int toolType = VirtualTouchEvent.TOOL_TYPE_FINGER;
+        final float x = 100.5f;
+        final float y = 200.5f;
+        final int action = VirtualTouchEvent.ACTION_UP;
+        mInputController.mInputDeviceFds.put(BINDER, fd);
+        mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x)
+                .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType).build());
+        verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, Float.NaN,
+                Float.NaN);
+    }
+
+    @Test
+    public void sendTouchEvent_hasFd_writesEvent() {
+        final int fd = 1;
+        final int pointerId = 5;
+        final int toolType = VirtualTouchEvent.TOOL_TYPE_FINGER;
+        final float x = 100.5f;
+        final float y = 200.5f;
+        final int action = VirtualTouchEvent.ACTION_UP;
+        final float pressure = 1.0f;
+        final float majorAxisSize = 10.0f;
+        mInputController.mInputDeviceFds.put(BINDER, fd);
+        mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x)
+                .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType)
+                .setPressure(pressure).setMajorAxisSize(majorAxisSize).build());
+        verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, pressure,
+                majorAxisSize);
+    }
+}
diff --git a/telephony/java/android/telephony/AccessNetworkConstants.java b/telephony/java/android/telephony/AccessNetworkConstants.java
index 1d7a476..4469ffc 100644
--- a/telephony/java/android/telephony/AccessNetworkConstants.java
+++ b/telephony/java/android/telephony/AccessNetworkConstants.java
@@ -17,6 +17,7 @@
 package android.telephony;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.hardware.radio.V1_5.AccessNetwork;
 
@@ -28,6 +29,8 @@
  */
 public final class AccessNetworkConstants {
 
+    private static final String TAG = AccessNetworkConstants.class.getSimpleName();
+
     /**
      * Wireless transportation type
      *
@@ -108,6 +111,21 @@
                 default: return Integer.toString(type);
             }
         }
+
+        /** @hide */
+        public static @RadioAccessNetworkType int fromString(@NonNull String str) {
+            switch (str.toUpperCase()) {
+                case "GERAN" : return GERAN;
+                case "UTRAN" : return UTRAN;
+                case "EUTRAN" : return EUTRAN;
+                case "CDMA2000" : return CDMA2000;
+                case "IWLAN" : return IWLAN;
+                case "NGRAN" : return NGRAN;
+                default:
+                    Rlog.e(TAG, "Invalid access network type " + str);
+                    return UNKNOWN;
+            }
+        }
     }
 
     /**
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 7d24b76..c80d35b 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -5381,6 +5381,34 @@
     public static final String KEY_UNTHROTTLE_DATA_RETRY_WHEN_TAC_CHANGES_BOOL =
             "unthrottle_data_retry_when_tac_changes_bool";
 
+    /**
+     * IWLAN handover rules that determine whether handover is allowed or disallowed between
+     * cellular and IWLAN.
+     *
+     * The handover rules will be matched in the order. Here are some sample rules.
+     * <string-array name="iwlan_handover_rules" num="5">
+     *     <!-- Handover from IWLAN to 2G/3G is not allowed -->
+     *     <item value="source=IWLAN, target=GERAN|UTRAN, type=disallowed"/>
+     *     <!-- Handover from 2G/3G to IWLAN is not allowed -->
+     *     <item value="source=GERAN|UTRAN, target:IWLAN, type=disallowed"/>
+     *     <!-- Handover from IWLAN to 3G/4G/5G is not allowed if the device is roaming. -->
+     *     <item value="source=IWLAN, target=UTRAN|EUTRAN|NGRAN, roaming=true, type=disallowed"/>
+     *     <!-- Handover from 4G to IWLAN is not allowed -->
+     *     <item value="source=EUTRAN, target=IWLAN, type=disallowed"/>
+     *     <!-- Handover is always allowed in any condition. -->
+     *     <item value="source=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN,
+     *         target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"/>
+     * </string-array>
+     *
+     * When handover is not allowed, frameworks will tear down the data network on source transport,
+     * and then setup a new one on the target transport when Qualified Network Service changes the
+     * preferred access networks for particular APN types.
+     *
+     * @hide
+     */
+    public static final String KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY =
+            "iwlan_handover_policy_string_array";
+
     /** The default value for every variable. */
     private final static PersistableBundle sDefaults;
 
@@ -5953,6 +5981,7 @@
         sDefaults.putInt(
                 KEY_OPPORTUNISTIC_TIME_TO_SCAN_AFTER_CAPABILITY_SWITCH_TO_PRIMARY_LONG,
                 120000);
+        sDefaults.putAll(ImsServiceEntitlement.getDefaults());
         sDefaults.putAll(Gps.getDefaults());
         sDefaults.putIntArray(KEY_CDMA_ENHANCED_ROAMING_INDICATOR_FOR_HOME_NETWORK_INT_ARRAY,
                 new int[] {
@@ -6043,6 +6072,9 @@
         sDefaults.putBoolean(KEY_UNTHROTTLE_DATA_RETRY_WHEN_TAC_CHANGES_BOOL, false);
         sDefaults.putBoolean(KEY_VONR_SETTING_VISIBILITY_BOOL, true);
         sDefaults.putBoolean(KEY_VONR_ENABLED_BOOL, false);
+        sDefaults.putStringArray(KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY, new String[]{
+                "source=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, "
+                        + "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"});
     }
 
     /**
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index ae22247..cca5e2f 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -8551,12 +8551,58 @@
     public NetworkScan requestNetworkScan(
             NetworkScanRequest request, Executor executor,
             TelephonyScanManager.NetworkScanCallback callback) {
-        synchronized (this) {
+        return requestNetworkScan(false, request, executor, callback);
+    }
+
+    /**
+     * Request a network scan.
+     *
+     * This method is asynchronous, so the network scan results will be returned by callback.
+     * The returned NetworkScan will contain a callback method which can be used to stop the scan.
+     *
+     * <p>Requires Permission:
+     * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the calling
+     * app has carrier privileges (see {@link #hasCarrierPrivileges})
+     * and {@link android.Manifest.permission#ACCESS_FINE_LOCATION}.
+     *
+     * If the system-wide location switch is off, apps may still call this API, with the
+     * following constraints:
+     * <ol>
+     *     <li>The app must hold the {@code android.permission.NETWORK_SCAN} permission.</li>
+     *     <li>The app must not supply any specific bands or channels to scan.</li>
+     *     <li>The app must only specify MCC/MNC pairs that are
+     *     associated to a SIM in the device.</li>
+     *     <li>Returned results will have no meaningful info other than signal strength
+     *     and MCC/MNC info.</li>
+     * </ol>
+     *
+     * @param renounceFineLocationAccess Set this to true if the caller would not like to receive
+     * location related information which will be sent if the caller already possess
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and do not renounce the permission
+     * @param request Contains all the RAT with bands/channels that need to be scanned.
+     * @param executor The executor through which the callback should be invoked. Since the scan
+     *        request may trigger multiple callbacks and they must be invoked in the same order as
+     *        they are received by the platform, the user should provide an executor which executes
+     *        tasks one at a time in serial order. For example AsyncTask.SERIAL_EXECUTOR.
+     * @param callback Returns network scan results or errors.
+     * @return A NetworkScan obj which contains a callback which can be used to stop the scan.
+     */
+    @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.MODIFY_PHONE_STATE,
+            Manifest.permission.ACCESS_FINE_LOCATION
+    })
+    public @Nullable NetworkScan requestNetworkScan(
+            boolean renounceFineLocationAccess, @NonNull NetworkScanRequest request,
+            @NonNull Executor executor,
+            @NonNull TelephonyScanManager.NetworkScanCallback callback) {
+        synchronized (sCacheLock) {
             if (mTelephonyScanManager == null) {
                 mTelephonyScanManager = new TelephonyScanManager();
             }
         }
-        return mTelephonyScanManager.requestNetworkScan(getSubId(), request, executor, callback,
+        return mTelephonyScanManager.requestNetworkScan(getSubId(), renounceFineLocationAccess,
+                request, executor, callback,
                 getOpPackageName(), getAttributionTag());
     }
 
@@ -11613,7 +11659,6 @@
      * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
      * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges})
      * and {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}.
-     *
      * May return {@code null} when the subscription is inactive or when there was an error
      * communicating with the phone process.
      */
@@ -11623,7 +11668,72 @@
             Manifest.permission.ACCESS_COARSE_LOCATION
     })
     public @Nullable ServiceState getServiceState() {
-        return getServiceStateForSubscriber(getSubId());
+        return getServiceState(false, false);
+    }
+
+    /**
+     * Returns the current {@link ServiceState} information.
+     *
+     * <p>If this object has been created with {@link #createForSubscriptionId}, applies to the
+     * given subId. Otherwise, applies to {@link SubscriptionManager#getDefaultSubscriptionId()}
+     *
+     * If you want continuous updates of service state info, register a {@link PhoneStateListener}
+     * via {@link #listen} with the {@link PhoneStateListener#LISTEN_SERVICE_STATE} event.
+     *
+     * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
+     * or that the calling app has carrier privileges (see {@link #hasCarrierPrivileges})
+     * and {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}.
+     * @param renounceFineLocationAccess Set this to true if the caller would not like to receive
+     * location related information which will be sent if the caller already possess
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and do not renounce the permission
+     * @param renounceCoarseLocationAccess Set this to true if the caller would not like to
+     * receive location related information which will be sent if the caller already possess
+     * {@link Manifest.permission#ACCESS_COARSE_LOCATION} and do not renounce the permissions.
+     * May return {@code null} when the subscription is inactive or when there was an error
+     * communicating with the phone process.
+     */
+    @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
+    @RequiresPermission(allOf = {
+            Manifest.permission.READ_PHONE_STATE,
+            Manifest.permission.ACCESS_COARSE_LOCATION
+    })
+    public @Nullable ServiceState getServiceState(boolean renounceFineLocationAccess,
+            boolean renounceCoarseLocationAccess) {
+        return getServiceStateForSubscriber(getSubId(), renounceFineLocationAccess,
+                renounceCoarseLocationAccess);
+    }
+
+    /**
+     * Returns the service state information on specified subscription. Callers require
+     * either READ_PRIVILEGED_PHONE_STATE or READ_PHONE_STATE to retrieve the information.
+     *
+     * May return {@code null} when the subscription is inactive or when there was an error
+     * communicating with the phone process.
+     * @param renounceFineLocationAccess Set this to true if the caller would not like to receive
+     * location related information which will be sent if the caller already possess
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and do not renounce the permission
+     * @param renounceCoarseLocationAccess Set this to true if the caller would not like to
+     * receive location related information which will be sent if the caller already possess
+     * {@link Manifest.permission#ACCESS_COARSE_LOCATION} and do not renounce the permissions.
+     */
+    private ServiceState getServiceStateForSubscriber(int subId,
+            boolean renounceFineLocationAccess,
+            boolean renounceCoarseLocationAccess) {
+        try {
+            ITelephony service = getITelephony();
+            if (service != null) {
+                return service.getServiceStateForSubscriber(subId, renounceFineLocationAccess,
+                        renounceCoarseLocationAccess,
+                        getOpPackageName(), getAttributionTag());
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling ITelephony#getServiceStateForSubscriber", e);
+        } catch (NullPointerException e) {
+            AnomalyReporter.reportAnomaly(
+                    UUID.fromString("a3ab0b9d-f2aa-4baf-911d-7096c0d4645a"),
+                    "getServiceStateForSubscriber " + subId + " NPE");
+        }
+        return null;
     }
 
     /**
@@ -11636,20 +11746,7 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
     public ServiceState getServiceStateForSubscriber(int subId) {
-        try {
-            ITelephony service = getITelephony();
-            if (service != null) {
-                return service.getServiceStateForSubscriber(subId, getOpPackageName(),
-                        getAttributionTag());
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Error calling ITelephony#getServiceStateForSubscriber", e);
-        } catch (NullPointerException e) {
-            AnomalyReporter.reportAnomaly(
-                    UUID.fromString("a3ab0b9d-f2aa-4baf-911d-7096c0d4645a"),
-                    "getServiceStateForSubscriber " + subId + " NPE");
-        }
-        return null;
+        return getServiceStateForSubscriber(getSubId(), false, false);
     }
 
     /**
@@ -15500,7 +15597,49 @@
      */
     public void registerTelephonyCallback(@NonNull @CallbackExecutor Executor executor,
             @NonNull TelephonyCallback callback) {
+        registerTelephonyCallback(false, false, executor, callback);
+    }
 
+    /**
+     * Registers a callback object to receive notification of changes in specified telephony states.
+     * <p>
+     * To register a callback, pass a {@link TelephonyCallback} which implements
+     * interfaces of events. For example,
+     * FakeServiceStateCallback extends {@link TelephonyCallback} implements
+     * {@link TelephonyCallback.ServiceStateListener}.
+     *
+     * At registration, and when a specified telephony state changes, the telephony manager invokes
+     * the appropriate callback method on the callback object and passes the current (updated)
+     * values.
+     * <p>
+     *
+     * If this TelephonyManager object has been created with {@link #createForSubscriptionId},
+     * applies to the given subId. Otherwise, applies to
+     * {@link SubscriptionManager#getDefaultSubscriptionId()}. To register events for multiple
+     * subIds, pass a separate callback object to each TelephonyManager object created with
+     * {@link #createForSubscriptionId}.
+     *
+     * Note: if you call this method while in the middle of a binder transaction, you <b>must</b>
+     * call {@link android.os.Binder#clearCallingIdentity()} before calling this method. A
+     * {@link SecurityException} will be thrown otherwise.
+     *
+     * This API should be used sparingly -- large numbers of callbacks will cause system
+     * instability. If a process has registered too many callbacks without unregistering them, it
+     * may encounter an {@link IllegalStateException} when trying to register more callbacks.
+     *
+     * @param renounceFineLocationAccess Set this to true if the caller would not like to receive
+     * location related information which will be sent if the caller already possess
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and do not renounce the permissions.
+     * @param renounceCoarseLocationAccess Set this to true if the caller would not like to
+     * receive location related information which will be sent if the caller already possess
+     * {@link Manifest.permission#ACCESS_COARSE_LOCATION} and do not renounce the permissions.
+     * @param executor The executor of where the callback will execute.
+     * @param callback The {@link TelephonyCallback} object to register.
+     */
+    public void registerTelephonyCallback(boolean renounceFineLocationAccess,
+            boolean renounceCoarseLocationAccess,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull TelephonyCallback callback) {
         if (mContext == null) {
             throw new IllegalStateException("telephony service is null.");
         }
@@ -15511,7 +15650,8 @@
         mTelephonyRegistryMgr = (TelephonyRegistryManager)
                 mContext.getSystemService(Context.TELEPHONY_REGISTRY_SERVICE);
         if (mTelephonyRegistryMgr != null) {
-            mTelephonyRegistryMgr.registerTelephonyCallback(executor, mSubId, getOpPackageName(),
+            mTelephonyRegistryMgr.registerTelephonyCallback(renounceFineLocationAccess,
+                    renounceCoarseLocationAccess, executor, mSubId, getOpPackageName(),
                     getAttributionTag(), callback, getITelephony() != null);
         } else {
             throw new IllegalStateException("telephony service is null.");
diff --git a/telephony/java/android/telephony/TelephonyScanManager.java b/telephony/java/android/telephony/TelephonyScanManager.java
index 9572154..122662d 100644
--- a/telephony/java/android/telephony/TelephonyScanManager.java
+++ b/telephony/java/android/telephony/TelephonyScanManager.java
@@ -228,7 +228,9 @@
      * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and
      *   {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
      * Or the calling app has carrier privileges. @see #hasCarrierPrivileges
-     *
+     * @param renounceFineLocationAccess Set this to true if the caller would not like to receive
+     * location related information which will be sent if the caller already possess
+     * {@link android.Manifest.permission.ACCESS_FINE_LOCATION} and do not renounce the permission
      * @param request Contains all the RAT with bands/channels that need to be scanned.
      * @param callback Returns network scan results or errors.
      * @param callingPackage The package name of the caller
@@ -237,6 +239,7 @@
      * @hide
      */
     public NetworkScan requestNetworkScan(int subId,
+            boolean renounceFineLocationAccess,
             NetworkScanRequest request, Executor executor, NetworkScanCallback callback,
             String callingPackage, @Nullable String callingFeatureId) {
         try {
@@ -252,7 +255,8 @@
             // the record to the ScanInfo cache.
             synchronized (mScanInfo) {
                 int scanId = telephony.requestNetworkScan(
-                        subId, request, mMessenger, new Binder(), callingPackage,
+                        subId, renounceFineLocationAccess, request, mMessenger,
+                        new Binder(), callingPackage,
                         callingFeatureId);
                 if (scanId == INVALID_SCAN_ID) {
                     Rlog.e(TAG, "Failed to initiate network scan");
diff --git a/telephony/java/android/telephony/ims/RcsClientConfiguration.java b/telephony/java/android/telephony/ims/RcsClientConfiguration.java
index 793c377..c25ace0 100644
--- a/telephony/java/android/telephony/ims/RcsClientConfiguration.java
+++ b/telephony/java/android/telephony/ims/RcsClientConfiguration.java
@@ -50,6 +50,7 @@
     private String mRcsProfile;
     private String mClientVendor;
     private String mClientVersion;
+    private boolean mRcsEnabledByUser;
 
     /**
      * Create a RcsClientConfiguration object.
@@ -63,14 +64,41 @@
      * @param clientVersion Identifies the RCS client version. Refer to GSMA
      * RCC.07 "client_version" parameter.
      * Example:client_version=RCSAndrd-1.0
+     * @deprecated Use {@link #RcsClientConfiguration(String, String, String, String, boolean)}
+     * instead. Deprecated prototype assumes that the user setting controlling RCS is enabled.
      */
+    @Deprecated
     public RcsClientConfiguration(@NonNull String rcsVersion,
             @NonNull @StringRcsProfile String rcsProfile,
             @NonNull String clientVendor, @NonNull String clientVersion) {
+        this(rcsVersion, rcsProfile, clientVendor, clientVersion, true);
+    }
+
+    /**
+     * Create a RcsClientConfiguration object.
+     * Default messaging application must pass a valid configuration object
+     * @param rcsVersion The parameter identifies the RCS version supported
+     * by the client. Refer to GSMA RCC.07 "rcs_version" parameter.
+     * @param rcsProfile Identifies a fixed set of RCS services that are
+     * supported by the client. See {@link #RCS_PROFILE_1_0 } or
+     * {@link #RCS_PROFILE_2_3 }
+     * @param clientVendor Identifies the vendor providing the RCS client.
+     * @param clientVersion Identifies the RCS client version. Refer to GSMA
+     * RCC.07 "client_version" parameter.
+     * Example:client_version=RCSAndrd-1.0
+     * @param isRcsEnabledByUser The current user setting for whether or not the user has
+     * enabled or disabled RCS. Please refer to GSMA RCC.07 "rcs_state" parameter for how this
+     * can affect provisioning.
+     */
+    public RcsClientConfiguration(@NonNull String rcsVersion,
+            @NonNull @StringRcsProfile String rcsProfile,
+            @NonNull String clientVendor, @NonNull String clientVersion,
+            boolean isRcsEnabledByUser) {
         mRcsVersion = rcsVersion;
         mRcsProfile = rcsProfile;
         mClientVendor = clientVendor;
         mClientVersion = clientVersion;
+        mRcsEnabledByUser = isRcsEnabledByUser;
     }
 
     /**
@@ -102,6 +130,18 @@
     }
 
     /**
+     * The current user setting provided by the RCS messaging application that determines
+     * whether or not the user has enabled RCS.
+     * <p>
+     * See GSMA RCC.07 "rcs_state" parameter for more information about how this setting
+     * affects provisioning.
+     * @return true if RCS is enabled by the user, false if RCS is disabled by the user.
+     */
+    public boolean isRcsEnabledByUser() {
+        return mRcsEnabledByUser;
+    }
+
+    /**
      * {@link Parcelable#writeToParcel}
      */
     @Override
@@ -110,6 +150,7 @@
         out.writeString(mRcsProfile);
         out.writeString(mClientVendor);
         out.writeString(mClientVersion);
+        out.writeBoolean(mRcsEnabledByUser);
     }
 
     /**
@@ -124,8 +165,9 @@
                     String rcsProfile = in.readString();
                     String clientVendor = in.readString();
                     String clientVersion = in.readString();
+                    Boolean rcsEnabledByUser = in.readBoolean();
                     return new RcsClientConfiguration(rcsVersion, rcsProfile,
-                            clientVendor, clientVersion);
+                            clientVendor, clientVersion, rcsEnabledByUser);
                 }
 
                 @Override
@@ -152,11 +194,13 @@
 
         return mRcsVersion.equals(other.mRcsVersion) && mRcsProfile.equals(other.mRcsProfile)
                 && mClientVendor.equals(other.mClientVendor)
-                && mClientVersion.equals(other.mClientVersion);
+                && mClientVersion.equals(other.mClientVersion)
+                && (mRcsEnabledByUser == other.mRcsEnabledByUser);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mRcsVersion, mRcsProfile, mClientVendor, mClientVersion);
+        return Objects.hash(mRcsVersion, mRcsProfile, mClientVendor, mClientVersion,
+                mRcsEnabledByUser);
     }
 }
diff --git a/telephony/java/android/telephony/ims/RcsUceAdapter.java b/telephony/java/android/telephony/ims/RcsUceAdapter.java
index 7a1c092..61de3ac 100644
--- a/telephony/java/android/telephony/ims/RcsUceAdapter.java
+++ b/telephony/java/android/telephony/ims/RcsUceAdapter.java
@@ -28,13 +28,10 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyFrameworkInitializer;
 import android.telephony.ims.aidl.IImsRcsController;
 import android.telephony.ims.aidl.IRcsUceControllerCallback;
 import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
-import android.telephony.ims.feature.RcsFeature;
 import android.util.Log;
 
 import java.lang.annotation.Retention;
@@ -337,6 +334,14 @@
     @SystemApi
     public static final int PUBLISH_STATE_OTHER_ERROR = 6;
 
+    /**
+     * The device is currently trying to publish its capabilities to the network.
+     * @hide
+     */
+    @SystemApi
+    public static final int PUBLISH_STATE_PUBLISHING = 7;
+
+
     /**@hide*/
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = "PUBLISH_STATE_", value = {
@@ -345,7 +350,8 @@
             PUBLISH_STATE_VOICE_PROVISION_ERROR,
             PUBLISH_STATE_RCS_PROVISION_ERROR,
             PUBLISH_STATE_REQUEST_TIMEOUT,
-            PUBLISH_STATE_OTHER_ERROR
+            PUBLISH_STATE_OTHER_ERROR,
+            PUBLISH_STATE_PUBLISHING
     })
     public @interface PublishState {}
 
@@ -480,9 +486,12 @@
      * <p>
      * Be sure to check the availability of this feature using
      * {@link ImsRcsManager#isAvailable(int, int)} and ensuring
-     * {@link RcsFeature.RcsImsCapabilities#CAPABILITY_TYPE_OPTIONS_UCE} or
-     * {@link RcsFeature.RcsImsCapabilities#CAPABILITY_TYPE_PRESENCE_UCE} is enabled or else
-     * this operation will fail with {@link #ERROR_NOT_AVAILABLE} or {@link #ERROR_NOT_ENABLED}.
+     * {@link
+     * android.telephony.ims.feature.RcsFeature.RcsImsCapabilities#CAPABILITY_TYPE_OPTIONS_UCE} or
+     * {@link
+     * android.telephony.ims.feature.RcsFeature.RcsImsCapabilities#CAPABILITY_TYPE_PRESENCE_UCE} is
+     * enabled or else this operation will fail with {@link #ERROR_NOT_AVAILABLE} or
+     * {@link #ERROR_NOT_ENABLED}.
      *
      * @param contactNumbers A list of numbers that the capabilities are being requested for.
      * @param executor The executor that will be used when the request is completed and the
@@ -573,8 +582,10 @@
      * <p>
      * Be sure to check the availability of this feature using
      * {@link ImsRcsManager#isAvailable(int, int)} and ensuring
-     * {@link RcsFeature.RcsImsCapabilities#CAPABILITY_TYPE_OPTIONS_UCE} or
-     * {@link RcsFeature.RcsImsCapabilities#CAPABILITY_TYPE_PRESENCE_UCE} is
+     * {@link
+     * android.telephony.ims.feature.RcsFeature.RcsImsCapabilities#CAPABILITY_TYPE_OPTIONS_UCE} or
+     * {@link
+     * android.telephony.ims.feature.RcsFeature.RcsImsCapabilities#CAPABILITY_TYPE_PRESENCE_UCE} is
      * enabled or else this operation will fail with
      * {@link #ERROR_NOT_AVAILABLE} or {@link #ERROR_NOT_ENABLED}.
      *
@@ -690,7 +701,8 @@
      * Registers a {@link OnPublishStateChangedListener} with the system, which will provide publish
      * state updates for the subscription specified in {@link ImsManager@getRcsManager(subid)}.
      * <p>
-     * Use {@link SubscriptionManager.OnSubscriptionsChangedListener} to listen to subscription
+     * Use {@link android.telephony.SubscriptionManager.OnSubscriptionsChangedListener} to listen
+     * to subscription
      * changed events and call
      * {@link #removeOnPublishStateChangedListener(OnPublishStateChangedListener)} to clean up.
      * <p>
@@ -792,7 +804,8 @@
      * cache associated with those contacts as the local cache becomes stale.
      * <p>
      * This setting will only enable this feature if
-     * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} is also enabled.
+     * {@link android.telephony.CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} is
+     * also enabled.
      * <p>
      * Note: This setting does not affect whether or not the device publishes its service
      * capabilities if the subscription supports presence publication.
@@ -843,7 +856,8 @@
      * session.
      * <p>
      * This setting will only enable this feature if
-     * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} is also enabled.
+     * {@link android.telephony.CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} is
+     * also enabled.
      * <p>
      * Note: This setting does not affect whether or not the device publishes its service
      * capabilities if the subscription supports presence publication.
diff --git a/telephony/java/android/telephony/ims/aidl/CapabilityExchangeAidlWrapper.java b/telephony/java/android/telephony/ims/aidl/CapabilityExchangeAidlWrapper.java
index c3d7325..c27fa4f 100644
--- a/telephony/java/android/telephony/ims/aidl/CapabilityExchangeAidlWrapper.java
+++ b/telephony/java/android/telephony/ims/aidl/CapabilityExchangeAidlWrapper.java
@@ -81,6 +81,26 @@
     }
 
     /**
+     * Receives the status of changes in the publishing connection from ims service
+     * and deliver this callback to the framework.
+     */
+    public void onPublishUpdated(int reasonCode, @NonNull String reasonPhrase,
+            int reasonHeaderCause, @NonNull String reasonHeaderText) throws ImsException {
+        ICapabilityExchangeEventListener listener = mListenerBinder;
+        if (listener == null) {
+            return;
+        }
+        try {
+            listener.onPublishUpdated(reasonCode, reasonPhrase,
+                    reasonHeaderCause, reasonHeaderText);
+        } catch (RemoteException e) {
+            Log.w(LOG_TAG, "onPublishUpdated exception: " + e);
+            throw new ImsException("Remote is not available",
+                    ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+    }
+
+    /**
      * Receives the callback of the remote capability request from the network and deliver this
      * request to the framework.
      */
diff --git a/telephony/java/android/telephony/ims/aidl/ICapabilityExchangeEventListener.aidl b/telephony/java/android/telephony/ims/aidl/ICapabilityExchangeEventListener.aidl
index 078ac91..c675bc3 100644
--- a/telephony/java/android/telephony/ims/aidl/ICapabilityExchangeEventListener.aidl
+++ b/telephony/java/android/telephony/ims/aidl/ICapabilityExchangeEventListener.aidl
@@ -31,6 +31,8 @@
 oneway interface ICapabilityExchangeEventListener {
     void onRequestPublishCapabilities(int publishTriggerType);
     void onUnpublish();
+    void onPublishUpdated(int reasonCode, String reasonPhrase, int reasonHeaderCause,
+            String reasonHeaderText);
     void onRemoteCapabilityRequest(in Uri contactUri,
             in List<String> remoteCapabilities, IOptionsRequestCallback cb);
 }
diff --git a/telephony/java/android/telephony/ims/stub/CapabilityExchangeEventListener.java b/telephony/java/android/telephony/ims/stub/CapabilityExchangeEventListener.java
index a3be8da..9293a40 100644
--- a/telephony/java/android/telephony/ims/stub/CapabilityExchangeEventListener.java
+++ b/telephony/java/android/telephony/ims/stub/CapabilityExchangeEventListener.java
@@ -90,6 +90,30 @@
     void onUnpublish() throws ImsException;
 
     /**
+     * Notify the framework that the ImsService has refreshed the PUBLISH
+     * internally, which has resulted in a new PUBLISH result.
+     * <p>
+     * This method must return both SUCCESS (200 OK) and FAILURE (300+) codes in order to
+     * keep the AOSP stack up to date.
+     * @param reasonCode The SIP response code sent from the network.
+     * @param reasonPhrase The optional reason response from the network. If the
+     * network provided no reason with the sip code, the string should be empty.
+     * @param reasonHeaderCause The “cause” parameter of the “reason” header
+     * included in the SIP message.
+     * @param reasonHeaderText The “text” parameter of the “reason” header
+     * included in the SIP message.
+     * @throws ImsException If this {@link RcsCapabilityExchangeImplBase} instance is not
+     * currently connected to the framework. This can happen if the {@link RcsFeature} is not
+     * {@link ImsFeature#STATE_READY} and the {@link RcsFeature} has not received
+     * the {@link ImsFeature#onFeatureReady()} callback. This may also happen in rare
+     * cases when the Telephony stack has crashed.
+     *
+     */
+    default void onPublishUpdated(int reasonCode, @NonNull String reasonPhrase,
+            int reasonHeaderCause, @NonNull String reasonHeaderText) throws ImsException {
+    }
+
+    /**
      * Inform the framework of an OPTIONS query from a remote device for this device's UCE
      * capabilities.
      * <p>
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 1f336f7..f317c92 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -871,6 +871,8 @@
      * Perform a radio network scan and return the id of this scan.
      *
      * @param subId the id of the subscription.
+     * @param renounceFineLocationAccess Set this to true if the caller would not like to
+     * receive fine location related information
      * @param request Defines all the configs for network scan.
      * @param messenger Callback messages will be sent using this messenger.
      * @param binder the binder object instantiated in TelephonyManager.
@@ -878,8 +880,9 @@
      * @param callingFeatureId The feature in the package
      * @return An id for this scan.
      */
-    int requestNetworkScan(int subId, in NetworkScanRequest request, in Messenger messenger,
-            in IBinder binder, in String callingPackage, String callingFeatureId);
+    int requestNetworkScan(int subId, in boolean renounceFineLocationAccess,
+            in NetworkScanRequest request, in Messenger messenger, in IBinder binder,
+	    in String callingPackage, String callingFeatureId);
 
     /**
      * Stop an existing radio network scan.
@@ -1353,12 +1356,17 @@
     /**
      * Get the service state on specified subscription
      * @param subId Subscription id
+     * @param renounceFineLocationAccess Set this to true if the caller would not like to
+     * receive fine location related information
+     * @param renounceCoarseLocationAccess Set this to true if the caller would not like to
+     * receive coarse location related information
      * @param callingPackage The package making the call
      * @param callingFeatureId The feature in the package
      * @return Service state on specified subscription.
      */
-    ServiceState getServiceStateForSubscriber(int subId, String callingPackage,
-            String callingFeatureId);
+    ServiceState getServiceStateForSubscriber(int subId, boolean renounceFineLocationAccess,
+            boolean renounceCoarseLocationAccess,
+            String callingPackage, String callingFeatureId);
 
     /**
      * Returns the URI for the per-account voicemail ringtone set in Phone settings.
diff --git a/telephony/java/com/android/internal/telephony/RILConstants.java b/telephony/java/com/android/internal/telephony/RILConstants.java
index 866fd2c..ba95841 100644
--- a/telephony/java/com/android/internal/telephony/RILConstants.java
+++ b/telephony/java/com/android/internal/telephony/RILConstants.java
@@ -530,6 +530,8 @@
     int RIL_REQUEST_GET_SLICING_CONFIG = 224;
     int RIL_REQUEST_ENABLE_VONR = 225;
     int RIL_REQUEST_IS_VONR_ENABLED = 226;
+    int RIL_REQUEST_SET_USAGE_SETTING = 227;
+    int RIL_REQUEST_GET_USAGE_SETTING = 228;
 
     /* Responses begin */
     int RIL_RESPONSE_ACKNOWLEDGEMENT = 800;
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
index 5d0d718..3914ef6 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
@@ -231,7 +231,7 @@
     @Test
     fun screenLockedStart() {
         testSpec.assertLayersStart {
-            isVisible(colorFadComponent)
+            isEmpty()
         }
     }
 
diff --git a/tests/MultiUser/Android.bp b/tests/MultiUser/Android.bp
new file mode 100644
index 0000000..bde309f
--- /dev/null
+++ b/tests/MultiUser/Android.bp
@@ -0,0 +1,27 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "MultiUserTests",
+    platform_apis: true,
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.test.rules",
+        "androidx.test.runner",
+        "services.core",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+    ],
+    certificate: "platform",
+    test_suites: ["device-tests"],
+}
diff --git a/tests/MultiUser/AndroidManifest.xml b/tests/MultiUser/AndroidManifest.xml
new file mode 100644
index 0000000..9a25c09
--- /dev/null
+++ b/tests/MultiUser/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.test.multiuser">
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+    <uses-permission android:name="android.permission.MANAGE_USERS" />
+
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.test.multiuser">
+    </instrumentation>
+</manifest>
diff --git a/tests/MultiUser/TEST_MAPPING b/tests/MultiUser/TEST_MAPPING
new file mode 100644
index 0000000..0dbef6c
--- /dev/null
+++ b/tests/MultiUser/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "MultiUserTests"
+    }
+  ]
+}
diff --git a/tests/MultiUser/src/com/android/test/multiuser/MultiUserSettingsTests.java b/tests/MultiUser/src/com/android/test/multiuser/MultiUserSettingsTests.java
new file mode 100644
index 0000000..1521cc6
--- /dev/null
+++ b/tests/MultiUser/src/com/android/test/multiuser/MultiUserSettingsTests.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 com.android.test.multiuser;
+
+import static android.provider.Settings.Secure.FONT_WEIGHT_ADJUSTMENT;
+import static android.provider.Settings.System.FONT_SCALE;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MultiUserSettingsTests {
+    private final Context mContext = getInstrumentation().getTargetContext();
+    private final ContentResolver mContentResolver = mContext.getContentResolver();
+
+    private static void waitForBroadcastIdle() throws InterruptedException {
+        final int sleepDuration = 1000;
+        final String cmdAmWaitForBroadcastIdle = "am wait-for-broadcast-idle";
+
+        Thread.sleep(sleepDuration);
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .executeShellCommand(cmdAmWaitForBroadcastIdle);
+        Thread.sleep(sleepDuration);
+    }
+
+    private float getGlobalFontScale() {
+        return mContext.getResources().getConfiguration().fontScale;
+    }
+
+    private int getGlobalFontWeight() {
+        return mContext.getResources().getConfiguration().fontWeightAdjustment;
+    }
+
+    private float getFontScaleOfUser(int userId) {
+        return Settings.System.getFloatForUser(mContentResolver, FONT_SCALE, 1, userId);
+    }
+
+    private int getFontWeightOfUser(int userId) {
+        return Settings.Secure.getIntForUser(mContentResolver, FONT_WEIGHT_ADJUSTMENT, 1, userId);
+    }
+
+    private void setFontScaleOfUser(float fontScale, int userId) throws InterruptedException {
+        Settings.System.putFloatForUser(mContentResolver, FONT_SCALE, fontScale, userId);
+        waitForBroadcastIdle();
+    }
+
+    private void setFontWeightOfUser(int fontWeight, int userId) throws InterruptedException {
+        Settings.Secure.putIntForUser(mContentResolver, FONT_WEIGHT_ADJUSTMENT, fontWeight, userId);
+        waitForBroadcastIdle();
+    }
+
+    @Test
+    public void testChangingFontScaleOfABackgroundUser_shouldNotAffectUI()
+            throws InterruptedException {
+
+        Assume.assumeTrue(UserManager.supportsMultipleUsers());
+
+        UserManager userManager = UserManager.get(mContext);
+
+        final int backgroundUserId = userManager.createUser("test_user",
+                UserManager.USER_TYPE_FULL_SECONDARY, 0).id;
+        final float oldFontScaleOfBgUser = getFontScaleOfUser(backgroundUserId);
+        final float oldGlobalFontScale = getGlobalFontScale();
+        final float newFontScaleOfBgUser = 1 + Math.max(oldGlobalFontScale, oldFontScaleOfBgUser);
+
+        try {
+            setFontScaleOfUser(newFontScaleOfBgUser, backgroundUserId);
+            final float newGlobalFontScale = getGlobalFontScale();
+            assertEquals(oldGlobalFontScale, newGlobalFontScale, 0);
+        } finally {
+            setFontScaleOfUser(oldFontScaleOfBgUser, backgroundUserId);
+            userManager.removeUser(backgroundUserId);
+        }
+    }
+
+    @Test
+    public void testChangingFontWeightOfABackgroundUser_shouldNotAffectUI()
+            throws InterruptedException {
+
+        Assume.assumeTrue(UserManager.supportsMultipleUsers());
+
+        UserManager userManager = UserManager.get(mContext);
+
+        final int backgroundUserId = userManager.createUser("test_user",
+                UserManager.USER_TYPE_FULL_SECONDARY, 0).id;
+        final int oldFontWeightOfBgUser = getFontWeightOfUser(backgroundUserId);
+        final int oldGlobalFontWeight = getGlobalFontWeight();
+        final int newFontWeightOfBgUser = 2 * Math.max(oldGlobalFontWeight, oldFontWeightOfBgUser);
+
+        try {
+            setFontWeightOfUser(newFontWeightOfBgUser, backgroundUserId);
+            final int newGlobalFontWeight = getGlobalFontWeight();
+            assertEquals(oldGlobalFontWeight, newGlobalFontWeight);
+        } finally {
+            setFontWeightOfUser(oldFontWeightOfBgUser, backgroundUserId);
+            userManager.removeUser(backgroundUserId);
+        }
+    }
+
+    @Test
+    public void testChangingFontScaleOfTheForegroundUser_shouldAffectUI()
+            throws InterruptedException {
+
+        Assume.assumeTrue(UserManager.supportsMultipleUsers());
+
+        final int currentUserId = mContext.getUserId();
+        final float oldFontScale = getFontScaleOfUser(currentUserId);
+        final float newFontScale = 1 + oldFontScale;
+
+        try {
+            setFontScaleOfUser(newFontScale, currentUserId);
+            final float globalFontScale = getGlobalFontScale();
+            assertEquals(newFontScale, globalFontScale, 0);
+        } finally {
+            setFontScaleOfUser(oldFontScale, currentUserId);
+        }
+    }
+
+    @Test
+    public void testChangingFontWeightOfTheForegroundUser_shouldAffectUI()
+            throws InterruptedException {
+
+        Assume.assumeTrue(UserManager.supportsMultipleUsers());
+
+        final int currentUserId = mContext.getUserId();
+        final int oldFontWeight = getFontWeightOfUser(currentUserId);
+        final int newFontWeight = 2 * oldFontWeight;
+
+        try {
+            setFontWeightOfUser(newFontWeight, currentUserId);
+            final int globalFontWeight = getGlobalFontWeight();
+            assertEquals(newFontWeight, globalFontWeight);
+        } finally {
+            setFontWeightOfUser(oldFontWeight, currentUserId);
+        }
+    }
+}
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp
index d204190..d432341 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -588,10 +588,21 @@
             child_el->name == "provider" || child_el->name == "receiver" ||
             child_el->name == "service") {
           FullyQualifyClassName(original_package, xml::kSchemaAndroid, "name", child_el);
+          continue;
         }
 
         if (child_el->name == "activity-alias") {
           FullyQualifyClassName(original_package, xml::kSchemaAndroid, "targetActivity", child_el);
+          continue;
+        }
+
+        if (child_el->name == "processes") {
+          for (xml::Element* grand_child_el : child_el->GetChildElements()) {
+            if (grand_child_el->name == "process") {
+              FullyQualifyClassName(original_package, xml::kSchemaAndroid, "name", grand_child_el);
+            }
+          }
+          continue;
         }
       }
     }