Merge "Support slice in Spa."
diff --git a/core/api/current.txt b/core/api/current.txt
index af69bfb..3a0788d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -7669,6 +7669,7 @@
     method public boolean updateOverrideApn(@NonNull android.content.ComponentName, int, @NonNull android.telephony.data.ApnSetting);
     method public void wipeData(int);
     method public void wipeData(int, @NonNull CharSequence);
+    method public void wipeDevice(int);
     field public static final String ACTION_ADD_DEVICE_ADMIN = "android.app.action.ADD_DEVICE_ADMIN";
     field public static final String ACTION_ADMIN_POLICY_COMPLIANCE = "android.app.action.ADMIN_POLICY_COMPLIANCE";
     field public static final String ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED = "android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED";
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index f4cee5a..6fedb41 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -171,6 +171,7 @@
     private final boolean mParentInstance;
     private final DevicePolicyResourcesManager mResourcesManager;
 
+
     /** @hide */
     public DevicePolicyManager(Context context, IDevicePolicyManager service) {
         this(context, service, false);
@@ -6207,46 +6208,46 @@
     public static final int WIPE_SILENTLY = 0x0008;
 
     /**
-     * Ask that all user data be wiped. If called as a secondary user, the user will be removed and
-     * other users will remain unaffected. Calling from the primary user will cause the device to
-     * reboot, erasing all device data - including all the secondary users and their data - while
-     * booting up.
-     * <p>
-     * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
-     * be able to call this method; if it has not, a security exception will be thrown.
-     *
-     * If the caller is a profile owner of an organization-owned managed profile, it may
-     * additionally call this method on the parent instance.
-     * Calling this method on the parent {@link DevicePolicyManager} instance would wipe the
-     * entire device, while calling it on the current profile instance would relinquish the device
-     * for personal use, removing the managed profile and all policies set by the profile owner.
+     * See {@link #wipeData(int, CharSequence)}
      *
      * @param flags Bit mask of additional options: currently supported flags are
-     *            {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
-     *            {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
-     * @throws SecurityException if the calling application does not own an active administrator
-     *            that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} or is not granted the
-     *            {@link android.Manifest.permission#MASTER_CLEAR} permission.
+     *              {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
+     *              {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
+     * @throws SecurityException     if the calling application does not own an active
+     *                               administrator
+     *                               that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is
+     *                               not granted the
+     *                               {@link android.Manifest.permission#MASTER_CLEAR} permission.
+     * @throws IllegalStateException if called on last full-user or system-user
+     * @see #wipeDevice(int)
+     * @see #wipeData(int, CharSequence)
      */
     public void wipeData(int flags) {
-        wipeDataInternal(flags, "");
+        wipeDataInternal(flags,
+                /* wipeReasonForUser= */ "",
+                /* factoryReset= */ false);
     }
 
     /**
-     * Ask that all user data be wiped. If called as a secondary user, the user will be removed and
-     * other users will remain unaffected, the provided reason for wiping data can be shown to
-     * user. Calling from the primary user will cause the device to reboot, erasing all device data
-     * - including all the secondary users and their data - while booting up. In this case, we don't
-     * show the reason to the user since the device would be factory reset.
-     * <p>
-     * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
-     * be able to call this method; if it has not, a security exception will be thrown.
+     * Ask that all user data be wiped.
      *
-     * If the caller is a profile owner of an organization-owned managed profile, it may
-     * additionally call this method on the parent instance.
-     * Calling this method on the parent {@link DevicePolicyManager} instance would wipe the
-     * entire device, while calling it on the current profile instance would relinquish the device
-     * for personal use, removing the managed profile and all policies set by the profile owner.
+     * <p>
+     * If called as a secondary user or managed profile, the user itself and its associated user
+     * data will be wiped. In particular, If the caller is a profile owner of an
+     * organization-owned managed profile, calling this method will relinquish the device for
+     * personal use, removing the managed profile and all policies set by the profile owner.
+     * </p>
+     *
+     * <p>
+     * Calling this method from the primary user will only work if the calling app is targeting
+     * Android 13 or below, in which case it will cause the device to reboot, erasing all device
+     * data - including all the secondary users and their data - while booting up. If an app
+     * targeting Android 13+ is calling this method from the primary user or last full user,
+     * {@link IllegalStateException} will be thrown.
+     * </p>
+     *
+     * If an app wants to wipe the entire device irrespective of which user they are from, they
+     * should use {@link #wipeDevice} instead.
      *
      * @param flags Bit mask of additional options: currently supported flags are
      *            {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA} and
@@ -6254,30 +6255,61 @@
      * @param reason a string that contains the reason for wiping data, which can be
      *            presented to the user.
      * @throws SecurityException if the calling application does not own an active administrator
-     *            that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} or is not granted the
+     *            that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is not granted the
      *            {@link android.Manifest.permission#MASTER_CLEAR} permission.
      * @throws IllegalArgumentException if the input reason string is null or empty, or if
      *            {@link #WIPE_SILENTLY} is set.
+     * @throws IllegalStateException if called on last full-user or system-user
+     * @see #wipeDevice(int)
+     * @see #wipeData(int)
      */
     public void wipeData(int flags, @NonNull CharSequence reason) {
         Objects.requireNonNull(reason, "reason string is null");
         Preconditions.checkStringNotEmpty(reason, "reason string is empty");
         Preconditions.checkArgument((flags & WIPE_SILENTLY) == 0, "WIPE_SILENTLY cannot be set");
-        wipeDataInternal(flags, reason.toString());
+        wipeDataInternal(flags, reason.toString(), /* factoryReset= */ false);
     }
 
     /**
-     * Internal function for both {@link #wipeData(int)} and
-     * {@link #wipeData(int, CharSequence)} to call.
+     * Ask that the device be wiped and factory reset.
      *
+     * <p>
+     * The calling Device Owner or Organization Owned Profile Owner must have requested
+     * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to be able to call this method; if it has
+     * not, a security exception will be thrown.
+     *
+     * @param flags Bit mask of additional options: currently supported flags are
+     *              {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
+     *              {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
+     * @throws SecurityException if the calling application does not own an active administrator
+     *                           that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is not
+     *                           granted the {@link android.Manifest.permission#MASTER_CLEAR}
+     *                           permission.
      * @see #wipeData(int)
      * @see #wipeData(int, CharSequence)
-     * @hide
      */
-    private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser) {
+    // TODO(b/255323293) Add host-side tests
+    public void wipeDevice(int flags) {
+        wipeDataInternal(flags,
+                /* wipeReasonForUser= */ "",
+                /* factoryReset= */ true);
+    }
+
+    /**
+     * Internal function for {@link #wipeData(int)}, {@link #wipeData(int, CharSequence)}
+     * and {@link #wipeDevice(int)} to call.
+     *
+     * @hide
+     * @see #wipeData(int)
+     * @see #wipeData(int, CharSequence)
+     * @see #wipeDevice(int)
+     */
+    private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser,
+            boolean factoryReset) {
         if (mService != null) {
             try {
-                mService.wipeDataWithReason(flags, wipeReasonForUser, mParentInstance);
+                mService.wipeDataWithReason(flags, wipeReasonForUser, mParentInstance,
+                        factoryReset);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -8642,7 +8674,7 @@
     public void reportFailedPasswordAttempt(int userHandle) {
         if (mService != null) {
             try {
-                mService.reportFailedPasswordAttempt(userHandle);
+                mService.reportFailedPasswordAttempt(userHandle, mParentInstance);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 75bfc25..6c27dd7 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -117,7 +117,10 @@
 
     void lockNow(int flags, boolean parent);
 
-    void wipeDataWithReason(int flags, String wipeReasonForUser, boolean parent);
+    /**
+    * @param factoryReset only applicable when `targetSdk >= U`, either tries to factoryReset/fail or removeUser/fail otherwise
+    **/
+    void wipeDataWithReason(int flags, String wipeReasonForUser, boolean parent, boolean factoryReset);
 
     void setFactoryResetProtectionPolicy(in ComponentName who, in FactoryResetProtectionPolicy policy);
     FactoryResetProtectionPolicy getFactoryResetProtectionPolicy(in ComponentName who);
@@ -161,7 +164,7 @@
     boolean hasGrantedPolicy(in ComponentName policyReceiver, int usesPolicy, int userHandle);
 
     void reportPasswordChanged(in PasswordMetrics metrics, int userId);
-    void reportFailedPasswordAttempt(int userHandle);
+    void reportFailedPasswordAttempt(int userHandle, boolean parent);
     void reportSuccessfulPasswordAttempt(int userHandle);
     void reportFailedBiometricAttempt(int userHandle);
     void reportSuccessfulBiometricAttempt(int userHandle);
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 0d3c8db..fd35378 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -45,6 +45,7 @@
     private static final String ATTR_START_WITH_PARENT = "startWithParent";
     private static final String ATTR_SHOW_IN_SETTINGS = "showInSettings";
     private static final String ATTR_INHERIT_DEVICE_POLICY = "inheritDevicePolicy";
+    private static final String ATTR_USE_PARENTS_CONTACTS = "useParentsContacts";
 
     /** Index values of each property (to indicate whether they are present in this object). */
     @IntDef(prefix = "INDEX_", value = {
@@ -52,6 +53,7 @@
             INDEX_START_WITH_PARENT,
             INDEX_SHOW_IN_SETTINGS,
             INDEX_INHERIT_DEVICE_POLICY,
+            INDEX_USE_PARENTS_CONTACTS,
     })
     @Retention(RetentionPolicy.SOURCE)
     private @interface PropertyIndex {
@@ -60,6 +62,7 @@
     private static final int INDEX_START_WITH_PARENT = 1;
     private static final int INDEX_SHOW_IN_SETTINGS = 2;
     private static final int INDEX_INHERIT_DEVICE_POLICY = 3;
+    private static final int INDEX_USE_PARENTS_CONTACTS = 4;
     /** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
     private long mPropertiesPresent = 0;
 
@@ -200,6 +203,7 @@
         if (hasManagePermission) {
             // Add items that require MANAGE_USERS or stronger.
             setShowInSettings(orig.getShowInSettings());
+            setUseParentsContacts(orig.getUseParentsContacts());
         }
         if (hasQueryOrManagePermission) {
             // Add items that require QUERY_USERS or stronger.
@@ -317,6 +321,39 @@
     }
     private @InheritDevicePolicy int mInheritDevicePolicy;
 
+    /**
+     * Returns whether the current user must use parent user's contacts. If true, writes to the
+     * ContactsProvider corresponding to the current user will be disabled and reads will be
+     * redirected to the parent.
+     *
+     * This only applies to users that have parents (i.e. profiles) and is used to ensure
+     * they can access contacts from the parent profile. This will be generally inapplicable for
+     * non-profile users.
+     *
+     * Please note that in case of the clone profiles, only the allow-listed apps would be allowed
+     * to access contacts across profiles and other apps will not see any contacts.
+     * TODO(b/256126819) Add link to the method returning apps allow-listed for app-cloning
+     *
+     * @return whether contacts access from an associated profile is enabled for the user
+     * @hide
+     */
+    public boolean getUseParentsContacts() {
+        if (isPresent(INDEX_USE_PARENTS_CONTACTS)) return mUseParentsContacts;
+        if (mDefaultProperties != null) return mDefaultProperties.mUseParentsContacts;
+        throw new SecurityException("You don't have permission to query useParentsContacts");
+    }
+    /** @hide */
+    public void setUseParentsContacts(boolean val) {
+        this.mUseParentsContacts = val;
+        setPresent(INDEX_USE_PARENTS_CONTACTS);
+    }
+    /**
+     * Indicates whether the current user should use parent user's contacts.
+     * If this property is set true, the user will be blocked from storing any contacts in its
+     * own contacts database and will serve all read contacts calls through the parent's contacts.
+     */
+    private boolean mUseParentsContacts;
+
     @Override
     public String toString() {
         // Please print in increasing order of PropertyIndex.
@@ -326,6 +363,7 @@
                 + ", mStartWithParent=" + getStartWithParent()
                 + ", mShowInSettings=" + getShowInSettings()
                 + ", mInheritDevicePolicy=" + getInheritDevicePolicy()
+                + ", mUseParentsContacts=" + getUseParentsContacts()
                 + "}";
     }
 
@@ -341,6 +379,7 @@
         pw.println(prefix + "    mStartWithParent=" + getStartWithParent());
         pw.println(prefix + "    mShowInSettings=" + getShowInSettings());
         pw.println(prefix + "    mInheritDevicePolicy=" + getInheritDevicePolicy());
+        pw.println(prefix + "    mUseParentsContacts=" + getUseParentsContacts());
     }
 
     /**
@@ -386,6 +425,9 @@
                 case ATTR_INHERIT_DEVICE_POLICY:
                     setInheritDevicePolicy(parser.getAttributeInt(i));
                     break;
+                case ATTR_USE_PARENTS_CONTACTS:
+                    setUseParentsContacts(parser.getAttributeBoolean(i));
+                    break;
                 default:
                     Slog.w(LOG_TAG, "Skipping unknown property " + attributeName);
             }
@@ -416,6 +458,10 @@
             serializer.attributeInt(null, ATTR_INHERIT_DEVICE_POLICY,
                     mInheritDevicePolicy);
         }
+        if (isPresent(INDEX_USE_PARENTS_CONTACTS)) {
+            serializer.attributeBoolean(null, ATTR_USE_PARENTS_CONTACTS,
+                    mUseParentsContacts);
+        }
     }
 
     // For use only with an object that has already had any permission-lacking fields stripped out.
@@ -426,6 +472,7 @@
         dest.writeBoolean(mStartWithParent);
         dest.writeInt(mShowInSettings);
         dest.writeInt(mInheritDevicePolicy);
+        dest.writeBoolean(mUseParentsContacts);
     }
 
     /**
@@ -440,6 +487,7 @@
         mStartWithParent = source.readBoolean();
         mShowInSettings = source.readInt();
         mInheritDevicePolicy = source.readInt();
+        mUseParentsContacts = source.readBoolean();
     }
 
     @Override
@@ -468,6 +516,7 @@
         private boolean mStartWithParent = false;
         private @ShowInSettings int mShowInSettings = SHOW_IN_SETTINGS_WITH_PARENT;
         private @InheritDevicePolicy int mInheritDevicePolicy = INHERIT_DEVICE_POLICY_NO;
+        private boolean mUseParentsContacts = false;
 
         public Builder setShowInLauncher(@ShowInLauncher int showInLauncher) {
             mShowInLauncher = showInLauncher;
@@ -492,13 +541,19 @@
             return this;
         }
 
+        public Builder setUseParentsContacts(boolean useParentsContacts) {
+            mUseParentsContacts = useParentsContacts;
+            return this;
+        }
+
         /** Builds a UserProperties object with *all* values populated. */
         public UserProperties build() {
             return new UserProperties(
                     mShowInLauncher,
                     mStartWithParent,
                     mShowInSettings,
-                    mInheritDevicePolicy);
+                    mInheritDevicePolicy,
+                    mUseParentsContacts);
         }
     } // end Builder
 
@@ -507,12 +562,14 @@
             @ShowInLauncher int showInLauncher,
             boolean startWithParent,
             @ShowInSettings int showInSettings,
-            @InheritDevicePolicy int inheritDevicePolicy) {
+            @InheritDevicePolicy int inheritDevicePolicy,
+            boolean useParentsContacts) {
 
         mDefaultProperties = null;
         setShowInLauncher(showInLauncher);
         setStartWithParent(startWithParent);
         setShowInSettings(showInSettings);
         setInheritDevicePolicy(inheritDevicePolicy);
+        setUseParentsContacts(useParentsContacts);
     }
 }
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index d23fb36..d55367f 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -32,6 +32,7 @@
 import android.util.Log;
 import android.view.InputChannel;
 import android.view.MotionEvent;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethod;
@@ -93,9 +94,10 @@
     final int mTargetSdkVersion;
 
     /**
-     * This is not {@null} only between {@link #bindInput(InputBinding)} and {@link #unbindInput()}
-     * so that {@link RemoteInputConnection} can query if {@link #unbindInput()} has already been
-     * called or not, mainly to avoid unnecessary blocking operations.
+     * This is not {@code null} only between {@link #bindInput(InputBinding)} and
+     * {@link #unbindInput()} so that {@link RemoteInputConnection} can query if
+     * {@link #unbindInput()} has already been called or not, mainly to avoid unnecessary
+     * blocking operations.
      *
      * <p>This field must be set and cleared only from the binder thread(s), where the system
      * guarantees that {@link #bindInput(InputBinding)},
@@ -219,18 +221,26 @@
                 return;
             case DO_SHOW_SOFT_INPUT: {
                 final SomeArgs args = (SomeArgs) msg.obj;
+                final ImeTracker.Token statsToken = (ImeTracker.Token) args.arg3;
                 if (isValid(inputMethod, target, "DO_SHOW_SOFT_INPUT")) {
+                    ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
                     inputMethod.showSoftInputWithToken(
-                            msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1);
+                            msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1, statsToken);
+                } else {
+                    ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
                 }
                 args.recycle();
                 return;
             }
             case DO_HIDE_SOFT_INPUT: {
                 final SomeArgs args = (SomeArgs) msg.obj;
+                final ImeTracker.Token statsToken = (ImeTracker.Token) args.arg3;
                 if (isValid(inputMethod, target, "DO_HIDE_SOFT_INPUT")) {
+                    ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
                     inputMethod.hideSoftInputWithToken(msg.arg1, (ResultReceiver) args.arg2,
-                            (IBinder) args.arg1);
+                            (IBinder) args.arg1, statsToken);
+                } else {
+                    ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
                 }
                 args.recycle();
                 return;
@@ -416,16 +426,20 @@
 
     @BinderThread
     @Override
-    public void showSoftInput(IBinder showInputToken, int flags, ResultReceiver resultReceiver) {
-        mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_SHOW_SOFT_INPUT,
-                flags, showInputToken, resultReceiver));
+    public void showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
+            int flags, ResultReceiver resultReceiver) {
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
+        mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_SHOW_SOFT_INPUT,
+                flags, showInputToken, resultReceiver, statsToken));
     }
 
     @BinderThread
     @Override
-    public void hideSoftInput(IBinder hideInputToken, int flags, ResultReceiver resultReceiver) {
-        mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_HIDE_SOFT_INPUT,
-                flags, hideInputToken, resultReceiver));
+    public void hideSoftInput(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken,
+            int flags, ResultReceiver resultReceiver) {
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
+        mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_HIDE_SOFT_INPUT,
+                flags, hideInputToken, resultReceiver, statsToken));
     }
 
     @BinderThread
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index d902486..bf4fc4a 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -124,6 +124,7 @@
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InlineSuggestionsRequest;
 import android.view.inputmethod.InlineSuggestionsResponse;
 import android.view.inputmethod.InputBinding;
@@ -669,6 +670,10 @@
      */
     private IBinder mCurHideInputToken;
 
+    /** The token tracking the current IME request or {@code null} otherwise. */
+    @Nullable
+    private ImeTracker.Token mCurStatsToken;
+
     final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {
         onComputeInsets(mTmpInsets);
         if (!mViewsCreated) {
@@ -870,10 +875,12 @@
         @MainThread
         @Override
         public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
-                IBinder hideInputToken) {
+                IBinder hideInputToken, @Nullable ImeTracker.Token statsToken) {
             mSystemCallingHideSoftInput = true;
             mCurHideInputToken = hideInputToken;
+            mCurStatsToken = statsToken;
             hideSoftInput(flags, resultReceiver);
+            mCurStatsToken = null;
             mCurHideInputToken = null;
             mSystemCallingHideSoftInput = false;
         }
@@ -884,6 +891,7 @@
         @MainThread
         @Override
         public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
+            ImeTracker.get().onProgress(mCurStatsToken, ImeTracker.PHASE_IME_HIDE_SOFT_INPUT);
             if (DEBUG) Log.v(TAG, "hideSoftInput()");
             if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R
                     && !mSystemCallingHideSoftInput) {
@@ -918,12 +926,17 @@
         @MainThread
         @Override
         public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
-                IBinder showInputToken) {
+                IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
             mSystemCallingShowSoftInput = true;
             mCurShowInputToken = showInputToken;
-            showSoftInput(flags, resultReceiver);
-            mCurShowInputToken = null;
-            mSystemCallingShowSoftInput = false;
+            mCurStatsToken = statsToken;
+            try {
+                showSoftInput(flags, resultReceiver);
+            } finally {
+                mCurStatsToken = null;
+                mCurShowInputToken = null;
+                mSystemCallingShowSoftInput = false;
+            }
         }
 
         /**
@@ -932,6 +945,7 @@
         @MainThread
         @Override
         public void showSoftInput(int flags, ResultReceiver resultReceiver) {
+            ImeTracker.get().onProgress(mCurStatsToken, ImeTracker.PHASE_IME_SHOW_SOFT_INPUT);
             if (DEBUG) Log.v(TAG, "showSoftInput()");
             // TODO(b/148086656): Disallow IME developers from calling InputMethodImpl methods.
             if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R
@@ -947,7 +961,12 @@
                     null /* icProto */);
             final boolean wasVisible = isInputViewShown();
             if (dispatchOnShowInputRequested(flags, false)) {
+                ImeTracker.get().onProgress(mCurStatsToken,
+                        ImeTracker.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE);
                 showWindow(true);
+            } else {
+                ImeTracker.get().onFailed(mCurStatsToken,
+                        ImeTracker.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE);
             }
             setImeWindowStatus(mapToImeWindowStatus(), mBackDisposition);
 
@@ -2923,8 +2942,10 @@
         ImeTracing.getInstance().triggerServiceDump(
                 "InputMethodService#applyVisibilityInInsetsConsumerIfNecessary", mDumper,
                 null /* icProto */);
+        ImeTracker.get().onProgress(mCurStatsToken,
+                ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
         mPrivOps.applyImeVisibilityAsync(setVisible
-                ? mCurShowInputToken : mCurHideInputToken, setVisible);
+                ? mCurShowInputToken : mCurHideInputToken, setVisible, mCurStatsToken);
     }
 
     private void finishViews(boolean finishingInput) {
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index 6091bf9..bf72b1d 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -21,6 +21,7 @@
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
+import android.hardware.vibrator.IVibrator;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Range;
@@ -313,8 +314,14 @@
         private static final float EPSILON = 1e-5f;
 
         public MultiVibratorInfo(VibratorInfo[] vibrators) {
+            // Need to use an extra constructor to share the computation in super initialization.
+            this(vibrators, frequencyProfileIntersection(vibrators));
+        }
+
+        private MultiVibratorInfo(VibratorInfo[] vibrators,
+                VibratorInfo.FrequencyProfile mergedProfile) {
             super(/* id= */ -1,
-                    capabilitiesIntersection(vibrators),
+                    capabilitiesIntersection(vibrators, mergedProfile.isEmpty()),
                     supportedEffectsIntersection(vibrators),
                     supportedBrakingIntersection(vibrators),
                     supportedPrimitivesAndDurationsIntersection(vibrators),
@@ -323,14 +330,19 @@
                     integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax),
                     integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax),
                     floatPropertyIntersection(vibrators, VibratorInfo::getQFactor),
-                    frequencyProfileIntersection(vibrators));
+                    mergedProfile);
         }
 
-        private static int capabilitiesIntersection(VibratorInfo[] infos) {
+        private static int capabilitiesIntersection(VibratorInfo[] infos,
+                boolean frequencyProfileIsEmpty) {
             int intersection = ~0;
             for (VibratorInfo info : infos) {
                 intersection &= info.getCapabilities();
             }
+            if (frequencyProfileIsEmpty) {
+                // Revoke frequency control if the merged frequency profile ended up empty.
+                intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL;
+            }
             return intersection;
         }
 
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 336f183..52b1adb 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10431,11 +10431,11 @@
         public static final String QS_AUTO_ADDED_TILES = "qs_auto_tiles";
 
         /**
-         * The duration of timeout, in milliseconds, to switch from a non-primary user to the
-         * primary user when the device is docked.
+         * The duration of timeout, in milliseconds, to switch from a non-Dock User to the
+         * Dock User when the device is docked.
          * @hide
          */
-        public static final String TIMEOUT_TO_USER_ZERO = "timeout_to_user_zero";
+        public static final String TIMEOUT_TO_DOCK_USER = "timeout_to_dock_user";
 
         /**
          * Backup manager behavioral parameters.
diff --git a/core/java/android/view/IDisplayWindowInsetsController.aidl b/core/java/android/view/IDisplayWindowInsetsController.aidl
index 0769f12..91270d4 100644
--- a/core/java/android/view/IDisplayWindowInsetsController.aidl
+++ b/core/java/android/view/IDisplayWindowInsetsController.aidl
@@ -19,6 +19,7 @@
 import android.content.ComponentName;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
+import android.view.inputmethod.ImeTracker;
 
 /**
  * Singular controller of insets to use when there isn't another obvious controller available.
@@ -48,10 +49,10 @@
     /**
      * @see IWindow#showInsets
      */
-    void showInsets(int types, boolean fromIme);
+    void showInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
 
     /**
      * @see IWindow#hideInsets
      */
-    void hideInsets(int types, boolean fromIme);
+    void hideInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
 }
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index a856474..8e16f24 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -29,6 +29,7 @@
 import android.view.IScrollCaptureResponseListener;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -68,16 +69,18 @@
      *
      * @param types internal insets types (WindowInsets.Type.InsetsType) to show
      * @param fromIme true if this request originated from IME (InputMethodService).
+     * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
      */
-    void showInsets(int types, boolean fromIme);
+    void showInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
 
     /**
      * Called when a set of insets source window should be hidden by policy.
      *
      * @param types internal insets types (WindowInsets.Type.InsetsType) to hide
      * @param fromIme true if this request originated from IME (InputMethodService).
+     * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
      */
-    void hideInsets(int types, boolean fromIme);
+    void hideInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
 
     void moved(int newX, int newY);
     void dispatchAppVisibility(boolean visible);
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 27b4d87..e775969 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -58,6 +58,7 @@
 import android.view.WindowInsetsAnimation.Bounds;
 import android.view.WindowManager.LayoutParams;
 import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -68,7 +69,7 @@
  * Implements {@link WindowInsetsAnimationController}
  * @hide
  */
-@VisibleForTesting
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
 public class InsetsAnimationControlImpl implements InternalInsetsAnimationController,
         InsetsAnimationControlRunner {
 
@@ -96,6 +97,8 @@
     /** @see WindowInsetsAnimationController#hasZeroInsetsIme */
     private final boolean mHasZeroInsetsIme;
     private final CompatibilityInfo.Translator mTranslator;
+    @Nullable
+    private final ImeTracker.Token mStatsToken;
     private Insets mCurrentInsets;
     private Insets mPendingInsets;
     private float mPendingFraction;
@@ -114,7 +117,7 @@
             @InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
             Interpolator interpolator, @AnimationType int animationType,
             @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
-            CompatibilityInfo.Translator translator) {
+            CompatibilityInfo.Translator translator, @Nullable ImeTracker.Token statsToken) {
         mControls = controls;
         mListener = listener;
         mTypes = types;
@@ -152,6 +155,7 @@
         mAnimationType = animationType;
         mLayoutInsetsDuringAnimation = layoutInsetsDuringAnimation;
         mTranslator = translator;
+        mStatsToken = statsToken;
         mController.startAnimation(this, listener, types, mAnimation,
                 new Bounds(mHiddenInsets, mShownInsets));
     }
@@ -228,6 +232,11 @@
     }
 
     @Override
+    public ImeTracker.Token getStatsToken() {
+        return mStatsToken;
+    }
+
+    @Override
     public void setInsetsAndAlpha(Insets insets, float alpha, float fraction) {
         setInsetsAndAlpha(insets, alpha, fraction, false /* allowWhenFinished */);
     }
@@ -253,10 +262,10 @@
         }
     }
 
-    @VisibleForTesting
     /**
      * @return Whether the finish callback of this animation should be invoked.
      */
+    @VisibleForTesting
     public boolean applyChangeInsets(@Nullable InsetsState outState) {
         if (mCancelled) {
             if (DEBUG) Log.d(TAG, "applyChangeInsets canceled");
diff --git a/core/java/android/view/InsetsAnimationControlRunner.java b/core/java/android/view/InsetsAnimationControlRunner.java
index 291351e..cf40e7e 100644
--- a/core/java/android/view/InsetsAnimationControlRunner.java
+++ b/core/java/android/view/InsetsAnimationControlRunner.java
@@ -16,10 +16,12 @@
 
 package android.view;
 
+import android.annotation.Nullable;
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
 import android.view.InsetsController.AnimationType;
 import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
 
 /**
  * Interface representing a runner for an insets animation.
@@ -74,6 +76,12 @@
     @AnimationType int getAnimationType();
 
     /**
+     * @return The token tracking the current IME request or {@code null} otherwise.
+     */
+    @Nullable
+    ImeTracker.Token getStatsToken();
+
+    /**
      *
      * Export the state of classes that implement this interface into a protocol buffer
      * output stream.
diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java
index fc97541..f7b9aa2 100644
--- a/core/java/android/view/InsetsAnimationThreadControlRunner.java
+++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java
@@ -34,6 +34,7 @@
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsAnimation.Bounds;
 import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
 
 /**
  * Insets animation runner that uses {@link InsetsAnimationThread} to run the animation off from the
@@ -112,12 +113,13 @@
             @InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
             Interpolator interpolator, @AnimationType int animationType,
             @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
-            CompatibilityInfo.Translator translator, Handler mainThreadHandler) {
+            CompatibilityInfo.Translator translator, Handler mainThreadHandler,
+            @Nullable ImeTracker.Token statsToken) {
         mMainThreadHandler = mainThreadHandler;
         mOuterCallbacks = controller;
         mControl = new InsetsAnimationControlImpl(controls, frame, state, listener, types,
                 mCallbacks, durationMs, interpolator, animationType, layoutInsetsDuringAnimation,
-                translator);
+                translator, statsToken);
         InsetsAnimationThread.getHandler().post(() -> {
             if (mControl.isCancelled()) {
                 return;
@@ -141,6 +143,11 @@
     }
 
     @Override
+    public ImeTracker.Token getStatsToken() {
+        return mControl.getStatsToken();
+    }
+
+    @Override
     @UiThread
     public int getTypes() {
         return mControl.getTypes();
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 35838a3..fbd8226 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -44,6 +44,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Trace;
+import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
@@ -60,6 +61,7 @@
 import android.view.animation.Interpolator;
 import android.view.animation.LinearInterpolator;
 import android.view.animation.PathInterpolator;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodManager;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -929,10 +931,12 @@
         hideTypes[0] &= ~animatingTypes;
 
         if (showTypes[0] != 0) {
-            applyAnimation(showTypes[0], true /* show */, false /* fromIme */);
+            applyAnimation(showTypes[0], true /* show */, false /* fromIme */,
+                    null /* statsToken */);
         }
         if (hideTypes[0] != 0) {
-            applyAnimation(hideTypes[0], false /* show */, false /* fromIme */);
+            applyAnimation(hideTypes[0], false /* show */, false /* fromIme */,
+                    null /* statsToken */);
         }
 
         if (mControllableTypes != controllableTypes) {
@@ -948,11 +952,12 @@
 
     @Override
     public void show(@InsetsType int types) {
-        show(types, false /* fromIme */);
+        show(types, false /* fromIme */, null /* statsToken */);
     }
 
-    @VisibleForTesting
-    public void show(@InsetsType int types, boolean fromIme) {
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public void show(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
         if ((types & ime()) != 0) {
             Log.d(TAG, "show(ime(), fromIme=" + fromIme + ")");
         }
@@ -979,7 +984,7 @@
                     true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator,
                     pendingRequest.animationType,
                     pendingRequest.layoutInsetsDuringAnimation,
-                    pendingRequest.useInsetsAnimationThread);
+                    pendingRequest.useInsetsAnimationThread, statsToken);
             return;
         }
 
@@ -990,8 +995,9 @@
             if ((types & type) == 0) {
                 continue;
             }
-            final @AnimationType int animationType = getAnimationType(type);
+            @AnimationType final int animationType = getAnimationType(type);
             final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0;
+            final boolean isImeAnimation = type == ime();
             if (requestedVisible && animationType == ANIMATION_TYPE_NONE
                     || animationType == ANIMATION_TYPE_SHOW) {
                 // no-op: already shown or animating in (because window visibility is
@@ -999,25 +1005,36 @@
                 if (DEBUG) Log.d(TAG, String.format(
                         "show ignored for type: %d animType: %d requestedVisible: %s",
                         type, animationType, requestedVisible));
+                if (isImeAnimation) {
+                    ImeTracker.get().onCancelled(statsToken,
+                            ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+                }
                 continue;
             }
             if (fromIme && animationType == ANIMATION_TYPE_USER) {
                 // App is already controlling the IME, don't cancel it.
+                if (isImeAnimation) {
+                    ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+                }
                 continue;
             }
+            if (isImeAnimation) {
+                ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+            }
             typesReady |= type;
         }
         if (DEBUG) Log.d(TAG, "show typesReady: " + typesReady);
-        applyAnimation(typesReady, true /* show */, fromIme);
+        applyAnimation(typesReady, true /* show */, fromIme, statsToken);
     }
 
     @Override
     public void hide(@InsetsType int types) {
-        hide(types, false /* fromIme */);
+        hide(types, false /* fromIme */, null /* statsToken */);
     }
 
     @VisibleForTesting
-    public void hide(@InsetsType int types, boolean fromIme) {
+    public void hide(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
         if (fromIme) {
             ImeTracing.getInstance().triggerClientDump("InsetsController#hide",
                     mHost.getInputMethodManager(), null /* icProto */);
@@ -1030,16 +1047,25 @@
             if ((types & type) == 0) {
                 continue;
             }
-            final @AnimationType int animationType = getAnimationType(type);
+            @AnimationType final int animationType = getAnimationType(type);
             final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0;
+            final boolean isImeAnimation = type == ime();
             if (!requestedVisible && animationType == ANIMATION_TYPE_NONE
                     || animationType == ANIMATION_TYPE_HIDE) {
-                // no-op: already hidden or animating out.
+                // no-op: already hidden or animating out (because window visibility is
+                // applied before starting animation).
+                if (isImeAnimation) {
+                    ImeTracker.get().onCancelled(statsToken,
+                            ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+                }
                 continue;
             }
+            if (isImeAnimation) {
+                ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+            }
             typesReady |= type;
         }
-        applyAnimation(typesReady, false /* show */, fromIme /* fromIme */);
+        applyAnimation(typesReady, false /* show */, fromIme, statsToken);
     }
 
     @Override
@@ -1068,7 +1094,7 @@
 
         controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs,
                 interpolator, animationType, getLayoutInsetsDuringAnimationMode(types),
-                false /* useInsetsAnimationThread */);
+                false /* useInsetsAnimationThread */, null /* statsToken */);
     }
 
     private void controlAnimationUnchecked(@InsetsType int types,
@@ -1077,7 +1103,8 @@
             long durationMs, Interpolator interpolator,
             @AnimationType int animationType,
             @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
-            boolean useInsetsAnimationThread) {
+            boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) {
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_CONTROL_ANIMATION);
         if ((types & mTypesBeingCancelled) != 0) {
             throw new IllegalStateException("Cannot start a new insets animation of "
                     + Type.toString(types)
@@ -1152,14 +1179,16 @@
                 ? new InsetsAnimationThreadControlRunner(controls,
                         frame, mState, listener, typesReady, this, durationMs, interpolator,
                         animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
-                        mHost.getHandler())
+                        mHost.getHandler(), statsToken)
                 : new InsetsAnimationControlImpl(controls,
                         frame, mState, listener, typesReady, this, durationMs, interpolator,
-                        animationType, layoutInsetsDuringAnimation, mHost.getTranslator());
+                        animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
+                        statsToken);
         if ((typesReady & WindowInsets.Type.ime()) != 0) {
             ImeTracing.getInstance().triggerClientDump("InsetsAnimationControlImpl",
                     mHost.getInputMethodManager(), null /* icProto */);
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING);
         mRunningAnimations.add(new RunningAnimation(runner, animationType));
         if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
                 + useInsetsAnimationThread);
@@ -1311,11 +1340,18 @@
             // requested visibility.
             return;
         }
+        final ImeTracker.Token statsToken = runner.getStatsToken();
         if (shown) {
+            ImeTracker.get().onProgress(statsToken,
+                    ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_SHOW);
             showDirectly(runner.getTypes(), true /* fromIme */);
+            ImeTracker.get().onShown(statsToken);
         } else {
+            ImeTracker.get().onProgress(statsToken,
+                    ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE);
             hideDirectly(runner.getTypes(), true /* animationFinished */,
                     runner.getAnimationType(), true /* fromIme */);
+            ImeTracker.get().onHidden(statsToken);
         }
     }
 
@@ -1339,10 +1375,19 @@
     }
 
     private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) {
-        if (DEBUG) Log.d(TAG, String.format("cancelAnimation of types: %d, animType: %d, host: %s",
-                control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle()));
         if (invokeCallback) {
+            ImeTracker.get().onCancelled(control.getStatsToken(),
+                    ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL);
             control.cancel();
+        } else {
+            // Succeeds if invokeCallback is false (i.e. when called from notifyFinished).
+            ImeTracker.get().onProgress(control.getStatsToken(),
+                    ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL);
+        }
+        if (DEBUG) {
+            Log.d(TAG, TextUtils.formatSimple(
+                    "cancelAnimation of types: %d, animType: %d, host: %s",
+                    control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle()));
         }
         boolean stateChanged = false;
         for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
@@ -1452,7 +1497,8 @@
     }
 
     @VisibleForTesting
-    public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme) {
+    public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
         // TODO(b/166736352): We should only skip the animation of specific types, not all types.
         boolean skipAnim = false;
         if ((types & ime()) != 0) {
@@ -1465,12 +1511,12 @@
                         && consumer.hasViewFocusWhenWindowFocusGain();
             }
         }
-        applyAnimation(types, show, fromIme, skipAnim);
+        applyAnimation(types, show, fromIme, skipAnim, statsToken);
     }
 
     @VisibleForTesting
     public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
-            boolean skipAnim) {
+            boolean skipAnim, @Nullable ImeTracker.Token statsToken) {
         if (types == 0) {
             // nothing to animate.
             if (DEBUG) Log.d(TAG, "applyAnimation, nothing to animate");
@@ -1490,12 +1536,11 @@
                 listener.getDurationMs(), listener.getInsetsInterpolator(),
                 show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE,
                 show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
-                !hasAnimationCallbacks /* useInsetsAnimationThread */);
+                !hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken);
     }
 
-    private void hideDirectly(
-            @InsetsType int types, boolean animationFinished, @AnimationType int animationType,
-            boolean fromIme) {
+    private void hideDirectly(@InsetsType int types, boolean animationFinished,
+            @AnimationType int animationType, boolean fromIme) {
         if ((types & ime()) != 0) {
             ImeTracing.getInstance().triggerClientDump("InsetsController#hideDirectly",
                     mHost.getInputMethodManager(), null /* icProto */);
diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java
index edcfc95..778c677 100644
--- a/core/java/android/view/InsetsResizeAnimationRunner.java
+++ b/core/java/android/view/InsetsResizeAnimationRunner.java
@@ -37,6 +37,7 @@
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsAnimation.Bounds;
 import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
 
 /**
  * Runs a fake animation of resizing insets to produce insets animation callbacks.
@@ -92,6 +93,12 @@
     }
 
     @Override
+    public ImeTracker.Token getStatsToken() {
+        // Return null as resizing the IME view is not explicitly tracked.
+        return null;
+    }
+
+    @Override
     public void cancel() {
         if (mCancelled || mFinished) {
             return;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 43f0947..63602393 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -192,6 +192,7 @@
 import android.view.contentcapture.ContentCaptureManager;
 import android.view.contentcapture.ContentCaptureSession;
 import android.view.contentcapture.MainContentCaptureSession;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Scroller;
 import android.window.ClientWindowFrames;
@@ -5649,17 +5650,23 @@
                     break;
                 }
                 case MSG_SHOW_INSETS: {
+                    final ImeTracker.Token statsToken = (ImeTracker.Token) msg.obj;
+                    ImeTracker.get().onProgress(statsToken,
+                            ImeTracker.PHASE_CLIENT_HANDLE_SHOW_INSETS);
                     if (mView == null) {
                         Log.e(TAG,
                                 String.format("Calling showInsets(%d,%b) on window that no longer"
                                         + " has views.", msg.arg1, msg.arg2 == 1));
                     }
                     clearLowProfileModeIfNeeded(msg.arg1, msg.arg2 == 1);
-                    mInsetsController.show(msg.arg1, msg.arg2 == 1);
+                    mInsetsController.show(msg.arg1, msg.arg2 == 1, statsToken);
                     break;
                 }
                 case MSG_HIDE_INSETS: {
-                    mInsetsController.hide(msg.arg1, msg.arg2 == 1);
+                    final ImeTracker.Token statsToken = (ImeTracker.Token) msg.obj;
+                    ImeTracker.get().onProgress(statsToken,
+                            ImeTracker.PHASE_CLIENT_HANDLE_HIDE_INSETS);
+                    mInsetsController.hide(msg.arg1, msg.arg2 == 1, statsToken);
                     break;
                 }
                 case MSG_WINDOW_MOVED:
@@ -8811,12 +8818,14 @@
         mHandler.obtainMessage(MSG_INSETS_CONTROL_CHANGED, args).sendToTarget();
     }
 
-    private void showInsets(@InsetsType int types, boolean fromIme) {
-        mHandler.obtainMessage(MSG_SHOW_INSETS, types, fromIme ? 1 : 0).sendToTarget();
+    private void showInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
+        mHandler.obtainMessage(MSG_SHOW_INSETS, types, fromIme ? 1 : 0, statsToken).sendToTarget();
     }
 
-    private void hideInsets(@InsetsType int types, boolean fromIme) {
-        mHandler.obtainMessage(MSG_HIDE_INSETS, types, fromIme ? 1 : 0).sendToTarget();
+    private void hideInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
+        mHandler.obtainMessage(MSG_HIDE_INSETS, types, fromIme ? 1 : 0, statsToken).sendToTarget();
     }
 
     public void dispatchMoved(int newX, int newY) {
@@ -10180,7 +10189,8 @@
         }
 
         @Override
-        public void showInsets(@InsetsType int types, boolean fromIme) {
+        public void showInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             final ViewRootImpl viewAncestor = mViewAncestor.get();
             if (fromIme) {
                 ImeTracing.getInstance().triggerClientDump("ViewRootImpl.W#showInsets",
@@ -10188,13 +10198,16 @@
                         null /* icProto */);
             }
             if (viewAncestor != null) {
-                viewAncestor.showInsets(types, fromIme);
+                ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_SHOW_INSETS);
+                viewAncestor.showInsets(types, fromIme, statsToken);
+            } else {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_SHOW_INSETS);
             }
         }
 
         @Override
-        public void hideInsets(@InsetsType int types, boolean fromIme) {
-
+        public void hideInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             final ViewRootImpl viewAncestor = mViewAncestor.get();
             if (fromIme) {
                 ImeTracing.getInstance().triggerClientDump("ViewRootImpl.W#hideInsets",
@@ -10202,7 +10215,10 @@
                         null /* icProto */);
             }
             if (viewAncestor != null) {
-                viewAncestor.hideInsets(types, fromIme);
+                ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_HIDE_INSETS);
+                viewAncestor.hideInsets(types, fromIme, statsToken);
+            } else {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_HIDE_INSETS);
             }
         }
 
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index a66c67b..6eae63a 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -275,15 +275,16 @@
 
     @AnyThread
     static boolean showSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
-            int flags, int lastClickToolType, @Nullable ResultReceiver resultReceiver,
+            @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
+            @Nullable ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason) {
         final IInputMethodManager service = getService();
         if (service == null) {
             return false;
         }
         try {
-            return service.showSoftInput(
-                    client, windowToken, flags, lastClickToolType, resultReceiver, reason);
+            return service.showSoftInput(client, windowToken, statsToken, flags, lastClickToolType,
+                    resultReceiver, reason);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -291,14 +292,15 @@
 
     @AnyThread
     static boolean hideSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
-            int flags, @Nullable ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
+            @Nullable ImeTracker.Token statsToken, int flags,
+            @Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
         final IInputMethodManager service = getService();
         if (service == null) {
             return false;
         }
         try {
-            return service.hideSoftInput(client, windowToken, flags, resultReceiver, reason);
+            return service.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver,
+                    reason);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/view/inputmethod/ImeTracker.aidl b/core/java/android/view/inputmethod/ImeTracker.aidl
new file mode 100644
index 0000000..1988f48
--- /dev/null
+++ b/core/java/android/view/inputmethod/ImeTracker.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod;
+
+parcelable ImeTracker.Token;
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
new file mode 100644
index 0000000..f4ecdff
--- /dev/null
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -0,0 +1,488 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod;
+
+import static android.view.inputmethod.ImeTracker.Debug.originToString;
+import static android.view.inputmethod.ImeTracker.Debug.phaseToString;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Random;
+import java.util.stream.Collectors;
+
+/** @hide */
+public interface ImeTracker {
+
+    String TAG = "ImeTracker";
+
+    /**
+     * The origin of the IME request
+     *
+     * The name follows the format {@code PHASE_x_...} where {@code x} denotes
+     * where the origin is (i.e. {@code PHASE_SERVER_...} occurs in the server).
+     */
+    @IntDef(prefix = { "ORIGIN_" }, value = {
+            ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+            ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+            ORIGIN_SERVER_START_INPUT,
+            ORIGIN_SERVER_HIDE_INPUT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface Origin {}
+
+    /**
+     * The IME show request originated in the client.
+     */
+    int ORIGIN_CLIENT_SHOW_SOFT_INPUT = 0;
+
+    /**
+     * The IME hide request originated in the client.
+     */
+    int ORIGIN_CLIENT_HIDE_SOFT_INPUT = 1;
+
+    /**
+     * The IME show request originated in the server.
+     */
+    int ORIGIN_SERVER_START_INPUT = 2;
+
+    /**
+     * The IME hide request originated in the server.
+     */
+    int ORIGIN_SERVER_HIDE_INPUT = 3;
+
+    /**
+     * The current phase of the IME request.
+     *
+     * The name follows the format {@code PHASE_x_...} where {@code x} denotes
+     * where the phase is (i.e. {@code PHASE_SERVER_...} occurs in the server).
+     */
+    @IntDef(prefix = { "PHASE_" }, value = {
+            PHASE_CLIENT_VIEW_SERVED,
+            PHASE_SERVER_CLIENT_KNOWN,
+            PHASE_SERVER_CLIENT_FOCUSED,
+            PHASE_SERVER_ACCESSIBILITY,
+            PHASE_SERVER_SYSTEM_READY,
+            PHASE_SERVER_HIDE_IMPLICIT,
+            PHASE_SERVER_HIDE_NOT_ALWAYS,
+            PHASE_SERVER_WAIT_IME,
+            PHASE_SERVER_HAS_IME,
+            PHASE_SERVER_SHOULD_HIDE,
+            PHASE_IME_WRAPPER,
+            PHASE_IME_WRAPPER_DISPATCH,
+            PHASE_IME_SHOW_SOFT_INPUT,
+            PHASE_IME_HIDE_SOFT_INPUT,
+            PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE,
+            PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER,
+            PHASE_SERVER_APPLY_IME_VISIBILITY,
+            PHASE_WM_SHOW_IME_RUNNER,
+            PHASE_WM_SHOW_IME_READY,
+            PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET,
+            PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS,
+            PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS,
+            PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS,
+            PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS,
+            PHASE_WM_REMOTE_INSETS_CONTROLLER,
+            PHASE_WM_ANIMATION_CREATE,
+            PHASE_WM_ANIMATION_RUNNING,
+            PHASE_CLIENT_SHOW_INSETS,
+            PHASE_CLIENT_HIDE_INSETS,
+            PHASE_CLIENT_HANDLE_SHOW_INSETS,
+            PHASE_CLIENT_HANDLE_HIDE_INSETS,
+            PHASE_CLIENT_APPLY_ANIMATION,
+            PHASE_CLIENT_CONTROL_ANIMATION,
+            PHASE_CLIENT_ANIMATION_RUNNING,
+            PHASE_CLIENT_ANIMATION_CANCEL,
+            PHASE_CLIENT_ANIMATION_FINISHED_SHOW,
+            PHASE_CLIENT_ANIMATION_FINISHED_HIDE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface Phase {}
+
+    /** The view that requested the IME has been served by the IMM. */
+    int PHASE_CLIENT_VIEW_SERVED = 0;
+
+    /** The IME client that requested the IME has window manager focus. */
+    int PHASE_SERVER_CLIENT_KNOWN = 1;
+
+    /** The IME client that requested the IME has IME focus. */
+    int PHASE_SERVER_CLIENT_FOCUSED = 2;
+
+    /** The IME request complies with the current accessibility settings. */
+    int PHASE_SERVER_ACCESSIBILITY = 3;
+
+    /** The server is ready to run third party code. */
+    int PHASE_SERVER_SYSTEM_READY = 4;
+
+    /** Checked the implicit hide request against any explicit show requests. */
+    int PHASE_SERVER_HIDE_IMPLICIT = 5;
+
+    /** Checked the not-always hide request against any forced show requests. */
+    int PHASE_SERVER_HIDE_NOT_ALWAYS = 6;
+
+    /** The server is waiting for a connection to the IME. */
+    int PHASE_SERVER_WAIT_IME = 7;
+
+    /** The server has a connection to the IME. */
+    int PHASE_SERVER_HAS_IME = 8;
+
+    /** The server decided the IME should be hidden. */
+    int PHASE_SERVER_SHOULD_HIDE = 9;
+
+    /** Reached the IME wrapper. */
+    int PHASE_IME_WRAPPER = 10;
+
+    /** Dispatched from the IME wrapper to the IME. */
+    int PHASE_IME_WRAPPER_DISPATCH = 11;
+
+    /** Reached the IME' showSoftInput method. */
+    int PHASE_IME_SHOW_SOFT_INPUT = 12;
+
+    /** Reached the IME' hideSoftInput method. */
+    int PHASE_IME_HIDE_SOFT_INPUT = 13;
+
+    /** The server decided the IME should be shown. */
+    int PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE = 14;
+
+    /** Requested applying the IME visibility in the insets source consumer. */
+    int PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER = 15;
+
+    /** Applied the IME visibility. */
+    int PHASE_SERVER_APPLY_IME_VISIBILITY = 16;
+
+    /** Created the show IME runner. */
+    int PHASE_WM_SHOW_IME_RUNNER = 17;
+
+    /** Ready to show IME. */
+    int PHASE_WM_SHOW_IME_READY = 18;
+
+    /** The Window Manager has a connection to the IME insets control target. */
+    int PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET = 19;
+
+    /** Reached the window insets control target's show insets method. */
+    int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS = 20;
+
+    /** Reached the window insets control target's hide insets method. */
+    int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS = 21;
+
+    /** Reached the remote insets control target's show insets method. */
+    int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS = 22;
+
+    /** Reached the remote insets control target's hide insets method. */
+    int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS = 23;
+
+    /** Reached the remote insets controller. */
+    int PHASE_WM_REMOTE_INSETS_CONTROLLER = 24;
+
+    /** Created the IME window insets show animation. */
+    int PHASE_WM_ANIMATION_CREATE = 25;
+
+    /** Started the IME window insets show animation. */
+    int PHASE_WM_ANIMATION_RUNNING = 26;
+
+    /** Reached the client's show insets method. */
+    int PHASE_CLIENT_SHOW_INSETS = 27;
+
+    /** Reached the client's hide insets method. */
+    int PHASE_CLIENT_HIDE_INSETS = 28;
+
+    /** Handling the IME window insets show request. */
+    int PHASE_CLIENT_HANDLE_SHOW_INSETS = 29;
+
+    /** Handling the IME window insets hide request. */
+    int PHASE_CLIENT_HANDLE_HIDE_INSETS = 30;
+
+    /** Applied the IME window insets show animation. */
+    int PHASE_CLIENT_APPLY_ANIMATION = 31;
+
+    /** Started the IME window insets show animation. */
+    int PHASE_CLIENT_CONTROL_ANIMATION = 32;
+
+    /** Queued the IME window insets show animation. */
+    int PHASE_CLIENT_ANIMATION_RUNNING = 33;
+
+    /** Cancelled the IME window insets show animation. */
+    int PHASE_CLIENT_ANIMATION_CANCEL = 34;
+
+    /** Finished the IME window insets show animation. */
+    int PHASE_CLIENT_ANIMATION_FINISHED_SHOW = 35;
+
+    /** Finished the IME window insets hide animation. */
+    int PHASE_CLIENT_ANIMATION_FINISHED_HIDE = 36;
+
+    /**
+     * Called when an IME show request is created.
+     *
+     * @param token the token tracking the current IME show request or {@code null} otherwise.
+     * @param origin the origin of the IME show request.
+     * @param reason the reason why the IME show request was created.
+     */
+    void onRequestShow(@Nullable Token token, @Origin int origin,
+            @SoftInputShowHideReason int reason);
+
+    /**
+     * Called when an IME hide request is created.
+     *
+     * @param token the token tracking the current IME hide request or {@code null} otherwise.
+     * @param origin the origin of the IME hide request.
+     * @param reason the reason why the IME hide request was created.
+     */
+    void onRequestHide(@Nullable Token token, @Origin int origin,
+            @SoftInputShowHideReason int reason);
+
+    /**
+     * Called when an IME request progresses to a further phase.
+     *
+     * @param token the token tracking the current IME request or {@code null} otherwise.
+     * @param phase the new phase the IME request reached.
+     */
+    void onProgress(@Nullable Token token, @Phase int phase);
+
+    /**
+     * Called when an IME request fails.
+     *
+     * @param token the token tracking the current IME request or {@code null} otherwise.
+     * @param phase the phase the IME request failed at.
+     */
+    void onFailed(@Nullable Token token, @Phase int phase);
+
+    /**
+     * Called when an IME request reached a flow that is not yet implemented.
+     *
+     * @param token the token tracking the current IME request or {@code null} otherwise.
+     * @param phase the phase the IME request was currently at.
+     */
+    void onTodo(@Nullable Token token, @Phase int phase);
+
+    /**
+     * Called when an IME request is cancelled.
+     *
+     * @param token the token tracking the current IME request or {@code null} otherwise.
+     * @param phase the phase the IME request was cancelled at.
+     */
+    void onCancelled(@Nullable Token token, @Phase int phase);
+
+    /**
+     * Called when the IME show request is successful.
+     *
+     * @param token the token tracking the current IME show request or {@code null} otherwise.
+     */
+    void onShown(@Nullable Token token);
+
+    /**
+     * Called when the IME hide request is successful.
+     *
+     * @param token the token tracking the current IME hide request or {@code null} otherwise.
+     */
+    void onHidden(@Nullable Token token);
+
+    /**
+     * Get the singleton instance of this class.
+     *
+     * @return the singleton instance of this class
+     */
+    @NonNull
+    static ImeTracker get() {
+        return SystemProperties.getBoolean("persist.debug.imetracker", false)
+                ? LOGGER
+                : NOOP_LOGGER;
+    }
+
+    /** The singleton IME tracker instance. */
+    ImeTracker LOGGER = new ImeTracker() {
+
+        @Override
+        public void onRequestShow(@Nullable Token token, int origin,
+                @SoftInputShowHideReason int reason) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onRequestShow at " + originToString(origin)
+                    + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason));
+        }
+
+        @Override
+        public void onRequestHide(@Nullable Token token, int origin,
+                @SoftInputShowHideReason int reason) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onRequestHide at " + originToString(origin)
+                    + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason));
+        }
+
+        @Override
+        public void onProgress(@Nullable Token token, int phase) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onProgress at " + phaseToString(phase));
+        }
+
+        @Override
+        public void onFailed(@Nullable Token token, int phase) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onFailed at " + phaseToString(phase));
+        }
+
+        @Override
+        public void onTodo(@Nullable Token token, int phase) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onTodo at " + phaseToString(phase));
+        }
+
+        @Override
+        public void onCancelled(@Nullable Token token, int phase) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onCancelled at " + phaseToString(phase));
+        }
+
+        @Override
+        public void onShown(@Nullable Token token) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onShown");
+        }
+
+        @Override
+        public void onHidden(@Nullable Token token) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onHidden");
+        }
+    };
+
+    /** The singleton no-op IME tracker instance. */
+    ImeTracker NOOP_LOGGER = new ImeTracker() {
+
+        @Override
+        public void onRequestShow(@Nullable Token token, int origin,
+                @SoftInputShowHideReason int reason) {}
+
+        @Override
+        public void onRequestHide(@Nullable Token token, int origin,
+                @SoftInputShowHideReason int reason) {}
+
+        @Override
+        public void onProgress(@Nullable Token token, int phase) {}
+
+        @Override
+        public void onFailed(@Nullable Token token, int phase) {}
+
+        @Override
+        public void onTodo(@Nullable Token token, int phase) {}
+
+        @Override
+        public void onCancelled(@Nullable Token token, int phase) {}
+
+        @Override
+        public void onShown(@Nullable Token token) {}
+
+        @Override
+        public void onHidden(@Nullable Token token) {}
+    };
+
+    /** A token that tracks the progress of an IME request. */
+    class Token implements Parcelable {
+
+        private final IBinder mBinder;
+        private final String mTag;
+
+        public Token() {
+            this(ActivityThread.currentProcessName());
+        }
+
+        public Token(String component) {
+            this(new Binder(), component + ":" + Integer.toHexString((new Random().nextInt())));
+        }
+
+        private Token(IBinder binder, String tag) {
+            mBinder = binder;
+            mTag = tag;
+        }
+
+        /** For Parcelable, no special marshalled objects. */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeStrongBinder(mBinder);
+            dest.writeString8(mTag);
+        }
+
+        @NonNull
+        public static final Creator<Token> CREATOR = new Creator<>() {
+            @Override
+            public Token createFromParcel(Parcel source) {
+                IBinder binder = source.readStrongBinder();
+                String tag = source.readString8();
+                return new Token(binder, tag);
+            }
+
+            @Override
+            public Token[] newArray(int size) {
+                return new Token[size];
+            }
+        };
+    }
+
+    /**
+     * Utilities for mapping phases and origins IntDef values to their names.
+     *
+     * Note: This is held in a separate class so that it only gets initialized when actually needed.
+     */
+    class Debug {
+
+        private static final Map<Integer, String> sOrigins =
+                getFieldMapping(ImeTracker.class, "ORIGIN_");
+        private static final Map<Integer, String> sPhases =
+                getFieldMapping(ImeTracker.class, "PHASE_");
+
+        public static String originToString(int origin) {
+            return sOrigins.getOrDefault(origin, "ORIGIN_" + origin);
+        }
+
+        public static String phaseToString(int phase) {
+            return sPhases.getOrDefault(phase, "PHASE_" + phase);
+        }
+
+        private static Map<Integer, String> getFieldMapping(Class<?> cls, String fieldPrefix) {
+            return Arrays.stream(cls.getDeclaredFields())
+                    .filter(field -> field.getName().startsWith(fieldPrefix))
+                    .collect(Collectors.toMap(Debug::getFieldValue, Field::getName));
+        }
+
+        private static int getFieldValue(Field field) {
+            try {
+                return field.getInt(null);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+}
diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java
index 4d5a17d..92380ed 100644
--- a/core/java/android/view/inputmethod/InputMethod.java
+++ b/core/java/android/view/inputmethod/InputMethod.java
@@ -300,11 +300,12 @@
      * @param showInputToken an opaque {@link android.os.Binder} token to identify which API call
      *        of {@link InputMethodManager#showSoftInput(View, int)} is associated with
      *        this callback.
+     * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
      * @hide
      */
     @MainThread
-    default public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
-            IBinder showInputToken) {
+    public default void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+            IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
         showSoftInput(flags, resultReceiver);
     }
 
@@ -338,11 +339,14 @@
      * @param hideInputToken an opaque {@link android.os.Binder} token to identify which API call
      *         of {@link InputMethodManager#hideSoftInputFromWindow(IBinder, int)}} is associated
      *         with this callback.
+     * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
      * @hide
      */
     @MainThread
-    public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
-            IBinder hideInputToken);
+    public default void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+            IBinder hideInputToken, @Nullable ImeTracker.Token statsToken) {
+        hideSoftInput(flags, resultReceiver);
+    }
 
     /**
      * Request that any soft input part of the input method be hidden from the user.
@@ -369,7 +373,7 @@
 
     /**
      * Checks if IME is ready to start stylus handwriting session.
-     * If yes, {@link #startStylusHandwriting(InputChannel, List)} is called.
+     * If yes, {@link #startStylusHandwriting(int, InputChannel, List)} is called.
      * @param requestId
      * @hide
      */
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 74afced..ee31fd5 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -632,6 +632,8 @@
 
     private final DelegateImpl mDelegate = new DelegateImpl();
 
+    private static boolean sPreventImeStartupUnlessTextEditor;
+
     // -----------------------------------------------------------
 
     private static final int MSG_DUMP = 1;
@@ -1435,6 +1437,10 @@
         // display case.
         final Looper looper = displayId == Display.DEFAULT_DISPLAY
                 ? Looper.getMainLooper() : context.getMainLooper();
+        // Keep track of whether to expect the IME to be unavailable so as to avoid log spam in
+        // sendInputEventOnMainLooperLocked() by not logging a verbose message on every DPAD event
+        sPreventImeStartupUnlessTextEditor = context.getResources().getBoolean(
+                com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
         return forContextInternal(displayId, looper);
     }
 
@@ -2001,6 +2007,10 @@
 
     private boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason) {
+        final ImeTracker.Token statsToken = new ImeTracker.Token();
+        ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+                reason);
+
         ImeTracing.getInstance().triggerClientDump("InputMethodManager#showSoftInput", this,
                 null /* icProto */);
         // Re-dispatch if there is a context mismatch.
@@ -2012,10 +2022,13 @@
         checkFocus();
         synchronized (mH) {
             if (!hasServedByInputMethodLocked(view)) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
                 Log.w(TAG, "Ignoring showSoftInput() as view=" + view + " is not served.");
                 return false;
             }
 
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
             // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
             // TODO(b/229426865): call WindowInsetsController#show instead.
             mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED));
@@ -2024,6 +2037,7 @@
             return IInputMethodManagerGlobalInvoker.showSoftInput(
                     mClient,
                     view.getWindowToken(),
+                    statsToken,
                     flags,
                     mCurRootView.getLastClickToolType(),
                     resultReceiver,
@@ -2043,19 +2057,28 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768499)
     public void showSoftInputUnchecked(int flags, ResultReceiver resultReceiver) {
         synchronized (mH) {
+            final ImeTracker.Token statsToken = new ImeTracker.Token();
+            ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+                    SoftInputShowHideReason.SHOW_SOFT_INPUT);
+
             Log.w(TAG, "showSoftInputUnchecked() is a hidden method, which will be"
                     + " removed soon. If you are using androidx.appcompat.widget.SearchView,"
                     + " please update to version 26.0 or newer version.");
             if (mCurRootView == null || mCurRootView.getView() == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
                 Log.w(TAG, "No current root view, ignoring showSoftInputUnchecked()");
                 return;
             }
+
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
             // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
             // TODO(b/229426865): call WindowInsetsController#show instead.
             mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED));
             IInputMethodManagerGlobalInvoker.showSoftInput(
                     mClient,
                     mCurRootView.getView().getWindowToken(),
+                    statsToken,
                     flags,
                     mCurRootView.getLastClickToolType(),
                     resultReceiver,
@@ -2125,17 +2148,24 @@
 
     private boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
             ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+        final ImeTracker.Token statsToken = new ImeTracker.Token();
+        ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+                reason);
+
         ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromWindow",
                 this, null /* icProto */);
         checkFocus();
         synchronized (mH) {
             final View servedView = getServedViewLocked();
             if (servedView == null || servedView.getWindowToken() != windowToken) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
                 return false;
             }
 
-            return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, flags,
-                    resultReceiver, reason);
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
+            return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, statsToken,
+                    flags, resultReceiver, reason);
         }
     }
 
@@ -2763,14 +2793,23 @@
 
     @UnsupportedAppUsage
     void closeCurrentInput() {
+        final ImeTracker.Token statsToken = new ImeTracker.Token();
+        ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+                SoftInputShowHideReason.HIDE_SOFT_INPUT);
+
         synchronized (mH) {
             if (mCurRootView == null || mCurRootView.getView() == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
                 Log.w(TAG, "No current root view, ignoring closeCurrentInput()");
                 return;
             }
+
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
             IInputMethodManagerGlobalInvoker.hideSoftInput(
                     mClient,
                     mCurRootView.getView().getWindowToken(),
+                    statsToken,
                     HIDE_NOT_ALWAYS,
                     null,
                     SoftInputShowHideReason.HIDE_SOFT_INPUT);
@@ -2839,15 +2878,24 @@
      * @hide
      */
     public void notifyImeHidden(IBinder windowToken) {
+        final ImeTracker.Token statsToken = new ImeTracker.Token();
+        ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+                SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+
         ImeTracing.getInstance().triggerClientDump("InputMethodManager#notifyImeHidden", this,
                 null /* icProto */);
         synchronized (mH) {
-            if (isImeSessionAvailableLocked() && mCurRootView != null
-                    && mCurRootView.getWindowToken() == windowToken) {
-                IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, 0 /* flags */,
-                        null /* resultReceiver */,
-                        SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+            if (!isImeSessionAvailableLocked() || mCurRootView == null
+                    || mCurRootView.getWindowToken() != windowToken) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+                return;
             }
+
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
+            IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, statsToken,
+                    0 /* flags */, null /* resultReceiver */,
+                    SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
         }
     }
 
@@ -3364,8 +3412,12 @@
                 return DISPATCH_IN_PROGRESS;
             }
 
-            Log.w(TAG, "Unable to send input event to IME: " + getImeIdLocked()
-                    + " dropping: " + event);
+            if (sPreventImeStartupUnlessTextEditor) {
+                Log.d(TAG, "Dropping event because IME is evicted: " + event);
+            } else {
+                Log.w(TAG, "Unable to send input event to IME: " + getImeIdLocked()
+                        + " dropping: " + event);
+            }
         }
         return DISPATCH_NOT_HANDLED;
     }
@@ -3967,7 +4019,7 @@
 
         /**
          * As reported by {@link InputBindResult}. This value is determined by
-         * {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecking}.
+         * {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecker}.
          */
         final boolean mIsInputMethodSuppressingSpellChecker;
 
diff --git a/core/java/com/android/internal/inputmethod/IInputMethod.aidl b/core/java/com/android/internal/inputmethod/IInputMethod.aidl
index c62fba9..1e3714e 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethod.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethod.aidl
@@ -21,6 +21,7 @@
 import android.view.InputChannel;
 import android.view.MotionEvent;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputMethodSubtype;
 import android.window.ImeOnBackInvokedDispatcher;
@@ -69,9 +70,11 @@
 
     void setSessionEnabled(IInputMethodSession session, boolean enabled);
 
-    void showSoftInput(in IBinder showInputToken, int flags, in ResultReceiver resultReceiver);
+    void showSoftInput(in IBinder showInputToken, in @nullable ImeTracker.Token statsToken,
+            int flags, in ResultReceiver resultReceiver);
 
-    void hideSoftInput(in IBinder hideInputToken, int flags, in ResultReceiver resultReceiver);
+    void hideSoftInput(in IBinder hideInputToken, in @nullable ImeTracker.Token statsToken,
+            int flags, in ResultReceiver resultReceiver);
 
     void updateEditorToolType(int toolType);
 
diff --git a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
index 4babb70..f77e962 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
@@ -17,6 +17,7 @@
 package com.android.internal.inputmethod;
 
 import android.net.Uri;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.infra.AndroidFuture;
@@ -41,7 +42,8 @@
     void switchToNextInputMethod(boolean onlyCurrentIme, in AndroidFuture future /* T=Boolean */);
     void shouldOfferSwitchingToNextInputMethod(in AndroidFuture future /* T=Boolean */);
     void notifyUserActionAsync();
-    void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible);
+    void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
+            in @nullable ImeTracker.Token statsToken);
     void onStylusHandwritingReady(int requestId, int pid);
     void resetStylusHandwriting(int requestId);
 }
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 67c2103..66e3333 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -25,6 +25,7 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.View;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.annotations.GuardedBy;
@@ -100,7 +101,7 @@
     }
 
     /**
-     * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int}.
+     * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int)}.
      *
      * @param vis visibility flags
      * @param backDisposition disposition flags
@@ -250,7 +251,7 @@
     }
 
     /**
-     * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, IVoidResultCallback)}
+     * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, int, AndroidFuture)}
      *
      * @param flags additional operating flags
      * @param reason the reason to hide soft input
@@ -316,7 +317,7 @@
 
     /**
      * Calls {@link IInputMethodPrivilegedOperations#switchToNextInputMethod(boolean,
-     * IBooleanResultCallback)}
+     * AndroidFuture)}
      *
      * @param onlyCurrentIme {@code true} to switch to a {@link InputMethodSubtype} within the same
      *                       IME
@@ -375,22 +376,25 @@
     }
 
     /**
-     * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean)}.
+     * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean,
+     * ImeTracker.Token)}.
      *
      * @param showOrHideInputToken placeholder token that maps to window requesting
      *        {@link android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} or
-     *        {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow
-     *        (IBinder, int)}
+     *        {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow(IBinder,
+     *        int)}
      * @param setVisible {@code true} to set IME visible, else hidden.
+     * @param statsToken the token tracking the current IME request or {@code null} otherwise.
      */
     @AnyThread
-    public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible) {
+    public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
+            @Nullable ImeTracker.Token statsToken) {
         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
         if (ops == null) {
             return;
         }
         try {
-            ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible);
+            ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index fe77236..2ac4309 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.view;
 
+import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.hardware.input.InputManager;
 import android.os.Bundle;
@@ -31,6 +32,7 @@
 import android.view.PointerIcon;
 import android.view.ScrollCaptureResponse;
 import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -66,11 +68,13 @@
     }
 
     @Override
-    public void showInsets(@InsetsType int types, boolean fromIme) {
+    public void showInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
     }
 
     @Override
-    public void hideInsets(@InsetsType int types, boolean fromIme) {
+    public void hideInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
     }
 
     @Override
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 40c6a05..00bc3f2 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -17,6 +17,7 @@
 package com.android.internal.view;
 
 import android.os.ResultReceiver;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 import android.view.inputmethod.EditorInfo;
@@ -54,11 +55,12 @@
             + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
     InputMethodSubtype getLastInputMethodSubtype(int userId);
 
-    boolean showSoftInput(in IInputMethodClient client, @nullable IBinder windowToken, int flags,
-            int lastClickToolType, in @nullable ResultReceiver resultReceiver, int reason);
-    boolean hideSoftInput(in IInputMethodClient client, @nullable IBinder windowToken, int flags,
+    boolean showSoftInput(in IInputMethodClient client, @nullable IBinder windowToken,
+            in @nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
             in @nullable ResultReceiver resultReceiver, int reason);
-
+    boolean hideSoftInput(in IInputMethodClient client, @nullable IBinder windowToken,
+            in @nullable ImeTracker.Token statsToken, int flags,
+            in @nullable ResultReceiver resultReceiver, int reason);
     // If windowToken is null, this just does startInput().  Otherwise this reports that a window
     // has gained focus, and if 'editorInfo' is non-null then also does startInput.
     // @NonNull
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2157270..ac383e6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2426,8 +2426,8 @@
     <integer name="config_dreamsBatteryLevelDrainCutoff">5</integer>
     <!-- Limit of how long the device can remain unlocked due to attention checking.  -->
     <integer name="config_attentionMaximumExtension">900000</integer> <!-- 15 minutes.  -->
-    <!-- Is the system user the only user allowed to dream. -->
-    <bool name="config_dreamsOnlyEnabledForSystemUser">false</bool>
+    <!-- Whether there is to be a chosen Dock User who is the only user allowed to dream. -->
+    <bool name="config_dreamsOnlyEnabledForDockUser">false</bool>
     <!-- Whether dreams are disabled when ambient mode is suppressed. -->
     <bool name="config_dreamsDisabledByAmbientModeSuppressionConfig">false</bool>
 
@@ -2676,9 +2676,9 @@
          Should be false for most devices, except automotive vehicle with passenger displays. -->
     <bool name="config_multiuserUsersOnSecondaryDisplays">false</bool>
 
-    <!-- Whether to automatically switch a non-primary user back to the primary user after a
-         timeout when the device is docked.  -->
-    <bool name="config_enableTimeoutToUserZeroWhenDocked">false</bool>
+    <!-- Whether to automatically switch to the designated Dock User (the user chosen for
+         displaying dreams, etc.) after a timeout when the device is docked.  -->
+    <bool name="config_enableTimeoutToDockUserWhenDocked">false</bool>
 
     <!-- Whether to only install system packages on a user if they're allowlisted for that user
          type. These are flags and can be freely combined.
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 5eb629192..89ec5ba 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -466,7 +466,7 @@
   <java-symbol type="integer" name="config_multiuserMaxRunningUsers" />
   <java-symbol type="bool" name="config_multiuserDelayUserDataLocking" />
   <java-symbol type="bool" name="config_multiuserUsersOnSecondaryDisplays" />
-  <java-symbol type="bool" name="config_enableTimeoutToUserZeroWhenDocked" />
+  <java-symbol type="bool" name="config_enableTimeoutToDockUserWhenDocked" />
   <java-symbol type="integer" name="config_userTypePackageWhitelistMode"/>
   <java-symbol type="xml" name="config_user_types" />
   <java-symbol type="integer" name="config_safe_media_volume_index" />
@@ -2236,7 +2236,7 @@
   <java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" />
   <java-symbol type="string" name="config_dreamsDefaultComponent" />
   <java-symbol type="bool" name="config_dreamsDisabledByAmbientModeSuppressionConfig" />
-  <java-symbol type="bool" name="config_dreamsOnlyEnabledForSystemUser" />
+  <java-symbol type="bool" name="config_dreamsOnlyEnabledForDockUser" />
   <java-symbol type="integer" name="config_dreamOpenAnimationDuration" />
   <java-symbol type="integer" name="config_dreamCloseAnimationDuration" />
   <java-symbol type="array" name="config_supportedDreamComplications" />
diff --git a/core/tests/coretests/src/android/os/VibratorTest.java b/core/tests/coretests/src/android/os/VibratorTest.java
index 7ebebc9..c59a3f5 100644
--- a/core/tests/coretests/src/android/os/VibratorTest.java
+++ b/core/tests/coretests/src/android/os/VibratorTest.java
@@ -246,10 +246,12 @@
     @Test
     public void getQFactorAndResonantFrequency_differentValues_returnsNaN() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setQFactor(1f)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
                 .build();
         VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setQFactor(2f)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 2, 2, null))
                 .build();
@@ -258,6 +260,7 @@
 
         assertTrue(Float.isNaN(info.getQFactor()));
         assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+        assertEmptyFrequencyProfileAndControl(info);
 
         // One vibrator with values undefined.
         VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3).build();
@@ -266,16 +269,19 @@
 
         assertTrue(Float.isNaN(info.getQFactor()));
         assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+        assertEmptyFrequencyProfileAndControl(info);
     }
 
     @Test
     public void getQFactorAndResonantFrequency_sameValues_returnsValue() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setQFactor(10f)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
                         /* resonantFrequencyHz= */ 11, 10, 0.5f, null))
                 .build();
         VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setQFactor(10f)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
                         /* resonantFrequencyHz= */ 11, 5, 1, null))
@@ -285,113 +291,131 @@
 
         assertEquals(10f, info.getQFactor(), TEST_TOLERANCE);
         assertEquals(11f, info.getResonantFrequencyHz(), TEST_TOLERANCE);
+
+        // No frequency range defined.
+        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
     }
 
     @Test
     public void getFrequencyProfile_noVibrator_returnsEmpty() {
         VibratorInfo info = new SystemVibrator.NoVibratorInfo();
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
     }
 
     @Test
     public void getFrequencyProfile_differentResonantFrequencyOrResolutionValues_returnsEmpty() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
                         new float[] { 0, 1 }))
                 .build();
         VibratorInfo differentResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 1, 1,
                         new float[] { 0, 1 }))
                 .build();
         VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, differentResonantFrequency});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
 
         VibratorInfo differentFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 2,
                         new float[] { 0, 1 }))
                 .build();
         info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, differentFrequencyResolution});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
     }
 
     @Test
     public void getFrequencyProfile_missingValues_returnsEmpty() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
                         new float[] { 0, 1 }))
                 .build();
         VibratorInfo missingResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(Float.NaN, 1, 1,
                         new float[] { 0, 1 }))
                 .build();
         VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, missingResonantFrequency});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
 
         VibratorInfo missingMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, Float.NaN, 1,
                         new float[] { 0, 1 }))
                 .build();
         info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, missingMinFrequency});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
 
         VibratorInfo missingFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, Float.NaN,
                         new float[] { 0, 1 }))
                 .build();
         info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, missingFrequencyResolution});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
 
         VibratorInfo missingMaxAmplitudes = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
                 .build();
         info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, missingMaxAmplitudes});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
     }
 
     @Test
     public void getFrequencyProfile_unalignedMaxAmplitudes_returnsEmpty() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
                         new float[] { 0, 1, 1, 0 }))
                 .build();
         VibratorInfo unalignedMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.1f, 0.5f,
                         new float[] { 0, 1, 1, 0 }))
                 .build();
         VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
                         new float[] { 0, 1, 1, 0 }))
                 .build();
         VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, unalignedMinFrequency, thirdVibrator});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
     }
 
     @Test
     public void getFrequencyProfile_alignedProfiles_returnsIntersection() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
                         new float[] { 0.5f, 1, 1, 0.5f }))
                 .build();
         VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
                         new float[] { 1, 1, 1 }))
                 .build();
         VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
                         new float[] { 0.8f, 1, 0.8f, 0.5f }))
                 .build();
@@ -401,6 +425,20 @@
         assertEquals(
                 new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
                 info.getFrequencyProfile());
+        assertEquals(true, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+
+        // Third vibrator without frequency control capability.
+        thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+                        new float[] { 0.8f, 1, 0.8f, 0.5f }))
+                .build();
+        info = new SystemVibrator.MultiVibratorInfo(
+                new VibratorInfo[]{firstVibrator, secondVibrator, thirdVibrator});
+
+        assertEquals(
+                new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
+                info.getFrequencyProfile());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
     }
 
     @Test
@@ -547,4 +585,12 @@
         VibrationAttributes vibrationAttributes = captor.getValue();
         assertEquals(new VibrationAttributes.Builder().build(), vibrationAttributes);
     }
+
+    /**
+     * Asserts that the frequency profile is empty, and therefore frequency control isn't supported.
+     */
+    void assertEmptyFrequencyProfileAndControl(VibratorInfo info) {
+        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+    }
 }
diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index ddcb175..0bf133f 100644
--- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
@@ -24,6 +24,7 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -98,12 +99,12 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             // test if setVisibility can show IME
             mImeConsumer.onWindowFocusGained(true);
-            mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+            mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
             mController.cancelExistingAnimations();
             assertTrue((mController.getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0);
 
             // test if setVisibility can hide IME
-            mController.hide(WindowInsets.Type.ime(), true /* fromIme */);
+            mController.hide(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
             mController.cancelExistingAnimations();
             assertFalse((mController.getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0);
         });
@@ -117,7 +118,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             // Request IME visible before control is available.
             mImeConsumer.onWindowFocusGained(true);
-            mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+            mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
 
             // set control and verify visibility is applied.
             InsetsSourceControl control =
@@ -125,9 +126,11 @@
             mController.onControlsChanged(new InsetsSourceControl[] { control });
             // IME show animation should be triggered when control becomes available.
             verify(mController).applyAnimation(
-                    eq(WindowInsets.Type.ime()), eq(true) /* show */, eq(true) /* fromIme */);
+                    eq(WindowInsets.Type.ime()), eq(true) /* show */, eq(true) /* fromIme */,
+                    any() /* statsToken */);
             verify(mController, never()).applyAnimation(
-                    eq(WindowInsets.Type.ime()), eq(false) /* show */, eq(true) /* fromIme */);
+                    eq(WindowInsets.Type.ime()), eq(false) /* show */, eq(true) /* fromIme */,
+                    any() /* statsToken */);
         });
     }
 
@@ -153,7 +156,8 @@
             mImeConsumer.onWindowFocusGained(hasWindowFocus);
             final boolean imeVisible = hasWindowFocus && hasViewFocus;
             if (imeVisible) {
-                mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+                mController.show(WindowInsets.Type.ime(), true /* fromIme */,
+                        null /* statsToken */);
             }
 
             // set control and verify visibility is applied.
@@ -169,20 +173,21 @@
                 verify(control).getAndClearSkipAnimationOnce();
                 verify(mController).applyAnimation(eq(WindowInsets.Type.ime()),
                         eq(true) /* show */, eq(false) /* fromIme */,
-                        eq(expectSkipAnim) /* skipAnim */);
+                        eq(expectSkipAnim) /* skipAnim */, null /* statsToken */);
             }
 
             // If previously hasViewFocus is false, verify when requesting the IME visible next
             // time will not skip animation.
             if (!hasViewFocus) {
-                mController.show(WindowInsets.Type.ime(), true);
+                mController.show(WindowInsets.Type.ime(), true /* fromIme */,
+                        null /* statsToken */);
                 mController.onControlsChanged(new InsetsSourceControl[]{ control });
                 // Verify IME show animation should be triggered when control becomes available and
                 // the animation will be skipped by getAndClearSkipAnimationOnce invoked.
                 verify(control).getAndClearSkipAnimationOnce();
                 verify(mController).applyAnimation(eq(WindowInsets.Type.ime()),
                         eq(true) /* show */, eq(true) /* fromIme */,
-                        eq(false) /* skipAnim */);
+                        eq(false) /* skipAnim */, null /* statsToken */);
             }
         });
     }
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index e9cd8ad..c88255e 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -109,7 +109,8 @@
         mController = new InsetsAnimationControlImpl(controls,
                 new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(),
                 mMockController, 10 /* durationMs */, new LinearInterpolator(),
-                0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */);
+                0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */,
+                null /* statsToken */);
         mController.setReadyDispatched(true);
     }
 
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 409bae8..c6fa778 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -248,7 +248,7 @@
             mController.setSystemDrivenInsetsAnimationLoggingListener(loggingListener);
             mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
             // since there is no focused view, forcefully make IME visible.
-            mController.show(ime(), true /* fromIme */);
+            mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
             verify(loggingListener).onReady(notNull(), anyInt());
         });
     }
@@ -260,14 +260,14 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
             // since there is no focused view, forcefully make IME visible.
-            mController.show(ime(), true /* fromIme */);
+            mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
             mController.show(all());
             // quickly jump to final state by cancelling it.
             mController.cancelExistingAnimations();
             final @InsetsType int types = navigationBars() | statusBars() | ime();
             assertEquals(types, mController.getRequestedVisibleTypes() & types);
 
-            mController.hide(ime(), true /* fromIme */);
+            mController.hide(ime(), true /* fromIme */, null /* statsToken */);
             mController.hide(all());
             mController.cancelExistingAnimations();
             assertEquals(0, mController.getRequestedVisibleTypes() & types);
@@ -282,10 +282,10 @@
         mController.onControlsChanged(new InsetsSourceControl[] { ime });
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
-            mController.show(ime(), true /* fromIme */);
+            mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
             mController.cancelExistingAnimations();
             assertTrue(isRequestedVisible(mController, ime()));
-            mController.hide(ime(), true /* fromIme */);
+            mController.hide(ime(), true /* fromIme */, null /* statsToken */);
             mController.cancelExistingAnimations();
             assertFalse(isRequestedVisible(mController, ime()));
             mController.getSourceConsumer(ITYPE_IME).onWindowFocusLost();
@@ -452,7 +452,7 @@
             assertFalse(mController.getState().getSource(ITYPE_IME).isVisible());
 
             // Pretend IME is calling
-            mController.show(ime(), true /* fromIme */);
+            mController.show(ime(), true /* fromIme */, null /* statsToken */);
 
             // Gaining control shortly after
             mController.onControlsChanged(createSingletonControl(ITYPE_IME));
@@ -476,7 +476,7 @@
             mController.onControlsChanged(createSingletonControl(ITYPE_IME));
 
             // Pretend IME is calling
-            mController.show(ime(), true /* fromIme */);
+            mController.show(ime(), true /* fromIme */, null /* statsToken */);
 
             assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime()));
             mController.cancelExistingAnimations();
@@ -554,7 +554,7 @@
             verify(listener, never()).onReady(any(), anyInt());
 
             // Pretend that IME is calling.
-            mController.show(ime(), true);
+            mController.show(ime(), true /* fromIme */, null /* statsToken */);
 
             // Ready gets deferred until next predraw
             mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw();
@@ -638,7 +638,7 @@
             mController.onControlsChanged(createSingletonControl(ITYPE_IME));
 
             // Pretend IME is calling
-            mController.show(ime(), true /* fromIme */);
+            mController.show(ime(), true /* fromIme */, null /* statsToken */);
 
             InsetsState copy = new InsetsState(mController.getState(), true /* copySources */);
             copy.getSource(ITYPE_IME).setFrame(0, 1, 2, 3);
@@ -845,7 +845,7 @@
 
             // Showing invisible ime should only causes insets change once.
             clearInvocations(mTestHost);
-            mController.show(ime(), true /* fromIme */);
+            mController.show(ime(), true /* fromIme */, null /* statsToken */);
             verify(mTestHost, times(1)).notifyInsetsChanged();
 
             // Sending the same insets state should not cause insets change.
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index df5f921..c6197c8 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -111,4 +111,8 @@
 
     <!-- Whether to dim a split-screen task when the other is the IME target -->
     <bool name="config_dimNonImeAttachedSide">true</bool>
+
+    <!-- Components support to launch multiple instances into split-screen -->
+    <string-array name="config_componentsSupportMultiInstancesSplit">
+    </string-array>
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index bb7c4134..d9b4f47 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -20,6 +20,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.res.Configuration;
 import android.graphics.Point;
@@ -38,6 +39,7 @@
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodManagerGlobal;
 
 import androidx.annotation.VisibleForTesting;
@@ -112,7 +114,7 @@
         }
         if (mDisplayController.getDisplayLayout(displayId).rotation()
                 != pd.mRotation && isImeShowing(displayId)) {
-            pd.startAnimation(true, false /* forceRestart */);
+            pd.startAnimation(true, false /* forceRestart */, null /* statsToken */);
         }
     }
 
@@ -244,7 +246,7 @@
             mInsetsState.set(insetsState, true /* copySources */);
             if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) {
                 if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation");
-                startAnimation(mImeShowing, true /* forceRestart */);
+                startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
             }
         }
 
@@ -280,7 +282,7 @@
                         !haveSameLeash(mImeSourceControl, imeSourceControl);
                 if (mAnimation != null) {
                     if (positionChanged) {
-                        startAnimation(mImeShowing, true /* forceRestart */);
+                        startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
                     }
                 } else {
                     if (leashChanged) {
@@ -312,21 +314,23 @@
         }
 
         @Override
-        public void showInsets(int types, boolean fromIme) {
+        public void showInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             if ((types & WindowInsets.Type.ime()) == 0) {
                 return;
             }
             if (DEBUG) Slog.d(TAG, "Got showInsets for ime");
-            startAnimation(true /* show */, false /* forceRestart */);
+            startAnimation(true /* show */, false /* forceRestart */, statsToken);
         }
 
         @Override
-        public void hideInsets(int types, boolean fromIme) {
+        public void hideInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             if ((types & WindowInsets.Type.ime()) == 0) {
                 return;
             }
             if (DEBUG) Slog.d(TAG, "Got hideInsets for ime");
-            startAnimation(false /* show */, false /* forceRestart */);
+            startAnimation(false /* show */, false /* forceRestart */, statsToken);
         }
 
         @Override
@@ -367,9 +371,11 @@
                     .navBarFrameHeight();
         }
 
-        private void startAnimation(final boolean show, final boolean forceRestart) {
+        private void startAnimation(final boolean show, final boolean forceRestart,
+                @Nullable ImeTracker.Token statsToken) {
             final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME);
             if (imeSource == null || mImeSourceControl == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
                 return;
             }
             final Rect newFrame = imeSource.getFrame();
@@ -390,8 +396,9 @@
                         + (mAnimationDirection == DIRECTION_SHOW ? "SHOW"
                         : (mAnimationDirection == DIRECTION_HIDE ? "HIDE" : "NONE")));
             }
-            if (!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show)
+            if ((!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show))
                     || (mAnimationDirection == DIRECTION_HIDE && !show)) {
+                ImeTracker.get().onCancelled(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
                 return;
             }
             boolean seek = false;
@@ -435,8 +442,11 @@
                 mTransactionPool.release(t);
             });
             mAnimation.setInterpolator(INTERPOLATOR);
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
             mAnimation.addListener(new AnimatorListenerAdapter() {
                 private boolean mCancelled = false;
+                @Nullable
+                private final ImeTracker.Token mStatsToken = statsToken;
 
                 @Override
                 public void onAnimationStart(Animator animation) {
@@ -455,6 +465,8 @@
                             : 1.f;
                     t.setAlpha(mImeSourceControl.getLeash(), alpha);
                     if (mAnimationDirection == DIRECTION_SHOW) {
+                        ImeTracker.get().onProgress(mStatsToken,
+                                ImeTracker.PHASE_WM_ANIMATION_RUNNING);
                         t.show(mImeSourceControl.getLeash());
                     }
                     t.apply();
@@ -476,8 +488,16 @@
                     }
                     dispatchEndPositioning(mDisplayId, mCancelled, t);
                     if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
+                        ImeTracker.get().onProgress(mStatsToken,
+                                ImeTracker.PHASE_WM_ANIMATION_RUNNING);
                         t.hide(mImeSourceControl.getLeash());
                         removeImeSurface();
+                        ImeTracker.get().onHidden(mStatsToken);
+                    } else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) {
+                        ImeTracker.get().onShown(mStatsToken);
+                    } else if (mCancelled) {
+                        ImeTracker.get().onCancelled(mStatsToken,
+                                ImeTracker.PHASE_WM_ANIMATION_RUNNING);
                     }
                     t.apply();
                     mTransactionPool.release(t);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
index 8d4a09d..8759301 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.common;
 
+import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -25,6 +26,7 @@
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
 
 import androidx.annotation.BinderThread;
 
@@ -156,23 +158,29 @@
             }
         }
 
-        private void showInsets(int types, boolean fromIme) {
+        private void showInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
             if (listeners == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
                 return;
             }
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
             for (OnInsetsChangedListener listener : listeners) {
-                listener.showInsets(types, fromIme);
+                listener.showInsets(types, fromIme, statsToken);
             }
         }
 
-        private void hideInsets(int types, boolean fromIme) {
+        private void hideInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
             if (listeners == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
                 return;
             }
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
             for (OnInsetsChangedListener listener : listeners) {
-                listener.hideInsets(types, fromIme);
+                listener.hideInsets(types, fromIme, statsToken);
             }
         }
 
@@ -214,16 +222,18 @@
             }
 
             @Override
-            public void showInsets(int types, boolean fromIme) throws RemoteException {
+            public void showInsets(@InsetsType int types, boolean fromIme,
+                    @Nullable ImeTracker.Token statsToken) throws RemoteException {
                 mMainExecutor.execute(() -> {
-                    PerDisplay.this.showInsets(types, fromIme);
+                    PerDisplay.this.showInsets(types, fromIme, statsToken);
                 });
             }
 
             @Override
-            public void hideInsets(int types, boolean fromIme) throws RemoteException {
+            public void hideInsets(@InsetsType int types, boolean fromIme,
+                    @Nullable ImeTracker.Token statsToken) throws RemoteException {
                 mMainExecutor.execute(() -> {
-                    PerDisplay.this.hideInsets(types, fromIme);
+                    PerDisplay.this.hideInsets(types, fromIme, statsToken);
                 });
             }
         }
@@ -263,15 +273,21 @@
          *
          * @param types {@link InsetsType} to show
          * @param fromIme true if this request originated from IME (InputMethodService).
+         * @param statsToken the token tracking the current IME show request
+         *                   or {@code null} otherwise.
          */
-        default void showInsets(@InsetsType int types, boolean fromIme) {}
+        default void showInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {}
 
         /**
          * Called when a set of insets source window should be hidden by policy.
          *
          * @param types {@link InsetsType} to hide
          * @param fromIme true if this request originated from IME (InputMethodService).
+         * @param statsToken the token tracking the current IME hide request
+         *                   or {@code null} otherwise.
          */
-        default void hideInsets(@InsetsType int types, boolean fromIme) {}
+        default void hideInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {}
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index e270edb..af13bf5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -19,6 +19,7 @@
 import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Region;
@@ -46,6 +47,7 @@
 import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -351,10 +353,10 @@
                 InsetsSourceControl[] activeControls) {}
 
         @Override
-        public void showInsets(int types, boolean fromIme) {}
+        public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
 
         @Override
-        public void hideInsets(int types, boolean fromIme) {}
+        public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
 
         @Override
         public void moved(int newX, int newY) {}
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 839edc8..3de1045 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
@@ -601,7 +601,7 @@
         animator.start();
     }
 
-    /** Swich both surface position with animation. */
+    /** Switch both surface position with animation. */
     public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1,
             SurfaceControl leash2, Consumer<Rect> finishCallback) {
         final boolean isLandscape = isLandscape();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
index eb08d0e..5533ad5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
@@ -86,8 +86,8 @@
     /**
      * Starts a pair of intent and task in one transition.
      */
-    oneway void startIntentAndTask(in PendingIntent pendingIntent, in Intent fillInIntent,
-            in Bundle options1, int taskId, in Bundle options2, int sidePosition, float splitRatio,
+    oneway void startIntentAndTask(in PendingIntent pendingIntent, in Bundle options1, int taskId,
+            in Bundle options2, int sidePosition, float splitRatio,
             in RemoteTransition remoteTransition, in InstanceId instanceId) = 16;
 
     /**
@@ -108,9 +108,8 @@
      * Starts a pair of intent and task using legacy transition system.
      */
     oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent,
-            in Intent fillInIntent, in Bundle options1, int taskId, in Bundle options2,
-            int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter,
-            in InstanceId instanceId) = 12;
+            in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio,
+            in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12;
 
     /**
      * Starts a pair of shortcut and task using legacy transition system.
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 c6a2b83..cdc8cdd 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
@@ -18,6 +18,7 @@
 
 import static android.app.ActivityManager.START_SUCCESS;
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -32,6 +33,8 @@
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
@@ -60,13 +63,12 @@
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.InstanceId;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.R;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
@@ -166,8 +168,11 @@
     private final IconProvider mIconProvider;
     private final Optional<RecentTasksController> mRecentTasksOptional;
     private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler;
+    private final String[] mMultiInstancesComponents;
 
-    private StageCoordinator mStageCoordinator;
+    @VisibleForTesting
+    StageCoordinator mStageCoordinator;
+
     // Only used for the legacy recents animation from splitscreen to allow the tasks to be animated
     // outside the bounds of the roots by being reparented into a higher level fullscreen container
     private SurfaceControl mGoingToRecentsTasksLayer;
@@ -210,6 +215,51 @@
         if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
             shellInit.addInitCallback(this::onInit, this);
         }
+
+        // TODO(255224696): Remove the config once having a way for client apps to opt-in
+        //                  multi-instances split.
+        mMultiInstancesComponents = mContext.getResources()
+                .getStringArray(R.array.config_componentsSupportMultiInstancesSplit);
+    }
+
+    @VisibleForTesting
+    SplitScreenController(Context context,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
+            ShellController shellController,
+            ShellTaskOrganizer shellTaskOrganizer,
+            SyncTransactionQueue syncQueue,
+            RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+            DisplayController displayController,
+            DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController,
+            DragAndDropController dragAndDropController,
+            Transitions transitions,
+            TransactionPool transactionPool,
+            IconProvider iconProvider,
+            RecentTasksController recentTasks,
+            ShellExecutor mainExecutor,
+            StageCoordinator stageCoordinator) {
+        mShellCommandHandler = shellCommandHandler;
+        mShellController = shellController;
+        mTaskOrganizer = shellTaskOrganizer;
+        mSyncQueue = syncQueue;
+        mContext = context;
+        mRootTDAOrganizer = rootTDAOrganizer;
+        mMainExecutor = mainExecutor;
+        mDisplayController = displayController;
+        mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
+        mDragAndDropController = dragAndDropController;
+        mTransitions = transitions;
+        mTransactionPool = transactionPool;
+        mIconProvider = iconProvider;
+        mRecentTasksOptional = Optional.of(recentTasks);
+        mStageCoordinator = stageCoordinator;
+        mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
+        shellInit.addInitCallback(this::onInit, this);
+        mMultiInstancesComponents = mContext.getResources()
+                .getStringArray(R.array.config_componentsSupportMultiInstancesSplit);
     }
 
     public SplitScreen asSplitScreen() {
@@ -471,72 +521,116 @@
         startIntent(intent, fillInIntent, position, options);
     }
 
+    private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
+            @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+            @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
+            InstanceId instanceId) {
+        Intent fillInIntent = null;
+        if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
+                && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+            fillInIntent = new Intent();
+            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+        }
+        mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent,
+                options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId);
+    }
+
+    private void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1,
+            int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+            float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+        Intent fillInIntent = null;
+        if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
+                && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+            fillInIntent = new Intent();
+            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+        }
+        mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId,
+                options2, splitPosition, splitRatio, remoteTransition, instanceId);
+    }
+
     @Override
     public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
             @SplitPosition int position, @Nullable Bundle options) {
-        if (fillInIntent == null) {
-            fillInIntent = new Intent();
-        }
-        // Flag this as a no-user-action launch to prevent sending user leaving event to the
-        // current top activity since it's going to be put into another side of the split. This
-        // prevents the current top activity from going into pip mode due to user leaving event.
+        // Flag this as a no-user-action launch to prevent sending user leaving event to the current
+        // top activity since it's going to be put into another side of the split. This prevents the
+        // current top activity from going into pip mode due to user leaving event.
+        if (fillInIntent == null) fillInIntent = new Intent();
         fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
 
-        // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
-        // split and there is no reusable background task.
-        if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) {
-            final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional.isPresent()
-                    ? mRecentTasksOptional.get().findTaskInBackground(
-                            intent.getIntent().getComponent())
-                    : null;
-            if (taskInfo != null) {
-                startTask(taskInfo.taskId, position, options);
+        if (launchSameComponentAdjacently(intent, position, INVALID_TASK_ID)) {
+            final ComponentName launching = intent.getIntent().getComponent();
+            if (supportMultiInstancesSplit(launching)) {
+                // To prevent accumulating large number of instances in the background, reuse task
+                // in the background with priority.
+                final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
+                        .map(recentTasks -> recentTasks.findTaskInBackground(launching))
+                        .orElse(null);
+                if (taskInfo != null) {
+                    startTask(taskInfo.taskId, position, options);
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                            "Start task in background");
+                    return;
+                }
+
+                // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of
+                // the split and there is no reusable background task.
+                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+            } else if (isSplitScreenVisible()) {
+                mStageCoordinator.switchSplitPosition("startIntent");
                 return;
             }
-            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
         }
 
-        if (!ENABLE_SHELL_TRANSITIONS) {
-            mStageCoordinator.startIntentLegacy(intent, fillInIntent, position, options);
-            return;
-        }
         mStageCoordinator.startIntent(intent, fillInIntent, position, options);
     }
 
     /** Returns {@code true} if it's launching the same component on both sides of the split. */
-    @VisibleForTesting
-    boolean shouldAddMultipleTaskFlag(@Nullable Intent startIntent, @SplitPosition int position) {
-        if (startIntent == null) {
-            return false;
-        }
+    private boolean launchSameComponentAdjacently(@Nullable PendingIntent pendingIntent,
+            @SplitPosition int position, int taskId) {
+        if (pendingIntent == null || pendingIntent.getIntent() == null) return false;
 
-        final ComponentName launchingActivity = startIntent.getComponent();
-        if (launchingActivity == null) {
-            return false;
-        }
+        final ComponentName launchingActivity = pendingIntent.getIntent().getComponent();
+        if (launchingActivity == null) return false;
 
-        if (isSplitScreenVisible()) {
-            // To prevent users from constantly dropping the same app to the same side resulting in
-            // a large number of instances in the background.
-            final ActivityManager.RunningTaskInfo targetTaskInfo = getTaskInfo(position);
-            final ComponentName targetActivity = targetTaskInfo != null
-                    ? targetTaskInfo.baseIntent.getComponent() : null;
-            if (Objects.equals(launchingActivity, targetActivity)) {
-                return false;
+        if (taskId != INVALID_TASK_ID) {
+            final ActivityManager.RunningTaskInfo taskInfo =
+                    mTaskOrganizer.getRunningTaskInfo(taskId);
+            if (taskInfo != null) {
+                return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
             }
-
-            // Allow users to start a new instance the same to adjacent side.
-            final ActivityManager.RunningTaskInfo pairedTaskInfo =
-                    getTaskInfo(SplitLayout.reversePosition(position));
-            final ComponentName pairedActivity = pairedTaskInfo != null
-                    ? pairedTaskInfo.baseIntent.getComponent() : null;
-            return Objects.equals(launchingActivity, pairedActivity);
+            return false;
         }
 
-        final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo();
-        if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) {
-            return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
+        if (!isSplitScreenVisible()) {
+            // Split screen is not yet activated, check if the current top running task is valid to
+            // split together.
+            final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo();
+            if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) {
+                return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
+            }
+            return false;
+        }
+
+        // Compare to the adjacent side of the split to determine if this is launching the same
+        // component adjacently.
+        final ActivityManager.RunningTaskInfo pairedTaskInfo =
+                getTaskInfo(SplitLayout.reversePosition(position));
+        final ComponentName pairedActivity = pairedTaskInfo != null
+                ? pairedTaskInfo.baseIntent.getComponent() : null;
+        return Objects.equals(launchingActivity, pairedActivity);
+    }
+
+    @VisibleForTesting
+    /** Returns {@code true} if the component supports multi-instances split. */
+    boolean supportMultiInstancesSplit(@Nullable ComponentName launching) {
+        if (launching == null) return false;
+
+        final String componentName = launching.flattenToString();
+        for (int i = 0; i < mMultiInstancesComponents.length; i++) {
+            if (mMultiInstancesComponents[i].equals(componentName)) {
+                return true;
+            }
         }
 
         return false;
@@ -839,14 +933,13 @@
 
         @Override
         public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
-                Intent fillInIntent, Bundle options1, int taskId, Bundle options2,
-                int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
-                InstanceId instanceId) {
+                Bundle options1, int taskId, Bundle options2, int splitPosition, float splitRatio,
+                RemoteAnimationAdapter adapter, InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController,
                     "startIntentAndTaskWithLegacyTransition", (controller) ->
-                            controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition(
-                                    pendingIntent, fillInIntent, options1, taskId, options2,
-                                    splitPosition, splitRatio, adapter, instanceId));
+                            controller.startIntentAndTaskWithLegacyTransition(pendingIntent,
+                                    options1, taskId, options2, splitPosition, splitRatio, adapter,
+                                    instanceId));
         }
 
         @Override
@@ -872,14 +965,13 @@
         }
 
         @Override
-        public void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent,
-                @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
-                @SplitPosition int splitPosition, float splitRatio,
-                @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+        public void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1,
+                int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+                float splitRatio, @Nullable RemoteTransition remoteTransition,
+                InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController, "startIntentAndTask",
-                    (controller) -> controller.mStageCoordinator.startIntentAndTask(pendingIntent,
-                            fillInIntent, options1, taskId, options2, splitPosition, splitRatio,
-                            remoteTransition, instanceId));
+                    (controller) -> controller.startIntentAndTask(pendingIntent, options1, taskId,
+                            options2, splitPosition, splitRatio, remoteTransition, instanceId));
         }
 
         @Override
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 e888c6f..c2ab7ef 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
@@ -24,7 +24,6 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
@@ -428,6 +427,11 @@
     /** Launches an activity into split. */
     void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
             @Nullable Bundle options) {
+        if (!ENABLE_SHELL_TRANSITIONS) {
+            startIntentLegacy(intent, fillInIntent, position, options);
+            return;
+        }
+
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
         prepareEvictChildTasks(position, evictWct);
@@ -441,13 +445,7 @@
         prepareEnterSplitScreen(wct, null /* taskInfo */, position);
 
         mSplitTransitions.startEnterTransition(transitType, wct, null, this,
-                aborted -> {
-                    // Switch the split position if launching as MULTIPLE_TASK failed.
-                    if (aborted && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
-                        setSideStagePositionAnimated(
-                                SplitLayout.reversePosition(mSideStagePosition));
-                    }
-                } /* consumedCallback */,
+                null /* consumedCallback */,
                 (finishWct, finishT) -> {
                     if (!evictWct.isEmpty()) {
                         finishWct.merge(evictWct, true);
@@ -457,7 +455,7 @@
 
     /** Launches an activity into split by legacy transition. */
     void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
-            @SplitPosition int position, @androidx.annotation.Nullable Bundle options) {
+            @SplitPosition int position, @Nullable Bundle options) {
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
         prepareEvictChildTasks(position, evictWct);
 
@@ -473,12 +471,6 @@
                                 exitSplitScreen(mMainStage.getChildCount() == 0
                                         ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
                         mSplitUnsupportedToast.show();
-                    } else {
-                        // Switch the split position if launching as MULTIPLE_TASK failed.
-                        if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
-                            setSideStagePosition(SplitLayout.reversePosition(
-                                    getSideStagePosition()), null);
-                        }
                     }
 
                     // Do nothing when the animation was cancelled.
@@ -771,9 +763,8 @@
         mSideStage.evictInvisibleChildren(wct);
     }
 
-    Bundle resolveStartStage(@StageType int stage,
-            @SplitPosition int position, @androidx.annotation.Nullable Bundle options,
-            @androidx.annotation.Nullable WindowContainerTransaction wct) {
+    Bundle resolveStartStage(@StageType int stage, @SplitPosition int position,
+            @Nullable Bundle options, @Nullable WindowContainerTransaction wct) {
         switch (stage) {
             case STAGE_TYPE_UNDEFINED: {
                 if (position != SPLIT_POSITION_UNDEFINED) {
@@ -844,9 +835,8 @@
                 : mMainStage.getTopVisibleChildTaskId();
     }
 
-    void setSideStagePositionAnimated(@SplitPosition int sideStagePosition) {
-        if (mSideStagePosition == sideStagePosition) return;
-        SurfaceControl.Transaction t = mTransactionPool.acquire();
+    void switchSplitPosition(String reason) {
+        final SurfaceControl.Transaction t = mTransactionPool.acquire();
         mTempRect1.setEmpty();
         final StageTaskListener topLeftStage =
                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
@@ -886,6 +876,11 @@
                         va.start();
                     });
                 });
+
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason);
+        mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                mSplitLayout.isLandscape());
     }
 
     void setSideStagePosition(@SplitPosition int sideStagePosition,
@@ -1617,10 +1612,7 @@
 
     @Override
     public void onDoubleTappedDivider() {
-        setSideStagePositionAnimated(SplitLayout.reversePosition(mSideStagePosition));
-        mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
-                getSideStagePosition(), mSideStage.getTopChildTaskUid(),
-                mSplitLayout.isLandscape());
+        switchSplitPosition("double tap");
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
index 4a3284e..ead451f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
@@ -41,7 +41,6 @@
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
 import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import org.junit.Assert.assertNotNull
 import java.util.Collections
 
 internal object SplitScreenUtils {
@@ -51,7 +50,7 @@
     private const val DIVIDER_BAR = "docked_divider_handle"
     private const val OVERVIEW_SNAPSHOT = "snapshot"
     private const val GESTURE_STEP_MS = 16L
-    private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
+    private const val LONG_PRESS_TIME_MS = 100L
     private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
 
     private val notificationScrollerSelector: BySelector
@@ -276,6 +275,13 @@
         }
     }
 
+    fun longPress(instrumentation: Instrumentation, point: Point) {
+        val downTime = SystemClock.uptimeMillis()
+        touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, point)
+        SystemClock.sleep(LONG_PRESS_TIME_MS)
+        touch(instrumentation, MotionEvent.ACTION_UP, downTime, downTime, TIMEOUT_MS, point)
+    }
+
     fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
         tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
         val allApps = tapl.workspace.switchToAllApps()
@@ -347,11 +353,9 @@
                 Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
                 TIMEOUT_MS
             )
-        assertNotNull("Unable to find the TextView", textView)
-        textView.click(LONG_PRESS_TIME_MS)
+        longPress(instrumentation, textView.visibleCenter)
 
         val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
-        assertNotNull("Unable to find the copy button", copyBtn)
         copyBtn.click()
 
         // Paste text to destinationApp
@@ -360,11 +364,9 @@
                 Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
                 TIMEOUT_MS
             )
-        assertNotNull("Unable to find the EditText", editText)
-        editText.click(LONG_PRESS_TIME_MS)
+        longPress(instrumentation, editText.visibleCenter)
 
         val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
-        assertNotNull("Unable to find the paste button", pasteBtn)
         pasteBtn.click()
 
         // Verify text
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index a6f19e7..40f2e88 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -97,13 +97,13 @@
 
     @Test
     public void showInsets_schedulesNoWorkOnExecutor() {
-        mPerDisplay.showInsets(ime(), true);
+        mPerDisplay.showInsets(ime(), true /* fromIme */, null /* statsToken */);
         verifyZeroInteractions(mExecutor);
     }
 
     @Test
     public void hideInsets_schedulesNoWorkOnExecutor() {
-        mPerDisplay.hideInsets(ime(), true);
+        mPerDisplay.hideInsets(ime(), true /* fromIme */, null /* statsToken */);
         verifyZeroInteractions(mExecutor);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
index 39db328..956f1cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
@@ -25,6 +25,7 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.os.RemoteException;
 import android.util.SparseArray;
@@ -33,6 +34,7 @@
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.WindowInsets;
+import android.view.inputmethod.ImeTracker;
 
 import androidx.test.filters.SmallTest;
 
@@ -111,8 +113,10 @@
                 WindowInsets.Type.defaultVisible());
         mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null);
         mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null);
-        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false);
-        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false);
+        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false,
+                null /* statsToken */);
+        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false,
+                null /* statsToken */);
         mExecutor.flushAll();
 
         assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
@@ -131,8 +135,10 @@
                 WindowInsets.Type.defaultVisible());
         mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null);
         mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null);
-        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false);
-        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false);
+        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false,
+                null /* statsToken */);
+        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false,
+                null /* statsToken */);
         mExecutor.flushAll();
 
         assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
@@ -191,12 +197,12 @@
         }
 
         @Override
-        public void showInsets(int types, boolean fromIme) {
+        public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {
             showInsetsCount++;
         }
 
         @Override
-        public void hideInsets(int types, boolean fromIme) {
+        public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {
             hideInsetsCount++;
         }
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index d01f3d3..38b75f8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -16,18 +16,24 @@
 
 package com.android.wm.shell.splitscreen;
 
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 
 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 static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -35,6 +41,8 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -65,11 +73,11 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.Optional;
-
 /**
  * Tests for {@link SplitScreenController}
  */
@@ -91,18 +99,21 @@
     @Mock Transitions mTransitions;
     @Mock TransactionPool mTransactionPool;
     @Mock IconProvider mIconProvider;
-    @Mock Optional<RecentTasksController> mRecentTasks;
+    @Mock StageCoordinator mStageCoordinator;
+    @Mock RecentTasksController mRecentTasks;
+    @Captor ArgumentCaptor<Intent> mIntentCaptor;
 
     private SplitScreenController mSplitScreenController;
 
     @Before
     public void setup() {
+        assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext));
         MockitoAnnotations.initMocks(this);
         mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
                 mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
                 mRootTDAOrganizer, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
-                mIconProvider, mRecentTasks, mMainExecutor));
+                mIconProvider, mRecentTasks, mMainExecutor, mStageCoordinator));
     }
 
     @Test
@@ -148,58 +159,100 @@
     }
 
     @Test
-    public void testShouldAddMultipleTaskFlag_notInSplitScreen() {
-        doReturn(false).when(mSplitScreenController).isSplitScreenVisible();
-        doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any());
-
-        // Verify launching the same activity returns true.
+    public void testStartIntent_appendsNoUserActionFlag() {
         Intent startIntent = createStartIntent("startActivity");
-        ActivityManager.RunningTaskInfo focusTaskInfo =
-                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
-        doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
-        assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
-                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
 
-        // Verify launching different activity returns false.
-        Intent diffIntent = createStartIntent("diffActivity");
-        focusTaskInfo =
-                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, diffIntent);
-        doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
-        assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
-                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+        verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
+                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
+        assertEquals(FLAG_ACTIVITY_NO_USER_ACTION,
+                mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION);
     }
 
     @Test
-    public void testShouldAddMultipleTaskFlag_inSplitScreen() {
-        doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
+    public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() {
+        doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
         Intent startIntent = createStartIntent("startActivity");
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+        // Put the same component into focus task
+        ActivityManager.RunningTaskInfo focusTaskInfo =
+                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
+        doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo();
+        doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
+
+        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+        verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
+                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
+        assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK,
+                mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK);
+    }
+
+    @Test
+    public void startIntent_multiInstancesSupported_startTaskInBackgroundBeforeSplitActivated() {
+        doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+        doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
+        Intent startIntent = createStartIntent("startActivity");
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+        // Put the same component into focus task
+        ActivityManager.RunningTaskInfo focusTaskInfo =
+                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
+        doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo();
+        doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
+        // Put the same component into a task in the background
+        ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
+        doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any());
+
+        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+        verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
+                isNull());
+    }
+
+    @Test
+    public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() {
+        doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+        doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
+        Intent startIntent = createStartIntent("startActivity");
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+        // Put the same component into another side of the split
+        doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
         ActivityManager.RunningTaskInfo sameTaskInfo =
                 createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
-        Intent diffIntent = createStartIntent("diffActivity");
-        ActivityManager.RunningTaskInfo differentTaskInfo =
-                createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent);
+        doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo(
+                SPLIT_POSITION_BOTTOM_OR_RIGHT);
+        // Put the same component into a task in the background
+        doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks)
+                .findTaskInBackground(any());
 
-        // Verify launching the same activity return false.
-        doReturn(sameTaskInfo).when(mSplitScreenController)
-                .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
-        assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
-                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
 
-        // Verify launching the same activity as adjacent returns true.
-        doReturn(differentTaskInfo).when(mSplitScreenController)
-                .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
-        doReturn(sameTaskInfo).when(mSplitScreenController)
-                .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
-        assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
-                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+        verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
+                isNull());
+    }
 
-        // Verify launching different activity from adjacent returns false.
-        doReturn(differentTaskInfo).when(mSplitScreenController)
-                .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
-        doReturn(differentTaskInfo).when(mSplitScreenController)
-                .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
-        assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
-                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+    @Test
+    public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() {
+        doReturn(false).when(mSplitScreenController).supportMultiInstancesSplit(any());
+        Intent startIntent = createStartIntent("startActivity");
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+        // Put the same component into another side of the split
+        doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
+        ActivityManager.RunningTaskInfo sameTaskInfo =
+                createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
+        doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo(
+                SPLIT_POSITION_BOTTOM_OR_RIGHT);
+
+        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+        verify(mStageCoordinator).switchSplitPosition(anyString());
     }
 
     private Intent createStartIntent(String activityName) {
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index 1bd12af..7e1bbe3 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -244,12 +244,9 @@
                 mCallback = null;
                 return;
             }
-            if (handler == null) {
-                handler = new Handler();
-            }
+            Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
             callback.mSession = this;
-            CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
-                    callback);
+            CallbackMessageHandler msgHandler = new CallbackMessageHandler(looper, callback);
             mCallback = msgHandler;
         }
     }
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
index 4fb77d7..b8fd579 100644
--- a/packages/SettingsLib/Spa/build.gradle
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -16,9 +16,10 @@
 
 buildscript {
     ext {
-        spa_min_sdk = 21
-        spa_target_sdk = 33
-        jetpack_compose_version = '1.3.0'
+        BUILD_TOOLS_VERSION = "30.0.3"
+        MIN_SDK = 21
+        TARGET_SDK = 33
+        jetpack_compose_version = '1.4.0-alpha01'
         jetpack_compose_compiler_version = '1.3.2'
     }
 }
diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle
index e04a9be..7868aff 100644
--- a/packages/SettingsLib/Spa/gallery/build.gradle
+++ b/packages/SettingsLib/Spa/gallery/build.gradle
@@ -21,12 +21,13 @@
 
 android {
     namespace 'com.android.settingslib.spa.gallery'
-    compileSdk spa_target_sdk
+    compileSdk TARGET_SDK
+    buildToolsVersion = BUILD_TOOLS_VERSION
 
     defaultConfig {
         applicationId "com.android.settingslib.spa.gallery"
-        minSdk spa_min_sdk
-        targetSdk spa_target_sdk
+        minSdk MIN_SDK
+        targetSdk TARGET_SDK
         versionCode 1
         versionName "1.0"
     }
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 5daea91f..4944784 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -21,11 +21,12 @@
 
 android {
     namespace 'com.android.settingslib.spa'
-    compileSdk spa_target_sdk
+    compileSdk TARGET_SDK
+    buildToolsVersion = BUILD_TOOLS_VERSION
 
     defaultConfig {
-        minSdk spa_min_sdk
-        targetSdk spa_target_sdk
+        minSdk MIN_SDK
+        targetSdk TARGET_SDK
     }
 
     sourceSets {
diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle
index 529a201..2d501fc 100644
--- a/packages/SettingsLib/Spa/tests/build.gradle
+++ b/packages/SettingsLib/Spa/tests/build.gradle
@@ -21,11 +21,12 @@
 
 android {
     namespace 'com.android.settingslib.spa.tests'
-    compileSdk spa_target_sdk
+    compileSdk TARGET_SDK
+    buildToolsVersion = BUILD_TOOLS_VERSION
 
     defaultConfig {
-        minSdk spa_min_sdk
-        targetSdk spa_target_sdk
+        minSdk MIN_SDK
+        targetSdk TARGET_SDK
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
@@ -60,7 +61,6 @@
 dependencies {
     androidTestImplementation project(":spa")
     androidTestImplementation project(":testutils")
-    androidTestImplementation "androidx.test.ext:junit-ktx:1.1.3"
     androidTestImplementation "androidx.compose.ui:ui-test-junit4:$jetpack_compose_version"
     androidTestImplementation "com.google.truth:truth:1.1.3"
     androidTestImplementation "org.mockito:mockito-android:3.4.6"
diff --git a/packages/SettingsLib/Spa/testutils/build.gradle b/packages/SettingsLib/Spa/testutils/build.gradle
index 71d7d8a..cbfbb9c 100644
--- a/packages/SettingsLib/Spa/testutils/build.gradle
+++ b/packages/SettingsLib/Spa/testutils/build.gradle
@@ -20,11 +20,12 @@
 }
 
 android {
-    compileSdk spa_target_sdk
+    compileSdk TARGET_SDK
+    buildToolsVersion = BUILD_TOOLS_VERSION
 
     defaultConfig {
-        minSdk spa_min_sdk
-        targetSdk spa_target_sdk
+        minSdk MIN_SDK
+        targetSdk TARGET_SDK
     }
 
     sourceSets {
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
index 5fa04f9..faea5b2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
@@ -412,14 +412,13 @@
     }
 
     companion object {
-        private const val TAG = "ThemedBatteryDrawable"
-        private const val WIDTH = 12f
-        private const val HEIGHT = 20f
+        const val WIDTH = 12f
+        const val HEIGHT = 20f
         private const val CRITICAL_LEVEL = 15
         // On a 12x20 grid, how wide to make the fill protection stroke.
         // Scales when our size changes
         private const val PROTECTION_STROKE_WIDTH = 3f
         // Arbitrarily chosen for visibility at small sizes
-        private const val PROTECTION_MIN_STROKE_WIDTH = 6f
+        const val PROTECTION_MIN_STROKE_WIDTH = 6f
     }
 }
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index aea2f52..f5c9bcd 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -884,7 +884,7 @@
                         Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING,
                         Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION,
                         Settings.Secure.SPATIAL_AUDIO_ENABLED,
-                        Settings.Secure.TIMEOUT_TO_USER_ZERO,
+                        Settings.Secure.TIMEOUT_TO_DOCK_USER,
                         Settings.Secure.UI_NIGHT_MODE_LAST_COMPUTED,
                         Settings.Secure.UI_NIGHT_MODE_OVERRIDE_OFF,
                         Settings.Secure.UI_NIGHT_MODE_OVERRIDE_ON);
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index ce9829b..55d6379 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -485,6 +485,12 @@
     <!-- Whether to show a severe low battery dialog. -->
     <bool name="config_severe_battery_dialog">false</bool>
 
+    <!-- A path representing a shield. Will sometimes be displayed with the battery icon when
+         needed. This path is a 10px wide and 13px tall. -->
+    <string name="config_batterymeterShieldPath" translatable="false">
+        M5 0L0 1.88V6.19C0 9.35 2.13 12.29 5 13.01C7.87 12.29 10 9.35 10 6.19V1.88L5 0Z
+    </string>
+
     <!-- A path similar to frameworks/base/core/res/res/values/config.xml
       config_mainBuiltInDisplayCutout that describes a path larger than the exact path of a display
       cutout. If present as well as config_enableDisplayCutoutProtection is set to true, then
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f9f2195..6577b07 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -105,6 +105,12 @@
     so the width of the icon should be 13.0dp * (12.0 / 20.0) -->
     <dimen name="status_bar_battery_icon_width">7.8dp</dimen>
 
+    <!-- The battery icon is 13dp tall, but the other system icons are 15dp tall (see
+         @*android:dimen/status_bar_system_icon_size) with some top and bottom padding embedded in
+         the drawables themselves. So, the battery icon may need an extra 1dp of spacing so that its
+         bottom still aligns with the bottom of all the other system icons. See b/258672854. -->
+    <dimen name="status_bar_battery_extra_vertical_spacing">1dp</dimen>
+
     <!-- The font size for the clock in the status bar. -->
     <dimen name="status_bar_clock_size">14sp</dimen>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 6824d7f..3270e45 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -316,7 +316,7 @@
     <!-- Content description of the QR Code scanner for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_qr_code_scanner_button">QR Code Scanner</string>
     <!-- Content description of the unlock button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
-    <string name="accessibility_unlock_button">Unlock</string>
+    <string name="accessibility_unlock_button">Unlocked</string>
     <!-- Content description of the lock icon for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_lock_icon">Device locked</string>
     <!-- Content description hint of the unlock button when fingerprint is on (not shown on the screen). [CHAR LIMIT=NONE] -->
@@ -439,11 +439,17 @@
     <string name="accessibility_battery_level">Battery <xliff:g id="number">%d</xliff:g> percent.</string>
 
     <!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery (not shown on the screen). [CHAR LIMIT=NONE] -->
-    <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$s</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage</string>
+    <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$d</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage</string>
 
     <!-- Content description of the battery level icon for accessibility while the device is charging (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_battery_level_charging">Battery charging, <xliff:g id="battery_percentage">%d</xliff:g> percent.</string>
 
+    <!-- Content description of the battery level icon for accessibility, with information that the device charging is paused in order to protect the lifetime of the battery (not shown on screen). [CHAR LIMIT=NONE] -->
+    <string name="accessibility_battery_level_charging_paused">Battery <xliff:g id="percentage" example="90%">%d</xliff:g> percent. Charging paused for battery protection.</string>
+
+    <!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery *and* information that the device charging is paused in order to protect the lifetime of the battery (not shown on screen). [CHAR LIMIT=NONE] -->
+    <string name="accessibility_battery_level_charging_paused_with_estimate">Battery <xliff:g id="percentage" example="90%">%1$d</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage. Charging paused for battery protection.</string>
+
     <!-- Content description of overflow icon container of the notifications for accessibility (not shown on the screen)[CHAR LIMIT=NONE] -->
     <string name="accessibility_overflow_action">See all notifications</string>
 
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index af4be1a..e07a6c1 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -25,7 +25,7 @@
         android:id="@+id/clock">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="0dp"
+            android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintStart_toStartOf="@id/begin_guide"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
@@ -42,7 +42,7 @@
     <Constraint
         android:id="@+id/date">
         <Layout
-            android:layout_width="0dp"
+            android:layout_width="wrap_content"
             android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             android:layout_marginStart="8dp"
             app:layout_constrainedWidth="true"
@@ -57,7 +57,7 @@
     <Constraint
         android:id="@+id/statusIcons">
         <Layout
-            android:layout_width="0dp"
+            android:layout_width="wrap_content"
             android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/date"
@@ -65,6 +65,7 @@
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintHorizontal_bias="1"
+            app:layout_constraintHorizontal_chainStyle="packed"
             />
     </Constraint>
 
@@ -80,12 +81,16 @@
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintHorizontal_bias="1"
+            app:layout_constraintHorizontal_chainStyle="packed"
             />
     </Constraint>
 
     <Constraint
         android:id="@+id/carrier_group">
         <Layout
+            app:layout_constraintWidth_min="48dp"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toTopOf="parent"
             />
diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml
index d8a4e77..982c422 100644
--- a/packages/SystemUI/res/xml/qs_header_new.xml
+++ b/packages/SystemUI/res/xml/qs_header_new.xml
@@ -43,6 +43,7 @@
             app:layout_constraintBottom_toBottomOf="@id/carrier_group"
             app:layout_constraintEnd_toStartOf="@id/carrier_group"
             app:layout_constraintHorizontal_bias="0"
+            app:layout_constraintHorizontal_chainStyle="spread_inside"
         />
         <Transform
             android:scaleX="2.57"
@@ -53,7 +54,7 @@
     <Constraint
         android:id="@+id/date">
         <Layout
-            android:layout_width="0dp"
+            android:layout_width="wrap_content"
             android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toStartOf="@id/space"
@@ -67,16 +68,15 @@
     <Constraint
         android:id="@+id/carrier_group">
         <Layout
-            app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
-            android:minHeight="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintWidth_min="48dp"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintStart_toEndOf="@id/clock"
             app:layout_constraintTop_toBottomOf="@id/privacy_container"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintHorizontal_bias="1"
             app:layout_constraintBottom_toTopOf="@id/batteryRemainingIcon"
+            app:layout_constraintHorizontal_chainStyle="spread_inside"
             />
         <PropertySet
             android:alpha="1"
@@ -86,7 +86,7 @@
     <Constraint
         android:id="@+id/statusIcons">
         <Layout
-            android:layout_width="0dp"
+            android:layout_width="wrap_content"
             android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/space"
@@ -108,6 +108,7 @@
             app:layout_constraintTop_toTopOf="@id/date"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintHorizontal_bias="1"
+            app:layout_constraintHorizontal_chainStyle="spread_inside"
             />
     </Constraint>
 
diff --git a/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
new file mode 100644
index 0000000..b52ddc1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.battery
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.graphics.drawable.DrawableWrapper
+import android.util.PathParser
+import com.android.settingslib.graph.ThemedBatteryDrawable
+import com.android.systemui.R
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.SHIELD_LEFT_OFFSET
+import com.android.systemui.battery.BatterySpecs.SHIELD_STROKE
+import com.android.systemui.battery.BatterySpecs.SHIELD_TOP_OFFSET
+
+/**
+ * A battery drawable that accessorizes [ThemedBatteryDrawable] with additional information if
+ * necessary.
+ *
+ * For now, it adds a shield in the bottom-right corner when [displayShield] is true.
+ */
+class AccessorizedBatteryDrawable(
+    private val context: Context,
+    frameColor: Int,
+) : DrawableWrapper(ThemedBatteryDrawable(context, frameColor)) {
+    private val mainBatteryDrawable: ThemedBatteryDrawable
+        get() = drawable as ThemedBatteryDrawable
+
+    private val shieldPath = Path()
+    private val scaledShield = Path()
+    private val scaleMatrix = Matrix()
+
+    private var shieldLeftOffsetScaled = SHIELD_LEFT_OFFSET
+    private var shieldTopOffsetScaled = SHIELD_TOP_OFFSET
+
+    private var density = context.resources.displayMetrics.density
+
+    private val dualTone =
+        context.resources.getBoolean(com.android.internal.R.bool.config_batterymeterDualTone)
+
+    private val shieldTransparentOutlinePaint =
+        Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
+            p.color = Color.TRANSPARENT
+            p.strokeWidth = ThemedBatteryDrawable.PROTECTION_MIN_STROKE_WIDTH
+            p.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
+            p.style = Paint.Style.FILL_AND_STROKE
+        }
+
+    private val shieldPaint =
+        Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
+            p.color = Color.MAGENTA
+            p.style = Paint.Style.FILL
+            p.isDither = true
+        }
+
+    init {
+        loadPaths()
+    }
+
+    override fun onBoundsChange(bounds: Rect) {
+        super.onBoundsChange(bounds)
+        updateSizes()
+    }
+
+    var displayShield: Boolean = false
+
+    private fun updateSizes() {
+        val b = bounds
+        if (b.isEmpty) {
+            return
+        }
+
+        val mainWidth = BatterySpecs.getMainBatteryWidth(b.width().toFloat(), displayShield)
+        val mainHeight = BatterySpecs.getMainBatteryHeight(b.height().toFloat(), displayShield)
+
+        drawable?.setBounds(
+            b.left,
+            b.top,
+            /* right= */ b.left + mainWidth.toInt(),
+            /* bottom= */ b.top + mainHeight.toInt()
+        )
+
+        if (displayShield) {
+            val sx = b.right / BATTERY_WIDTH_WITH_SHIELD
+            val sy = b.bottom / BATTERY_HEIGHT_WITH_SHIELD
+            scaleMatrix.setScale(sx, sy)
+            shieldPath.transform(scaleMatrix, scaledShield)
+
+            shieldLeftOffsetScaled = sx * SHIELD_LEFT_OFFSET
+            shieldTopOffsetScaled = sy * SHIELD_TOP_OFFSET
+
+            val scaledStrokeWidth =
+                (sx * SHIELD_STROKE).coerceAtLeast(
+                    ThemedBatteryDrawable.PROTECTION_MIN_STROKE_WIDTH
+                )
+            shieldTransparentOutlinePaint.strokeWidth = scaledStrokeWidth
+        }
+    }
+
+    override fun getIntrinsicHeight(): Int {
+        val height =
+            if (displayShield) {
+                BATTERY_HEIGHT_WITH_SHIELD
+            } else {
+                BATTERY_HEIGHT
+            }
+        return (height * density).toInt()
+    }
+
+    override fun getIntrinsicWidth(): Int {
+        val width =
+            if (displayShield) {
+                BATTERY_WIDTH_WITH_SHIELD
+            } else {
+                BATTERY_WIDTH
+            }
+        return (width * density).toInt()
+    }
+
+    override fun draw(c: Canvas) {
+        c.saveLayer(null, null)
+        // Draw the main battery icon
+        super.draw(c)
+
+        if (displayShield) {
+            c.translate(shieldLeftOffsetScaled, shieldTopOffsetScaled)
+            // We need a transparent outline around the shield, so first draw the transparent-ness
+            // then draw the shield
+            c.drawPath(scaledShield, shieldTransparentOutlinePaint)
+            c.drawPath(scaledShield, shieldPaint)
+        }
+        c.restore()
+    }
+
+    override fun getOpacity(): Int {
+        return PixelFormat.OPAQUE
+    }
+
+    override fun setAlpha(p0: Int) {
+        // Unused internally -- see [ThemedBatteryDrawable.setAlpha].
+    }
+
+    override fun setColorFilter(colorfilter: ColorFilter?) {
+        super.setColorFilter(colorFilter)
+        shieldPaint.colorFilter = colorFilter
+    }
+
+    /** Sets whether the battery is currently charging. */
+    fun setCharging(charging: Boolean) {
+        mainBatteryDrawable.charging = charging
+    }
+
+    /** Sets the current level (out of 100) of the battery. */
+    fun setBatteryLevel(level: Int) {
+        mainBatteryDrawable.setBatteryLevel(level)
+    }
+
+    /** Sets whether power save is enabled. */
+    fun setPowerSaveEnabled(powerSaveEnabled: Boolean) {
+        mainBatteryDrawable.powerSaveEnabled = powerSaveEnabled
+    }
+
+    /** Returns whether power save is currently enabled. */
+    fun getPowerSaveEnabled(): Boolean {
+        return mainBatteryDrawable.powerSaveEnabled
+    }
+
+    /** Sets the colors to use for the icon. */
+    fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) {
+        shieldPaint.color = if (dualTone) fgColor else singleToneColor
+        mainBatteryDrawable.setColors(fgColor, bgColor, singleToneColor)
+    }
+
+    /** Notifies this drawable that the density might have changed. */
+    fun notifyDensityChanged() {
+        density = context.resources.displayMetrics.density
+    }
+
+    private fun loadPaths() {
+        val shieldPathString = context.resources.getString(R.string.config_batterymeterShieldPath)
+        shieldPath.set(PathParser.createPathFromPathData(shieldPathString))
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 6a10d4a..03d999f 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -45,7 +45,6 @@
 import androidx.annotation.StyleRes;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.settingslib.graph.ThemedBatteryDrawable;
 import com.android.systemui.DualToneHandler;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
@@ -68,7 +67,7 @@
     public static final int MODE_OFF = 2;
     public static final int MODE_ESTIMATE = 3;
 
-    private final ThemedBatteryDrawable mDrawable;
+    private final AccessorizedBatteryDrawable mDrawable;
     private final ImageView mBatteryIconView;
     private TextView mBatteryPercentView;
 
@@ -77,7 +76,10 @@
     private int mLevel;
     private int mShowPercentMode = MODE_DEFAULT;
     private boolean mShowPercentAvailable;
+    private String mEstimateText = null;
     private boolean mCharging;
+    private boolean mIsOverheated;
+    private boolean mDisplayShieldEnabled;
     // Error state where we know nothing about the current battery state
     private boolean mBatteryStateUnknown;
     // Lazily-loaded since this is expected to be a rare-if-ever state
@@ -106,7 +108,7 @@
         final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor,
                 context.getColor(R.color.meter_background_color));
         mPercentageStyleId = atts.getResourceId(R.styleable.BatteryMeterView_textAppearance, 0);
-        mDrawable = new ThemedBatteryDrawable(context, frameColor);
+        mDrawable = new AccessorizedBatteryDrawable(context, frameColor);
         atts.recycle();
 
         mShowPercentAvailable = context.getResources().getBoolean(
@@ -170,12 +172,14 @@
         if (mode == mShowPercentMode) return;
         mShowPercentMode = mode;
         updateShowPercent();
+        updatePercentText();
     }
 
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         updatePercentView();
+        mDrawable.notifyDensityChanged();
     }
 
     public void setColorsFromContext(Context context) {
@@ -203,6 +207,17 @@
         mDrawable.setPowerSaveEnabled(isPowerSave);
     }
 
+    void onIsOverheatedChanged(boolean isOverheated) {
+        boolean valueChanged = mIsOverheated != isOverheated;
+        mIsOverheated = isOverheated;
+        if (valueChanged) {
+            updateContentDescription();
+            // The battery drawable is a different size depending on whether it's currently
+            // overheated or not, so we need to re-scale the view when overheated changes.
+            scaleBatteryMeterViews();
+        }
+    }
+
     private TextView loadPercentView() {
         return (TextView) LayoutInflater.from(getContext())
                 .inflate(R.layout.battery_percentage_view, null);
@@ -227,13 +242,17 @@
         mBatteryEstimateFetcher = fetcher;
     }
 
+    void setDisplayShieldEnabled(boolean displayShieldEnabled) {
+        mDisplayShieldEnabled = displayShieldEnabled;
+    }
+
     void updatePercentText() {
         if (mBatteryStateUnknown) {
-            setContentDescription(getContext().getString(R.string.accessibility_battery_unknown));
             return;
         }
 
         if (mBatteryEstimateFetcher == null) {
+            setPercentTextAtCurrentLevel();
             return;
         }
 
@@ -245,10 +264,9 @@
                         return;
                     }
                     if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
+                        mEstimateText = estimate;
                         mBatteryPercentView.setText(estimate);
-                        setContentDescription(getContext().getString(
-                                R.string.accessibility_battery_level_with_estimate,
-                                mLevel, estimate));
+                        updateContentDescription();
                     } else {
                         setPercentTextAtCurrentLevel();
                     }
@@ -257,28 +275,49 @@
                 setPercentTextAtCurrentLevel();
             }
         } else {
-            setContentDescription(
-                    getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
-                            : R.string.accessibility_battery_level, mLevel));
+            updateContentDescription();
         }
     }
 
     private void setPercentTextAtCurrentLevel() {
-        if (mBatteryPercentView == null) {
-            return;
+        if (mBatteryPercentView != null) {
+            mEstimateText = null;
+            String percentText = NumberFormat.getPercentInstance().format(mLevel / 100f);
+            // Setting text actually triggers a layout pass (because the text view is set to
+            // wrap_content width and TextView always relayouts for this). Avoid needless
+            // relayout if the text didn't actually change.
+            if (!TextUtils.equals(mBatteryPercentView.getText(), percentText)) {
+                mBatteryPercentView.setText(percentText);
+            }
         }
 
-        String percentText = NumberFormat.getPercentInstance().format(mLevel / 100f);
-        // Setting text actually triggers a layout pass (because the text view is set to
-        // wrap_content width and TextView always relayouts for this). Avoid needless
-        // relayout if the text didn't actually change.
-        if (!TextUtils.equals(mBatteryPercentView.getText(), percentText)) {
-            mBatteryPercentView.setText(percentText);
+        updateContentDescription();
+    }
+
+    private void updateContentDescription() {
+        Context context = getContext();
+
+        String contentDescription;
+        if (mBatteryStateUnknown) {
+            contentDescription = context.getString(R.string.accessibility_battery_unknown);
+        } else if (mShowPercentMode == MODE_ESTIMATE && !TextUtils.isEmpty(mEstimateText)) {
+            contentDescription = context.getString(
+                    mIsOverheated
+                            ? R.string.accessibility_battery_level_charging_paused_with_estimate
+                            : R.string.accessibility_battery_level_with_estimate,
+                    mLevel,
+                    mEstimateText);
+        } else if (mIsOverheated) {
+            contentDescription =
+                    context.getString(R.string.accessibility_battery_level_charging_paused, mLevel);
+        } else if (mCharging) {
+            contentDescription =
+                    context.getString(R.string.accessibility_battery_level_charging, mLevel);
+        } else {
+            contentDescription = context.getString(R.string.accessibility_battery_level, mLevel);
         }
 
-        setContentDescription(
-                getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
-                        : R.string.accessibility_battery_level, mLevel));
+        setContentDescription(contentDescription);
     }
 
     void updateShowPercent() {
@@ -329,6 +368,7 @@
         }
 
         mBatteryStateUnknown = isUnknown;
+        updateContentDescription();
 
         if (mBatteryStateUnknown) {
             mBatteryIconView.setImageDrawable(getUnknownStateDrawable());
@@ -349,15 +389,43 @@
         res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
         float iconScaleFactor = typedValue.getFloat();
 
-        int batteryHeight = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height);
-        int batteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width);
+        float mainBatteryHeight =
+                res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height) * iconScaleFactor;
+        float mainBatteryWidth =
+                res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor;
+
+        // If the battery is marked as overheated, we should display a shield indicating that the
+        // battery is being "defended".
+        boolean displayShield = mDisplayShieldEnabled && mIsOverheated;
+        float fullBatteryIconHeight =
+                BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield);
+        float fullBatteryIconWidth =
+                BatterySpecs.getFullBatteryWidth(mainBatteryWidth, displayShield);
+
+        int marginTop;
+        if (displayShield) {
+            // If the shield is displayed, we need some extra marginTop so that the bottom of the
+            // main icon is still aligned with the bottom of all the other system icons.
+            int shieldHeightAddition = Math.round(fullBatteryIconHeight - mainBatteryHeight);
+            // However, the other system icons have some embedded bottom padding that the battery
+            // doesn't have, so we shouldn't move the battery icon down by the full amount.
+            // See b/258672854.
+            marginTop = shieldHeightAddition
+                    - res.getDimensionPixelSize(R.dimen.status_bar_battery_extra_vertical_spacing);
+        } else {
+            marginTop = 0;
+        }
+
         int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom);
 
         LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
-                (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor));
-        scaledLayoutParams.setMargins(0, 0, 0, marginBottom);
+                Math.round(fullBatteryIconWidth),
+                Math.round(fullBatteryIconHeight));
+        scaledLayoutParams.setMargins(0, marginTop, 0, marginBottom);
 
+        mDrawable.setDisplayShield(displayShield);
         mBatteryIconView.setLayoutParams(scaledLayoutParams);
+        mBatteryIconView.invalidateDrawable(mDrawable);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index ae9a323..77cb9d1 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -29,6 +29,8 @@
 
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.settings.CurrentUserTracker;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -84,6 +86,11 @@
                 public void onBatteryUnknownStateChanged(boolean isUnknown) {
                     mView.onBatteryUnknownStateChanged(isUnknown);
                 }
+
+                @Override
+                public void onIsOverheatedChanged(boolean isOverheated) {
+                    mView.onIsOverheatedChanged(isOverheated);
+                }
             };
 
     // Some places may need to show the battery conditionally, and not obey the tuner
@@ -98,6 +105,7 @@
             BroadcastDispatcher broadcastDispatcher,
             @Main Handler mainHandler,
             ContentResolver contentResolver,
+            FeatureFlags featureFlags,
             BatteryController batteryController) {
         super(view);
         mConfigurationController = configurationController;
@@ -106,6 +114,7 @@
         mBatteryController = batteryController;
 
         mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString);
+        mView.setDisplayShieldEnabled(featureFlags.isEnabled(Flags.BATTERY_SHIELD_ICON));
 
         mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery);
         mSettingObserver = new SettingObserver(mainHandler);
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt b/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt
new file mode 100644
index 0000000..6455a96
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.battery
+
+import com.android.settingslib.graph.ThemedBatteryDrawable
+
+/** An object storing specs related to the battery icon in the status bar. */
+object BatterySpecs {
+
+    /** Width of the main battery icon, not including the shield. */
+    const val BATTERY_WIDTH = ThemedBatteryDrawable.WIDTH
+    /** Height of the main battery icon, not including the shield. */
+    const val BATTERY_HEIGHT = ThemedBatteryDrawable.HEIGHT
+
+    private const val SHIELD_WIDTH = 10f
+    private const val SHIELD_HEIGHT = 13f
+
+    /**
+     * Amount that the left side of the shield should be offset from the left side of the battery.
+     */
+    const val SHIELD_LEFT_OFFSET = 8f
+    /** Amount that the top of the shield should be offset from the top of the battery. */
+    const val SHIELD_TOP_OFFSET = 10f
+
+    const val SHIELD_STROKE = 4f
+
+    /** The full width of the battery icon, including the main battery icon *and* the shield. */
+    const val BATTERY_WIDTH_WITH_SHIELD = SHIELD_LEFT_OFFSET + SHIELD_WIDTH
+    /** The full height of the battery icon, including the main battery icon *and* the shield. */
+    const val BATTERY_HEIGHT_WITH_SHIELD = SHIELD_TOP_OFFSET + SHIELD_HEIGHT
+
+    /**
+     * Given the desired height of the main battery icon in pixels, returns the height that the full
+     * battery icon will take up in pixels.
+     *
+     * If there's no shield, this will just return [mainBatteryHeight]. Otherwise, the shield
+     * extends slightly below the bottom of the main battery icon so we need some extra height.
+     */
+    @JvmStatic
+    fun getFullBatteryHeight(mainBatteryHeight: Float, displayShield: Boolean): Float {
+        return if (!displayShield) {
+            mainBatteryHeight
+        } else {
+            val verticalScaleFactor = mainBatteryHeight / BATTERY_HEIGHT
+            verticalScaleFactor * BATTERY_HEIGHT_WITH_SHIELD
+        }
+    }
+
+    /**
+     * Given the desired width of the main battery icon in pixels, returns the width that the full
+     * battery icon will take up in pixels.
+     *
+     * If there's no shield, this will just return [mainBatteryWidth]. Otherwise, the shield extends
+     * past the right side of the main battery icon so we need some extra width.
+     */
+    @JvmStatic
+    fun getFullBatteryWidth(mainBatteryWidth: Float, displayShield: Boolean): Float {
+        return if (!displayShield) {
+            mainBatteryWidth
+        } else {
+            val horizontalScaleFactor = mainBatteryWidth / BATTERY_WIDTH
+            horizontalScaleFactor * BATTERY_WIDTH_WITH_SHIELD
+        }
+    }
+
+    /**
+     * Given the height of the full battery icon, return how tall the main battery icon should be.
+     *
+     * If there's no shield, this will just return [fullBatteryHeight]. Otherwise, the shield takes
+     * up some of the view's height so the main battery width will be just a portion of
+     * [fullBatteryHeight].
+     */
+    @JvmStatic
+    fun getMainBatteryHeight(fullBatteryHeight: Float, displayShield: Boolean): Float {
+        return if (!displayShield) {
+            fullBatteryHeight
+        } else {
+            return (BATTERY_HEIGHT / BATTERY_HEIGHT_WITH_SHIELD) * fullBatteryHeight
+        }
+    }
+
+    /**
+     * Given the width of the full battery icon, return how wide the main battery icon should be.
+     *
+     * If there's no shield, this will just return [fullBatteryWidth]. Otherwise, the shield takes
+     * up some of the view's width so the main battery width will be just a portion of
+     * [fullBatteryWidth].
+     */
+    @JvmStatic
+    fun getMainBatteryWidth(fullBatteryWidth: Float, displayShield: Boolean): Float {
+        return if (!displayShield) {
+            fullBatteryWidth
+        } else {
+            return (BATTERY_WIDTH / BATTERY_WIDTH_WITH_SHIELD) * fullBatteryWidth
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
index fe89c9a..9e8c0ec 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
@@ -21,24 +21,21 @@
 import com.android.systemui.dagger.qualifiers.InstrumentationTest;
 import com.android.systemui.util.InitializationChecker;
 
-import javax.inject.Singleton;
-
 import dagger.BindsInstance;
-import dagger.Component;
 
 /**
  * Base root component for Dagger injection.
  *
+ * This class is not actually annotated as a Dagger component, since it is not used directly as one.
+ * Doing so generates unnecessary code bloat.
+ *
  * See {@link ReferenceGlobalRootComponent} for the one actually used by AOSP.
  */
-@Singleton
-@Component(modules = {GlobalModule.class})
 public interface GlobalRootComponent {
 
     /**
      * Builder for a GlobalRootComponent.
      */
-    @Component.Builder
     interface Builder {
         @BindsInstance
         Builder context(Context context);
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 f9dca08..101f4a4 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -44,7 +44,7 @@
             DreamOverlayComponent.class,
         })
 public interface DreamModule {
-    String DREAM_ONLY_ENABLED_FOR_SYSTEM_USER = "dream_only_enabled_for_system_user";
+    String DREAM_ONLY_ENABLED_FOR_DOCK_USER = "dream_only_enabled_for_dock_user";
 
     String DREAM_SUPPORTED = "dream_supported";
 
@@ -70,10 +70,10 @@
 
     /** */
     @Provides
-    @Named(DREAM_ONLY_ENABLED_FOR_SYSTEM_USER)
-    static boolean providesDreamOnlyEnabledForSystemUser(@Main Resources resources) {
+    @Named(DREAM_ONLY_ENABLED_FOR_DOCK_USER)
+    static boolean providesDreamOnlyEnabledForDockUser(@Main Resources resources) {
         return resources.getBoolean(
-                com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser);
+                com.android.internal.R.bool.config_dreamsOnlyEnabledForDockUser);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index c4cc338..ea627ac 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -239,6 +239,9 @@
     // TODO(b/256613548): Tracking Bug
     val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend")
 
+    // TODO(b/256623670): Tracking Bug
+    @JvmField val BATTERY_SHIELD_ICON = unreleasedFlag(610, "battery_shield_icon")
+
     // 700 - dialer/calls
     // TODO(b/254512734): Tracking Bug
     val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip")
@@ -380,7 +383,8 @@
 
     // TODO(b/254513155): Tracking Bug
     @JvmField
-    val SCREENSHOT_WORK_PROFILE_POLICY = unreleasedFlag(1301, "screenshot_work_profile_policy")
+    val SCREENSHOT_WORK_PROFILE_POLICY =
+        unreleasedFlag(1301, "screenshot_work_profile_policy", teamfood = true)
 
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
index bebd580..4abe309 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
@@ -70,7 +70,7 @@
     private final SettingObserver mDreamSettingObserver;
     private final UserTracker mUserTracker;
     private final boolean mDreamSupported;
-    private final boolean mDreamOnlyEnabledForSystemUser;
+    private final boolean mDreamOnlyEnabledForDockUser;
 
     private boolean mIsDocked = false;
 
@@ -100,8 +100,8 @@
             BroadcastDispatcher broadcastDispatcher,
             UserTracker userTracker,
             @Named(DreamModule.DREAM_SUPPORTED) boolean dreamSupported,
-            @Named(DreamModule.DREAM_ONLY_ENABLED_FOR_SYSTEM_USER)
-                    boolean dreamOnlyEnabledForSystemUser
+            @Named(DreamModule.DREAM_ONLY_ENABLED_FOR_DOCK_USER)
+                    boolean dreamOnlyEnabledForDockUser
     ) {
         super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                 statusBarStateController, activityStarter, qsLogger);
@@ -123,7 +123,7 @@
         };
         mUserTracker = userTracker;
         mDreamSupported = dreamSupported;
-        mDreamOnlyEnabledForSystemUser = dreamOnlyEnabledForSystemUser;
+        mDreamOnlyEnabledForDockUser = dreamOnlyEnabledForDockUser;
     }
 
     @Override
@@ -203,7 +203,8 @@
         // For now, restrict to debug users.
         return Build.isDebuggable()
                 && mDreamSupported
-                && (!mDreamOnlyEnabledForSystemUser || mUserTracker.getUserHandle().isSystem());
+                // TODO(b/257333623): Allow the Dock User to be non-SystemUser user in HSUM.
+                && (!mDreamOnlyEnabledForDockUser || mUserTracker.getUserHandle().isSystem());
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
index 4063af3..954534d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
@@ -92,7 +92,6 @@
                     centerEnd,
                     ConstraintSet.END
                 )
-                constrainWidth(R.id.statusIcons, 0)
             },
             qsConstraintsChanges = {
                 setGuidelineBegin(centerStart, offsetFromEdge)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 0369845..2d580ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -225,6 +225,7 @@
             BroadcastDispatcher broadcastDispatcher,
             @Main Handler mainHandler,
             ContentResolver contentResolver,
+            FeatureFlags featureFlags,
             BatteryController batteryController
     ) {
         return new BatteryMeterViewController(
@@ -234,6 +235,7 @@
                 broadcastDispatcher,
                 mainHandler,
                 contentResolver,
+                featureFlags,
                 batteryController);
 
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 149ed0a..d10d7cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -155,6 +155,9 @@
 
         default void onWirelessChargingChanged(boolean isWirlessCharging) {
         }
+
+        default void onIsOverheatedChanged(boolean isOverheated) {
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index c7ad767..3c2ac7b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.statusbar.policy;
 
+import static android.os.BatteryManager.BATTERY_HEALTH_OVERHEAT;
+import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN;
+import static android.os.BatteryManager.EXTRA_HEALTH;
 import static android.os.BatteryManager.EXTRA_PRESENT;
 
 import android.annotation.WorkerThread;
@@ -87,6 +90,7 @@
     protected boolean mPowerSave;
     private boolean mAodPowerSave;
     private boolean mWirelessCharging;
+    private boolean mIsOverheated = false;
     private boolean mTestMode = false;
     @VisibleForTesting
     boolean mHasReceivedBattery = false;
@@ -184,6 +188,7 @@
         cb.onPowerSaveChanged(mPowerSave);
         cb.onBatteryUnknownStateChanged(mStateUnknown);
         cb.onWirelessChargingChanged(mWirelessCharging);
+        cb.onIsOverheatedChanged(mIsOverheated);
     }
 
     @Override
@@ -222,6 +227,13 @@
                 fireBatteryUnknownStateChanged();
             }
 
+            int batteryHealth = intent.getIntExtra(EXTRA_HEALTH, BATTERY_HEALTH_UNKNOWN);
+            boolean isOverheated = batteryHealth == BATTERY_HEALTH_OVERHEAT;
+            if (isOverheated != mIsOverheated) {
+                mIsOverheated = isOverheated;
+                fireIsOverheatedChanged();
+            }
+
             fireBatteryLevelChanged();
         } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) {
             updatePowerSave();
@@ -292,6 +304,10 @@
         return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_WIRELESS;
     }
 
+    public boolean isOverheated() {
+        return mIsOverheated;
+    }
+
     @Override
     public void getEstimatedTimeRemainingString(EstimateFetchCompletion completion) {
         // Need to fetch or refresh the estimate, but it may involve binder calls so offload the
@@ -402,6 +418,15 @@
         }
     }
 
+    private void fireIsOverheatedChanged() {
+        synchronized (mChangeCallbacks) {
+            final int n = mChangeCallbacks.size();
+            for (int i = 0; i < n; i++) {
+                mChangeCallbacks.get(i).onIsOverheatedChanged(mIsOverheated);
+            }
+        }
+    }
+
     @Override
     public void dispatchDemoCommand(String command, Bundle args) {
         if (!mDemoModeController.isInDemoMode()) {
@@ -412,6 +437,7 @@
         String plugged = args.getString("plugged");
         String powerSave = args.getString("powersave");
         String present = args.getString("present");
+        String overheated = args.getString("overheated");
         if (level != null) {
             mLevel = Math.min(Math.max(Integer.parseInt(level), 0), 100);
         }
@@ -426,6 +452,10 @@
             mStateUnknown = !present.equals("true");
             fireBatteryUnknownStateChanged();
         }
+        if (overheated != null) {
+            mIsOverheated = overheated.equals("true");
+            fireIsOverheatedChanged();
+        }
         fireBatteryLevelChanged();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt
new file mode 100644
index 0000000..982f033
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.battery
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class AccessorizedBatteryDrawableTest : SysuiTestCase() {
+    @Test
+    fun intrinsicSize_shieldFalse_isBatterySize() {
+        val drawable = AccessorizedBatteryDrawable(context, frameColor = 0)
+        drawable.displayShield = false
+
+        val density = context.resources.displayMetrics.density
+        assertThat(drawable.intrinsicHeight).isEqualTo((BATTERY_HEIGHT * density).toInt())
+        assertThat(drawable.intrinsicWidth).isEqualTo((BATTERY_WIDTH * density).toInt())
+    }
+
+    @Test
+    fun intrinsicSize_shieldTrue_isBatteryPlusShieldSize() {
+        val drawable = AccessorizedBatteryDrawable(context, frameColor = 0)
+        drawable.displayShield = true
+
+        val density = context.resources.displayMetrics.density
+        assertThat(drawable.intrinsicHeight)
+            .isEqualTo((BATTERY_HEIGHT_WITH_SHIELD * density).toInt())
+        assertThat(drawable.intrinsicWidth).isEqualTo((BATTERY_WIDTH_WITH_SHIELD * density).toInt())
+    }
+
+    // TODO(b/255625888): Screenshot tests for this drawable would be amazing!
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
index 1d038a4..bc8f961 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
@@ -35,6 +35,8 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.tuner.TunerService;
@@ -59,6 +61,7 @@
     private Handler mHandler;
     @Mock
     private ContentResolver mContentResolver;
+    private FakeFeatureFlags mFeatureFlags;
     @Mock
     private BatteryController mBatteryController;
 
@@ -71,19 +74,13 @@
         when(mBatteryMeterView.getContext()).thenReturn(mContext);
         when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources());
 
-        mController = new BatteryMeterViewController(
-                mBatteryMeterView,
-                mConfigurationController,
-                mTunerService,
-                mBroadcastDispatcher,
-                mHandler,
-                mContentResolver,
-                mBatteryController
-        );
+        mFeatureFlags = new FakeFeatureFlags();
+        mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, false);
     }
 
     @Test
     public void onViewAttached_callbacksRegistered() {
+        initController();
         mController.onViewAttached();
 
         verify(mConfigurationController).addCallback(any());
@@ -101,6 +98,7 @@
 
     @Test
     public void onViewDetached_callbacksUnregistered() {
+        initController();
         // Set everything up first.
         mController.onViewAttached();
 
@@ -114,6 +112,7 @@
 
     @Test
     public void ignoreTunerUpdates_afterOnViewAttached_callbackUnregistered() {
+        initController();
         // Start out receiving tuner updates
         mController.onViewAttached();
 
@@ -124,10 +123,43 @@
 
     @Test
     public void ignoreTunerUpdates_beforeOnViewAttached_callbackNeverRegistered() {
+        initController();
+
         mController.ignoreTunerUpdates();
 
         mController.onViewAttached();
 
         verify(mTunerService, never()).addTunable(any(), any());
     }
+
+    @Test
+    public void shieldFlagDisabled_viewNotified() {
+        mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, false);
+
+        initController();
+
+        verify(mBatteryMeterView).setDisplayShieldEnabled(false);
+    }
+
+    @Test
+    public void shieldFlagEnabled_viewNotified() {
+        mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, true);
+
+        initController();
+
+        verify(mBatteryMeterView).setDisplayShieldEnabled(true);
+    }
+
+    private void initController() {
+        mController = new BatteryMeterViewController(
+                mBatteryMeterView,
+                mConfigurationController,
+                mTunerService,
+                mBroadcastDispatcher,
+                mHandler,
+                mContentResolver,
+                mFeatureFlags,
+                mBatteryController
+        );
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
index b4ff2a5..eb7d9c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
@@ -17,7 +17,9 @@
 
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
+import android.widget.ImageView
 import androidx.test.filters.SmallTest
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher
 import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion
@@ -58,6 +60,182 @@
         // No assert needed
     }
 
+    @Test
+    fun contentDescription_unknown() {
+        mBatteryMeterView.onBatteryUnknownStateChanged(true)
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_unknown)
+        )
+    }
+
+    @Test
+    fun contentDescription_estimate() {
+        mBatteryMeterView.onBatteryLevelChanged(15, false)
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(
+                        R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+                )
+        )
+    }
+
+    @Test
+    fun contentDescription_estimateAndOverheated() {
+        mBatteryMeterView.onBatteryLevelChanged(17, false)
+        mBatteryMeterView.onIsOverheatedChanged(true)
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(
+                        R.string.accessibility_battery_level_charging_paused_with_estimate,
+                        17,
+                        ESTIMATE,
+                )
+        )
+    }
+
+    @Test
+    fun contentDescription_overheated() {
+        mBatteryMeterView.onBatteryLevelChanged(90, false)
+        mBatteryMeterView.onIsOverheatedChanged(true)
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level_charging_paused, 90)
+        )
+    }
+
+    @Test
+    fun contentDescription_charging() {
+        mBatteryMeterView.onBatteryLevelChanged(45, true)
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level_charging, 45)
+        )
+    }
+
+    @Test
+    fun contentDescription_notCharging() {
+        mBatteryMeterView.onBatteryLevelChanged(45, false)
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level, 45)
+        )
+    }
+
+    @Test
+    fun changesFromEstimateToPercent_textAndContentDescriptionChanges() {
+        mBatteryMeterView.onBatteryLevelChanged(15, false)
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(
+                        R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+                )
+        )
+
+        // Update the show mode from estimate to percent
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
+
+        assertThat(mBatteryMeterView.batteryPercentViewText).isEqualTo("15%")
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level, 15)
+        )
+    }
+
+    @Test
+    fun contentDescription_manyUpdates_alwaysUpdated() {
+        // Overheated
+        mBatteryMeterView.onBatteryLevelChanged(90, false)
+        mBatteryMeterView.onIsOverheatedChanged(true)
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level_charging_paused, 90)
+        )
+
+        // Overheated & estimate
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+        mBatteryMeterView.updatePercentText()
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(
+                        R.string.accessibility_battery_level_charging_paused_with_estimate,
+                        90,
+                        ESTIMATE,
+                )
+        )
+
+        // Just estimate
+        mBatteryMeterView.onIsOverheatedChanged(false)
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(
+                        R.string.accessibility_battery_level_with_estimate,
+                        90,
+                        ESTIMATE,
+                )
+        )
+
+        // Just percent
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level, 90)
+        )
+
+        // Charging
+        mBatteryMeterView.onBatteryLevelChanged(90, true)
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level_charging, 90)
+        )
+    }
+
+    @Test
+    fun isOverheatedChanged_true_drawableGetsTrue() {
+        mBatteryMeterView.setDisplayShieldEnabled(true)
+        val drawable = getBatteryDrawable()
+
+        mBatteryMeterView.onIsOverheatedChanged(true)
+
+        assertThat(drawable.displayShield).isTrue()
+    }
+
+    @Test
+    fun isOverheatedChanged_false_drawableGetsFalse() {
+        mBatteryMeterView.setDisplayShieldEnabled(true)
+        val drawable = getBatteryDrawable()
+
+        // Start as true
+        mBatteryMeterView.onIsOverheatedChanged(true)
+
+        // Update to false
+        mBatteryMeterView.onIsOverheatedChanged(false)
+
+        assertThat(drawable.displayShield).isFalse()
+    }
+
+    @Test
+    fun isOverheatedChanged_true_featureflagOff_drawableGetsFalse() {
+        mBatteryMeterView.setDisplayShieldEnabled(false)
+        val drawable = getBatteryDrawable()
+
+        mBatteryMeterView.onIsOverheatedChanged(true)
+
+        assertThat(drawable.displayShield).isFalse()
+    }
+
+    private fun getBatteryDrawable(): AccessorizedBatteryDrawable {
+        return (mBatteryMeterView.getChildAt(0) as ImageView)
+                .drawable as AccessorizedBatteryDrawable
+    }
+
     private class Fetcher : BatteryEstimateFetcher {
         override fun fetchBatteryTimeRemainingEstimate(
                 completion: EstimateFetchCompletion) {
@@ -68,4 +246,4 @@
     private companion object {
         const val ESTIMATE = "2 hours 2 minutes"
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt
new file mode 100644
index 0000000..39cb047
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.battery
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class BatterySpecsTest : SysuiTestCase() {
+    @Test
+    fun getFullBatteryHeight_shieldFalse_returnsMainHeight() {
+        val fullHeight = BatterySpecs.getFullBatteryHeight(56f, displayShield = false)
+
+        assertThat(fullHeight).isEqualTo(56f)
+    }
+
+    @Test
+    fun getFullBatteryHeight_shieldTrue_returnsMainHeightPlusShield() {
+        val mainHeight = BATTERY_HEIGHT * 5
+        val fullHeight = BatterySpecs.getFullBatteryHeight(mainHeight, displayShield = true)
+
+        // Since the main battery was scaled 5x, the output height should also be scaled 5x
+        val expectedFullHeight = BATTERY_HEIGHT_WITH_SHIELD * 5
+
+        assertThat(fullHeight).isWithin(.0001f).of(expectedFullHeight)
+    }
+
+    @Test
+    fun getFullBatteryWidth_shieldFalse_returnsMainWidth() {
+        val fullWidth = BatterySpecs.getFullBatteryWidth(33f, displayShield = false)
+
+        assertThat(fullWidth).isEqualTo(33f)
+    }
+
+    @Test
+    fun getFullBatteryWidth_shieldTrue_returnsMainWidthPlusShield() {
+        val mainWidth = BATTERY_WIDTH * 3.3f
+
+        val fullWidth = BatterySpecs.getFullBatteryWidth(mainWidth, displayShield = true)
+
+        // Since the main battery was scaled 3.3x, the output width should also be scaled 5x
+        val expectedFullWidth = BATTERY_WIDTH_WITH_SHIELD * 3.3f
+        assertThat(fullWidth).isWithin(.0001f).of(expectedFullWidth)
+    }
+
+    @Test
+    fun getMainBatteryHeight_shieldFalse_returnsFullHeight() {
+        val mainHeight = BatterySpecs.getMainBatteryHeight(89f, displayShield = false)
+
+        assertThat(mainHeight).isEqualTo(89f)
+    }
+
+    @Test
+    fun getMainBatteryHeight_shieldTrue_returnsNotFullHeight() {
+        val fullHeight = BATTERY_HEIGHT_WITH_SHIELD * 7.7f
+
+        val mainHeight = BatterySpecs.getMainBatteryHeight(fullHeight, displayShield = true)
+
+        // Since the full height was scaled 7.7x, the main height should also be scaled 7.7x.
+        val expectedHeight = BATTERY_HEIGHT * 7.7f
+        assertThat(mainHeight).isWithin(.0001f).of(expectedHeight)
+    }
+
+    @Test
+    fun getMainBatteryWidth_shieldFalse_returnsFullWidth() {
+        val mainWidth = BatterySpecs.getMainBatteryWidth(2345f, displayShield = false)
+
+        assertThat(mainWidth).isEqualTo(2345f)
+    }
+
+    @Test
+    fun getMainBatteryWidth_shieldTrue_returnsNotFullWidth() {
+        val fullWidth = BATTERY_WIDTH_WITH_SHIELD * 0.6f
+
+        val mainWidth = BatterySpecs.getMainBatteryWidth(fullWidth, displayShield = true)
+
+        // Since the full width was scaled 0.6x, the main height should also be scaled 0.6x.
+        val expectedWidth = BATTERY_WIDTH * 0.6f
+        assertThat(mainWidth).isWithin(.0001f).of(expectedWidth)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
index 0ce9056..d7eb337 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -320,6 +321,48 @@
         assertThat(changes.largeScreenConstraintsChanges).isNull()
     }
 
+    @Test
+    fun testRelevantViewsAreNotMatchConstraints() {
+        val views = mapOf(
+                R.id.clock to "clock",
+                R.id.date to "date",
+                R.id.statusIcons to "icons",
+                R.id.privacy_container to "privacy",
+                R.id.carrier_group to "carriers",
+                R.id.batteryRemainingIcon to "battery",
+        )
+        views.forEach { (id, name) ->
+            assertWithMessage("$name has 0 height in qqs")
+                    .that(qqsConstraint.getConstraint(id).layout.mHeight).isNotEqualTo(0)
+            assertWithMessage("$name has 0 width in qqs")
+                    .that(qqsConstraint.getConstraint(id).layout.mWidth).isNotEqualTo(0)
+            assertWithMessage("$name has 0 height in qs")
+                    .that(qsConstraint.getConstraint(id).layout.mHeight).isNotEqualTo(0)
+            assertWithMessage("$name has 0 width in qs")
+                    .that(qsConstraint.getConstraint(id).layout.mWidth).isNotEqualTo(0)
+        }
+    }
+
+    @Test
+    fun testCheckViewsDontChangeSizeBetweenAnimationConstraints() {
+        val views = mapOf(
+                R.id.clock to "clock",
+                R.id.date to "date",
+                R.id.statusIcons to "icons",
+                R.id.privacy_container to "privacy",
+                R.id.carrier_group to "carriers",
+                R.id.batteryRemainingIcon to "battery",
+        )
+        views.forEach { (id, name) ->
+            assertWithMessage("$name changes height")
+                    .that(qqsConstraint.getConstraint(id).layout.mHeight)
+                    .isEqualTo(qsConstraint.getConstraint(id).layout.mHeight)
+            assertWithMessage("$name changes width")
+                    .that(qqsConstraint.getConstraint(id).layout.mWidth)
+                    .isEqualTo(qsConstraint.getConstraint(id).layout.mWidth)
+        }
+    }
+
     private operator fun ConstraintsChanges.invoke() {
         qqsConstraintsChanges?.invoke(qqsConstraint)
         qsConstraintsChanges?.invoke(qsConstraint)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index 43d0fe9..1eee08c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -221,4 +221,33 @@
 
         Assert.assertFalse(mBatteryController.isChargingSourceDock());
     }
+
+    @Test
+    public void batteryStateChanged_healthNotOverheated_outputsFalse() {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_GOOD);
+
+        mBatteryController.onReceive(getContext(), intent);
+
+        Assert.assertFalse(mBatteryController.isOverheated());
+    }
+
+    @Test
+    public void batteryStateChanged_healthOverheated_outputsTrue() {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT);
+
+        mBatteryController.onReceive(getContext(), intent);
+
+        Assert.assertTrue(mBatteryController.isOverheated());
+    }
+
+    @Test
+    public void batteryStateChanged_noHealthGiven_outputsFalse() {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+
+        mBatteryController.onReceive(getContext(), intent);
+
+        Assert.assertFalse(mBatteryController.isOverheated());
+    }
 }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index c06101f..bf0b388 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -2511,19 +2511,22 @@
         float appliedThermalCapNits =
                 event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
                 ? -1f : convertToNits(event.getThermalMax());
-
-        FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
-                convertToNits(event.getInitialBrightness()),
-                convertToNits(event.getBrightness()),
-                event.getSlowAmbientLux(),
-                event.getPhysicalDisplayId(),
-                event.isShortTermModelActive(),
-                appliedLowPowerMode,
-                appliedRbcStrength,
-                appliedHbmMaxNits,
-                appliedThermalCapNits,
-                event.isAutomaticBrightnessEnabled(),
-                FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
+        if (mLogicalDisplay.getPrimaryDisplayDeviceLocked() != null
+                && mLogicalDisplay.getPrimaryDisplayDeviceLocked()
+                .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL) {
+            FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
+                    convertToNits(event.getInitialBrightness()),
+                    convertToNits(event.getBrightness()),
+                    event.getSlowAmbientLux(),
+                    event.getPhysicalDisplayId(),
+                    event.isShortTermModelActive(),
+                    appliedLowPowerMode,
+                    appliedRbcStrength,
+                    appliedHbmMaxNits,
+                    appliedThermalCapNits,
+                    event.isAutomaticBrightnessEnabled(),
+                    FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
+        }
     }
 
     private final class DisplayControllerHandler extends Handler {
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index facc6b2..4a0ba22 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -116,7 +116,7 @@
     private final DreamUiEventLogger mDreamUiEventLogger;
     private final ComponentName mAmbientDisplayComponent;
     private final boolean mDismissDreamOnActivityStart;
-    private final boolean mDreamsOnlyEnabledForSystemUser;
+    private final boolean mDreamsOnlyEnabledForDockUser;
     private final boolean mDreamsEnabledByDefaultConfig;
     private final boolean mDreamsActivatedOnChargeByDefault;
     private final boolean mDreamsActivatedOnDockByDefault;
@@ -214,8 +214,8 @@
                 mContext.getResources().getStringArray(R.array.config_loggable_dream_prefixes));
         AmbientDisplayConfiguration adc = new AmbientDisplayConfiguration(mContext);
         mAmbientDisplayComponent = ComponentName.unflattenFromString(adc.ambientDisplayComponent());
-        mDreamsOnlyEnabledForSystemUser =
-                mContext.getResources().getBoolean(R.bool.config_dreamsOnlyEnabledForSystemUser);
+        mDreamsOnlyEnabledForDockUser =
+                mContext.getResources().getBoolean(R.bool.config_dreamsOnlyEnabledForDockUser);
         mDismissDreamOnActivityStart = mContext.getResources().getBoolean(
                 R.bool.config_dismissDreamOnActivityStart);
 
@@ -292,10 +292,9 @@
             pw.println();
             pw.println("mCurrentDream=" + mCurrentDream);
             pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled);
-            pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser);
+            pw.println("mDreamsOnlyEnabledForDockUser=" + mDreamsOnlyEnabledForDockUser);
             pw.println("mDreamsEnabledSetting=" + mDreamsEnabledSetting);
             pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled);
-            pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser);
             pw.println("mDreamsActivatedOnDockByDefault=" + mDreamsActivatedOnDockByDefault);
             pw.println("mDreamsActivatedOnChargeByDefault=" + mDreamsActivatedOnChargeByDefault);
             pw.println("mIsDocked=" + mIsDocked);
@@ -602,7 +601,8 @@
     }
 
     private boolean dreamsEnabledForUser(int userId) {
-        return !mDreamsOnlyEnabledForSystemUser || (userId == UserHandle.USER_SYSTEM);
+        // TODO(b/257333623): Support non-system Dock Users in HSUM.
+        return !mDreamsOnlyEnabledForDockUser || (userId == UserHandle.USER_SYSTEM);
     }
 
     private ServiceInfo getServiceInfo(ComponentName name) {
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index 1a0f6f7..015e576 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -28,6 +28,7 @@
 import android.view.InputChannel;
 import android.view.MotionEvent;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputMethodSubtype;
 import android.window.ImeOnBackInvokedDispatcher;
@@ -198,9 +199,10 @@
 
     // TODO(b/192412909): Convert this back to void method
     @AnyThread
-    boolean showSoftInput(IBinder showInputToken, int flags, ResultReceiver resultReceiver) {
+    boolean showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken, int flags,
+            ResultReceiver resultReceiver) {
         try {
-            mTarget.showSoftInput(showInputToken, flags, resultReceiver);
+            mTarget.showSoftInput(showInputToken, statsToken, flags, resultReceiver);
         } catch (RemoteException e) {
             logRemoteException(e);
             return false;
@@ -210,9 +212,10 @@
 
     // TODO(b/192412909): Convert this back to void method
     @AnyThread
-    boolean hideSoftInput(IBinder hideInputToken, int flags, ResultReceiver resultReceiver) {
+    boolean hideSoftInput(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken, int flags,
+            ResultReceiver resultReceiver) {
         try {
-            mTarget.hideSoftInput(hideInputToken, flags, resultReceiver);
+            mTarget.hideSoftInput(hideInputToken, statsToken, flags, resultReceiver);
         } catch (RemoteException e) {
             logRemoteException(e);
             return false;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 114c512..8b083bd 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -129,6 +129,7 @@
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethod;
@@ -151,6 +152,7 @@
 import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
 import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
 import com.android.internal.inputmethod.IInputContentUriToken;
+import com.android.internal.inputmethod.IInputMethod;
 import com.android.internal.inputmethod.IInputMethodClient;
 import com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
 import com.android.internal.inputmethod.IInputMethodSession;
@@ -642,6 +644,10 @@
      */
     private boolean mInputShown;
 
+    /** The token tracking the current IME request or {@code null} otherwise. */
+    @Nullable
+    private ImeTracker.Token mCurStatsToken;
+
     /**
      * {@code true} if the current input method is in fullscreen mode.
      */
@@ -761,7 +767,7 @@
      * <dd>
      *   If this bit is ON, some of IME view, e.g. software input, candidate view, is visible.
      * </dd>
-     * dt>{@link InputMethodService#IME_INVISIBLE}</dt>
+     * <dt>{@link InputMethodService#IME_INVISIBLE}</dt>
      * <dd> If this bit is ON, IME is ready with views from last EditorInfo but is
      *    currently invisible.
      * </dd>
@@ -785,8 +791,7 @@
 
     /**
      * Internal state snapshot when
-     * {@link com.android.internal.view.IInputMethod#startInput(IBinder, IRemoteInputConnection, EditorInfo,
-     * boolean)} is about to be called.
+     * {@link IInputMethod#startInput(IInputMethod.StartInputParams)} is about to be called.
      *
      * <p>Calling that IPC endpoint basically means that
      * {@link InputMethodService#doStartInput(InputConnection, EditorInfo, boolean)} will be called
@@ -1071,7 +1076,7 @@
 
         /**
          * Add a new entry and discard the oldest entry as needed.
-         * @param info {@lin StartInputInfo} to be added.
+         * @param info {@link StartInputInfo} to be added.
          */
         void addEntry(@NonNull StartInputInfo info) {
             final int index = mNextIndex;
@@ -1189,18 +1194,18 @@
                 } else if (accessibilityRequestingNoImeUri.equals(uri)) {
                     final int accessibilitySoftKeyboardSetting = Settings.Secure.getIntForUser(
                             mContext.getContentResolver(),
-                            Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0, mUserId);
+                            Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0 /* def */, mUserId);
                     mAccessibilityRequestingNoSoftKeyboard =
                             (accessibilitySoftKeyboardSetting & AccessibilityService.SHOW_MODE_MASK)
                                     == AccessibilityService.SHOW_MODE_HIDDEN;
                     if (mAccessibilityRequestingNoSoftKeyboard) {
                         final boolean showRequested = mShowRequested;
-                        hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                        hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+                                0 /* flags */, null /* resultReceiver */,
                                 SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE);
                         mShowRequested = showRequested;
                     } else if (mShowRequested) {
-                        showCurrentInputLocked(mCurFocusedWindow,
-                                InputMethodManager.SHOW_IMPLICIT, null,
+                        showCurrentInputImplicitLocked(mCurFocusedWindow,
                                 SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE);
                     }
                 } else {
@@ -1666,8 +1671,8 @@
         }
         // Hide soft input before user switch task since switch task may block main handler a while
         // and delayed the hideCurrentInputLocked().
-        hideCurrentInputLocked(
-                mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_SWITCH_USER);
+        hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                null /* resultReceiver */, SoftInputShowHideReason.HIDE_SWITCH_USER);
         final UserSwitchHandlerTask task = new UserSwitchHandlerTask(this, userId,
                 clientToBeReset);
         mUserSwitchHandlerTask = task;
@@ -2222,7 +2227,7 @@
             }
             final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);
             try {
-                client.asBinder().linkToDeath(deathRecipient, 0);
+                client.asBinder().linkToDeath(deathRecipient, 0 /* flags */);
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
@@ -2247,7 +2252,7 @@
         synchronized (ImfLock.class) {
             ClientState cs = mClients.remove(client.asBinder());
             if (cs != null) {
-                client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0);
+                client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */);
                 clearClientSessionLocked(cs);
                 clearClientSessionForAccessibilityLocked(cs);
 
@@ -2260,8 +2265,8 @@
                 }
 
                 if (mCurClient == cs) {
-                    hideCurrentInputLocked(
-                            mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
+                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                            null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
                     if (mBoundToMethod) {
                         mBoundToMethod = false;
                         IInputMethodInvoker curMethod = getCurMethodLocked();
@@ -2308,6 +2313,8 @@
             mCurClient.mSessionRequestedForAccessibility = false;
             mCurClient = null;
             mCurVirtualDisplayToScreenMatrix = null;
+            ImeTracker.get().onFailed(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+            mCurStatsToken = null;
 
             mMenuController.hideInputMethodMenuLocked();
         }
@@ -2380,8 +2387,11 @@
                 navButtonFlags, mCurImeDispatcher);
         if (mShowRequested) {
             if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
-            showCurrentInputLocked(mCurFocusedWindow, getAppShowFlagsLocked(), null,
-                    SoftInputShowHideReason.ATTACH_NEW_INPUT);
+            // Re-use current statsToken, if it exists.
+            final ImeTracker.Token statsToken = mCurStatsToken;
+            mCurStatsToken = null;
+            showCurrentInputLocked(mCurFocusedWindow, statsToken, getAppShowFlagsLocked(),
+                    null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT);
         }
 
         String curId = getCurIdLocked();
@@ -2500,7 +2510,8 @@
 
         if (mDisplayIdToShowIme == INVALID_DISPLAY) {
             mImeHiddenByDisplayPolicy = true;
-            hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+            hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                    null /* resultReceiver */,
                     SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE);
             return InputBindResult.NO_IME;
         }
@@ -3278,22 +3289,23 @@
     }
 
     @Override
-    public boolean showSoftInput(IInputMethodClient client, IBinder windowToken, int flags,
-            int lastClickTooType, ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
+    public boolean showSoftInput(IInputMethodClient client, IBinder windowToken,
+            @Nullable ImeTracker.Token statsToken, int flags, int lastClickTooType,
+            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInput");
         int uid = Binder.getCallingUid();
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#showSoftInput");
         synchronized (ImfLock.class) {
-            if (!canInteractWithImeLocked(uid, client, "showSoftInput")) {
+            if (!canInteractWithImeLocked(uid, client, "showSoftInput", statsToken)) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
                 return false;
             }
             final long ident = Binder.clearCallingIdentity();
             try {
                 if (DEBUG) Slog.v(TAG, "Client requesting input be shown");
-                return showCurrentInputLocked(
-                        windowToken, flags, lastClickTooType, resultReceiver, reason);
+                return showCurrentInputLocked(windowToken, statsToken, flags, lastClickTooType,
+                        resultReceiver, reason);
             } finally {
                 Binder.restoreCallingIdentity(ident);
                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -3310,7 +3322,8 @@
                     "InputMethodManagerService#startStylusHandwriting");
             int uid = Binder.getCallingUid();
             synchronized (ImfLock.class) {
-                if (!canInteractWithImeLocked(uid, client, "startStylusHandwriting")) {
+                if (!canInteractWithImeLocked(uid, client, "startStylusHandwriting",
+                        null /* statsToken */)) {
                     return;
                 }
                 if (!hasSupportedStylusLocked()) {
@@ -3365,19 +3378,33 @@
     }
 
     @GuardedBy("ImfLock.class")
-    boolean showCurrentInputLocked(IBinder windowToken, int flags,
-            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
-        return showCurrentInputLocked(
-                windowToken, flags, MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
+    boolean showCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+            int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+        return showCurrentInputLocked(windowToken, statsToken, flags,
+                MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
     }
 
     @GuardedBy("ImfLock.class")
-    private boolean showCurrentInputLocked(IBinder windowToken, int flags, int lastClickToolType,
+    private boolean showCurrentInputLocked(IBinder windowToken,
+            @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
             ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+        // Create statsToken is none exists.
+        if (statsToken == null) {
+            String packageName = null;
+            if (mCurEditorInfo != null) {
+                packageName = mCurEditorInfo.packageName;
+            }
+            statsToken = new ImeTracker.Token(packageName);
+            ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_SERVER_START_INPUT,
+                    reason);
+        }
+
         mShowRequested = true;
         if (mAccessibilityRequestingNoSoftKeyboard || mImeHiddenByDisplayPolicy) {
+            ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
             return false;
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
 
         if ((flags & InputMethodManager.SHOW_FORCED) != 0) {
             mShowExplicitlyRequested = true;
@@ -3387,8 +3414,10 @@
         }
 
         if (!mSystemReady) {
+            ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY);
             return false;
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY);
 
         mBindingController.setCurrentMethodVisible();
         final IInputMethodInvoker curMethod = getCurMethodLocked();
@@ -3396,6 +3425,9 @@
             // create a placeholder token for IMS so that IMS cannot inject windows into client app.
             Binder showInputToken = new Binder();
             mShowRequestWindowMap.put(showInputToken, windowToken);
+            ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
+            mCurStatsToken = null;
             final int showFlags = getImeShowFlagsLocked();
             if (DEBUG) {
                 Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken
@@ -3407,23 +3439,34 @@
                 curMethod.updateEditorToolType(lastClickToolType);
             }
             // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
-            if (curMethod.showSoftInput(showInputToken, showFlags, resultReceiver)) {
+            if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) {
                 onShowHideSoftInputRequested(true /* show */, windowToken, reason);
             }
             mInputShown = true;
             return true;
+        } else {
+            ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+            mCurStatsToken = statsToken;
         }
         return false;
     }
 
     @Override
-    public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken, int flags,
-            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+    public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken,
+            @Nullable ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver,
+            @SoftInputShowHideReason int reason) {
         int uid = Binder.getCallingUid();
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#hideSoftInput");
         synchronized (ImfLock.class) {
-            if (!canInteractWithImeLocked(uid, client, "hideSoftInput")) {
+            if (!canInteractWithImeLocked(uid, client, "hideSoftInput", statsToken)) {
+                if (mInputShown) {
+                    ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+                } else {
+                    ImeTracker.get().onCancelled(statsToken,
+                            ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+                }
                 return false;
             }
             final long ident = Binder.clearCallingIdentity();
@@ -3431,7 +3474,7 @@
                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInput");
                 if (DEBUG) Slog.v(TAG, "Client requesting input be hidden");
                 return InputMethodManagerService.this.hideCurrentInputLocked(windowToken,
-                        flags, resultReceiver, reason);
+                        statsToken, flags, resultReceiver, reason);
             } finally {
                 Binder.restoreCallingIdentity(ident);
                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -3440,17 +3483,32 @@
     }
 
     @GuardedBy("ImfLock.class")
-    boolean hideCurrentInputLocked(IBinder windowToken, int flags, ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
+    boolean hideCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+            int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+        // Create statsToken is none exists.
+        if (statsToken == null) {
+            String packageName = null;
+            if (mCurEditorInfo != null) {
+                packageName = mCurEditorInfo.packageName;
+            }
+            statsToken = new ImeTracker.Token(packageName);
+            ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason);
+        }
+
         if ((flags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
                 && (mShowExplicitlyRequested || mShowForced)) {
             if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
+            ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT);
             return false;
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT);
+
         if (mShowForced && (flags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) {
             if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide");
+            ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
             return false;
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
 
         // There is a chance that IMM#hideSoftInput() is called in a transient state where
         // IMMS#InputShown is already updated to be true whereas IMMS#mImeWindowVis is still waiting
@@ -3461,8 +3519,8 @@
         // IMMS#InputShown indicates that the software keyboard is shown.
         // TODO: Clean up, IMMS#mInputShown, IMMS#mImeWindowVis and mShowRequested.
         IInputMethodInvoker curMethod = getCurMethodLocked();
-        final boolean shouldHideSoftInput = (curMethod != null) && (mInputShown
-                || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
+        final boolean shouldHideSoftInput = (curMethod != null)
+                && (mInputShown || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
         boolean res;
         if (shouldHideSoftInput) {
             final Binder hideInputToken = new Binder();
@@ -3471,17 +3529,20 @@
             // delivered to the IME process as an IPC.  Hence the inconsistency between
             // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in
             // the final state.
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
             if (DEBUG) {
                 Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken
                         + ", " + resultReceiver + ") for reason: "
                         + InputMethodDebug.softInputDisplayReasonToString(reason));
             }
             // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
-            if (curMethod.hideSoftInput(hideInputToken, 0 /* flags */, resultReceiver)) {
+            if (curMethod.hideSoftInput(hideInputToken, statsToken, 0 /* flags */,
+                    resultReceiver)) {
                 onShowHideSoftInputRequested(false /* show */, windowToken, reason);
             }
             res = true;
         } else {
+            ImeTracker.get().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
             res = false;
         }
         mBindingController.setCurrentMethodNotVisible();
@@ -3489,6 +3550,9 @@
         mShowRequested = false;
         mShowExplicitlyRequested = false;
         mShowForced = false;
+        // Cancel existing statsToken for show IME as we got a hide request.
+        ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+        mCurStatsToken = null;
         return res;
     }
 
@@ -3646,8 +3710,8 @@
             Slog.w(TAG, "If you need to impersonate a foreground user/profile from"
                     + " a background user, use EditorInfo.targetInputMethodUser with"
                     + " INTERACT_ACROSS_USERS_FULL permission.");
-            hideCurrentInputLocked(
-                    mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_INVALID_USER);
+            hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                    null /* resultReceiver */, SoftInputShowHideReason.HIDE_INVALID_USER);
             return InputBindResult.INVALID_USER;
         }
 
@@ -3703,7 +3767,7 @@
         boolean didStart = false;
 
         InputBindResult res = null;
-        // We shows the IME when the system allows the IME focused target window to restore the
+        // We show the IME when the system allows the IME focused target window to restore the
         // IME visibility (e.g. switching to the app task when last time the IME is visible).
         // Note that we don't restore IME visibility for some cases (e.g. when the soft input
         // state is ALWAYS_HIDDEN or STATE_HIDDEN with forward navigation).
@@ -3715,7 +3779,7 @@
             res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection,
                     editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion,
                     imeDispatcher);
-            showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+            showCurrentInputImplicitLocked(windowToken,
                     SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY);
             return res;
         }
@@ -3728,8 +3792,8 @@
                         // be behind any soft input window, so hide the
                         // soft input window if it is shown.
                         if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
-                        hideCurrentInputLocked(
-                                mCurFocusedWindow, InputMethodManager.HIDE_NOT_ALWAYS, null,
+                        hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+                                InputMethodManager.HIDE_NOT_ALWAYS, null /* resultReceiver */,
                                 SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);
 
                         // If focused display changed, we should unbind current method
@@ -3758,10 +3822,7 @@
                                 imeDispatcher);
                         didStart = true;
                     }
-                    showCurrentInputLocked(
-                            windowToken,
-                            InputMethodManager.SHOW_IMPLICIT,
-                            null,
+                    showCurrentInputImplicitLocked(windowToken,
                             SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
                 }
                 break;
@@ -3774,14 +3835,16 @@
             case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
                 if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
                     if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
-                    hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                            null /* resultReceiver */,
                             SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV);
                 }
                 break;
             case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
                 if (!sameWindowFocused) {
                     if (DEBUG) Slog.v(TAG, "Window asks to hide input");
-                    hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                            null /* resultReceiver */,
                             SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE);
                 }
                 break;
@@ -3797,7 +3860,7 @@
                                     imeDispatcher);
                             didStart = true;
                         }
-                        showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+                        showCurrentInputImplicitLocked(windowToken,
                                 SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
                     } else {
                         Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
@@ -3818,7 +3881,7 @@
                                     imeDispatcher);
                             didStart = true;
                         }
-                        showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+                        showCurrentInputImplicitLocked(windowToken,
                                 SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE);
                     }
                 } else {
@@ -3840,7 +3903,8 @@
                     // an editor upon refocusing a window.
                     if (startInputByWinGainedFocus) {
                         if (DEBUG) Slog.v(TAG, "Same window without editor will hide input");
-                        hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                        hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+                                0 /* flags */, null /* resultReceiver */,
                                 SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
                     }
                 }
@@ -3854,7 +3918,8 @@
                     // 2) SOFT_INPUT_STATE_VISIBLE state without an editor
                     // 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor
                     if (DEBUG) Slog.v(TAG, "Window without editor will hide input");
-                    hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                            null /* resultReceiver */,
                             SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR);
                 }
                 res = startInputUncheckedLocked(cs, inputContext,
@@ -3869,8 +3934,15 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private boolean canInteractWithImeLocked(
-            int uid, IInputMethodClient client, String methodName) {
+    private void showCurrentInputImplicitLocked(@NonNull IBinder windowToken,
+            @SoftInputShowHideReason int reason) {
+        showCurrentInputLocked(windowToken, null /* statsToken */, InputMethodManager.SHOW_IMPLICIT,
+                null /* resultReceiver */, reason);
+    }
+
+    @GuardedBy("ImfLock.class")
+    private boolean canInteractWithImeLocked(int uid, IInputMethodClient client, String methodName,
+            @Nullable ImeTracker.Token statsToken) {
         if (mCurClient == null || client == null
                 || mCurClient.mClient.asBinder() != client.asBinder()) {
             // We need to check if this is the current client with
@@ -3878,13 +3950,16 @@
             // be made before input is started in it.
             final ClientState cs = mClients.get(client.asBinder());
             if (cs == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
                 throw new IllegalArgumentException("unknown client " + client.asBinder());
             }
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
             if (!isImeClientFocused(mCurFocusedWindow, cs)) {
                 Slog.w(TAG, String.format("Ignoring %s of uid %d : %s", methodName, uid, client));
                 return false;
             }
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
         return true;
     }
 
@@ -4237,7 +4312,7 @@
             final int curTokenDisplayId;
             synchronized (ImfLock.class) {
                 if (!canInteractWithImeLocked(callingUid, client,
-                        "getInputMethodWindowVisibleHeight")) {
+                        "getInputMethodWindowVisibleHeight", null /* statsToken */)) {
                     if (!mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.get(callingUid)) {
                         EventLog.writeEvent(0x534e4554, "204906124", callingUid, "");
                         mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.put(callingUid, true);
@@ -4460,7 +4535,8 @@
 
         int uid = Binder.getCallingUid();
         synchronized (ImfLock.class) {
-            if (!canInteractWithImeLocked(uid, client, "addVirtualStylusIdForTestSession")) {
+            if (!canInteractWithImeLocked(uid, client, "addVirtualStylusIdForTestSession",
+                    null /* statsToken */)) {
                 return;
             }
             final long ident = Binder.clearCallingIdentity();
@@ -4487,7 +4563,8 @@
 
         int uid = Binder.getCallingUid();
         synchronized (ImfLock.class) {
-            if (!canInteractWithImeLocked(uid, client, "setStylusWindowIdleTimeoutForTest")) {
+            if (!canInteractWithImeLocked(uid, client, "setStylusWindowIdleTimeoutForTest",
+                    null /* statsToken */)) {
                 return;
             }
             final long ident = Binder.clearCallingIdentity();
@@ -4672,7 +4749,8 @@
     }
 
     @BinderThread
-    private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible) {
+    private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible,
+            @Nullable ImeTracker.Token statsToken) {
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility");
         synchronized (ImfLock.class) {
             if (!calledWithValidTokenLocked(token)) {
@@ -4680,13 +4758,22 @@
             }
             if (!setVisible) {
                 if (mCurClient != null) {
+                    ImeTracker.get().onProgress(statsToken,
+                            ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
+
                     mWindowManagerInternal.hideIme(
                             mHideRequestWindowMap.get(windowToken),
-                            mCurClient.mSelfReportedDisplayId);
+                            mCurClient.mSelfReportedDisplayId, statsToken);
+                } else {
+                    ImeTracker.get().onFailed(statsToken,
+                            ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
                 }
             } else {
+                ImeTracker.get().onProgress(statsToken,
+                        ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
                 // Send to window manager to show IME after IME layout finishes.
-                mWindowManagerInternal.showImePostLayout(mShowRequestWindowMap.get(windowToken));
+                mWindowManagerInternal.showImePostLayout(mShowRequestWindowMap.get(windowToken),
+                        statsToken);
             }
         }
         Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -4753,7 +4840,8 @@
             }
             final long ident = Binder.clearCallingIdentity();
             try {
-                hideCurrentInputLocked(mLastImeTargetWindow, flags, null, reason);
+                hideCurrentInputLocked(mLastImeTargetWindow, null /* statsToken */, flags,
+                        null /* resultReceiver */, reason);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -4770,7 +4858,8 @@
             }
             final long ident = Binder.clearCallingIdentity();
             try {
-                showCurrentInputLocked(mLastImeTargetWindow, flags, null,
+                showCurrentInputLocked(mLastImeTargetWindow, null /* statsToken */, flags,
+                        null /* resultReceiver */,
                         SoftInputShowHideReason.SHOW_SOFT_INPUT_FROM_IME);
             } finally {
                 Binder.restoreCallingIdentity(ident);
@@ -4861,7 +4950,8 @@
             case MSG_HIDE_CURRENT_INPUT_METHOD:
                 synchronized (ImfLock.class) {
                     final @SoftInputShowHideReason int reason = (int) msg.obj;
-                    hideCurrentInputLocked(mCurFocusedWindow, 0, null, reason);
+                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                            null /* resultReceiver */, reason);
 
                 }
                 return true;
@@ -6349,7 +6439,8 @@
                     final String nextIme;
                     final List<InputMethodInfo> nextEnabledImes;
                     if (userId == mSettings.getCurrentUserId()) {
-                        hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                        hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+                                0 /* flags */, null /* resultReceiver */,
                                 SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND);
                         mBindingController.unbindCurrentMethod();
                         // Reset the current IME
@@ -6614,8 +6705,9 @@
 
         @BinderThread
         @Override
-        public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible) {
-            mImms.applyImeVisibility(mToken, windowToken, setVisible);
+        public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible,
+                @Nullable ImeTracker.Token statsToken) {
+            mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken);
         }
 
         @BinderThread
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index bf00a33..5b8ee2b 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -125,6 +125,14 @@
     ActivityInfo getActivityInfo(ComponentName component, long flags, int userId);
 
     /**
+     * Similar to {@link Computer#getActivityInfo(android.content.ComponentName, long, int)} but
+     * only visible as internal service. This method bypass INTERACT_ACROSS_USERS or
+     * INTERACT_ACROSS_USERS_FULL permission checks and only to be used for intent resolution across
+     * chained cross profiles
+     */
+    ActivityInfo getActivityInfoCrossProfile(ComponentName component, long flags, int userId);
+
+    /**
      * Important: The provided filterCallingUid is used exclusively to filter out activities
      * that can be seen based on user state. It's typically the original caller uid prior
      * to clearing. Because it can only be provided by trusted code, its value can be
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index b285136..a8534b0 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -835,6 +835,24 @@
     }
 
     /**
+     * Similar to {@link Computer#getActivityInfo(android.content.ComponentName, long, int)} but
+     * only visible as internal service. This method bypass INTERACT_ACROSS_USERS or
+     * INTERACT_ACROSS_USERS_FULL permission checks and only to be used for intent resolution across
+     * chained cross profiles
+     * @param component application's component
+     * @param flags resolve info flags
+     * @param userId user id where activity resides
+     * @return ActivityInfo corresponding to requested component.
+     */
+    public final ActivityInfo getActivityInfoCrossProfile(ComponentName component,
+            @PackageManager.ResolveInfoFlagsBits long flags, int userId) {
+        if (!mUserManager.exists(userId)) return null;
+        flags = updateFlagsForComponent(flags, userId);
+
+        return getActivityInfoInternalBody(component, flags, Binder.getCallingUid(), userId);
+    }
+
+    /**
      * Important: The provided filterCallingUid is used exclusively to filter out activities
      * that can be seen based on user state. It's typically the original caller uid prior
      * to clearing. Because it can only be provided by trusted code, its value can be
@@ -1711,7 +1729,7 @@
         ComponentName forwardingActivityComponentName = new ComponentName(
                 androidApplication().packageName, className);
         ActivityInfo forwardingActivityInfo =
-                getActivityInfo(forwardingActivityComponentName, 0,
+                getActivityInfoCrossProfile(forwardingActivityComponentName, 0,
                         sourceUserId);
         if (!targetIsProfile) {
             forwardingActivityInfo.showUserIcon = targetUserId;
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index b6a58ab..b8950e1 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -131,7 +131,8 @@
                         .setStartWithParent(true)
                         .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT)
                         .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_WITH_PARENT)
-                        .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT));
+                        .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT)
+                        .setUseParentsContacts(true));
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index eba3375..f6dce3b 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6908,11 +6908,8 @@
      *         {@link android.view.Display#INVALID_DISPLAY} if not attached.
      */
     int getDisplayId() {
-        final Task rootTask = getRootTask();
-        if (rootTask == null) {
-            return INVALID_DISPLAY;
-        }
-        return rootTask.getDisplayId();
+        return task != null && task.mDisplayContent != null
+                 ? task.mDisplayContent.mDisplayId : INVALID_DISPLAY;
     }
 
     final boolean isDestroyable() {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 3c457e1..2f70eda 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -27,6 +27,7 @@
 import static android.app.ActivityManager.START_FLAG_NATIVE_DEBUGGING;
 import static android.app.ActivityManager.START_FLAG_TRACK_ALLOCATION;
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityOptions.ANIM_REMOTE_ANIMATION;
 import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY;
 import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
 import static android.app.WaitResult.INVALID_DELAY;
@@ -2577,6 +2578,10 @@
                         // Apply options to prevent pendingOptions be taken when scheduling
                         // activity lifecycle transaction to make sure the override pending app
                         // transition will be applied immediately.
+                        if (activityOptions.getAnimationType() == ANIM_REMOTE_ANIMATION) {
+                            targetActivity.mPendingRemoteAnimation =
+                                    activityOptions.getRemoteAnimationAdapter();
+                        }
                         targetActivity.applyOptionsAnimation();
                         if (activityOptions != null && activityOptions.getLaunchCookie() != null) {
                             targetActivity.mLaunchCookie = activityOptions.getLaunchCookie();
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index b84b2d8..bedeabe 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -369,6 +369,55 @@
     }
 
     @Override
+    ActivityRecord getActivity(Predicate<ActivityRecord> callback, boolean traverseTopToBottom,
+            ActivityRecord boundary) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return null;
+        }
+        return super.getActivity(callback, traverseTopToBottom, boundary);
+    }
+
+    @Override
+    Task getTask(Predicate<Task> callback, boolean traverseTopToBottom) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return null;
+        }
+        return super.getTask(callback, traverseTopToBottom);
+    }
+
+    @Override
+    boolean forAllActivities(Predicate<ActivityRecord> callback, boolean traverseTopToBottom) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return false;
+        }
+        return super.forAllActivities(callback, traverseTopToBottom);
+    }
+
+    @Override
+    boolean forAllRootTasks(Predicate<Task> callback, boolean traverseTopToBottom) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return false;
+        }
+        return super.forAllRootTasks(callback, traverseTopToBottom);
+    }
+
+    @Override
+    boolean forAllTasks(Predicate<Task> callback) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return false;
+        }
+        return super.forAllTasks(callback);
+    }
+
+    @Override
+    boolean forAllLeafTasks(Predicate<Task> callback) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return false;
+        }
+        return super.forAllLeafTasks(callback);
+    }
+
+    @Override
     void forAllDisplayAreas(Consumer<DisplayArea> callback) {
         super.forAllDisplayAreas(callback);
         callback.accept(this);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 8b34443..12efe0d 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -230,6 +230,7 @@
 import android.view.WindowManager;
 import android.view.WindowManager.DisplayImePolicy;
 import android.view.WindowManagerPolicyConstants.PointerEventListener;
+import android.view.inputmethod.ImeTracker;
 import android.window.DisplayWindowPolicyController;
 import android.window.IDisplayAreaOrganizer;
 import android.window.ScreenCapture;
@@ -454,7 +455,7 @@
 
     /**
      * Compat metrics computed based on {@link #mDisplayMetrics}.
-     * @see #updateDisplayAndOrientation(int, Configuration)
+     * @see #updateDisplayAndOrientation(Configuration)
      */
     private final DisplayMetrics mCompatDisplayMetrics = new DisplayMetrics();
 
@@ -5031,7 +5032,7 @@
      *   layer has been assigned since), to facilitate assigning the layer from the IME target, or
      *   fall back if there is no target.
      * - the container doesn't always participate in window traversal, according to
-     *   {@link #skipImeWindowsDuringTraversal()}
+     *   {@link #skipImeWindowsDuringTraversal(DisplayContent)}
      */
     private static class ImeContainer extends DisplayArea.Tokens {
         boolean mNeedsLayer = false;
@@ -6712,25 +6713,35 @@
                 mRemoteInsetsController.insetsControlChanged(stateController.getRawInsetsState(),
                         stateController.getControlsForDispatch(this));
             } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to deliver inset state change", e);
+                Slog.w(TAG, "Failed to deliver inset control state change", e);
             }
         }
 
         @Override
-        public void showInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme) {
+        public void showInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             try {
-                mRemoteInsetsController.showInsets(types, fromIme);
+                ImeTracker.get().onProgress(statsToken,
+                        ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS);
+                mRemoteInsetsController.showInsets(types, fromIme, statsToken);
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to deliver showInsets", e);
+                ImeTracker.get().onFailed(statsToken,
+                        ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS);
             }
         }
 
         @Override
-        public void hideInsets(@InsetsType int types, boolean fromIme) {
+        public void hideInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             try {
-                mRemoteInsetsController.hideInsets(types, fromIme);
+                ImeTracker.get().onProgress(statsToken,
+                        ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS);
+                mRemoteInsetsController.hideInsets(types, fromIme, statsToken);
             } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to deliver showInsets", e);
+                Slog.w(TAG, "Failed to deliver hideInsets", e);
+                ImeTracker.get().onFailed(statsToken,
+                        ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS);
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 2589148..1fef3c2 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2071,7 +2071,8 @@
             // Don't show status bar when swiping on already visible navigation bar.
             // But restore the position of navigation bar if it has been moved by the control
             // target.
-            controlTarget.showInsets(Type.navigationBars(), false);
+            controlTarget.showInsets(Type.navigationBars(), false /* fromIme */,
+                    null /* statsToken */);
             return;
         }
 
@@ -2079,10 +2080,12 @@
             // Show transient bars if they are hidden; restore position if they are visible.
             mDisplayContent.getInsetsPolicy().showTransient(SHOW_TYPES_FOR_SWIPE,
                     isGestureOnSystemBar);
-            controlTarget.showInsets(restorePositionTypes, false);
+            controlTarget.showInsets(restorePositionTypes, false /* fromIme */,
+                    null /* statsToken */);
         } else {
             // Restore visibilities and positions of system bars.
-            controlTarget.showInsets(Type.statusBars() | Type.navigationBars(), false);
+            controlTarget.showInsets(Type.statusBars() | Type.navigationBars(),
+                    false /* fromIme */, null /* statsToken */);
             // To further allow the pull-down-from-the-top gesture to pull down the notification
             // shade as a consistent motion, we reroute the touch events here from the currently
             // touched window to the status bar after making it visible.
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 554791a..7fd093f 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -33,9 +33,11 @@
 import android.os.Trace;
 import android.util.proto.ProtoOutputStream;
 import android.view.InsetsSource;
+import android.view.InsetsSourceConsumer;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.WindowInsets;
+import android.view.inputmethod.ImeTracker;
 import android.window.TaskSnapshot;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -49,6 +51,9 @@
  */
 final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider {
 
+    /** The token tracking the current IME request or {@code null} otherwise. */
+    @Nullable
+    private ImeTracker.Token mImeRequesterStatsToken;
     private InsetsControlTarget mImeRequester;
     private Runnable mShowImeRunner;
     private boolean mIsImeLayoutDrawn;
@@ -162,14 +167,20 @@
     }
 
     /**
-     * Called from {@link WindowManagerInternal#showImePostLayout} when {@link InputMethodService}
-     * requests to show IME on {@param imeTarget}.
+     * Called from {@link WindowManagerInternal#showImePostLayout}
+     * when {@link android.inputmethodservice.InputMethodService} requests to show IME
+     * on {@param imeTarget}.
      *
-     * @param imeTarget imeTarget on which IME request is coming from.
+     * @param imeTarget imeTarget on which IME show request is coming from.
+     * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
      */
-    void scheduleShowImePostLayout(InsetsControlTarget imeTarget) {
+    void scheduleShowImePostLayout(InsetsControlTarget imeTarget,
+            @Nullable ImeTracker.Token statsToken) {
         boolean targetChanged = isTargetChangedWithinActivity(imeTarget);
         mImeRequester = imeTarget;
+        // There was still a stats token, so that request presumably failed.
+        ImeTracker.get().onFailed(mImeRequesterStatsToken, ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
+        mImeRequesterStatsToken = statsToken;
         if (targetChanged) {
             // target changed, check if new target can show IME.
             ProtoLog.d(WM_DEBUG_IME, "IME target changed within ActivityRecord");
@@ -183,15 +194,20 @@
         ProtoLog.d(WM_DEBUG_IME, "Schedule IME show for %s", mImeRequester.getWindow() == null
                 ? mImeRequester : mImeRequester.getWindow().getName());
         mShowImeRunner = () -> {
+            ImeTracker.get().onProgress(mImeRequesterStatsToken,
+                    ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
             ProtoLog.d(WM_DEBUG_IME, "Run showImeRunner");
             // Target should still be the same.
             if (isReadyToShowIme()) {
+                ImeTracker.get().onProgress(mImeRequesterStatsToken,
+                        ImeTracker.PHASE_WM_SHOW_IME_READY);
                 final InsetsControlTarget target = mDisplayContent.getImeTarget(IME_TARGET_CONTROL);
 
                 ProtoLog.i(WM_DEBUG_IME, "call showInsets(ime) on %s",
                         target.getWindow() != null ? target.getWindow().getName() : "");
                 setImeShowing(true);
-                target.showInsets(WindowInsets.Type.ime(), true /* fromIme */);
+                target.showInsets(WindowInsets.Type.ime(), true /* fromIme */,
+                        mImeRequesterStatsToken);
                 Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, "WMS.showImePostLayout", 0);
                 if (target != mImeRequester && mImeRequester != null) {
                     ProtoLog.w(WM_DEBUG_IME,
@@ -199,7 +215,12 @@
                             (mImeRequester.getWindow() != null
                                     ? mImeRequester.getWindow().getName() : ""));
                 }
+            } else {
+                ImeTracker.get().onFailed(mImeRequesterStatsToken,
+                        ImeTracker.PHASE_WM_SHOW_IME_READY);
             }
+            // Clear token here so we don't report an error in abortShowImePostLayout().
+            mImeRequesterStatsToken = null;
             abortShowImePostLayout();
         };
         mDisplayContent.mWmService.requestTraversal();
@@ -234,6 +255,8 @@
         mImeRequester = null;
         mIsImeLayoutDrawn = false;
         mShowImeRunner = null;
+        ImeTracker.get().onCancelled(mImeRequesterStatsToken, ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
+        mImeRequesterStatsToken = null;
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java
index d35b7c3..8ecbc17 100644
--- a/services/core/java/com/android/server/wm/InsetsControlTarget.java
+++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java
@@ -16,9 +16,11 @@
 
 package com.android.server.wm;
 
+import android.annotation.Nullable;
 import android.inputmethodservice.InputMethodService;
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
 
 /**
  * Generalization of an object that can control insets state.
@@ -57,8 +59,10 @@
      *
      * @param types to specify which types of insets source window should be shown.
      * @param fromIme {@code true} if IME show request originated from {@link InputMethodService}.
+     * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
      */
-    default void showInsets(@InsetsType int types, boolean fromIme) {
+    default void showInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
     }
 
     /**
@@ -66,8 +70,10 @@
      *
      * @param types to specify which types of insets source window should be hidden.
      * @param fromIme {@code true} if IME hide request originated from {@link InputMethodService}.
+     * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
      */
-    default void hideInsets(@InsetsType int types, boolean fromIme) {
+    default void hideInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index b9fa80c..f66fa0f 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -799,7 +799,7 @@
                         show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE, show
                                 ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN
                                 : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
-                        null /* translator */);
+                        null /* translator */, null /* statsToken */);
                 SurfaceAnimationThread.getHandler().post(
                         () -> mListener.onReady(mAnimationControl, typesReady));
             }
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index c206a15..bab3a05 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -43,6 +43,7 @@
 import android.view.SurfaceControlViewHost;
 import android.view.WindowInfo;
 import android.view.WindowManager.DisplayImePolicy;
+import android.view.inputmethod.ImeTracker;
 
 import com.android.internal.policy.KeyInterceptionInfo;
 import com.android.server.input.InputManagerService;
@@ -729,16 +730,20 @@
      * Show IME on imeTargetWindow once IME has finished layout.
      *
      * @param imeTargetWindowToken token of the (IME target) window on which IME should be shown.
+     * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
      */
-    public abstract void showImePostLayout(IBinder imeTargetWindowToken);
+    public abstract void showImePostLayout(IBinder imeTargetWindowToken,
+            @Nullable ImeTracker.Token statsToken);
 
     /**
      * Hide IME using imeTargetWindow when requested.
      *
      * @param imeTargetWindowToken token of the (IME target) window on which IME should be hidden.
      * @param displayId the id of the display the IME is on.
+     * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
      */
-    public abstract void hideIme(IBinder imeTargetWindowToken, int displayId);
+    public abstract void hideIme(IBinder imeTargetWindowToken, int displayId,
+            @Nullable ImeTracker.Token statsToken);
 
     /**
      * Tell window manager about a package that should be running with a restricted range of
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4080223..3419207 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -292,6 +292,7 @@
 import android.view.WindowManagerPolicyConstants.PointerEventListener;
 import android.view.displayhash.DisplayHash;
 import android.view.displayhash.VerifiedDisplayHash;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 import android.window.ITaskFpsCallback;
 import android.window.ScreenCapture;
@@ -8014,7 +8015,8 @@
         }
 
         @Override
-        public void showImePostLayout(IBinder imeTargetWindowToken) {
+        public void showImePostLayout(IBinder imeTargetWindowToken,
+                @Nullable ImeTracker.Token statsToken) {
             synchronized (mGlobalLock) {
                 InputTarget imeTarget = getInputTargetFromWindowTokenLocked(imeTargetWindowToken);
                 if (imeTarget == null) {
@@ -8023,17 +8025,18 @@
                 Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.showImePostLayout", 0);
                 final InsetsControlTarget controlTarget = imeTarget.getImeControlTarget();
                 imeTarget = controlTarget.getWindow();
-                // If InsetsControlTarget doesn't have a window, its using remoteControlTarget which
-                // is controlled by default display
+                // If InsetsControlTarget doesn't have a window, it's using remoteControlTarget
+                // which is controlled by default display
                 final DisplayContent dc = imeTarget != null
                         ? imeTarget.getDisplayContent() : getDefaultDisplayContentLocked();
                 dc.getInsetsStateController().getImeSourceProvider()
-                        .scheduleShowImePostLayout(controlTarget);
+                        .scheduleShowImePostLayout(controlTarget, statsToken);
             }
         }
 
         @Override
-        public void hideIme(IBinder imeTargetWindowToken, int displayId) {
+        public void hideIme(IBinder imeTargetWindowToken, int displayId,
+                @Nullable ImeTracker.Token statsToken) {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.hideIme");
             synchronized (mGlobalLock) {
                 WindowState imeTarget = mWindowMap.get(imeTargetWindowToken);
@@ -8049,10 +8052,15 @@
                     dc.getInsetsStateController().getImeSourceProvider().abortShowImePostLayout();
                 }
                 if (dc != null && dc.getImeTarget(IME_TARGET_CONTROL) != null) {
+                    ImeTracker.get().onProgress(statsToken,
+                            ImeTracker.PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET);
                     ProtoLog.d(WM_DEBUG_IME, "hideIme Control target: %s ",
                             dc.getImeTarget(IME_TARGET_CONTROL));
-                    dc.getImeTarget(IME_TARGET_CONTROL).hideInsets(
-                            WindowInsets.Type.ime(), true /* fromIme */);
+                    dc.getImeTarget(IME_TARGET_CONTROL).hideInsets(WindowInsets.Type.ime(),
+                            true /* fromIme */, statsToken);
+                } else {
+                    ImeTracker.get().onFailed(statsToken,
+                            ImeTracker.PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET);
                 }
                 if (dc != null) {
                     dc.getInsetsStateController().getImeSourceProvider().setImeShowing(false);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 2712cb7..5c5c703 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -249,6 +249,7 @@
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 import android.window.OnBackInvokedCallbackInfo;
 
@@ -4018,7 +4019,7 @@
             mClient.insetsControlChanged(getCompatInsetsState(),
                     stateController.getControlsForDispatch(this));
         } catch (RemoteException e) {
-            Slog.w(TAG, "Failed to deliver inset state change to w=" + this, e);
+            Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e);
         }
     }
 
@@ -4028,20 +4029,30 @@
     }
 
     @Override
-    public void showInsets(@InsetsType int types, boolean fromIme) {
+    public void showInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
         try {
-            mClient.showInsets(types, fromIme);
+            ImeTracker.get().onProgress(statsToken,
+                    ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS);
+            mClient.showInsets(types, fromIme, statsToken);
         } catch (RemoteException e) {
             Slog.w(TAG, "Failed to deliver showInsets", e);
+            ImeTracker.get().onFailed(statsToken,
+                    ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS);
         }
     }
 
     @Override
-    public void hideInsets(@InsetsType int types, boolean fromIme) {
+    public void hideInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
         try {
-            mClient.hideInsets(types, fromIme);
+            ImeTracker.get().onProgress(statsToken,
+                    ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS);
+            mClient.hideInsets(types, fromIme, statsToken);
         } catch (RemoteException e) {
-            Slog.w(TAG, "Failed to deliver showInsets", e);
+            Slog.w(TAG, "Failed to deliver hideInsets", e);
+            ImeTracker.get().onFailed(statsToken,
+                    ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS);
         }
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 316c736..89cbf53 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -646,6 +646,15 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
     private static final long USE_SET_LOCATION_ENABLED = 117835097L;
 
+    /**
+     * Forces wipeDataNoLock to attempt removing the user or throw an error as
+     * opposed to trying to factory reset the device first and only then falling back to user
+     * removal.
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long EXPLICIT_WIPE_BEHAVIOUR = 242193913L;
+
     // Only add to the end of the list. Do not change or rearrange these values, that will break
     // historical data. Do not use negative numbers or zero, logger only handles positive
     // integers.
@@ -6699,8 +6708,8 @@
     }
 
     @Override
-    public void wipeDataWithReason(int flags, String wipeReasonForUser,
-            boolean calledOnParentInstance) {
+    public void wipeDataWithReason(int flags, @NonNull String wipeReasonForUser,
+            boolean calledOnParentInstance, boolean factoryReset) {
         if (!mHasFeature && !hasCallingOrSelfPermission(permission.MASTER_CLEAR)) {
             return;
         }
@@ -6782,7 +6791,8 @@
                 "DevicePolicyManager.wipeDataWithReason() from %s, organization-owned? %s",
                 adminName, calledByProfileOwnerOnOrgOwnedDevice);
 
-        wipeDataNoLock(adminComp, flags, internalReason, wipeReasonForUser, userId);
+        wipeDataNoLock(adminComp, flags, internalReason, wipeReasonForUser, userId,
+                calledOnParentInstance, factoryReset);
     }
 
     private String getGenericWipeReason(
@@ -6844,8 +6854,13 @@
         Slogf.i(LOG_TAG, "Cleaning up device-wide policies done.");
     }
 
+    /**
+     * @param factoryReset null: legacy behaviour, false: attempt to remove user, true: attempt to
+     *                     factory reset
+     */
     private void wipeDataNoLock(ComponentName admin, int flags, String internalReason,
-                                String wipeReasonForUser, int userId) {
+            @NonNull String wipeReasonForUser, int userId, boolean calledOnParentInstance,
+            @Nullable Boolean factoryReset) {
         wtfIfInLock();
 
         mInjector.binderWithCleanCallingIdentity(() -> {
@@ -6863,7 +6878,37 @@
                         + " restriction is set for user " + userId);
             }
 
-            if (userId == UserHandle.USER_SYSTEM) {
+            boolean isSystemUser = userId == UserHandle.USER_SYSTEM;
+            boolean wipeDevice;
+            if (factoryReset == null || !CompatChanges.isChangeEnabled(EXPLICIT_WIPE_BEHAVIOUR)) {
+                // Legacy mode
+                wipeDevice = isSystemUser;
+            } else {
+                // Explicit behaviour
+                if (factoryReset) {
+                    // TODO(b/254031494) Replace with new factory reset permission checks
+                    boolean hasPermission = isDeviceOwnerUserId(userId)
+                            || (isOrganizationOwnedDeviceWithManagedProfile()
+                            && calledOnParentInstance);
+                    Preconditions.checkState(hasPermission,
+                            "Admin %s does not have permission to factory reset the device.",
+                            userId);
+                    wipeDevice = true;
+                } else {
+                    Preconditions.checkCallAuthorization(!isSystemUser,
+                            "User %s is a system user and cannot be removed", userId);
+                    boolean isLastNonHeadlessUser = getUserInfo(userId).isFull()
+                            && mUserManager.getAliveUsers().stream()
+                            .filter((it) -> it.getUserHandle().getIdentifier() != userId)
+                            .noneMatch(UserInfo::isFull);
+                    Preconditions.checkState(!isLastNonHeadlessUser,
+                            "Removing user %s would leave the device without any active users. "
+                                    + "Consider factory resetting the device instead.",
+                            userId);
+                    wipeDevice = false;
+                }
+            }
+            if (wipeDevice) {
                 forceWipeDeviceNoLock(
                         (flags & WIPE_EXTERNAL_STORAGE) != 0,
                         internalReason,
@@ -7131,7 +7176,7 @@
     }
 
     @Override
-    public void reportFailedPasswordAttempt(int userHandle) {
+    public void reportFailedPasswordAttempt(int userHandle, boolean parent) {
         Preconditions.checkArgumentNonnegative(userHandle, "Invalid userId");
 
         final CallerIdentity caller = getCallerIdentity();
@@ -7153,7 +7198,7 @@
                 saveSettingsLocked(userHandle);
                 if (mHasFeature) {
                     strictestAdmin = getAdminWithMinimumFailedPasswordsForWipeLocked(
-                            userHandle, /* parent */ false);
+                            userHandle, /* parent= */ false);
                     int max = strictestAdmin != null
                             ? strictestAdmin.maximumFailedPasswordsForWipe : 0;
                     if (max > 0 && policy.mFailedPasswordAttempts >= max) {
@@ -7183,10 +7228,13 @@
             // IMPORTANT: Call without holding the lock to prevent deadlock.
             try {
                 wipeDataNoLock(strictestAdmin.info.getComponent(),
-                        /*flags=*/ 0,
-                        /*reason=*/ "reportFailedPasswordAttempt()",
+                        /* flags= */ 0,
+                        /* reason= */ "reportFailedPasswordAttempt()",
                         getFailedPasswordAttemptWipeMessage(),
-                        userId);
+                        userId,
+                        /* calledOnParentInstance= */ parent,
+                        // factoryReset=null to enable U- behaviour
+                        /* factoryReset= */ null);
             } catch (SecurityException e) {
                 Slogf.w(LOG_TAG, "Failed to wipe user " + userId
                         + " after max failed password attempts reached.", e);
@@ -7195,7 +7243,7 @@
 
         if (mInjector.securityLogIsLoggingEnabled()) {
             SecurityLog.writeEvent(SecurityLog.TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT,
-                    /*result*/ 0, /*method strength*/ 1);
+                    /* result= */ 0, /* method strength= */ 1);
         }
     }
 
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index 3fbc400..640bde3 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -244,7 +244,7 @@
                     .setCurrentMethodVisible();
         }
         verify(mMockInputMethod, times(showSoftInput ? 1 : 0))
-                .showSoftInput(any(), anyInt(), any());
+                .showSoftInput(any(), any(), anyInt(), any());
     }
 
     protected void verifyHideSoftInput(boolean setNotVisible, boolean hideSoftInput)
@@ -254,6 +254,6 @@
                     .setCurrentMethodNotVisible();
         }
         verify(mMockInputMethod, times(hideSoftInput ? 1 : 0))
-                .hideSoftInput(any(), anyInt(), any());
+                .hideSoftInput(any(), any(), anyInt(), any());
     }
 }
diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
index b27f49d..1a6dae37 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_profile.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
@@ -33,6 +33,7 @@
         <user-properties
             showInLauncher='2020'
             startWithParent='false'
+            useParentsContacts='false'
         />
     </profile-type>
     <profile-type name='custom.test.1' max-allowed-per-parent='14' />
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index ddb3049..8e669f0 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -140,7 +140,7 @@
 import android.security.keystore.AttestationUtils;
 import android.telephony.TelephonyManager;
 import android.telephony.data.ApnSetting;
-import android.test.MoreAsserts; // TODO(b/171932723): replace by Truth
+import android.test.MoreAsserts;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
@@ -5087,7 +5087,7 @@
     }
 
     @Test
-    public void testWipeDataDeviceOwner() throws Exception {
+    public void testWipeDevice_DeviceOwner() throws Exception {
         setDeviceOwner();
         when(getServices().userManager.getUserRestrictionSource(
                 UserManager.DISALLOW_FACTORY_RESET,
@@ -5096,7 +5096,7 @@
         when(mContext.getResources().getString(R.string.work_profile_deleted_description_dpm_wipe)).
                 thenReturn("Just a test string.");
 
-        dpm.wipeData(0);
+        dpm.wipeDevice(0);
 
         verifyRebootWipeUserData(/* wipeEuicc= */ false);
     }
@@ -5111,13 +5111,13 @@
         when(mContext.getResources().getString(R.string.work_profile_deleted_description_dpm_wipe)).
                 thenReturn("Just a test string.");
 
-        dpm.wipeData(WIPE_EUICC);
+        dpm.wipeDevice(WIPE_EUICC);
 
         verifyRebootWipeUserData(/* wipeEuicc= */ true);
     }
 
     @Test
-    public void testWipeDataDeviceOwnerDisallowed() throws Exception {
+    public void testWipeDevice_DeviceOwnerDisallowed() throws Exception {
         setDeviceOwner();
         when(getServices().userManager.getUserRestrictionSource(
                 UserManager.DISALLOW_FACTORY_RESET,
@@ -5128,7 +5128,7 @@
         // The DO is not allowed to wipe the device if the user restriction was set
         // by the system
         assertExpectException(SecurityException.class, /* messageRegex= */ null,
-                () -> dpm.wipeData(0));
+                () -> dpm.wipeDevice(0));
     }
 
     @Test
@@ -7986,7 +7986,7 @@
     }
 
     @Test
-    public void testWipeData_financeDo_success() throws Exception {
+    public void testWipeDevice_financeDo_success() throws Exception {
         setDeviceOwner();
         dpm.setDeviceOwnerType(admin1, DEVICE_OWNER_TYPE_FINANCED);
         when(getServices().userManager.getUserRestrictionSource(
@@ -7997,7 +7997,7 @@
                 .getString(R.string.work_profile_deleted_description_dpm_wipe))
                 .thenReturn("Test string");
 
-        dpm.wipeData(0);
+        dpm.wipeDevice(0);
 
         verifyRebootWipeUserData(/* wipeEuicc= */ false);
     }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 5ece871..1f952c4 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -61,11 +61,13 @@
                 .setStartWithParent(false)
                 .setShowInSettings(45)
                 .setInheritDevicePolicy(67)
+                .setUseParentsContacts(false)
                 .build();
         final UserProperties actualProps = new UserProperties(defaultProps);
         actualProps.setShowInLauncher(14);
         actualProps.setShowInSettings(32);
         actualProps.setInheritDevicePolicy(51);
+        actualProps.setUseParentsContacts(true);
 
         // Write the properties to xml.
         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -152,11 +154,14 @@
         // Items requiring hasManagePermission - put them here using hasManagePermission.
         assertEqualGetterOrThrows(orig::getShowInSettings, copy::getShowInSettings,
                 hasManagePermission);
+        assertEqualGetterOrThrows(orig::getUseParentsContacts,
+                copy::getUseParentsContacts, hasManagePermission);
 
         // Items requiring hasQueryPermission - put them here using hasQueryPermission.
 
         // Items with no permission requirements.
         assertEqualGetterOrThrows(orig::getShowInLauncher, copy::getShowInLauncher, true);
+
     }
 
     /**
@@ -196,7 +201,7 @@
         assertThat(expected.getShowInLauncher()).isEqualTo(actual.getShowInLauncher());
         assertThat(expected.getStartWithParent()).isEqualTo(actual.getStartWithParent());
         assertThat(expected.getShowInSettings()).isEqualTo(actual.getShowInSettings());
-        assertThat(expected.getInheritDevicePolicy()).isEqualTo(
-                actual.getInheritDevicePolicy());
+        assertThat(expected.getInheritDevicePolicy()).isEqualTo(actual.getInheritDevicePolicy());
+        assertThat(expected.getUseParentsContacts()).isEqualTo(actual.getUseParentsContacts());
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 5f48004..6fe33ee 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -83,7 +83,8 @@
                 /* flags= */0,
                 /* letsPersonalDataIntoProfile= */false).build());
         final UserProperties.Builder userProps = new UserProperties.Builder()
-                .setShowInLauncher(17);
+                .setShowInLauncher(17)
+                .setUseParentsContacts(true);
         final UserTypeDetails type = new UserTypeDetails.Builder()
                 .setName("a.name")
                 .setEnabled(1)
@@ -140,6 +141,7 @@
         }
 
         assertEquals(17, type.getDefaultUserPropertiesReference().getShowInLauncher());
+        assertTrue(type.getDefaultUserPropertiesReference().getUseParentsContacts());
 
         assertEquals(23, type.getBadgeLabel(0));
         assertEquals(24, type.getBadgeLabel(1));
@@ -182,6 +184,7 @@
         final UserProperties props = type.getDefaultUserPropertiesReference();
         assertNotNull(props);
         assertFalse(props.getStartWithParent());
+        assertFalse(props.getUseParentsContacts());
         assertEquals(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT, props.getShowInLauncher());
 
         assertFalse(type.hasBadge());
@@ -263,7 +266,8 @@
         final Bundle restrictions = makeRestrictionsBundle("no_config_vpn", "no_config_tethering");
         final UserProperties.Builder props = new UserProperties.Builder()
                 .setShowInLauncher(19)
-                .setStartWithParent(true);
+                .setStartWithParent(true)
+                .setUseParentsContacts(true);
         final ArrayMap<String, UserTypeDetails.Builder> builders = new ArrayMap<>();
         builders.put(userTypeAosp1, new UserTypeDetails.Builder()
                 .setName(userTypeAosp1)
@@ -289,7 +293,9 @@
         assertEquals(Resources.ID_NULL, aospType.getIconBadge());
         assertTrue(UserRestrictionsUtils.areEqual(restrictions, aospType.getDefaultRestrictions()));
         assertEquals(19, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
-        assertEquals(true, aospType.getDefaultUserPropertiesReference().getStartWithParent());
+        assertTrue(aospType.getDefaultUserPropertiesReference().getStartWithParent());
+        assertTrue(aospType.getDefaultUserPropertiesReference()
+                .getUseParentsContacts());
 
         // userTypeAosp2 should be modified.
         aospType = builders.get(userTypeAosp2).createUserTypeDetails();
@@ -319,7 +325,9 @@
                 makeRestrictionsBundle("no_remove_user", "no_bluetooth"),
                 aospType.getDefaultRestrictions()));
         assertEquals(2020, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
-        assertEquals(false, aospType.getDefaultUserPropertiesReference().getStartWithParent());
+        assertFalse(aospType.getDefaultUserPropertiesReference().getStartWithParent());
+        assertFalse(aospType.getDefaultUserPropertiesReference()
+                .getUseParentsContacts());
 
         // userTypeOem1 should be created.
         UserTypeDetails.Builder customType = builders.get(userTypeOem1);
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index a3c45b7..2e7e583 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 import static org.testng.Assert.assertThrows;
@@ -164,6 +165,14 @@
 
     @Test
     public void testCloneUser() throws Exception {
+
+        // Get the default properties for clone user type.
+        final UserTypeDetails userTypeDetails =
+                UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_PROFILE_CLONE);
+        assertWithMessage("No %s type on device", UserManager.USER_TYPE_PROFILE_CLONE)
+                .that(userTypeDetails).isNotNull();
+        final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference();
+
         // Test that only one clone user can be created
         final int primaryUserId = mUserManager.getPrimaryUser().id;
         UserInfo userInfo = createProfileForUser("Clone user1",
@@ -187,6 +196,16 @@
                 .collect(Collectors.toList());
         assertThat(cloneUsers.size()).isEqualTo(1);
 
+        // Check that the new clone user has the expected properties (relative to the defaults)
+        // provided that the test caller has the necessary permissions.
+        UserProperties cloneUserProperties =
+                mUserManager.getUserProperties(UserHandle.of(userInfo.id));
+        assertThat(typeProps.getUseParentsContacts())
+                .isEqualTo(cloneUserProperties.getUseParentsContacts());
+        assertThat(typeProps.getShowInLauncher())
+                .isEqualTo(cloneUserProperties.getShowInLauncher());
+        assertThrows(SecurityException.class, cloneUserProperties::getStartWithParent);
+
         // Verify clone user parent
         assertThat(mUserManager.getProfileParent(primaryUserId)).isNull();
         UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id);
@@ -600,6 +619,7 @@
         // provided that the test caller has the necessary permissions.
         assertThat(userProps.getShowInLauncher()).isEqualTo(typeProps.getShowInLauncher());
         assertThat(userProps.getShowInSettings()).isEqualTo(typeProps.getShowInSettings());
+        assertFalse(userProps.getUseParentsContacts());
         assertThrows(SecurityException.class, userProps::getStartWithParent);
         assertThrows(SecurityException.class, userProps::getInheritDevicePolicy);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index 52af8ad..d99946f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -45,6 +45,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
@@ -375,10 +376,10 @@
 
         displayPolicy.setCanSystemBarsBeShownByUser(false);
         displayPolicy.requestTransientBars(windowState, true);
-        verify(controlTarget, never()).showInsets(anyInt(), anyBoolean());
+        verify(controlTarget, never()).showInsets(anyInt(), anyBoolean(), any() /* statsToken */);
 
         displayPolicy.setCanSystemBarsBeShownByUser(true);
         displayPolicy.requestTransientBars(windowState, true);
-        verify(controlTarget).showInsets(anyInt(), anyBoolean());
+        verify(controlTarget).showInsets(anyInt(), anyBoolean(), any() /* statsToken */);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
index eb8b89d..a26cad9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
@@ -55,7 +55,7 @@
         mDisplayContent.setImeControlTarget(popup);
         mDisplayContent.setImeLayeringTarget(appWin);
         popup.mAttrs.format = PixelFormat.TRANSPARENT;
-        mImeProvider.scheduleShowImePostLayout(appWin);
+        mImeProvider.scheduleShowImePostLayout(appWin, null /* statsToken */);
         assertTrue(mImeProvider.isReadyToShowIme());
     }
 
@@ -64,7 +64,7 @@
         WindowState target = createWindow(null, TYPE_APPLICATION, "app");
         mDisplayContent.setImeLayeringTarget(target);
         mDisplayContent.updateImeInputAndControlTarget(target);
-        mImeProvider.scheduleShowImePostLayout(target);
+        mImeProvider.scheduleShowImePostLayout(target, null /* statsToken */);
         assertTrue(mImeProvider.isReadyToShowIme());
     }
 
@@ -78,7 +78,7 @@
         mDisplayContent.setImeLayeringTarget(target);
         mDisplayContent.setImeControlTarget(target);
 
-        mImeProvider.scheduleShowImePostLayout(target);
+        mImeProvider.scheduleShowImePostLayout(target, null /* statsToken */);
         assertFalse(mImeProvider.isImeShowing());
         mImeProvider.checkShowImePostLayout();
         assertTrue(mImeProvider.isImeShowing());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index bb5aceb..6e72bf3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import android.annotation.Nullable;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -26,6 +27,7 @@
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.ScrollCaptureResponse;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -117,10 +119,12 @@
     }
 
     @Override
-    public void showInsets(int types, boolean fromIme) throws RemoteException {
+    public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken)
+            throws RemoteException {
     }
 
     @Override
-    public void hideInsets(int types, boolean fromIme) throws RemoteException {
+    public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken)
+            throws RemoteException {
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 0139f6a..6bd3412 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -1000,7 +1000,7 @@
         mDisplayContent.setImeLayeringTarget(app);
         mDisplayContent.setImeInputTarget(app);
         assertTrue(mDisplayContent.shouldImeAttachedToApp());
-        controller.getImeSourceProvider().scheduleShowImePostLayout(app);
+        controller.getImeSourceProvider().scheduleShowImePostLayout(app, null /* statsToken */);
         controller.getImeSourceProvider().getSource().setVisible(true);
         controller.updateAboveInsetsState(false);
 
@@ -1037,7 +1037,7 @@
         mDisplayContent.setImeLayeringTarget(app);
         mDisplayContent.setImeInputTarget(app);
         assertTrue(mDisplayContent.shouldImeAttachedToApp());
-        controller.getImeSourceProvider().scheduleShowImePostLayout(app);
+        controller.getImeSourceProvider().scheduleShowImePostLayout(app, null /* statsToken */);
         controller.getImeSourceProvider().getSource().setVisible(true);
         controller.updateAboveInsetsState(false);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index eca7cbb..ab042d1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -100,6 +100,7 @@
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManager.DisplayImePolicy;
+import android.view.inputmethod.ImeTracker;
 import android.window.ITransitionPlayer;
 import android.window.ScreenCapture;
 import android.window.StartingWindowInfo;
@@ -848,11 +849,13 @@
             }
 
             @Override
-            public void showInsets(int i, boolean b) throws RemoteException {
+            public void showInsets(int i, boolean b, @Nullable ImeTracker.Token t)
+                    throws RemoteException {
             }
 
             @Override
-            public void hideInsets(int i, boolean b) throws RemoteException {
+            public void hideInsets(int i, boolean b, @Nullable ImeTracker.Token t)
+                    throws RemoteException {
             }
 
             @Override