Merge "Dump cancelled jobs for testing."
diff --git a/Android.bp b/Android.bp
index 30b38d3..ef25ec2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -326,6 +326,7 @@
             "packages/modules/Connectivity/framework/aidl-export",
             "packages/modules/Media/apex/aidl/stable",
             "hardware/interfaces/graphics/common/aidl",
+            "frameworks/native/libs/permission/aidl",
         ],
     },
     dxflags: [
@@ -595,6 +596,7 @@
             "packages/modules/Connectivity/framework/aidl-export",
             "packages/modules/Media/apex/aidl/stable",
             "hardware/interfaces/graphics/common/aidl",
+            "frameworks/native/libs/permission/aidl",
         ],
     },
     // These are libs from framework-internal-utils that are required (i.e. being referenced)
diff --git a/core/api/current.txt b/core/api/current.txt
index 32bdec0..9ebd118 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9126,6 +9126,7 @@
     method @Nullable public String getAttributionTag();
     method @Nullable public android.content.AttributionSource getNext();
     method @Nullable public String getPackageName();
+    method public int getPid();
     method public int getUid();
     method public boolean isTrusted(@NonNull android.content.Context);
     method @NonNull public static android.content.AttributionSource myAttributionSource();
@@ -9140,6 +9141,7 @@
     method @NonNull public android.content.AttributionSource.Builder setAttributionTag(@Nullable String);
     method @NonNull public android.content.AttributionSource.Builder setNext(@Nullable android.content.AttributionSource);
     method @NonNull public android.content.AttributionSource.Builder setPackageName(@Nullable String);
+    method @NonNull public android.content.AttributionSource.Builder setPid(int);
   }
 
   public abstract class BroadcastReceiver {
@@ -12448,6 +12450,16 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.SigningInfo> CREATOR;
   }
 
+  public final class UserProperties implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getShowInLauncher();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.UserProperties> CREATOR;
+    field public static final int SHOW_IN_LAUNCHER_NO = 2; // 0x2
+    field public static final int SHOW_IN_LAUNCHER_SEPARATE = 1; // 0x1
+    field public static final int SHOW_IN_LAUNCHER_WITH_PARENT = 0; // 0x0
+  }
+
   public final class VersionedPackage implements android.os.Parcelable {
     ctor public VersionedPackage(@NonNull String, int);
     ctor public VersionedPackage(@NonNull String, long);
@@ -16903,6 +16915,7 @@
     method public int describeContents();
     method public int getFormat();
     method public int getHeight();
+    method public long getId();
     method public int getLayers();
     method public long getUsage();
     method public int getWidth();
@@ -31994,6 +32007,7 @@
     method @Deprecated public static final boolean supportsProcesses();
     field public static final int BLUETOOTH_UID = 1002; // 0x3ea
     field public static final int FIRST_APPLICATION_UID = 10000; // 0x2710
+    field public static final int INVALID_PID = -1; // 0xffffffff
     field public static final int INVALID_UID = -1; // 0xffffffff
     field public static final int LAST_APPLICATION_UID = 19999; // 0x4e1f
     field public static final int PHONE_UID = 1001; // 0x3e9
@@ -32250,6 +32264,7 @@
     method public android.os.UserHandle getUserForSerialNumber(long);
     method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.CREATE_USERS", "android.permission.QUERY_USERS", android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED}) public String getUserName();
     method public java.util.List<android.os.UserHandle> getUserProfiles();
+    method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.QUERY_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.content.pm.UserProperties getUserProperties(@NonNull android.os.UserHandle);
     method public android.os.Bundle getUserRestrictions();
     method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.os.Bundle getUserRestrictions(android.os.UserHandle);
     method public boolean hasUserRestriction(String);
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 6b3dc82..b383d7d 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -107,7 +107,6 @@
 import android.net.Proxy;
 import android.net.TrafficStats;
 import android.net.Uri;
-import android.net.wifi.WifiFrameworkInitializer;
 import android.os.AsyncTask;
 import android.os.Binder;
 import android.os.BluetoothServiceManager;
@@ -7901,8 +7900,6 @@
         BluetoothFrameworkInitializer.setBluetoothServiceManager(new BluetoothServiceManager());
         BluetoothFrameworkInitializer.setBinderCallsStatsInitializer(context -> {
             BinderCallsStats.startForBluetooth(context); });
-        WifiFrameworkInitializer.setBinderCallsStatsInitializer(context -> {
-            BinderCallsStats.startForWifi(context); });
     }
 
     private void purgePendingResources() {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 9072b50..2f282f7 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -8578,9 +8578,9 @@
     public int noteProxyOp(int op, @Nullable String proxiedPackageName, int proxiedUid,
             @Nullable String proxiedAttributionTag, @Nullable String message) {
         return noteProxyOp(op, new AttributionSource(mContext.getAttributionSource(),
-                new AttributionSource(proxiedUid, proxiedPackageName, proxiedAttributionTag,
-                        mContext.getAttributionSource().getToken())), message,
-                        /*skipProxyOperation*/ false);
+                new AttributionSource(proxiedUid, /*pid*/ -1, proxiedPackageName,
+                        proxiedAttributionTag, mContext.getAttributionSource().getToken())),
+                        message, /*skipProxyOperation*/ false);
     }
 
     /**
@@ -8664,7 +8664,7 @@
     public int noteProxyOpNoThrow(@NonNull String op, @Nullable String proxiedPackageName,
             int proxiedUid, @Nullable String proxiedAttributionTag, @Nullable String message) {
         return noteProxyOpNoThrow(strOpToOp(op), new AttributionSource(
-                mContext.getAttributionSource(), new AttributionSource(proxiedUid,
+                mContext.getAttributionSource(), new AttributionSource(proxiedUid, /*pid*/ -1,
                         proxiedPackageName, proxiedAttributionTag, mContext.getAttributionSource()
                         .getToken())), message,/*skipProxyOperation*/ false);
     }
@@ -9076,9 +9076,9 @@
     public int startProxyOp(@NonNull String op, int proxiedUid, @NonNull String proxiedPackageName,
             @Nullable String proxiedAttributionTag, @Nullable String message) {
         return startProxyOp(op, new AttributionSource(mContext.getAttributionSource(),
-                new AttributionSource(proxiedUid, proxiedPackageName, proxiedAttributionTag,
-                        mContext.getAttributionSource().getToken())), message,
-                        /*skipProxyOperation*/ false);
+                new AttributionSource(proxiedUid, /*pid*/ -1, proxiedPackageName,
+                        proxiedAttributionTag, mContext.getAttributionSource().getToken())),
+                        message, /*skipProxyOperation*/ false);
     }
 
     /**
@@ -9124,7 +9124,7 @@
             @Nullable String message) {
         return startProxyOpNoThrow(AppOpsManager.strOpToOp(op), new AttributionSource(
                 mContext.getAttributionSource(), new AttributionSource(proxiedUid,
-                        proxiedPackageName, proxiedAttributionTag,
+                        /*pid*/ -1, proxiedPackageName, proxiedAttributionTag,
                         mContext.getAttributionSource().getToken())), message,
                         /*skipProxyOperation*/ false);
     }
@@ -9270,8 +9270,9 @@
     public void finishProxyOp(@NonNull String op, int proxiedUid,
             @NonNull String proxiedPackageName, @Nullable String proxiedAttributionTag) {
         finishProxyOp(op, new AttributionSource(mContext.getAttributionSource(),
-                new AttributionSource(proxiedUid, proxiedPackageName,  proxiedAttributionTag,
-                        mContext.getAttributionSource().getToken())), /*skipProxyOperation*/ false);
+                new AttributionSource(proxiedUid, /*pid*/ -1, proxiedPackageName,
+                        proxiedAttributionTag, mContext.getAttributionSource().getToken())),
+                        /*skipProxyOperation*/ false);
     }
 
     /**
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index f0e1448..aa5fa5b 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -528,6 +528,28 @@
         return mIsAlarmBroadcast;
     }
 
+    /**
+     * Did this broadcast originate from a push message from the server?
+     *
+     * @return true if this broadcast is a push message, false otherwise.
+     * @hide
+     */
+    public boolean isPushMessagingBroadcast() {
+        return mTemporaryAppAllowlistReasonCode == PowerExemptionManager.REASON_PUSH_MESSAGING;
+    }
+
+    /**
+     * Did this broadcast originate from a push message from the server which was over the allowed
+     * quota?
+     *
+     * @return true if this broadcast is a push message over quota, false otherwise.
+     * @hide
+     */
+    public boolean isPushMessagingOverQuotaBroadcast() {
+        return mTemporaryAppAllowlistReasonCode
+                == PowerExemptionManager.REASON_PUSH_MESSAGING_OVER_QUOTA;
+    }
+
     /** {@hide} */
     public long getRequireCompatChangeId() {
         return mRequireCompatChangeId;
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 4c7bc6d..c1a2183 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -3227,7 +3227,9 @@
             @Nullable AttributionSource nextAttributionSource,
             @Nullable Set<String> renouncedPermissions) {
         AttributionSource attributionSource = new AttributionSource(Process.myUid(),
-                mOpPackageName, attributionTag, renouncedPermissions, nextAttributionSource);
+                Process.myPid(), mOpPackageName, attributionTag,
+                (renouncedPermissions != null) ? renouncedPermissions.toArray(new String[0]) : null,
+                nextAttributionSource);
         // If we want to access protected data on behalf of another app we need to
         // tell the OS that we opt in to participate in the attribution chain.
         if (nextAttributionSource != null) {
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index 3f2fa21..272e235 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -100,22 +100,28 @@
     @TestApi
     public AttributionSource(int uid, @Nullable String packageName,
             @Nullable String attributionTag) {
-        this(uid, packageName, attributionTag, sDefaultToken);
+        this(uid, Process.INVALID_PID, packageName, attributionTag, sDefaultToken);
+    }
+
+    /** @hide */
+    public AttributionSource(int uid, int pid, @Nullable String packageName,
+            @Nullable String attributionTag) {
+        this(uid, pid, packageName, attributionTag, sDefaultToken);
     }
 
     /** @hide */
     @TestApi
     public AttributionSource(int uid, @Nullable String packageName,
             @Nullable String attributionTag, @NonNull IBinder token) {
-        this(uid, packageName, attributionTag, token, /*renouncedPermissions*/ null,
-                /*next*/ null);
+        this(uid, Process.INVALID_PID, packageName, attributionTag, token,
+                /*renouncedPermissions*/ null, /*next*/ null);
     }
 
     /** @hide */
-    public AttributionSource(int uid, @Nullable String packageName,
-            @Nullable String attributionTag, @NonNull IBinder token,
-            @Nullable AttributionSource next) {
-        this(uid, packageName, attributionTag, token, /*renouncedPermissions*/ null, next);
+    public AttributionSource(int uid, int pid, @Nullable String packageName,
+            @Nullable String attributionTag, @NonNull IBinder token) {
+        this(uid, pid, packageName, attributionTag, token, /*renouncedPermissions*/ null,
+                /*next*/ null);
     }
 
     /** @hide */
@@ -123,26 +129,33 @@
     public AttributionSource(int uid, @Nullable String packageName,
             @Nullable String attributionTag, @Nullable Set<String> renouncedPermissions,
             @Nullable AttributionSource next) {
-        this(uid, packageName, attributionTag, (renouncedPermissions != null)
-                ? renouncedPermissions.toArray(new String[0]) : null, next);
+        this(uid, Process.INVALID_PID, packageName, attributionTag, sDefaultToken,
+                (renouncedPermissions != null)
+                ? renouncedPermissions.toArray(new String[0]) : null, /*next*/ next);
     }
 
     /** @hide */
     public AttributionSource(@NonNull AttributionSource current, @Nullable AttributionSource next) {
-        this(current.getUid(), current.getPackageName(), current.getAttributionTag(),
-                current.getToken(), current.mAttributionSourceState.renouncedPermissions, next);
+        this(current.getUid(), current.getPid(), current.getPackageName(),
+                current.getAttributionTag(), current.getToken(),
+                current.mAttributionSourceState.renouncedPermissions, next);
     }
 
-    AttributionSource(int uid, @Nullable String packageName, @Nullable String attributionTag,
-            @Nullable String[] renouncedPermissions, @Nullable AttributionSource next) {
-        this(uid, packageName, attributionTag, sDefaultToken, renouncedPermissions, next);
+    /** @hide */
+    public AttributionSource(int uid, int pid, @Nullable String packageName,
+            @Nullable String attributionTag, @Nullable String[] renouncedPermissions,
+            @Nullable AttributionSource next) {
+        this(uid, pid, packageName, attributionTag, sDefaultToken, renouncedPermissions, next);
     }
 
-    AttributionSource(int uid, @Nullable String packageName, @Nullable String attributionTag,
-            @NonNull IBinder token, @Nullable String[] renouncedPermissions,
+    /** @hide */
+    public AttributionSource(int uid, int pid, @Nullable String packageName,
+            @Nullable String attributionTag, @NonNull IBinder token,
+            @Nullable String[] renouncedPermissions,
             @Nullable AttributionSource next) {
         mAttributionSourceState = new AttributionSourceState();
         mAttributionSourceState.uid = uid;
+        mAttributionSourceState.pid = pid;
         mAttributionSourceState.token = token;
         mAttributionSourceState.packageName = packageName;
         mAttributionSourceState.attributionTag = attributionTag;
@@ -156,7 +169,17 @@
 
         // Since we just unpacked this object as part of it transiting a Binder
         // call, this is the perfect time to enforce that its UID and PID can be trusted
-        enforceCallingUidAndPid();
+        enforceCallingUid();
+
+        // If this object is being constructed as part of a oneway Binder call, getCallingPid will
+        // return 0 instead of the true PID. In that case, invalidate the PID by setting it to
+        // INVALID_PID (-1).
+        final int callingPid = Binder.getCallingPid();
+        if (callingPid == 0) {
+            mAttributionSourceState.pid = Process.INVALID_PID;
+        }
+
+        enforceCallingPid();
     }
 
     /** @hide */
@@ -166,19 +189,19 @@
 
     /** @hide */
     public AttributionSource withNextAttributionSource(@Nullable AttributionSource next) {
-        return new AttributionSource(getUid(), getPackageName(), getAttributionTag(),
-                mAttributionSourceState.renouncedPermissions, next);
+        return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
+                getToken(), mAttributionSourceState.renouncedPermissions, next);
     }
 
     /** @hide */
     public AttributionSource withPackageName(@Nullable String packageName) {
-        return new AttributionSource(getUid(), packageName, getAttributionTag(),
-                mAttributionSourceState.renouncedPermissions, getNext());
+        return new AttributionSource(getUid(), getPid(), packageName, getAttributionTag(),
+               getToken(), mAttributionSourceState.renouncedPermissions, getNext());
     }
 
     /** @hide */
     public AttributionSource withToken(@NonNull Binder token) {
-        return new AttributionSource(getUid(), getPackageName(), getAttributionTag(),
+        return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
                 token, mAttributionSourceState.renouncedPermissions, getNext());
     }
 
@@ -222,6 +245,7 @@
         }
         try {
             return new AttributionSource.Builder(uid)
+                .setPid(Process.myPid())
                 .setPackageName(AppGlobals.getPackageManager().getPackagesForUid(uid)[0])
                 .build();
         } catch (Exception ignored) {
@@ -259,18 +283,6 @@
     }
 
     /**
-     * If you are handling an IPC and you don't trust the caller you need to validate whether the
-     * attribution source is one for the calling app to prevent the caller to pass you a source from
-     * another app without including themselves in the attribution chain.
-     *
-     * @throws SecurityException if the attribution source cannot be trusted to be from the caller.
-     */
-    private void enforceCallingUidAndPid() {
-        enforceCallingUid();
-        enforceCallingPid();
-    }
-
-    /**
      * If you are handling an IPC and you don't trust the caller you need to validate
      * whether the attribution source is one for the calling app to prevent the caller
      * to pass you a source from another app without including themselves in the
@@ -306,7 +318,10 @@
     }
 
     /**
-     * Validate that the pid being claimed for the calling app is not spoofed
+     * Validate that the pid being claimed for the calling app is not spoofed.
+     *
+     * Note that the PID may be unavailable, for example if we're in a oneway Binder call. In this
+     * case, calling enforceCallingPid is guaranteed to fail. The caller should anticipate this.
      *
      * @throws SecurityException if the attribution source cannot be trusted to be from the caller.
      * @hide
@@ -314,8 +329,12 @@
     @TestApi
     public void enforceCallingPid() {
         if (!checkCallingPid()) {
-            throw new SecurityException("Calling pid: " + Binder.getCallingPid()
-                    + " doesn't match source pid: " + mAttributionSourceState.pid);
+            if (Binder.getCallingPid() == 0) {
+                throw new SecurityException("Calling pid unavailable due to oneway Binder call.");
+            } else {
+                throw new SecurityException("Calling pid: " + Binder.getCallingPid()
+                        + " doesn't match source pid: " + mAttributionSourceState.pid);
+            }
         }
     }
 
@@ -326,7 +345,8 @@
      */
     private boolean checkCallingPid() {
         final int callingPid = Binder.getCallingPid();
-        if (mAttributionSourceState.pid != -1 && callingPid != mAttributionSourceState.pid) {
+        if (mAttributionSourceState.pid != Process.INVALID_PID
+                && callingPid != mAttributionSourceState.pid) {
             return false;
         }
         return true;
@@ -443,6 +463,13 @@
     }
 
     /**
+     * The PID that is accessing the permission protected data.
+     */
+    public int getPid() {
+        return mAttributionSourceState.pid;
+    }
+
+    /**
      * The package that is accessing the permission protected data.
      */
     public @Nullable String getPackageName() {
@@ -550,6 +577,7 @@
                 throw new IllegalArgumentException("current AttributionSource can not be null");
             }
             mAttributionSourceState.uid = current.getUid();
+            mAttributionSourceState.pid = current.getPid();
             mAttributionSourceState.packageName = current.getPackageName();
             mAttributionSourceState.attributionTag = current.getAttributionTag();
             mAttributionSourceState.token = current.getToken();
@@ -558,11 +586,24 @@
         }
 
         /**
+         * The PID of the process that is accessing the permission protected data.
+         *
+         * If not called, pid will default to Process.INVALID_PID (-1). This indicates that the PID
+         * data is missing. Supplying a PID is not required, but recommended when accessible.
+         */
+        public @NonNull Builder setPid(int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x2;
+            mAttributionSourceState.pid = value;
+            return this;
+        }
+
+        /**
          * The package that is accessing the permission protected data.
          */
         public @NonNull Builder setPackageName(@Nullable String value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x2;
+            mBuilderFieldsSet |= 0x4;
             mAttributionSourceState.packageName = value;
             return this;
         }
@@ -572,7 +613,7 @@
          */
         public @NonNull Builder setAttributionTag(@Nullable String value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x4;
+            mBuilderFieldsSet |= 0x8;
             mAttributionSourceState.attributionTag = value;
             return this;
         }
@@ -605,7 +646,7 @@
         @RequiresPermission(android.Manifest.permission.RENOUNCE_PERMISSIONS)
         public @NonNull Builder setRenouncedPermissions(@Nullable Set<String> value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x8;
+            mBuilderFieldsSet |= 0x10;
             mAttributionSourceState.renouncedPermissions = (value != null)
                     ? value.toArray(new String[0]) : null;
             return this;
@@ -616,7 +657,7 @@
          */
         public @NonNull Builder setNext(@Nullable AttributionSource value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x10;
+            mBuilderFieldsSet |= 0x20;
             mAttributionSourceState.next = (value != null) ? new AttributionSourceState[]
                     {value.mAttributionSourceState} : mAttributionSourceState.next;
             return this;
@@ -628,15 +669,18 @@
             mBuilderFieldsSet |= 0x40; // Mark builder used
 
             if ((mBuilderFieldsSet & 0x2) == 0) {
-                mAttributionSourceState.packageName = null;
+                mAttributionSourceState.pid = Process.INVALID_PID;
             }
             if ((mBuilderFieldsSet & 0x4) == 0) {
-                mAttributionSourceState.attributionTag = null;
+                mAttributionSourceState.packageName = null;
             }
             if ((mBuilderFieldsSet & 0x8) == 0) {
-                mAttributionSourceState.renouncedPermissions = null;
+                mAttributionSourceState.attributionTag = null;
             }
             if ((mBuilderFieldsSet & 0x10) == 0) {
+                mAttributionSourceState.renouncedPermissions = null;
+            }
+            if ((mBuilderFieldsSet & 0x20) == 0) {
                 mAttributionSourceState.next = null;
             }
 
diff --git a/core/java/android/content/PermissionChecker.java b/core/java/android/content/PermissionChecker.java
index 8d3452e..0e3217d 100644
--- a/core/java/android/content/PermissionChecker.java
+++ b/core/java/android/content/PermissionChecker.java
@@ -152,7 +152,7 @@
             @NonNull String permission, int pid, int uid, @Nullable String packageName,
             @Nullable String attributionTag, @Nullable String message, boolean startDataDelivery) {
         return checkPermissionForDataDelivery(context, permission, pid, new AttributionSource(uid,
-                packageName, attributionTag), message, startDataDelivery);
+                pid, packageName, attributionTag), message, startDataDelivery);
     }
 
     /**
diff --git a/core/java/android/content/pm/OWNERS b/core/java/android/content/pm/OWNERS
index 087e61d..f9d3222 100644
--- a/core/java/android/content/pm/OWNERS
+++ b/core/java/android/content/pm/OWNERS
@@ -11,3 +11,4 @@
 per-file AppSearchPerson.java = file:/core/java/android/content/pm/SHORTCUT_OWNERS
 per-file *Launcher* = file:/core/java/android/content/pm/LAUNCHER_OWNERS
 per-file UserInfo* = file:/MULTIUSER_OWNERS
+per-file *UserProperties* = file:/MULTIUSER_OWNERS
diff --git a/core/java/android/content/pm/UserProperties.aidl b/core/java/android/content/pm/UserProperties.aidl
new file mode 100644
index 0000000..4d37067
--- /dev/null
+++ b/core/java/android/content/pm/UserProperties.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.content.pm;
+
+parcelable UserProperties;
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
new file mode 100644
index 0000000..1a82e4d
--- /dev/null
+++ b/core/java/android/content/pm/UserProperties.java
@@ -0,0 +1,356 @@
+/*
+ * 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.content.pm;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Slog;
+import android.util.TypedXmlPullParser;
+import android.util.TypedXmlSerializer;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class holding the properties of a user that derive mostly from its user type.
+ */
+public final class UserProperties implements Parcelable {
+    private static final String LOG_TAG = UserProperties.class.getSimpleName();
+
+    // Attribute strings for reading/writing properties to/from XML.
+    private static final String ATTR_SHOW_IN_LAUNCHER = "showInLauncher";
+    private static final String ATTR_START_WITH_PARENT = "startWithParent";
+
+    /** Index values of each property (to indicate whether they are present in this object). */
+    @IntDef(prefix = "INDEX_", value = {
+            INDEX_SHOW_IN_LAUNCHER,
+            INDEX_START_WITH_PARENT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface PropertyIndex {
+    }
+    private static final int INDEX_SHOW_IN_LAUNCHER = 0;
+    private static final int INDEX_START_WITH_PARENT = 1;
+    /** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
+    private long mPropertiesPresent = 0;
+
+
+    /**
+     * Possible values for whether or how to show this user in the Launcher.
+     * @hide
+     */
+    @IntDef(prefix = "SHOW_IN_LAUNCHER_", value = {
+            SHOW_IN_LAUNCHER_WITH_PARENT,
+            SHOW_IN_LAUNCHER_SEPARATE,
+            SHOW_IN_LAUNCHER_NO,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ShowInLauncher {
+    }
+    /**
+     * Suggests that the launcher should show this user's apps in the main tab.
+     * That is, either this user is a full user, so its apps should be presented accordingly, or, if
+     * this user is a profile, then its apps should be shown alongside its parent's apps.
+     */
+    public static final int SHOW_IN_LAUNCHER_WITH_PARENT = 0;
+    /**
+     * Suggests that the launcher should show this user's apps, but separately from the apps of this
+     * user's parent.
+     */
+    public static final int SHOW_IN_LAUNCHER_SEPARATE = 1;
+    /**
+     * Suggests that the launcher should not show this user.
+     */
+    public static final int SHOW_IN_LAUNCHER_NO = 2;
+
+    /**
+     * Reference to the default user properties for this user's user type.
+     * <li>If non-null, then any absent property will use the default property from here instead.
+     * <li>If null, then any absent property indicates that the caller lacks permission to see it,
+     *          so attempting to get that property will trigger a SecurityException.
+     */
+    private final @Nullable UserProperties mDefaultProperties;
+
+    /**
+     * Creates a UserProperties (intended for the SystemServer) that stores a reference to the given
+     * default properties, which it uses for any property not subsequently set.
+     * @hide
+     */
+    public UserProperties(@NonNull UserProperties defaultProperties) {
+        mDefaultProperties = defaultProperties;
+        mPropertiesPresent = 0;
+    }
+
+    /**
+     * Copies the given UserProperties, excluding any information that doesn't satisfy the specified
+     * permissions.
+     * Can only be used on the original version (one that won't throw on permission errors).
+     * Note that, internally, this does not perform an exact copy.
+     * @hide
+     */
+    public UserProperties(UserProperties orig,
+            boolean exposeAllFields,
+            boolean hasManagePermission,
+            boolean hasQueryPermission) {
+
+        if (orig.mDefaultProperties == null) {
+            throw new IllegalArgumentException("Attempting to copy a non-original UserProperties.");
+        }
+
+        this.mDefaultProperties = null;
+
+        // NOTE: Copy each property using getters to ensure default values are copied if needed.
+        if (exposeAllFields) {
+            setStartWithParent(orig.getStartWithParent());
+        }
+        if (hasManagePermission) {
+            // Add any items that require this permission.
+        }
+        if (hasQueryPermission) {
+            // Add any items that require this permission.
+        }
+        // Add any items that require no permissions at all.
+        setShowInLauncher(orig.getShowInLauncher());
+    }
+
+    /**
+     * Indicates that the given property is being stored explicitly in this object.
+     * If false, it means that either
+     * <li>the default property for the user type should be used instead (for SystemServer callers)
+     * <li>the caller lacks permission to see this property (for all other callers)
+     */
+    private boolean isPresent(@PropertyIndex long index) {
+        return (mPropertiesPresent & (1L << index)) != 0;
+    }
+
+    /** Indicates that the given property is henceforth being explicitly stored in this object. */
+    private void setPresent(@PropertyIndex long index) {
+        mPropertiesPresent |= (1L << index);
+    }
+
+    /** @hide Returns the internal mPropertiesPresent value. Only for testing purposes. */
+    @VisibleForTesting
+    public long getPropertiesPresent() {
+        return mPropertiesPresent;
+    }
+
+    /**
+     * Returns whether, and how, a user should be shown in the Launcher.
+     * This is generally inapplicable for non-profile users.
+     *
+     * Possible return values include
+     *    {@link #SHOW_IN_LAUNCHER_WITH_PARENT}},
+     *    {@link #SHOW_IN_LAUNCHER_SEPARATE},
+     *    and {@link #SHOW_IN_LAUNCHER_NO}.
+     *
+     * @return whether, and how, a profile should be shown in the Launcher.
+     */
+    public @ShowInLauncher int getShowInLauncher() {
+        if (isPresent(INDEX_SHOW_IN_LAUNCHER)) return mShowInLauncher;
+        if (mDefaultProperties != null) return mDefaultProperties.mShowInLauncher;
+        throw new SecurityException("You don't have permission to query showInLauncher");
+    }
+    /** @hide */
+    public void setShowInLauncher(@ShowInLauncher int val) {
+        this.mShowInLauncher = val;
+        setPresent(INDEX_SHOW_IN_LAUNCHER);
+    }
+    private @ShowInLauncher int mShowInLauncher;
+
+    /**
+     * Returns whether a profile should be started when its parent starts (unless in quiet mode).
+     * This only applies for users that have parents (i.e. for profiles).
+     * @hide
+     */
+    public boolean getStartWithParent() {
+        if (isPresent(INDEX_START_WITH_PARENT)) return mStartWithParent;
+        if (mDefaultProperties != null) return mDefaultProperties.mStartWithParent;
+        throw new SecurityException("You don't have permission to query startWithParent");
+    }
+    /** @hide */
+    public void setStartWithParent(boolean val) {
+        this.mStartWithParent = val;
+        setPresent(INDEX_START_WITH_PARENT);
+    }
+    private boolean mStartWithParent;
+
+    @Override
+    public String toString() {
+        // Please print in increasing order of PropertyIndex.
+        return "UserProperties{"
+                + "mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent)
+                + ", mShowInLauncher=" + getShowInLauncher()
+                + ", mStartWithParent=" + getStartWithParent()
+                + "}";
+    }
+
+    /**
+     * Print the UserProperties to the given PrintWriter.
+     * @hide
+     */
+    public void println(PrintWriter pw, String prefix) {
+        // Please print in increasing order of PropertyIndex.
+        pw.println(prefix + "UserProperties:");
+        pw.println(prefix + "    mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent));
+        pw.println(prefix + "    mShowInLauncher=" + getShowInLauncher());
+        pw.println(prefix + "    mStartWithParent=" + getStartWithParent());
+    }
+
+    /**
+     * Reads in a UserProperties from an xml file, for use by the SystemServer.
+     *
+     * The serializer should already be inside a tag from which to read the user properties.
+     *
+     * @param defaultUserPropertiesReference the default UserProperties to use for this user type.
+     * @see #writeToXml
+     * @hide
+     */
+    public UserProperties(
+            TypedXmlPullParser parser,
+            @NonNull UserProperties defaultUserPropertiesReference)
+            throws IOException, XmlPullParserException {
+
+        this(defaultUserPropertiesReference);
+        updateFromXml(parser);
+    }
+
+    /**
+     * Parses the given xml file and updates this UserProperties with its data.
+     * I.e., if a piece of data is present in the xml, it will overwrite whatever was
+     * previously stored in this UserProperties.
+     * @hide
+     */
+    public void updateFromXml(TypedXmlPullParser parser)
+            throws IOException, XmlPullParserException {
+
+        final int attributeCount = parser.getAttributeCount();
+        for (int i = 0; i < attributeCount; i++) {
+            final String attributeName = parser.getAttributeName(i);
+            switch(attributeName) {
+                case ATTR_SHOW_IN_LAUNCHER:
+                    setShowInLauncher(parser.getAttributeInt(i));
+                    break;
+                case ATTR_START_WITH_PARENT:
+                    setStartWithParent(parser.getAttributeBoolean(i));
+                    break;
+                default:
+                    Slog.w(LOG_TAG, "Skipping unknown property " + attributeName);
+            }
+        }
+    }
+
+    /**
+     * Writes the UserProperties, as used by the SystemServer, to the xml file.
+     *
+     * The serializer should already be inside a tag in which to write the user properties.
+     *
+     * @see  #UserProperties(TypedXmlPullParser, UserProperties)
+     * @hide
+     */
+    public void writeToXml(TypedXmlSerializer serializer)
+            throws IOException, XmlPullParserException {
+
+        if (isPresent(INDEX_SHOW_IN_LAUNCHER)) {
+            serializer.attributeInt(null, ATTR_SHOW_IN_LAUNCHER, mShowInLauncher);
+        }
+        if (isPresent(INDEX_START_WITH_PARENT)) {
+            serializer.attributeBoolean(null, ATTR_START_WITH_PARENT, mStartWithParent);
+        }
+    }
+
+    // For use only with an object that has already had any permission-lacking fields stripped out.
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int parcelableFlags) {
+        dest.writeLong(mPropertiesPresent);
+        dest.writeInt(mShowInLauncher);
+        dest.writeBoolean(mStartWithParent);
+    }
+
+    /**
+     * Reads a UserProperties object from the parcel.
+     * Not suitable for the canonical SystemServer version since it lacks mDefaultProperties.
+      */
+    private UserProperties(@NonNull Parcel source) {
+        mDefaultProperties = null;
+
+        mPropertiesPresent = source.readLong();
+        mShowInLauncher = source.readInt();
+        mStartWithParent = source.readBoolean();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<UserProperties> CREATOR
+            = new Parcelable.Creator<UserProperties>() {
+        public UserProperties createFromParcel(Parcel source) {
+            return new UserProperties(source);
+        }
+        public UserProperties[] newArray(int size) {
+            return new UserProperties[size];
+        }
+    };
+
+    /**
+     * Builder for the SystemServer's {@link UserProperties}; see that class for documentation.
+     * Intended for building default values (and so all properties are present in the built object).
+     * @hide
+     */
+    public static final class Builder {
+        // UserProperties fields and their default values.
+        private @ShowInLauncher int mShowInLauncher = SHOW_IN_LAUNCHER_WITH_PARENT;
+        private boolean mStartWithParent = false;
+
+        public Builder setShowInLauncher(@ShowInLauncher int showInLauncher) {
+            mShowInLauncher = showInLauncher;
+            return this;
+        }
+
+        public Builder setStartWithParent(boolean startWithParent) {
+            mStartWithParent = startWithParent;
+            return this;
+        }
+
+        /** Builds a UserProperties object with *all* values populated. */
+        public UserProperties build() {
+            return new UserProperties(
+                    mShowInLauncher,
+                    mStartWithParent);
+        }
+    } // end Builder
+
+    /** Creates a UserProperties with the given properties. Intended for building default values. */
+    private UserProperties(
+            @ShowInLauncher int showInLauncher,
+            boolean startWithParent) {
+
+        mDefaultProperties = null;
+        setShowInLauncher(showInLauncher);
+        setStartWithParent(startWithParent);
+    }
+}
diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java
index 1e4c9501..11892fe 100644
--- a/core/java/android/hardware/HardwareBuffer.java
+++ b/core/java/android/hardware/HardwareBuffer.java
@@ -188,9 +188,6 @@
     public static HardwareBuffer create(
             @IntRange(from = 1) int width, @IntRange(from = 1) int height,
             @Format int format, @IntRange(from = 1) int layers, @Usage long usage) {
-        if (!HardwareBuffer.isSupportedFormat(format)) {
-            throw new IllegalArgumentException("Invalid pixel format " + format);
-        }
         if (width <= 0) {
             throw new IllegalArgumentException("Invalid width " + width);
         }
@@ -226,9 +223,6 @@
      */
     public static boolean isSupported(@IntRange(from = 1) int width, @IntRange(from = 1) int height,
             @Format int format, @IntRange(from = 1) int layers, @Usage long usage) {
-        if (!HardwareBuffer.isSupportedFormat(format)) {
-            throw new IllegalArgumentException("Invalid pixel format " + format);
-        }
         if (width <= 0) {
             throw new IllegalArgumentException("Invalid width " + width);
         }
@@ -286,10 +280,7 @@
      * Returns the width of this buffer in pixels.
      */
     public int getWidth() {
-        if (isClosed()) {
-            throw new IllegalStateException("This HardwareBuffer has been closed and its width "
-                    + "cannot be obtained.");
-        }
+        checkClosed("width");
         return nGetWidth(mNativeObject);
     }
 
@@ -297,10 +288,7 @@
      * Returns the height of this buffer in pixels.
      */
     public int getHeight() {
-        if (isClosed()) {
-            throw new IllegalStateException("This HardwareBuffer has been closed and its height "
-                    + "cannot be obtained.");
-        }
+        checkClosed("height");
         return nGetHeight(mNativeObject);
     }
 
@@ -309,10 +297,7 @@
      */
     @Format
     public int getFormat() {
-        if (isClosed()) {
-            throw new IllegalStateException("This HardwareBuffer has been closed and its format "
-                    + "cannot be obtained.");
-        }
+        checkClosed("format");
         return nGetFormat(mNativeObject);
     }
 
@@ -320,10 +305,7 @@
      * Returns the number of layers in this buffer.
      */
     public int getLayers() {
-        if (isClosed()) {
-            throw new IllegalStateException("This HardwareBuffer has been closed and its layer "
-                    + "count cannot be obtained.");
-        }
+        checkClosed("layer count");
         return nGetLayers(mNativeObject);
     }
 
@@ -331,14 +313,27 @@
      * Returns the usage flags of the usage hints set on this buffer.
      */
     public long getUsage() {
-        if (isClosed()) {
-            throw new IllegalStateException("This HardwareBuffer has been closed and its usage "
-                    + "cannot be obtained.");
-        }
+        checkClosed("usage");
         return nGetUsage(mNativeObject);
     }
 
     /**
+     * Returns the system-wide unique id for this buffer
+     *
+     */
+    public long getId() {
+        checkClosed("id");
+        return nGetId(mNativeObject);
+    }
+
+    private void checkClosed(String name) {
+        if (isClosed()) {
+            throw new IllegalStateException("This HardwareBuffer has been closed and its "
+                    + name + " cannot be obtained.");
+        }
+    }
+
+    /**
      * Destroys this buffer immediately. Calling this method frees up any
      * underlying native resources. After calling this method, this buffer
      * must not be used in any way.
@@ -407,36 +402,6 @@
         }
     };
 
-    /**
-     * Validates whether a particular format is supported by HardwareBuffer.
-     *
-     * @param format The format to validate.
-     *
-     * @return True if <code>format</code> is a supported format. false otherwise.
-     * See {@link #create(int, int, int, int, long)}.
-     */
-    private static boolean isSupportedFormat(@Format int format) {
-        switch(format) {
-            case RGBA_8888:
-            case RGBA_FP16:
-            case RGBA_1010102:
-            case RGBX_8888:
-            case RGB_565:
-            case RGB_888:
-            case BLOB:
-            case YCBCR_420_888:
-            case D_16:
-            case D_24:
-            case DS_24UI8:
-            case D_FP32:
-            case DS_FP32UI8:
-            case S_UI8:
-            case YCBCR_P010:
-                return true;
-        }
-        return false;
-    }
-
     private static native long nCreateHardwareBuffer(int width, int height, int format, int layers,
             long usage);
     private static native long nCreateFromGraphicBuffer(GraphicBuffer graphicBuffer);
@@ -457,4 +422,6 @@
             long usage);
     @CriticalNative
     private static native long nEstimateSize(long nativeObject);
+    @CriticalNative
+    private static native long nGetId(long nativeObject);
 }
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index a9d665c8..621eab5 100644
--- a/core/java/android/hardware/soundtrigger/SoundTrigger.java
+++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java
@@ -1963,7 +1963,6 @@
     }
 
     private static Object mServiceLock = new Object();
-    private static ISoundTriggerMiddlewareService mService;
 
     /**
      * Translate an exception thrown from interaction with the underlying service to an error code.
@@ -2217,20 +2216,12 @@
                     binder =
                             ServiceManager.getServiceOrThrow(
                                     Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE);
-                    binder.linkToDeath(() -> {
-                        synchronized (mServiceLock) {
-                            mService = null;
-                        }
-                    }, 0);
-                    mService = ISoundTriggerMiddlewareService.Stub.asInterface(binder);
-                    break;
+                    return ISoundTriggerMiddlewareService.Stub.asInterface(binder);
                 } catch (Exception e) {
                     Log.e(TAG, "Failed to bind to soundtrigger service", e);
                 }
             }
-            return  mService;
         }
-
     }
 
     /**
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 9d05cec0..26600e2 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -2317,11 +2317,6 @@
     public abstract void finishIteratingHistoryLocked();
 
     /**
-     * Return the base time offset for the battery history.
-     */
-    public abstract long getHistoryBaseTime();
-
-    /**
      * Returns the number of times the device has been started.
      */
     public abstract int getStartCount();
@@ -7606,8 +7601,6 @@
                 CHECKIN_VERSION, getParcelVersion(), getStartPlatformVersion(),
                 getEndPlatformVersion());
 
-        long now = getHistoryBaseTime() + SystemClock.elapsedRealtime();
-
         if ((flags & (DUMP_INCLUDE_HISTORY | DUMP_HISTORY_ONLY)) != 0) {
             if (startIteratingHistoryLocked()) {
                 try {
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index f69d6b0..62ee408 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -23,6 +23,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.content.IntentSender;
 import android.content.RestrictionEntry;
 import android.graphics.Bitmap;
@@ -71,6 +72,7 @@
     boolean isUserOfType(int userId, in String userType);
     @UnsupportedAppUsage
     UserInfo getUserInfo(int userId);
+    UserProperties getUserPropertiesCopy(int userId);
     String getUserAccount(int userId);
     void setUserAccount(int userId, String accountName);
     long getUserCreationTime(int userId);
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index b16360c..095d53e 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -364,6 +364,11 @@
     public static final int LAST_APPLICATION_CACHE_GID = 29999;
 
     /**
+     * An invalid PID value.
+     */
+    public static final int INVALID_PID = -1;
+
+    /**
      * Standard priority of application threads.
      * Use with {@link #setThreadPriority(int)} and
      * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
@@ -1463,18 +1468,6 @@
     public static final native int killProcessGroup(int uid, int pid);
 
     /**
-      * Freeze the cgroup for the given UID.
-      * This cgroup may contain child cgroups which will also be frozen. If this cgroup or its
-      * children contain processes with Binder interfaces, those interfaces should be frozen before
-      * the cgroup to avoid blocking synchronous callers indefinitely.
-      *
-      * @param uid The UID to be frozen
-      * @param freeze true = freeze; false = unfreeze
-      * @hide
-      */
-    public static final native void freezeCgroupUid(int uid, boolean freeze);
-
-    /**
      * Remove all process groups.  Expected to be called when ActivityManager
      * is restarted.
      * @hide
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 99cf737..ef04f64 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -51,6 +51,7 @@
 import android.content.IntentSender;
 import android.content.pm.UserInfo;
 import android.content.pm.UserInfo.UserInfoFlag;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -2786,7 +2787,7 @@
         return isUserRunning(user.getIdentifier());
     }
 
-    /** {@hide} */
+    /** @hide */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
     public boolean isUserRunning(@UserIdInt int userId) {
@@ -2969,7 +2970,7 @@
                 }
             };
 
-    /** {@hide} */
+    /** @hide */
     @UnsupportedAppUsage
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
@@ -2977,13 +2978,13 @@
         return mIsUserUnlockedCache.query(userId);
     }
 
-    /** {@hide} */
+    /** @hide */
     public void disableIsUserUnlockedCache() {
         mIsUserUnlockedCache.disableLocal();
         mIsUserUnlockingOrUnlockedCache.disableLocal();
     }
 
-    /** {@hide} */
+    /** @hide */
     public static final void invalidateIsUserUnlockedCache() {
         PropertyInvalidatedCache.invalidateCache(CACHE_KEY_IS_USER_UNLOCKED_PROPERTY);
     }
@@ -3013,7 +3014,7 @@
         return isUserUnlockingOrUnlocked(user.getIdentifier());
     }
 
-    /** {@hide} */
+    /** @hide */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
     public boolean isUserUnlockingOrUnlocked(@UserIdInt int userId) {
@@ -3084,6 +3085,30 @@
     }
 
     /**
+     * Returns a {@link UserProperties} object describing the properties of the given user.
+     *
+     * Note that the caller may not have permission to access all items; requesting any item for
+     * which permission is lacking will throw a {@link SecurityException}.
+     *
+     * <p> Requires
+     * {@code android.Manifest.permission#MANAGE_USERS},
+     * {@code android.Manifest.permission#QUERY_USERS}, or
+     * {@code android.Manifest.permission#INTERACT_ACROSS_USERS}
+     * permission, or else the caller must be in the same profile group as the caller.
+     *
+     * @param userHandle the user handle of the user whose information is being requested.
+     * @return a UserProperties object for a specific user.
+     * @throws IllegalArgumentException if {@code userHandle} doesn't correspond to an existing user
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.QUERY_USERS,
+            android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+    public @NonNull UserProperties getUserProperties(@NonNull UserHandle userHandle) {
+        return mUserPropertiesCache.query(userHandle.getIdentifier());
+    }
+
+    /**
      * @hide
      *
      * Returns who set a user restriction on a user.
@@ -5481,11 +5506,37 @@
                 }
             };
 
-    /** {@hide} */
+    /** @hide */
     public static final void invalidateStaticUserProperties() {
         PropertyInvalidatedCache.invalidateCache(CACHE_KEY_STATIC_USER_PROPERTIES);
     }
 
+    /* Cache key for UserProperties object. */
+    private static final String CACHE_KEY_USER_PROPERTIES = "cache_key.user_properties";
+
+    // TODO: It would be better to somehow have this as static, so that it can work cross-context.
+    private final PropertyInvalidatedCache<Integer, UserProperties> mUserPropertiesCache =
+            new PropertyInvalidatedCache<Integer, UserProperties>(16, CACHE_KEY_USER_PROPERTIES) {
+                @Override
+                public UserProperties recompute(Integer userId) {
+                    try {
+                        // If the userId doesn't exist, this will throw rather than cache garbage.
+                        return mService.getUserPropertiesCopy(userId);
+                    } catch (RemoteException re) {
+                        throw re.rethrowFromSystemServer();
+                    }
+                }
+                @Override
+                public boolean bypass(Integer query) {
+                    return query < 0;
+                }
+            };
+
+    /** @hide */
+    public static final void invalidateUserPropertiesCache() {
+        PropertyInvalidatedCache.invalidateCache(CACHE_KEY_USER_PROPERTIES);
+    }
+
     /**
      * @hide
      * User that enforces a restriction.
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index baf3eeca..e67b67d 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10841,6 +10841,23 @@
                 "accessibility_software_cursor_enabled";
 
         /**
+         * Software Cursor settings that specifies whether trigger hints are enabled.
+         *
+         * @hide
+         */
+        public static final String ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED =
+                "accessibility_software_cursor_trigger_hints_enabled";
+
+        /**
+         * Software Cursor settings that specifies whether triggers are shifted when the keyboard
+         * is shown.
+         *
+         * @hide
+         */
+        public static final String ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED =
+                "accessibility_software_cursor_keyboard_shift_enabled";
+
+        /**
          * Whether the Adaptive connectivity option is enabled.
          *
          * @hide
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index a2cb1d5..0e3bcd1 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -314,6 +314,9 @@
             (int) (startValue.right + fraction * (endValue.right - startValue.right)),
             (int) (startValue.bottom + fraction * (endValue.bottom - startValue.bottom)));
 
+    /** Logging listener. */
+    private WindowInsetsAnimationControlListener mLoggingListener;
+
     /**
      * The default implementation of listener, to be used by InsetsController and InsetsPolicy to
      * animate insets.
@@ -330,6 +333,7 @@
         private final long mDurationMs;
         private final boolean mDisable;
         private final int mFloatingImeBottomInset;
+        private final WindowInsetsAnimationControlListener mLoggingListener;
 
         private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
                 new ThreadLocal<AnimationHandler>() {
@@ -343,7 +347,7 @@
 
         public InternalAnimationControlListener(boolean show, boolean hasAnimationCallbacks,
                 @InsetsType int requestedTypes, @Behavior int behavior, boolean disable,
-                int floatingImeBottomInset) {
+                int floatingImeBottomInset, WindowInsetsAnimationControlListener loggingListener) {
             mShow = show;
             mHasAnimationCallbacks = hasAnimationCallbacks;
             mRequestedTypes = requestedTypes;
@@ -351,12 +355,16 @@
             mDurationMs = calculateDurationMs();
             mDisable = disable;
             mFloatingImeBottomInset = floatingImeBottomInset;
+            mLoggingListener = loggingListener;
         }
 
         @Override
         public void onReady(WindowInsetsAnimationController controller, int types) {
             mController = controller;
             if (DEBUG) Log.d(TAG, "default animation onReady types: " + types);
+            if (mLoggingListener != null) {
+                mLoggingListener.onReady(controller, types);
+            }
 
             if (mDisable) {
                 onAnimationFinish();
@@ -410,6 +418,9 @@
         public void onFinished(WindowInsetsAnimationController controller) {
             if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onFinished types:"
                     + Type.toString(mRequestedTypes));
+            if (mLoggingListener != null) {
+                mLoggingListener.onFinished(controller);
+            }
         }
 
         @Override
@@ -420,6 +431,9 @@
             }
             if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onCancelled types:"
                     + mRequestedTypes);
+            if (mLoggingListener != null) {
+                mLoggingListener.onCancelled(controller);
+            }
         }
 
         protected Interpolator getInsetsInterpolator() {
@@ -1147,6 +1161,13 @@
         updateRequestedVisibilities();
     }
 
+    // TODO(b/242962223): Make this setter restrictive.
+    @Override
+    public void setSystemDrivenInsetsAnimationLoggingListener(
+            @Nullable WindowInsetsAnimationControlListener listener) {
+        mLoggingListener = listener;
+    }
+
     /**
      * @return Pair of (types ready to animate, IME ready to animate).
      */
@@ -1460,7 +1481,8 @@
         boolean hasAnimationCallbacks = mHost.hasAnimationCallbacks();
         final InternalAnimationControlListener listener = new InternalAnimationControlListener(
                 show, hasAnimationCallbacks, types, mHost.getSystemBarsBehavior(),
-                skipAnim || mAnimationsDisabled, mHost.dipToPx(FLOATING_IME_BOTTOM_INSET_DP));
+                skipAnim || mAnimationsDisabled, mHost.dipToPx(FLOATING_IME_BOTTOM_INSET_DP),
+                mLoggingListener);
 
         // We are about to playing the default animation (show/hide). Passing a null frame indicates
         // the controlled types should be animated regardless of the frame.
diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java
index c61baf6..3fe9110 100644
--- a/core/java/android/view/PendingInsetsController.java
+++ b/core/java/android/view/PendingInsetsController.java
@@ -44,6 +44,7 @@
     private ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
             = new ArrayList<>();
     private int mCaptionInsetsHeight = 0;
+    private WindowInsetsAnimationControlListener mLoggingListener;
 
     @Override
     public void show(int types) {
@@ -176,6 +177,9 @@
             controller.addOnControllableInsetsChangedListener(
                     mControllableInsetsChangedListeners.get(i));
         }
+        if (mLoggingListener != null) {
+            controller.setSystemDrivenInsetsAnimationLoggingListener(mLoggingListener);
+        }
 
         // Reset all state so it doesn't get applied twice just in case
         mRequests.clear();
@@ -184,7 +188,7 @@
         mAppearance = 0;
         mAppearanceMask = 0;
         mAnimationsDisabled = false;
-
+        mLoggingListener = null;
         // After replaying, we forward everything directly to the replayed instance.
         mReplayedInsetsController = controller;
     }
@@ -198,6 +202,16 @@
     }
 
     @Override
+    public void setSystemDrivenInsetsAnimationLoggingListener(
+            @Nullable WindowInsetsAnimationControlListener listener) {
+        if (mReplayedInsetsController != null) {
+            mReplayedInsetsController.setSystemDrivenInsetsAnimationLoggingListener(listener);
+        } else {
+            mLoggingListener = listener;
+        }
+    }
+
+    @Override
     public void controlWindowInsetsAnimation(@InsetsType int types, long durationMillis,
             @Nullable Interpolator interpolator,
             CancellationSignal cancellationSignal,
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index 5721fa6..3acb053 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -28,8 +28,8 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.util.Log;
-import android.window.WindowTokenClient;
 import android.view.accessibility.IAccessibilityEmbeddedConnection;
+import android.window.WindowTokenClient;
 
 import java.util.Objects;
 
@@ -271,14 +271,8 @@
     /** @hide */
     public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d,
             @NonNull WindowlessWindowManager wwm) {
-        this(c, d, wwm, false /* useSfChoreographer */);
-    }
-
-    /** @hide */
-    public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d,
-            @NonNull WindowlessWindowManager wwm, boolean useSfChoreographer) {
         mWm = wwm;
-        mViewRoot = new ViewRootImpl(c, d, mWm, new WindowlessWindowLayout(), useSfChoreographer);
+        mViewRoot = new ViewRootImpl(c, d, mWm, new WindowlessWindowLayout());
         addConfigCallback(c, d);
 
         WindowManagerGlobal.getInstance().addWindowlessRoot(mViewRoot);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 674f0a2..9091b79 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -910,17 +910,11 @@
     private String mTag = TAG;
 
     public ViewRootImpl(Context context, Display display) {
-        this(context, display, WindowManagerGlobal.getWindowSession(), new WindowLayout(),
-                false /* useSfChoreographer */);
+        this(context, display, WindowManagerGlobal.getWindowSession(), new WindowLayout());
     }
 
     public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
             WindowLayout windowLayout) {
-        this(context, display, session, windowLayout, false /* useSfChoreographer */);
-    }
-
-    public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
-            WindowLayout windowLayout, boolean useSfChoreographer) {
         mContext = context;
         mWindowSession = session;
         mWindowLayout = windowLayout;
@@ -952,8 +946,7 @@
         mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi;
         mFallbackEventHandler = new PhoneFallbackEventHandler(context);
         // TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions
-        mChoreographer = useSfChoreographer
-                ? Choreographer.getSfInstance() : Choreographer.getInstance();
+        mChoreographer = Choreographer.getInstance();
         mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
         mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this));
         mHandwritingInitiator = new HandwritingInitiator(
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index 227b9f4..63f9e13 100644
--- a/core/java/android/view/WindowInsetsController.java
+++ b/core/java/android/view/WindowInsetsController.java
@@ -201,6 +201,21 @@
             @NonNull WindowInsetsAnimationControlListener listener);
 
     /**
+     * Lets the application add non-controllable listener object that can be called back
+     * when animation is invoked by the system by host calling methods such as {@link #show} or
+     * {@link #hide}.
+     *
+     * The listener is supposed to be used for logging only, using the control or
+     * relying on the timing of the callback in any other way is not supported.
+     *
+     * @param listener The {@link WindowInsetsAnimationControlListener} that gets called when
+     *                 the animation is driven by the system and not the host
+     * @hide
+     */
+    void setSystemDrivenInsetsAnimationLoggingListener(
+            @Nullable WindowInsetsAnimationControlListener listener);
+
+    /**
      * Controls the appearance of system bars.
      * <p>
      * For example, the following statement adds {@link #APPEARANCE_LIGHT_STATUS_BARS}:
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index fe8d64f..a49caaf 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1054,6 +1054,14 @@
         }
     }
 
+    /**
+     * Ensure scales are between 0 and 20.
+     * @hide
+     */
+    static float fixScale(float scale) {
+        return Math.max(Math.min(scale, 20), 0);
+    }
+
     public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
         /**
          * X position for this window.  With the default gravity it is ignored.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 8d3cf6d..6049613 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -91,6 +91,7 @@
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewRootImpl;
+import android.view.WindowInsets;
 import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillManager;
@@ -2150,8 +2151,9 @@
                 null /* icProto */);
         synchronized (mH) {
             final View view = getServedViewLocked();
-            if (mImeInsetsConsumer != null && view != null) {
-                if (mImeInsetsConsumer.isRequestedVisible()) {
+            if (view != null) {
+                final WindowInsets rootInsets = view.getRootWindowInsets();
+                if (rootInsets != null && rootInsets.isVisible(WindowInsets.Type.ime())) {
                     hideSoftInputFromWindow(view.getWindowToken(), hideFlags, null,
                             SoftInputShowHideReason.HIDE_TOGGLE_SOFT_INPUT);
                 } else {
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index e567ced..49849a4 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -16,7 +16,7 @@
 
 package android.window;
 
-import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENT_TO_TASK;
+import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
@@ -147,13 +147,25 @@
         }
     }
 
-    /** Called when a TaskFragment is created and organized by this organizer. */
+    /**
+     * Called when a TaskFragment is created and organized by this organizer.
+     *
+     * @param taskFragmentInfo  Info of the TaskFragment that is created.
+     */
     public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {}
 
-    /** Called when the status of an organized TaskFragment is changed. */
+    /**
+     * Called when the status of an organized TaskFragment is changed.
+     *
+     * @param taskFragmentInfo  Info of the TaskFragment that is changed.
+     */
     public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {}
 
-    /** Called when an organized TaskFragment is removed. */
+    /**
+     * Called when an organized TaskFragment is removed.
+     *
+     * @param taskFragmentInfo  Info of the TaskFragment that is removed.
+     */
     public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {}
 
     /**
@@ -176,6 +188,9 @@
      * For case like screen size change, it will trigger onTaskFragmentParentInfoChanged with new
      * Task bounds, but may not trigger onTaskFragmentInfoChanged because there can be an override
      * bounds.
+     *
+     * @param taskId    Id of the parent Task that is changed.
+     * @param parentConfig  Config of the parent Task.
      * @hide
      */
     public void onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig) {
@@ -208,7 +223,7 @@
     /**
      * Called when an Activity is reparented to the Task with organized TaskFragment. For example,
      * when an Activity enters and then exits Picture-in-picture, it will be reparented back to its
-     * orginial Task. In this case, we need to notify the organizer so that it can check if the
+     * original Task. In this case, we need to notify the organizer so that it can check if the
      * Activity matches any split rule.
      *
      * @param taskId            The Task that the activity is reparented to.
@@ -220,7 +235,7 @@
      *                          {@link WindowContainerTransaction} if needed.
      * @hide
      */
-    public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
+    public void onActivityReparentedToTask(int taskId, @NonNull Intent activityIntent,
             @NonNull IBinder activityToken) {}
 
     /**
@@ -280,8 +295,8 @@
                             errorBundle.getSerializable(KEY_ERROR_CALLBACK_EXCEPTION,
                                     java.lang.Throwable.class));
                     break;
-                case TYPE_ACTIVITY_REPARENT_TO_TASK:
-                    onActivityReparentToTask(
+                case TYPE_ACTIVITY_REPARENTED_TO_TASK:
+                    onActivityReparentedToTask(
                             change.getTaskId(),
                             change.getActivityIntent(),
                             change.getActivityToken());
diff --git a/core/java/android/window/TaskFragmentTransaction.java b/core/java/android/window/TaskFragmentTransaction.java
index 755864f..07e8e8c 100644
--- a/core/java/android/window/TaskFragmentTransaction.java
+++ b/core/java/android/window/TaskFragmentTransaction.java
@@ -122,7 +122,7 @@
      * then exits Picture-in-picture, it will be reparented back to its original Task. In this case,
      * we need to notify the organizer so that it can check if the Activity matches any split rule.
      */
-    public static final int TYPE_ACTIVITY_REPARENT_TO_TASK = 6;
+    public static final int TYPE_ACTIVITY_REPARENTED_TO_TASK = 6;
 
     @IntDef(prefix = { "TYPE_" }, value = {
             TYPE_TASK_FRAGMENT_APPEARED,
@@ -130,7 +130,7 @@
             TYPE_TASK_FRAGMENT_VANISHED,
             TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED,
             TYPE_TASK_FRAGMENT_ERROR,
-            TYPE_ACTIVITY_REPARENT_TO_TASK
+            TYPE_ACTIVITY_REPARENTED_TO_TASK
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface ChangeType {}
@@ -247,7 +247,7 @@
 
         /**
          * Intent of the activity that is reparented to the Task for
-         * {@link #TYPE_ACTIVITY_REPARENT_TO_TASK}.
+         * {@link #TYPE_ACTIVITY_REPARENTED_TO_TASK}.
          */
         public Change setActivityIntent(@NonNull Intent intent) {
             mActivityIntent = requireNonNull(intent);
@@ -255,7 +255,7 @@
         }
 
         /**
-         * Token of the reparent activity for {@link #TYPE_ACTIVITY_REPARENT_TO_TASK}.
+         * Token of the reparent activity for {@link #TYPE_ACTIVITY_REPARENTED_TO_TASK}.
          * If the activity belongs to the same process as the organizer, this will be the actual
          * activity token; if the activity belongs to a different process, the server will generate
          * a temporary token that the organizer can use to reparent the activity through
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index 962870e..6909965 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -17,25 +17,35 @@
 package com.android.internal.os;
 
 import android.annotation.Nullable;
-import android.os.BatteryStats;
+import android.os.BatteryManager;
+import android.os.BatteryStats.HistoryItem;
+import android.os.BatteryStats.HistoryStepDetails;
+import android.os.BatteryStats.HistoryTag;
 import android.os.Parcel;
+import android.os.ParcelFormatException;
+import android.os.Process;
 import android.os.StatFs;
 import android.os.SystemClock;
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ParseUtils;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
-import java.util.function.Supplier;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * BatteryStatsHistory encapsulates battery history files.
@@ -56,57 +66,62 @@
  * All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by
  * locks on BatteryStatsImpl object.
  */
-@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
 public class BatteryStatsHistory {
     private static final boolean DEBUG = false;
     private static final String TAG = "BatteryStatsHistory";
 
     // Current on-disk Parcel version. Must be updated when the format of the parcelable changes
-    public static final int VERSION = 208;
+    private static final int VERSION = 208;
 
-    public static final String HISTORY_DIR = "battery-history";
-    public static final String FILE_SUFFIX = ".bin";
+    private static final String HISTORY_DIR = "battery-history";
+    private static final String FILE_SUFFIX = ".bin";
     private static final int MIN_FREE_SPACE = 100 * 1024 * 1024;
+
     // Part of initial delta int that specifies the time delta.
-    public static final int DELTA_TIME_MASK = 0x7ffff;
-    public static final int DELTA_TIME_LONG = 0x7ffff;   // The delta is a following long
-    public static final int DELTA_TIME_INT = 0x7fffe;    // The delta is a following int
-    public static final int DELTA_TIME_ABS = 0x7fffd;    // Following is an entire abs update.
+    static final int DELTA_TIME_MASK = 0x7ffff;
+    static final int DELTA_TIME_LONG = 0x7ffff;   // The delta is a following long
+    static final int DELTA_TIME_INT = 0x7fffe;    // The delta is a following int
+    static final int DELTA_TIME_ABS = 0x7fffd;    // Following is an entire abs update.
     // Flag in delta int: a new battery level int follows.
-    public static final int DELTA_BATTERY_LEVEL_FLAG  = 0x00080000;
+    static final int DELTA_BATTERY_LEVEL_FLAG = 0x00080000;
     // Flag in delta int: a new full state and battery status int follows.
-    public static final int DELTA_STATE_FLAG          = 0x00100000;
+    static final int DELTA_STATE_FLAG = 0x00100000;
     // Flag in delta int: a new full state2 int follows.
-    public static final int DELTA_STATE2_FLAG         = 0x00200000;
+    static final int DELTA_STATE2_FLAG = 0x00200000;
     // Flag in delta int: contains a wakelock or wakeReason tag.
-    public static final int DELTA_WAKELOCK_FLAG       = 0x00400000;
+    static final int DELTA_WAKELOCK_FLAG = 0x00400000;
     // Flag in delta int: contains an event description.
-    public static final int DELTA_EVENT_FLAG          = 0x00800000;
+    static final int DELTA_EVENT_FLAG = 0x00800000;
     // Flag in delta int: contains the battery charge count in uAh.
-    public static final int DELTA_BATTERY_CHARGE_FLAG = 0x01000000;
+    static final int DELTA_BATTERY_CHARGE_FLAG = 0x01000000;
     // These upper bits are the frequently changing state bits.
-    public static final int DELTA_STATE_MASK          = 0xfe000000;
+    static final int DELTA_STATE_MASK = 0xfe000000;
     // These are the pieces of battery state that are packed in to the upper bits of
     // the state int that have been packed in to the first delta int.  They must fit
     // in STATE_BATTERY_MASK.
-    public static final int STATE_BATTERY_MASK         = 0xff000000;
-    public static final int STATE_BATTERY_STATUS_MASK  = 0x00000007;
-    public static final int STATE_BATTERY_STATUS_SHIFT = 29;
-    public static final int STATE_BATTERY_HEALTH_MASK  = 0x00000007;
-    public static final int STATE_BATTERY_HEALTH_SHIFT = 26;
-    public static final int STATE_BATTERY_PLUG_MASK    = 0x00000003;
-    public static final int STATE_BATTERY_PLUG_SHIFT   = 24;
+    static final int STATE_BATTERY_MASK = 0xff000000;
+    static final int STATE_BATTERY_STATUS_MASK = 0x00000007;
+    static final int STATE_BATTERY_STATUS_SHIFT = 29;
+    static final int STATE_BATTERY_HEALTH_MASK = 0x00000007;
+    static final int STATE_BATTERY_HEALTH_SHIFT = 26;
+    static final int STATE_BATTERY_PLUG_MASK = 0x00000003;
+    static final int STATE_BATTERY_PLUG_SHIFT = 24;
     // We use the low bit of the battery state int to indicate that we have full details
     // from a battery level change.
-    public static final int BATTERY_DELTA_LEVEL_FLAG   = 0x00000001;
+    static final int BATTERY_DELTA_LEVEL_FLAG = 0x00000001;
     // Flag in history tag index: indicates that this is the first occurrence of this tag,
     // therefore the tag value is written in the parcel
-    public static final int TAG_FIRST_OCCURRENCE_FLAG = 0x8000;
+    static final int TAG_FIRST_OCCURRENCE_FLAG = 0x8000;
 
-    @Nullable
-    private final Supplier<Integer> mMaxHistoryFiles;
     private final Parcel mHistoryBuffer;
+    private final File mSystemDir;
+    private final HistoryStepDetailsCalculator mStepDetailsCalculator;
     private final File mHistoryDir;
+    private final Clock mClock;
+
+    private int mMaxHistoryFiles;
+    private int mMaxHistoryBufferSize;
+
     /**
      * The active history file that the history buffer is backed up into.
      */
@@ -144,19 +159,77 @@
      */
     private int mParcelIndex = 0;
 
+    private final ReentrantLock mWriteLock = new ReentrantLock();
+
+    private final HistoryItem mHistoryCur = new HistoryItem();
+
+    private boolean mHaveBatteryLevel;
+    private boolean mRecordingHistory;
+
+    private static final int HISTORY_TAG_INDEX_LIMIT = 0x7ffe;
+    private static final int MAX_HISTORY_TAG_STRING_LENGTH = 1024;
+
+    private final HashMap<HistoryTag, Integer> mHistoryTagPool = new HashMap<>();
+    private SparseArray<HistoryTag> mHistoryTags;
+    private final HistoryItem mHistoryLastWritten = new HistoryItem();
+    private final HistoryItem mHistoryLastLastWritten = new HistoryItem();
+    private final HistoryItem mHistoryAddTmp = new HistoryItem();
+    private int mNextHistoryTagIdx = 0;
+    private int mNumHistoryTagChars = 0;
+    private int mHistoryBufferLastPos = -1;
+    private int mActiveHistoryStates = 0xffffffff;
+    private int mActiveHistoryStates2 = 0xffffffff;
+    private long mLastHistoryElapsedRealtimeMs = 0;
+    private long mTrackRunningHistoryElapsedRealtimeMs = 0;
+    private long mTrackRunningHistoryUptimeMs = 0;
+    private long mHistoryBaseTimeMs;
+
+    private byte mLastHistoryStepLevel = 0;
+
+    private BatteryStatsHistoryIterator mBatteryStatsHistoryIterator;
+
+    /**
+     * A delegate responsible for computing additional details for a step in battery history.
+     */
+    public interface HistoryStepDetailsCalculator {
+        /**
+         * Returns additional details for the current history step or null.
+         */
+        @Nullable
+        HistoryStepDetails getHistoryStepDetails();
+
+        /**
+         * Resets the calculator to get ready for a new battery session
+         */
+        void clear();
+    }
+
     /**
      * Constructor
      *
-     * @param historyBuffer   The in-memory history buffer.
-     * @param systemDir       typically /data/system
-     * @param maxHistoryFiles the largest number of history buffer files to keep
+     * @param systemDir            typically /data/system
+     * @param maxHistoryFiles      the largest number of history buffer files to keep
+     * @param maxHistoryBufferSize the most amount of RAM to used for buffering of history steps
      */
-    public BatteryStatsHistory(Parcel historyBuffer, File systemDir,
-            Supplier<Integer> maxHistoryFiles) {
-        mHistoryBuffer = historyBuffer;
-        mHistoryDir = new File(systemDir, HISTORY_DIR);
-        mMaxHistoryFiles = maxHistoryFiles;
+    public BatteryStatsHistory(File systemDir, int maxHistoryFiles, int maxHistoryBufferSize,
+            HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock) {
+        this(Parcel.obtain(), systemDir, maxHistoryFiles, maxHistoryBufferSize,
+                stepDetailsCalculator, clock);
+        initHistoryBuffer();
+    }
 
+    @VisibleForTesting
+    public BatteryStatsHistory(Parcel historyBuffer, File systemDir,
+            int maxHistoryFiles, int maxHistoryBufferSize,
+            HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock) {
+        mHistoryBuffer = historyBuffer;
+        mSystemDir = systemDir;
+        mMaxHistoryFiles = maxHistoryFiles;
+        mMaxHistoryBufferSize = maxHistoryBufferSize;
+        mStepDetailsCalculator = stepDetailsCalculator;
+        mClock = clock;
+
+        mHistoryDir = new File(systemDir, HISTORY_DIR);
         mHistoryDir.mkdirs();
         if (!mHistoryDir.exists()) {
             Slog.wtf(TAG, "HistoryDir does not exist:" + mHistoryDir.getPath());
@@ -192,19 +265,81 @@
         }
     }
 
-    /**
-     * Used when BatteryStatsImpl object is created from deserialization of a parcel,
-     * such as Settings app or checkin file.
-     * @param historyBuffer the history buffer
-     */
-    public BatteryStatsHistory(Parcel historyBuffer) {
+    public BatteryStatsHistory(HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock) {
+        mStepDetailsCalculator = stepDetailsCalculator;
+        mClock = clock;
+
+        mHistoryBuffer = Parcel.obtain();
+        mSystemDir = null;
         mHistoryDir = null;
-        mHistoryBuffer = historyBuffer;
-        mMaxHistoryFiles = null;
+        initHistoryBuffer();
     }
 
-    public File getHistoryDirectory() {
-        return mHistoryDir;
+    /**
+     * Used when BatteryStatsImpl object is created from deserialization of a parcel,
+     * such as a checkin file.
+     */
+    private BatteryStatsHistory(Parcel historyBuffer,
+            HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock) {
+        mHistoryBuffer = historyBuffer;
+        mClock = clock;
+        mSystemDir = null;
+        mHistoryDir = null;
+        mStepDetailsCalculator = stepDetailsCalculator;
+    }
+
+    private void initHistoryBuffer() {
+        mHistoryBaseTimeMs = 0;
+        mLastHistoryElapsedRealtimeMs = 0;
+        mTrackRunningHistoryElapsedRealtimeMs = 0;
+        mTrackRunningHistoryUptimeMs = 0;
+
+        mHistoryBuffer.setDataSize(0);
+        mHistoryBuffer.setDataPosition(0);
+        mHistoryBuffer.setDataCapacity(mMaxHistoryBufferSize / 2);
+        mHistoryLastLastWritten.clear();
+        mHistoryLastWritten.clear();
+        mHistoryTagPool.clear();
+        mNextHistoryTagIdx = 0;
+        mNumHistoryTagChars = 0;
+        mHistoryBufferLastPos = -1;
+        mActiveHistoryStates = 0xffffffff;
+        mActiveHistoryStates2 = 0xffffffff;
+        if (mStepDetailsCalculator != null) {
+            mStepDetailsCalculator.clear();
+        }
+    }
+
+    /**
+     * Changes the maximum number of history files to be kept.
+     */
+    public void setMaxHistoryFiles(int maxHistoryFiles) {
+        mMaxHistoryFiles = maxHistoryFiles;
+    }
+
+    /**
+     * Changes the maximum size of the history buffer, in bytes.
+     */
+    public void setMaxHistoryBufferSize(int maxHistoryBufferSize) {
+        mMaxHistoryBufferSize = maxHistoryBufferSize;
+    }
+
+    /**
+     * Creates a read-only copy of the battery history.  Does not copy the files stored
+     * in the system directory, so it is not safe while actively writing history.
+     */
+    public BatteryStatsHistory copy() {
+        // Make a copy of battery history to avoid concurrent modification.
+        Parcel historyBuffer = Parcel.obtain();
+        historyBuffer.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
+        return new BatteryStatsHistory(historyBuffer, mSystemDir, 0, 0, null, null);
+    }
+
+    /**
+     * Returns true if this instance only supports reading history.
+     */
+    public boolean isReadOnly() {
+        return mActiveFile == null;
     }
 
     /**
@@ -221,12 +356,13 @@
 
     /**
      * Create history AtomicFile from file number.
+     *
      * @param num file number.
      * @return AtomicFile object.
      */
     private AtomicFile getFile(int num) {
         return new AtomicFile(
-                new File(mHistoryDir,  num + FILE_SUFFIX));
+                new File(mHistoryDir, num + FILE_SUFFIX));
     }
 
     /**
@@ -234,7 +370,7 @@
      * create next history file.
      */
     public void startNextFile() {
-        if (mMaxHistoryFiles == null) {
+        if (mMaxHistoryFiles == 0) {
             Slog.wtf(TAG, "mMaxHistoryFiles should not be zero when writing history");
             return;
         }
@@ -264,7 +400,7 @@
         // if there are more history files than allowed, delete oldest history files.
         // mMaxHistoryFiles comes from Constants.MAX_HISTORY_FILES and can be updated by GService
         // config at run time.
-        while (mFileNumbers.size() > mMaxHistoryFiles.get()) {
+        while (mFileNumbers.size() > mMaxHistoryFiles) {
             int oldest = mFileNumbers.get(0);
             getFile(oldest).delete();
             mFileNumbers.remove(0);
@@ -272,36 +408,43 @@
     }
 
     /**
-     * Delete all existing history files. Active history file start from number 0 again.
+     * Clear history buffer and delete all existing history files. Active history file start from
+     * number 0 again.
      */
-    public void resetAllFiles() {
+    public void reset() {
+        if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!");
         for (Integer i : mFileNumbers) {
             getFile(i).delete();
         }
         mFileNumbers.clear();
         mFileNumbers.add(0);
         setActiveFile(0);
+
+        initHistoryBuffer();
     }
 
     /**
      * Start iterating history files and history buffer.
+     *
      * @return always return true.
      */
-    public boolean startIteratingHistory() {
+    public BatteryStatsHistoryIterator iterate() {
         mRecordCount = 0;
         mCurrentFileIndex = 0;
         mCurrentParcel = null;
         mCurrentParcelEnd = 0;
         mParcelIndex = 0;
-        return true;
+        mBatteryStatsHistoryIterator = new BatteryStatsHistoryIterator(this);
+        return mBatteryStatsHistoryIterator;
     }
 
     /**
      * Finish iterating history files and history buffer.
      */
-    public void finishIteratingHistory() {
+    void finishIteratingHistory() {
         // setDataPosition so mHistoryBuffer Parcel can be written.
         mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
+        mBatteryStatsHistoryIterator = null;
         if (DEBUG) {
             Slog.d(TAG, "Battery history records iterated: " + mRecordCount);
         }
@@ -311,11 +454,12 @@
      * When iterating history files and history buffer, always start from the lowest numbered
      * history file, when reached the mActiveFile (highest numbered history file), do not read from
      * mActiveFile, read from history buffer instead because the buffer has more updated data.
+     *
      * @param out a history item.
      * @return The parcel that has next record. null if finished all history files and history
-     *         buffer
+     * buffer
      */
-    public Parcel getNextParcel(BatteryStats.HistoryItem out) {
+    public Parcel getNextParcel(HistoryItem out) {
         if (mRecordCount == 0) {
             // reset out if it is the first record.
             out.clear();
@@ -323,8 +467,7 @@
         ++mRecordCount;
 
         // First iterate through all records in current parcel.
-        if (mCurrentParcel != null)
-        {
+        if (mCurrentParcel != null) {
             if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
                 // There are more records in current parcel.
                 return mCurrentParcel;
@@ -389,7 +532,8 @@
 
     /**
      * Read history file into a parcel.
-     * @param out the Parcel read into.
+     *
+     * @param out  the Parcel read into.
      * @param file the File to read from.
      * @return true if success, false otherwise.
      */
@@ -402,8 +546,8 @@
                 Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
                         + " duration ms:" + (SystemClock.uptimeMillis() - start));
             }
-        } catch(Exception e) {
-            Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
+        } catch (Exception e) {
+            Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
             return false;
         }
         out.unmarshall(raw, 0, raw.length);
@@ -413,6 +557,7 @@
 
     /**
      * Skip the header part of history parcel.
+     *
      * @param p history parcel to skip head.
      * @return true if version match, false if not.
      */
@@ -428,18 +573,68 @@
     }
 
     /**
+     * Writes the battery history contents for persistence.
+     */
+    public void writeSummaryToParcel(Parcel out, boolean inclHistory) {
+        out.writeBoolean(inclHistory);
+        if (inclHistory) {
+            writeToParcel(out);
+        }
+
+        out.writeInt(mHistoryTagPool.size());
+        for (Map.Entry<HistoryTag, Integer> ent : mHistoryTagPool.entrySet()) {
+            HistoryTag tag = ent.getKey();
+            out.writeInt(ent.getValue());
+            out.writeString(tag.string);
+            out.writeInt(tag.uid);
+        }
+    }
+
+    /**
+     * Reads battery history contents from a persisted parcel.
+     */
+    public void readSummaryFromParcel(Parcel in) {
+        boolean inclHistory = in.readBoolean();
+        if (inclHistory) {
+            readFromParcel(in);
+        }
+
+        mHistoryTagPool.clear();
+        mNextHistoryTagIdx = 0;
+        mNumHistoryTagChars = 0;
+
+        int numTags = in.readInt();
+        for (int i = 0; i < numTags; i++) {
+            int idx = in.readInt();
+            String str = in.readString();
+            int uid = in.readInt();
+            HistoryTag tag = new HistoryTag();
+            tag.string = str;
+            tag.uid = uid;
+            tag.poolIdx = idx;
+            mHistoryTagPool.put(tag, idx);
+            if (idx >= mNextHistoryTagIdx) {
+                mNextHistoryTagIdx = idx + 1;
+            }
+            mNumHistoryTagChars += tag.string.length() + 1;
+        }
+    }
+
+    /**
      * Read all history files and serialize into a big Parcel.
      * Checkin file calls this method.
      *
      * @param out the output parcel
      */
     public void writeToParcel(Parcel out) {
+        writeHistoryBuffer(out);
         writeToParcel(out, false /* useBlobs */);
     }
 
     /**
      * This is for Settings app, when Settings app receives big history parcel, it call
      * this method to parse it into list of parcels.
+     *
      * @param out the output parcel
      */
     public void writeToBatteryUsageStatsParcel(Parcel out) {
@@ -450,13 +645,13 @@
     private void writeToParcel(Parcel out, boolean useBlobs) {
         final long start = SystemClock.uptimeMillis();
         out.writeInt(mFileNumbers.size() - 1);
-        for(int i = 0;  i < mFileNumbers.size() - 1; i++) {
+        for (int i = 0; i < mFileNumbers.size() - 1; i++) {
             AtomicFile file = getFile(mFileNumbers.get(i));
             byte[] raw = new byte[0];
             try {
                 raw = file.readFully();
-            } catch(Exception e) {
-                Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
+            } catch (Exception e) {
+                Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
             }
             if (useBlobs) {
                 out.writeBlob(raw);
@@ -480,17 +675,55 @@
         Parcel historyBuffer = Parcel.obtain();
         historyBuffer.unmarshall(historyBlob, 0, historyBlob.length);
 
-        BatteryStatsHistory history = new BatteryStatsHistory(historyBuffer);
+        BatteryStatsHistory history = new BatteryStatsHistory(historyBuffer, null,
+                Clock.SYSTEM_CLOCK);
         history.readFromParcel(in, true /* useBlobs */);
         return history;
     }
 
     /**
+     * Read history from a check-in file.
+     */
+    public boolean readSummary() {
+        if (mActiveFile == null) {
+            Slog.w(TAG, "readSummary: no history file associated with this instance");
+            return false;
+        }
+
+        Parcel parcel = Parcel.obtain();
+        try {
+            final long start = SystemClock.uptimeMillis();
+            if (mActiveFile.exists()) {
+                byte[] raw = mActiveFile.readFully();
+                if (raw.length > 0) {
+                    parcel.unmarshall(raw, 0, raw.length);
+                    parcel.setDataPosition(0);
+                    readHistoryBuffer(parcel);
+                }
+                if (DEBUG) {
+                    Slog.d(TAG, "read history file::"
+                            + mActiveFile.getBaseFile().getPath()
+                            + " bytes:" + raw.length + " took ms:" + (SystemClock.uptimeMillis()
+                            - start));
+                }
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Error reading battery history", e);
+            reset();
+            return false;
+        } finally {
+            parcel.recycle();
+        }
+        return true;
+    }
+
+    /**
      * This is for the check-in file, which has all history files embedded.
      *
      * @param in the input parcel.
      */
     public void readFromParcel(Parcel in) {
+        readHistoryBuffer(in);
         readFromParcel(in, false /* useBlobs */);
     }
 
@@ -498,7 +731,7 @@
         final long start = SystemClock.uptimeMillis();
         mHistoryParcels = new ArrayList<>();
         final int count = in.readInt();
-        for(int i = 0; i < count; i++) {
+        for (int i = 0; i < count; i++) {
             byte[] temp = useBlobs ? in.readBlob() : in.createByteArray();
             if (temp == null || temp.length == 0) {
                 continue;
@@ -521,10 +754,12 @@
         return stats.getAvailableBytes() > MIN_FREE_SPACE;
     }
 
+    @VisibleForTesting
     public List<Integer> getFilesNumbers() {
         return mFileNumbers;
     }
 
+    @VisibleForTesting
     public AtomicFile getActiveFile() {
         return mActiveFile;
     }
@@ -534,15 +769,972 @@
      */
     public int getHistoryUsedSize() {
         int ret = 0;
-        for(int i = 0; i < mFileNumbers.size() - 1; i++) {
+        for (int i = 0; i < mFileNumbers.size() - 1; i++) {
             ret += getFile(mFileNumbers.get(i)).getBaseFile().length();
         }
         ret += mHistoryBuffer.dataSize();
         if (mHistoryParcels != null) {
-            for(int i = 0; i < mHistoryParcels.size(); i++) {
+            for (int i = 0; i < mHistoryParcels.size(); i++) {
                 ret += mHistoryParcels.get(i).dataSize();
             }
         }
         return ret;
     }
+
+    /**
+     * Enables/disables recording of history.  When disabled, all "record*" calls are a no-op.
+     */
+    public void setHistoryRecordingEnabled(boolean enabled) {
+        mRecordingHistory = enabled;
+    }
+
+    /**
+     * Returns true if history recording is enabled.
+     */
+    public boolean isRecordingHistory() {
+        return mRecordingHistory;
+    }
+
+    /**
+     * Forces history recording regardless of charging state.
+     */
+    @VisibleForTesting
+    public void forceRecordAllHistory() {
+        mHaveBatteryLevel = true;
+        mRecordingHistory = true;
+    }
+
+    /**
+     * Starts a history buffer by recording the current wall-clock time.
+     */
+    public void startRecordingHistory(final long elapsedRealtimeMs, final long uptimeMs,
+            boolean reset) {
+        mRecordingHistory = true;
+        mHistoryCur.currentTime = mClock.currentTimeMillis();
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur,
+                reset ? HistoryItem.CMD_RESET : HistoryItem.CMD_CURRENT_TIME);
+        mHistoryCur.currentTime = 0;
+    }
+
+    /**
+     * Prepares to continue recording after restoring previous history from persistent storage.
+     */
+    public void continueRecordingHistory() {
+        if (mHistoryBuffer.dataPosition() <= 0 && mFileNumbers.size() <= 1) {
+            return;
+        }
+
+        mRecordingHistory = true;
+        final long elapsedRealtimeMs = mClock.elapsedRealtime();
+        final long uptimeMs = mClock.uptimeMillis();
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_START);
+        startRecordingHistory(elapsedRealtimeMs, uptimeMs, false);
+    }
+
+    /**
+     * Notes the current battery state to be reflected in the next written history item.
+     */
+    public void setBatteryState(boolean charging, int status, int level, int chargeUah) {
+        mHaveBatteryLevel = true;
+        setChargingState(charging);
+        mHistoryCur.batteryStatus = (byte) status;
+        mHistoryCur.batteryLevel = (byte) level;
+        mHistoryCur.batteryChargeUah = chargeUah;
+    }
+
+    /**
+     * Notes the current battery state to be reflected in the next written history item.
+     */
+    public void setBatteryState(int status, int level, int health, int plugType, int temperature,
+            int voltageMv, int chargeUah) {
+        mHaveBatteryLevel = true;
+        mHistoryCur.batteryStatus = (byte) status;
+        mHistoryCur.batteryLevel = (byte) level;
+        mHistoryCur.batteryHealth = (byte) health;
+        mHistoryCur.batteryPlugType = (byte) plugType;
+        mHistoryCur.batteryTemperature = (short) temperature;
+        mHistoryCur.batteryVoltage = (char) voltageMv;
+        mHistoryCur.batteryChargeUah = chargeUah;
+    }
+
+    /**
+     * Notes the current power plugged-in state to be reflected in the next written history item.
+     */
+    public void setPluggedInState(boolean pluggedIn) {
+        if (pluggedIn) {
+            mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+        } else {
+            mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+        }
+    }
+
+    /**
+     * Notes the current battery charging state to be reflected in the next written history item.
+     */
+    public void setChargingState(boolean charging) {
+        if (charging) {
+            mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG;
+        } else {
+            mHistoryCur.states2 &= ~HistoryItem.STATE2_CHARGING_FLAG;
+        }
+    }
+
+    /**
+     * Records a history event with the given code, name and UID.
+     */
+    public void recordEvent(long elapsedRealtimeMs, long uptimeMs, int code, String name,
+            int uid) {
+        mHistoryCur.eventCode = code;
+        mHistoryCur.eventTag = mHistoryCur.localEventTag;
+        mHistoryCur.eventTag.string = name;
+        mHistoryCur.eventTag.uid = uid;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records a time change event.
+     */
+    public void recordCurrentTimeChange(long elapsedRealtimeMs, long uptimeMs, long currentTimeMs) {
+        if (!mRecordingHistory) {
+            return;
+        }
+
+        mHistoryCur.currentTime = currentTimeMs;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur,
+                HistoryItem.CMD_CURRENT_TIME);
+        mHistoryCur.currentTime = 0;
+    }
+
+    /**
+     * Records a system shutdown event.
+     */
+    public void recordShutdownEvent(long elapsedRealtimeMs, long uptimeMs, long currentTimeMs) {
+        if (!mRecordingHistory) {
+            return;
+        }
+
+        mHistoryCur.currentTime = currentTimeMs;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_SHUTDOWN);
+        mHistoryCur.currentTime = 0;
+    }
+
+    /**
+     * Records a battery state change event.
+     */
+    public void recordBatteryState(long elapsedRealtimeMs, long uptimeMs, int batteryLevel,
+            boolean isPlugged) {
+        mHistoryCur.batteryLevel = (byte) batteryLevel;
+        setPluggedInState(isPlugged);
+        if (DEBUG) {
+            Slog.v(TAG, "Battery unplugged to: "
+                    + Integer.toHexString(mHistoryCur.states));
+        }
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records a history item with the amount of charge consumed by WiFi.  Used on certain devices
+     * equipped with on-device power metering.
+     */
+    public void recordWifiConsumedCharge(long elapsedRealtimeMs, long uptimeMs,
+            double monitoredRailChargeMah) {
+        mHistoryCur.wifiRailChargeMah += monitoredRailChargeMah;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records a wakelock start event.
+     */
+    public void recordWakelockStartEvent(long elapsedRealtimeMs, long uptimeMs, String historyName,
+            int uid) {
+        mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
+        mHistoryCur.wakelockTag.string = historyName;
+        mHistoryCur.wakelockTag.uid = uid;
+        recordStateStartEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG);
+    }
+
+    /**
+     * Updates the previous history event with a wakelock name and UID.
+     */
+    public boolean maybeUpdateWakelockTag(long elapsedRealtimeMs, long uptimeMs, String historyName,
+            int uid) {
+        if (mHistoryLastWritten.cmd != HistoryItem.CMD_UPDATE) {
+            return false;
+        }
+        if (mHistoryLastWritten.wakelockTag != null) {
+            // We'll try to update the last tag.
+            mHistoryLastWritten.wakelockTag = null;
+            mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
+            mHistoryCur.wakelockTag.string = historyName;
+            mHistoryCur.wakelockTag.uid = uid;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
+        return true;
+    }
+
+    /**
+     * Records an event when some state flag changes to true.
+     */
+    public void recordStateStartEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
+        mHistoryCur.states |= stateFlags;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records an event when some state flag changes to false.
+     */
+    public void recordStateStopEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
+        mHistoryCur.states &= ~stateFlags;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records an event when some state flags change to true and some to false.
+     */
+    public void recordStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int stateStartFlags,
+            int stateStopFlags) {
+        mHistoryCur.states = (mHistoryCur.states | stateStartFlags) & ~stateStopFlags;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records an event when some state2 flag changes to true.
+     */
+    public void recordState2StartEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
+        mHistoryCur.states2 |= stateFlags;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records an event when some state2 flag changes to false.
+     */
+    public void recordState2StopEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
+        mHistoryCur.states2 &= ~stateFlags;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records an wakeup event.
+     */
+    public void recordWakeupEvent(long elapsedRealtimeMs, long uptimeMs, String reason) {
+        mHistoryCur.wakeReasonTag = mHistoryCur.localWakeReasonTag;
+        mHistoryCur.wakeReasonTag.string = reason;
+        mHistoryCur.wakeReasonTag.uid = 0;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records a screen brightness change event.
+     */
+    public void recordScreenBrightnessEvent(long elapsedRealtimeMs, long uptimeMs,
+            int brightnessBin) {
+        mHistoryCur.states = (mHistoryCur.states & ~HistoryItem.STATE_BRIGHTNESS_MASK)
+                | (brightnessBin << HistoryItem.STATE_BRIGHTNESS_SHIFT);
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records a GNSS signal level change event.
+     */
+    public void recordGpsSignalQualityEvent(long elapsedRealtimeMs, long uptimeMs,
+            int signalLevel) {
+        mHistoryCur.states2 = (mHistoryCur.states2 & ~HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK)
+                | (signalLevel << HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT);
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records a device idle mode change event.
+     */
+    public void recordDeviceIdleEvent(long elapsedRealtimeMs, long uptimeMs, int mode) {
+        mHistoryCur.states2 = (mHistoryCur.states2 & ~HistoryItem.STATE2_DEVICE_IDLE_MASK)
+                | (mode << HistoryItem.STATE2_DEVICE_IDLE_SHIFT);
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records a telephony state change event.
+     */
+    public void recordPhoneStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int addStateFlag,
+            int removeStateFlag, int state, int signalStrength) {
+        mHistoryCur.states = (mHistoryCur.states | addStateFlag) & ~removeStateFlag;
+        if (state != -1) {
+            mHistoryCur.states =
+                    (mHistoryCur.states & ~HistoryItem.STATE_PHONE_STATE_MASK)
+                            | (state << HistoryItem.STATE_PHONE_STATE_SHIFT);
+        }
+        if (signalStrength != -1) {
+            mHistoryCur.states =
+                    (mHistoryCur.states & ~HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK)
+                            | (signalStrength << HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT);
+        }
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records a data connection type change event.
+     */
+    public void recordDataConnectionTypeChangeEvent(long elapsedRealtimeMs, long uptimeMs,
+            int dataConnectionType) {
+        mHistoryCur.states = (mHistoryCur.states & ~HistoryItem.STATE_DATA_CONNECTION_MASK)
+                | (dataConnectionType << HistoryItem.STATE_DATA_CONNECTION_SHIFT);
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records a WiFi supplicant state change event.
+     */
+    public void recordWifiSupplicantStateChangeEvent(long elapsedRealtimeMs, long uptimeMs,
+            int supplState) {
+        mHistoryCur.states2 =
+                (mHistoryCur.states2 & ~HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK)
+                        | (supplState << HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT);
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Records a WiFi signal strength change event.
+     */
+    public void recordWifiSignalStrengthChangeEvent(long elapsedRealtimeMs, long uptimeMs,
+            int strengthBin) {
+        mHistoryCur.states2 =
+                (mHistoryCur.states2 & ~HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK)
+                        | (strengthBin << HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT);
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+    }
+
+    /**
+     * Writes the current history item to history.
+     */
+    public void writeHistoryItem(long elapsedRealtimeMs, long uptimeMs) {
+        if (mTrackRunningHistoryElapsedRealtimeMs != 0) {
+            final long diffElapsedMs = elapsedRealtimeMs - mTrackRunningHistoryElapsedRealtimeMs;
+            final long diffUptimeMs = uptimeMs - mTrackRunningHistoryUptimeMs;
+            if (diffUptimeMs < (diffElapsedMs - 20)) {
+                final long wakeElapsedTimeMs = elapsedRealtimeMs - (diffElapsedMs - diffUptimeMs);
+                mHistoryAddTmp.setTo(mHistoryLastWritten);
+                mHistoryAddTmp.wakelockTag = null;
+                mHistoryAddTmp.wakeReasonTag = null;
+                mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE;
+                mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG;
+                writeHistoryItem(wakeElapsedTimeMs, uptimeMs, mHistoryAddTmp);
+            }
+        }
+        mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG;
+        mTrackRunningHistoryElapsedRealtimeMs = elapsedRealtimeMs;
+        mTrackRunningHistoryUptimeMs = uptimeMs;
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur);
+    }
+
+    private void writeHistoryItem(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) {
+        if (!mHaveBatteryLevel || !mRecordingHistory) {
+            return;
+        }
+
+        final long timeDiffMs = (mHistoryBaseTimeMs + elapsedRealtimeMs) - mHistoryLastWritten.time;
+        final int diffStates = mHistoryLastWritten.states ^ (cur.states & mActiveHistoryStates);
+        final int diffStates2 = mHistoryLastWritten.states2 ^ (cur.states2 & mActiveHistoryStates2);
+        final int lastDiffStates = mHistoryLastWritten.states ^ mHistoryLastLastWritten.states;
+        final int lastDiffStates2 = mHistoryLastWritten.states2 ^ mHistoryLastLastWritten.states2;
+        if (DEBUG) {
+            Slog.i(TAG, "ADD: tdelta=" + timeDiffMs + " diff="
+                    + Integer.toHexString(diffStates) + " lastDiff="
+                    + Integer.toHexString(lastDiffStates) + " diff2="
+                    + Integer.toHexString(diffStates2) + " lastDiff2="
+                    + Integer.toHexString(lastDiffStates2));
+        }
+        if (mHistoryBufferLastPos >= 0 && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE
+                && timeDiffMs < 1000 && (diffStates & lastDiffStates) == 0
+                && (diffStates2 & lastDiffStates2) == 0
+                && (!mHistoryLastWritten.tagsFirstOccurrence && !cur.tagsFirstOccurrence)
+                && (mHistoryLastWritten.wakelockTag == null || cur.wakelockTag == null)
+                && (mHistoryLastWritten.wakeReasonTag == null || cur.wakeReasonTag == null)
+                && mHistoryLastWritten.stepDetails == null
+                && (mHistoryLastWritten.eventCode == HistoryItem.EVENT_NONE
+                || cur.eventCode == HistoryItem.EVENT_NONE)
+                && mHistoryLastWritten.batteryLevel == cur.batteryLevel
+                && mHistoryLastWritten.batteryStatus == cur.batteryStatus
+                && mHistoryLastWritten.batteryHealth == cur.batteryHealth
+                && mHistoryLastWritten.batteryPlugType == cur.batteryPlugType
+                && mHistoryLastWritten.batteryTemperature == cur.batteryTemperature
+                && mHistoryLastWritten.batteryVoltage == cur.batteryVoltage) {
+            // We can merge this new change in with the last one.  Merging is
+            // allowed as long as only the states have changed, and within those states
+            // as long as no bit has changed both between now and the last entry, as
+            // well as the last entry and the one before it (so we capture any toggles).
+            if (DEBUG) Slog.i(TAG, "ADD: rewinding back to " + mHistoryBufferLastPos);
+            mHistoryBuffer.setDataSize(mHistoryBufferLastPos);
+            mHistoryBuffer.setDataPosition(mHistoryBufferLastPos);
+            mHistoryBufferLastPos = -1;
+            elapsedRealtimeMs = mHistoryLastWritten.time - mHistoryBaseTimeMs;
+            // If the last written history had a wakelock tag, we need to retain it.
+            // Note that the condition above made sure that we aren't in a case where
+            // both it and the current history item have a wakelock tag.
+            if (mHistoryLastWritten.wakelockTag != null) {
+                cur.wakelockTag = cur.localWakelockTag;
+                cur.wakelockTag.setTo(mHistoryLastWritten.wakelockTag);
+            }
+            // If the last written history had a wake reason tag, we need to retain it.
+            // Note that the condition above made sure that we aren't in a case where
+            // both it and the current history item have a wakelock tag.
+            if (mHistoryLastWritten.wakeReasonTag != null) {
+                cur.wakeReasonTag = cur.localWakeReasonTag;
+                cur.wakeReasonTag.setTo(mHistoryLastWritten.wakeReasonTag);
+            }
+            // If the last written history had an event, we need to retain it.
+            // Note that the condition above made sure that we aren't in a case where
+            // both it and the current history item have an event.
+            if (mHistoryLastWritten.eventCode != HistoryItem.EVENT_NONE) {
+                cur.eventCode = mHistoryLastWritten.eventCode;
+                cur.eventTag = cur.localEventTag;
+                cur.eventTag.setTo(mHistoryLastWritten.eventTag);
+            }
+            mHistoryLastWritten.setTo(mHistoryLastLastWritten);
+        }
+        final int dataSize = mHistoryBuffer.dataSize();
+
+        if (dataSize >= mMaxHistoryBufferSize) {
+            if (mMaxHistoryBufferSize == 0) {
+                Slog.wtf(TAG, "mMaxHistoryBufferSize should not be zero when writing history");
+                mMaxHistoryBufferSize = 1024;
+            }
+
+            //open a new history file.
+            final long start = SystemClock.uptimeMillis();
+            writeHistory();
+            if (DEBUG) {
+                Slog.d(TAG, "addHistoryBufferLocked writeHistory took ms:"
+                        + (SystemClock.uptimeMillis() - start));
+            }
+            startNextFile();
+            mHistoryBuffer.setDataSize(0);
+            mHistoryBuffer.setDataPosition(0);
+            mHistoryBuffer.setDataCapacity(mMaxHistoryBufferSize / 2);
+            mHistoryBufferLastPos = -1;
+            mHistoryLastWritten.clear();
+            mHistoryLastLastWritten.clear();
+
+            // Mark every entry in the pool with a flag indicating that the tag
+            // has not yet been encountered while writing the current history buffer.
+            for (Map.Entry<HistoryTag, Integer> entry : mHistoryTagPool.entrySet()) {
+                entry.setValue(entry.getValue() | BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG);
+            }
+            // Make a copy of mHistoryCur.
+            HistoryItem copy = new HistoryItem();
+            copy.setTo(cur);
+            // startRecordingHistory will reset mHistoryCur.
+            startRecordingHistory(elapsedRealtimeMs, uptimeMs, false);
+            // Add the copy into history buffer.
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs, copy, HistoryItem.CMD_UPDATE);
+            return;
+        }
+
+        if (dataSize == 0) {
+            // The history is currently empty; we need it to start with a time stamp.
+            cur.currentTime = mClock.currentTimeMillis();
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs, cur, HistoryItem.CMD_RESET);
+        }
+        writeHistoryItem(elapsedRealtimeMs, uptimeMs, cur, HistoryItem.CMD_UPDATE);
+    }
+
+    private void writeHistoryItem(long elapsedRealtimeMs,
+            @SuppressWarnings("UnusedVariable") long uptimeMs, HistoryItem cur, byte cmd) {
+        if (mBatteryStatsHistoryIterator != null) {
+            throw new IllegalStateException("Can't do this while iterating history!");
+        }
+        mHistoryBufferLastPos = mHistoryBuffer.dataPosition();
+        mHistoryLastLastWritten.setTo(mHistoryLastWritten);
+        final boolean hasTags = mHistoryLastWritten.tagsFirstOccurrence || cur.tagsFirstOccurrence;
+        mHistoryLastWritten.setTo(mHistoryBaseTimeMs + elapsedRealtimeMs, cmd, cur);
+        mHistoryLastWritten.tagsFirstOccurrence = hasTags;
+        mHistoryLastWritten.states &= mActiveHistoryStates;
+        mHistoryLastWritten.states2 &= mActiveHistoryStates2;
+        writeHistoryDelta(mHistoryBuffer, mHistoryLastWritten, mHistoryLastLastWritten);
+        mLastHistoryElapsedRealtimeMs = elapsedRealtimeMs;
+        cur.wakelockTag = null;
+        cur.wakeReasonTag = null;
+        cur.eventCode = HistoryItem.EVENT_NONE;
+        cur.eventTag = null;
+        cur.tagsFirstOccurrence = false;
+        if (DEBUG) {
+            Slog.i(TAG, "Writing history buffer: was " + mHistoryBufferLastPos
+                    + " now " + mHistoryBuffer.dataPosition()
+                    + " size is now " + mHistoryBuffer.dataSize());
+        }
+    }
+
+    /*
+        The history delta format uses flags to denote further data in subsequent ints in the parcel.
+
+        There is always the first token, which may contain the delta time, or an indicator of
+        the length of the time (int or long) following this token.
+
+        First token: always present,
+        31              23              15               7             0
+        â–ˆM|L|K|J|I|H|G|Fâ–ˆE|D|C|B|A|T|T|Tâ–ˆT|T|T|T|T|T|T|Tâ–ˆT|T|T|T|T|T|T|Tâ–ˆ
+
+        T: the delta time if it is <= 0x7fffd. Otherwise 0x7fffe indicates an int immediately
+           follows containing the time, and 0x7ffff indicates a long immediately follows with the
+           delta time.
+        A: battery level changed and an int follows with battery data.
+        B: state changed and an int follows with state change data.
+        C: state2 has changed and an int follows with state2 change data.
+        D: wakelock/wakereason has changed and an wakelock/wakereason struct follows.
+        E: event data has changed and an event struct follows.
+        F: battery charge in coulombs has changed and an int with the charge follows.
+        G: state flag denoting that the mobile radio was active.
+        H: state flag denoting that the wifi radio was active.
+        I: state flag denoting that a wifi scan occurred.
+        J: state flag denoting that a wifi full lock was held.
+        K: state flag denoting that the gps was on.
+        L: state flag denoting that a wakelock was held.
+        M: state flag denoting that the cpu was running.
+
+        Time int/long: if T in the first token is 0x7ffff or 0x7fffe, then an int or long follows
+        with the time delta.
+
+        Battery level int: if A in the first token is set,
+        31              23              15               7             0
+        â–ˆL|L|L|L|L|L|L|Tâ–ˆT|T|T|T|T|T|T|Tâ–ˆT|V|V|V|V|V|V|Vâ–ˆV|V|V|V|V|V|V|Dâ–ˆ
+
+        D: indicates that extra history details follow.
+        V: the battery voltage.
+        T: the battery temperature.
+        L: the battery level (out of 100).
+
+        State change int: if B in the first token is set,
+        31              23              15               7             0
+        â–ˆS|S|S|H|H|H|P|Pâ–ˆF|E|D|C|B| | |Aâ–ˆ | | | | | | | â–ˆ | | | | | | | â–ˆ
+
+        A: wifi multicast was on.
+        B: battery was plugged in.
+        C: screen was on.
+        D: phone was scanning for signal.
+        E: audio was on.
+        F: a sensor was active.
+
+        State2 change int: if C in the first token is set,
+        31              23              15               7             0
+        â–ˆM|L|K|J|I|H|H|Gâ–ˆF|E|D|C| | | | â–ˆ | | | | | | | â–ˆ |B|B|B|A|A|A|Aâ–ˆ
+
+        A: 4 bits indicating the wifi supplicant state: {@link BatteryStats#WIFI_SUPPL_STATE_NAMES}.
+        B: 3 bits indicating the wifi signal strength: 0, 1, 2, 3, 4.
+        C: a bluetooth scan was active.
+        D: the camera was active.
+        E: bluetooth was on.
+        F: a phone call was active.
+        G: the device was charging.
+        H: 2 bits indicating the device-idle (doze) state: off, light, full
+        I: the flashlight was on.
+        J: wifi was on.
+        K: wifi was running.
+        L: video was playing.
+        M: power save mode was on.
+
+        Wakelock/wakereason struct: if D in the first token is set,
+        Event struct: if E in the first token is set,
+        History step details struct: if D in the battery level int is set,
+
+        Battery charge int: if F in the first token is set, an int representing the battery charge
+        in coulombs follows.
+     */
+    /**
+     * Writes the delta between the previous and current history items into history buffer.
+     */
+    public void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) {
+        if (last == null || cur.cmd != HistoryItem.CMD_UPDATE) {
+            dest.writeInt(BatteryStatsHistory.DELTA_TIME_ABS);
+            cur.writeToParcel(dest, 0);
+            return;
+        }
+
+        final long deltaTime = cur.time - last.time;
+        final int lastBatteryLevelInt = buildBatteryLevelInt(last);
+        final int lastStateInt = buildStateInt(last);
+
+        int deltaTimeToken;
+        if (deltaTime < 0 || deltaTime > Integer.MAX_VALUE) {
+            deltaTimeToken = BatteryStatsHistory.DELTA_TIME_LONG;
+        } else if (deltaTime >= BatteryStatsHistory.DELTA_TIME_ABS) {
+            deltaTimeToken = BatteryStatsHistory.DELTA_TIME_INT;
+        } else {
+            deltaTimeToken = (int) deltaTime;
+        }
+        int firstToken = deltaTimeToken | (cur.states & BatteryStatsHistory.DELTA_STATE_MASK);
+        final int includeStepDetails = mLastHistoryStepLevel > cur.batteryLevel
+                ? BatteryStatsHistory.BATTERY_DELTA_LEVEL_FLAG : 0;
+        mLastHistoryStepLevel = cur.batteryLevel;
+        final int batteryLevelInt = buildBatteryLevelInt(cur) | includeStepDetails;
+        final boolean batteryLevelIntChanged = batteryLevelInt != lastBatteryLevelInt;
+        if (batteryLevelIntChanged) {
+            firstToken |= BatteryStatsHistory.DELTA_BATTERY_LEVEL_FLAG;
+        }
+        final int stateInt = buildStateInt(cur);
+        final boolean stateIntChanged = stateInt != lastStateInt;
+        if (stateIntChanged) {
+            firstToken |= BatteryStatsHistory.DELTA_STATE_FLAG;
+        }
+        final boolean state2IntChanged = cur.states2 != last.states2;
+        if (state2IntChanged) {
+            firstToken |= BatteryStatsHistory.DELTA_STATE2_FLAG;
+        }
+        if (cur.wakelockTag != null || cur.wakeReasonTag != null) {
+            firstToken |= BatteryStatsHistory.DELTA_WAKELOCK_FLAG;
+        }
+        if (cur.eventCode != HistoryItem.EVENT_NONE) {
+            firstToken |= BatteryStatsHistory.DELTA_EVENT_FLAG;
+        }
+
+        final boolean batteryChargeChanged = cur.batteryChargeUah != last.batteryChargeUah;
+        if (batteryChargeChanged) {
+            firstToken |= BatteryStatsHistory.DELTA_BATTERY_CHARGE_FLAG;
+        }
+        dest.writeInt(firstToken);
+        if (DEBUG) {
+            Slog.i(TAG, "WRITE DELTA: firstToken=0x" + Integer.toHexString(firstToken)
+                    + " deltaTime=" + deltaTime);
+        }
+
+        if (deltaTimeToken >= BatteryStatsHistory.DELTA_TIME_INT) {
+            if (deltaTimeToken == BatteryStatsHistory.DELTA_TIME_INT) {
+                if (DEBUG) Slog.i(TAG, "WRITE DELTA: int deltaTime=" + (int) deltaTime);
+                dest.writeInt((int) deltaTime);
+            } else {
+                if (DEBUG) Slog.i(TAG, "WRITE DELTA: long deltaTime=" + deltaTime);
+                dest.writeLong(deltaTime);
+            }
+        }
+        if (batteryLevelIntChanged) {
+            dest.writeInt(batteryLevelInt);
+            if (DEBUG) {
+                Slog.i(TAG, "WRITE DELTA: batteryToken=0x"
+                        + Integer.toHexString(batteryLevelInt)
+                        + " batteryLevel=" + cur.batteryLevel
+                        + " batteryTemp=" + cur.batteryTemperature
+                        + " batteryVolt=" + (int) cur.batteryVoltage);
+            }
+        }
+        if (stateIntChanged) {
+            dest.writeInt(stateInt);
+            if (DEBUG) {
+                Slog.i(TAG, "WRITE DELTA: stateToken=0x"
+                        + Integer.toHexString(stateInt)
+                        + " batteryStatus=" + cur.batteryStatus
+                        + " batteryHealth=" + cur.batteryHealth
+                        + " batteryPlugType=" + cur.batteryPlugType
+                        + " states=0x" + Integer.toHexString(cur.states));
+            }
+        }
+        if (state2IntChanged) {
+            dest.writeInt(cur.states2);
+            if (DEBUG) {
+                Slog.i(TAG, "WRITE DELTA: states2=0x"
+                        + Integer.toHexString(cur.states2));
+            }
+        }
+        if (cur.wakelockTag != null || cur.wakeReasonTag != null) {
+            int wakeLockIndex;
+            int wakeReasonIndex;
+            if (cur.wakelockTag != null) {
+                wakeLockIndex = writeHistoryTag(cur.wakelockTag);
+                if (DEBUG) {
+                    Slog.i(TAG, "WRITE DELTA: wakelockTag=#" + cur.wakelockTag.poolIdx
+                            + " " + cur.wakelockTag.uid + ":" + cur.wakelockTag.string);
+                }
+            } else {
+                wakeLockIndex = 0xffff;
+            }
+            if (cur.wakeReasonTag != null) {
+                wakeReasonIndex = writeHistoryTag(cur.wakeReasonTag);
+                if (DEBUG) {
+                    Slog.i(TAG, "WRITE DELTA: wakeReasonTag=#" + cur.wakeReasonTag.poolIdx
+                            + " " + cur.wakeReasonTag.uid + ":" + cur.wakeReasonTag.string);
+                }
+            } else {
+                wakeReasonIndex = 0xffff;
+            }
+            dest.writeInt((wakeReasonIndex << 16) | wakeLockIndex);
+            if (cur.wakelockTag != null
+                    && (wakeLockIndex & BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG) != 0) {
+                cur.wakelockTag.writeToParcel(dest, 0);
+                cur.tagsFirstOccurrence = true;
+            }
+            if (cur.wakeReasonTag != null
+                    && (wakeReasonIndex & BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG) != 0) {
+                cur.wakeReasonTag.writeToParcel(dest, 0);
+                cur.tagsFirstOccurrence = true;
+            }
+        }
+        if (cur.eventCode != HistoryItem.EVENT_NONE) {
+            final int index = writeHistoryTag(cur.eventTag);
+            final int codeAndIndex = (cur.eventCode & 0xffff) | (index << 16);
+            dest.writeInt(codeAndIndex);
+            if ((index & BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG) != 0) {
+                cur.eventTag.writeToParcel(dest, 0);
+                cur.tagsFirstOccurrence = true;
+            }
+            if (DEBUG) {
+                Slog.i(TAG, "WRITE DELTA: event=" + cur.eventCode + " tag=#"
+                        + cur.eventTag.poolIdx + " " + cur.eventTag.uid + ":"
+                        + cur.eventTag.string);
+            }
+        }
+
+        cur.stepDetails = mStepDetailsCalculator.getHistoryStepDetails();
+        if (includeStepDetails != 0) {
+            cur.stepDetails.writeToParcel(dest);
+        }
+
+        if (batteryChargeChanged) {
+            if (DEBUG) Slog.i(TAG, "WRITE DELTA: batteryChargeUah=" + cur.batteryChargeUah);
+            dest.writeInt(cur.batteryChargeUah);
+        }
+        dest.writeDouble(cur.modemRailChargeMah);
+        dest.writeDouble(cur.wifiRailChargeMah);
+    }
+
+    private int buildBatteryLevelInt(HistoryItem h) {
+        return ((((int) h.batteryLevel) << 25) & 0xfe000000)
+                | ((((int) h.batteryTemperature) << 15) & 0x01ff8000)
+                | ((((int) h.batteryVoltage) << 1) & 0x00007ffe);
+    }
+
+    private int buildStateInt(HistoryItem h) {
+        int plugType = 0;
+        if ((h.batteryPlugType & BatteryManager.BATTERY_PLUGGED_AC) != 0) {
+            plugType = 1;
+        } else if ((h.batteryPlugType & BatteryManager.BATTERY_PLUGGED_USB) != 0) {
+            plugType = 2;
+        } else if ((h.batteryPlugType & BatteryManager.BATTERY_PLUGGED_WIRELESS) != 0) {
+            plugType = 3;
+        }
+        return ((h.batteryStatus & BatteryStatsHistory.STATE_BATTERY_STATUS_MASK)
+                << BatteryStatsHistory.STATE_BATTERY_STATUS_SHIFT)
+                | ((h.batteryHealth & BatteryStatsHistory.STATE_BATTERY_HEALTH_MASK)
+                << BatteryStatsHistory.STATE_BATTERY_HEALTH_SHIFT)
+                | ((plugType & BatteryStatsHistory.STATE_BATTERY_PLUG_MASK)
+                << BatteryStatsHistory.STATE_BATTERY_PLUG_SHIFT)
+                | (h.states & (~BatteryStatsHistory.STATE_BATTERY_MASK));
+    }
+
+    /**
+     * Returns the index for the specified tag. If this is the first time the tag is encountered
+     * while writing the current history buffer, the method returns
+     * <code>(index | TAG_FIRST_OCCURRENCE_FLAG)</code>
+     */
+    private int writeHistoryTag(HistoryTag tag) {
+        if (tag.string == null) {
+            Slog.wtfStack(TAG, "writeHistoryTag called with null name");
+        }
+
+        final int stringLength = tag.string.length();
+        if (stringLength > MAX_HISTORY_TAG_STRING_LENGTH) {
+            Slog.e(TAG, "Long battery history tag: " + tag.string);
+            tag.string = tag.string.substring(0, MAX_HISTORY_TAG_STRING_LENGTH);
+        }
+
+        Integer idxObj = mHistoryTagPool.get(tag);
+        int idx;
+        if (idxObj != null) {
+            idx = idxObj;
+            if ((idx & BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG) != 0) {
+                mHistoryTagPool.put(tag, idx & ~BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG);
+            }
+            return idx;
+        } else if (mNextHistoryTagIdx < HISTORY_TAG_INDEX_LIMIT) {
+            idx = mNextHistoryTagIdx;
+            HistoryTag key = new HistoryTag();
+            key.setTo(tag);
+            tag.poolIdx = idx;
+            mHistoryTagPool.put(key, idx);
+            mNextHistoryTagIdx++;
+
+            mNumHistoryTagChars += stringLength + 1;
+            if (mHistoryTags != null) {
+                mHistoryTags.put(idx, key);
+            }
+            return idx | BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG;
+        } else {
+            // Tag pool overflow: include the tag itself in the parcel
+            return HISTORY_TAG_INDEX_LIMIT | BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG;
+        }
+    }
+
+    /**
+     * Don't allow any more batching in to the current history event.
+     */
+    public void commitCurrentHistoryBatchLocked() {
+        mHistoryLastWritten.cmd = HistoryItem.CMD_NULL;
+    }
+
+    /**
+     * Saves the accumulated history buffer in the active file, see {@link #getActiveFile()} .
+     */
+    public void writeHistory() {
+        if (mActiveFile == null) {
+            Slog.w(TAG, "writeHistory: no history file associated with this instance");
+            return;
+        }
+
+        Parcel p = Parcel.obtain();
+        try {
+            final long start = SystemClock.uptimeMillis();
+            writeHistoryBuffer(p);
+            if (DEBUG) {
+                Slog.d(TAG, "writeHistoryBuffer duration ms:"
+                        + (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize());
+            }
+            writeParcelToFileLocked(p, mActiveFile);
+        } finally {
+            p.recycle();
+        }
+    }
+
+    /**
+     * Reads history buffer from a persisted Parcel.
+     */
+    public void readHistoryBuffer(Parcel in) throws ParcelFormatException {
+        final int version = in.readInt();
+        if (version != BatteryStatsHistory.VERSION) {
+            Slog.w("BatteryStats", "readHistoryBuffer: version got " + version
+                    + ", expected " + BatteryStatsHistory.VERSION + "; erasing old stats");
+            return;
+        }
+
+        final long historyBaseTime = in.readLong();
+
+        mHistoryBuffer.setDataSize(0);
+        mHistoryBuffer.setDataPosition(0);
+
+        int bufSize = in.readInt();
+        int curPos = in.dataPosition();
+        if (bufSize >= (mMaxHistoryBufferSize * 100)) {
+            throw new ParcelFormatException(
+                    "File corrupt: history data buffer too large " + bufSize);
+        } else if ((bufSize & ~3) != bufSize) {
+            throw new ParcelFormatException(
+                    "File corrupt: history data buffer not aligned " + bufSize);
+        } else {
+            if (DEBUG) {
+                Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize
+                        + " bytes at " + curPos);
+            }
+            mHistoryBuffer.appendFrom(in, curPos, bufSize);
+            in.setDataPosition(curPos + bufSize);
+        }
+
+        if (DEBUG) {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("****************** OLD mHistoryBaseTimeMs: ");
+            TimeUtils.formatDuration(mHistoryBaseTimeMs, sb);
+            Slog.i(TAG, sb.toString());
+        }
+        mHistoryBaseTimeMs = historyBaseTime;
+        if (DEBUG) {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("****************** NEW mHistoryBaseTimeMs: ");
+            TimeUtils.formatDuration(mHistoryBaseTimeMs, sb);
+            Slog.i(TAG, sb.toString());
+        }
+
+        // We are just arbitrarily going to insert 1 minute from the sample of
+        // the last run until samples in this run.
+        if (mHistoryBaseTimeMs > 0) {
+            long oldnow = mClock.elapsedRealtime();
+            mHistoryBaseTimeMs = mHistoryBaseTimeMs - oldnow + 1;
+            if (DEBUG) {
+                StringBuilder sb = new StringBuilder(128);
+                sb.append("****************** ADJUSTED mHistoryBaseTimeMs: ");
+                TimeUtils.formatDuration(mHistoryBaseTimeMs, sb);
+                Slog.i(TAG, sb.toString());
+            }
+        }
+    }
+
+    private void writeHistoryBuffer(Parcel out) {
+        if (DEBUG) {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("****************** WRITING mHistoryBaseTimeMs: ");
+            TimeUtils.formatDuration(mHistoryBaseTimeMs, sb);
+            sb.append(" mLastHistoryElapsedRealtimeMs: ");
+            TimeUtils.formatDuration(mLastHistoryElapsedRealtimeMs, sb);
+            Slog.i(TAG, sb.toString());
+        }
+        out.writeInt(BatteryStatsHistory.VERSION);
+        out.writeLong(mHistoryBaseTimeMs + mLastHistoryElapsedRealtimeMs);
+        out.writeInt(mHistoryBuffer.dataSize());
+        if (DEBUG) {
+            Slog.i(TAG, "***************** WRITING HISTORY: "
+                    + mHistoryBuffer.dataSize() + " bytes at " + out.dataPosition());
+        }
+        out.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
+    }
+
+    private void writeParcelToFileLocked(Parcel p, AtomicFile file) {
+        FileOutputStream fos = null;
+        mWriteLock.lock();
+        try {
+            final long startTimeMs = SystemClock.uptimeMillis();
+            fos = file.startWrite();
+            fos.write(p.marshall());
+            fos.flush();
+            file.finishWrite(fos);
+            if (DEBUG) {
+                Slog.d(TAG, "writeParcelToFileLocked file:" + file.getBaseFile().getPath()
+                        + " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs)
+                        + " bytes:" + p.dataSize());
+            }
+            com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
+                    "batterystats", SystemClock.uptimeMillis() - startTimeMs);
+        } catch (IOException e) {
+            Slog.w(TAG, "Error writing battery statistics", e);
+            file.failWrite(fos);
+        } finally {
+            mWriteLock.unlock();
+        }
+    }
+
+    /**
+     * Returns the total number of history tags in the tag pool.
+     */
+    public int getHistoryStringPoolSize() {
+        return mHistoryTagPool.size();
+    }
+
+    /**
+     * Returns the total number of bytes occupied by the history tag pool.
+     */
+    public int getHistoryStringPoolBytes() {
+        return mNumHistoryTagChars;
+    }
+
+    /**
+     * Returns the string held by the requested history tag.
+     */
+    public String getHistoryTagPoolString(int index) {
+        ensureHistoryTagArray();
+        HistoryTag historyTag = mHistoryTags.get(index);
+        return historyTag != null ? historyTag.string : null;
+    }
+
+    /**
+     * Returns the UID held by the requested history tag.
+     */
+    public int getHistoryTagPoolUid(int index) {
+        ensureHistoryTagArray();
+        HistoryTag historyTag = mHistoryTags.get(index);
+        return historyTag != null ? historyTag.uid : Process.INVALID_UID;
+    }
+
+    private void ensureHistoryTagArray() {
+        if (mHistoryTags != null) {
+            return;
+        }
+
+        mHistoryTags = new SparseArray<>(mHistoryTagPool.size());
+        for (Map.Entry<HistoryTag, Integer> entry : mHistoryTagPool.entrySet()) {
+            mHistoryTags.put(entry.getValue() & ~BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG,
+                    entry.getKey());
+        }
+    }
 }
diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
index de8b414..1bf878cb 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
@@ -36,7 +36,6 @@
 
     public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history) {
         mBatteryStatsHistory = history;
-        mBatteryStatsHistory.startIteratingHistory();
     }
 
     /**
@@ -231,4 +230,11 @@
         out.batteryTemperature = (short) ((batteryLevelInt & 0x01ff8000) >>> 15);
         out.batteryVoltage = (char) ((batteryLevelInt & 0x00007ffe) >>> 1);
     }
+
+    /**
+     * Should be called when iteration is complete.
+     */
+    public void close() {
+        mBatteryStatsHistory.finishIteratingHistory();
+    }
 }
diff --git a/core/java/com/android/internal/os/BinderCallsStats.java b/core/java/com/android/internal/os/BinderCallsStats.java
index fa6fa55..0a29fc52 100644
--- a/core/java/com/android/internal/os/BinderCallsStats.java
+++ b/core/java/com/android/internal/os/BinderCallsStats.java
@@ -1169,15 +1169,7 @@
 
     }
 
-    /** @hide */
-    public static void startForWifi(Context context) {
-        new BinderCallsStats.SettingsObserver(
-            context,
-            new BinderCallsStats(
-                new BinderCallsStats.Injector(),
-                com.android.internal.os.BinderLatencyProto.Dims.WIFI));
 
-    }
 
     /**
      * Settings observer for other processes (not system_server).
diff --git a/core/java/com/android/internal/view/inline/InlineTooltipUi.java b/core/java/com/android/internal/view/inline/InlineTooltipUi.java
index 3eae89e..836786d 100644
--- a/core/java/com/android/internal/view/inline/InlineTooltipUi.java
+++ b/core/java/com/android/internal/view/inline/InlineTooltipUi.java
@@ -170,9 +170,9 @@
 
             int delayTimeMs = mShowDelayConfigMs;
             try {
-                final float scale = Settings.Global.getFloat(
+                final float scale = WindowManager.fixScale(Settings.Global.getFloat(
                         anchor.getContext().getContentResolver(),
-                        Settings.Global.ANIMATOR_DURATION_SCALE);
+                        Settings.Global.ANIMATOR_DURATION_SCALE));
                 delayTimeMs *= scale;
             } catch (Settings.SettingNotFoundException e) {
                 // do nothing
diff --git a/core/jni/android_hardware_HardwareBuffer.cpp b/core/jni/android_hardware_HardwareBuffer.cpp
index f462523..5fcc46e 100644
--- a/core/jni/android_hardware_HardwareBuffer.cpp
+++ b/core/jni/android_hardware_HardwareBuffer.cpp
@@ -163,7 +163,7 @@
 static jlong android_hardware_HardwareBuffer_getUsage(JNIEnv* env,
     jobject clazz, jlong nativeObject) {
     GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject);
-    return AHardwareBuffer_convertFromGrallocUsageBits(buffer->getUsage());
+    return static_cast<jlong>(AHardwareBuffer_convertFromGrallocUsageBits(buffer->getUsage()));
 }
 
 static jlong android_hardware_HardwareBuffer_estimateSize(jlong nativeObject) {
@@ -177,7 +177,12 @@
 
     const uint32_t bufferStride =
             buffer->getStride() > 0 ? buffer->getStride() : buffer->getWidth();
-    return static_cast<jlong>(buffer->getHeight() * bufferStride * bpp);
+    return static_cast<jlong>(static_cast<uint64_t>(buffer->getHeight() * bufferStride * bpp));
+}
+
+static jlong android_hardware_HardwareBuffer_getId(jlong nativeObject) {
+    GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject);
+    return static_cast<jlong>(buffer->getId());
 }
 
 // ----------------------------------------------------------------------------
@@ -223,16 +228,6 @@
     }
 }
 
-GraphicBuffer* android_hardware_HardwareBuffer_getNativeGraphicBuffer(
-        JNIEnv* env, jobject hardwareBufferObj) {
-    if (env->IsInstanceOf(hardwareBufferObj, gHardwareBufferClassInfo.clazz)) {
-        return GraphicBufferWrapper_to_GraphicBuffer(
-                env->GetLongField(hardwareBufferObj, gHardwareBufferClassInfo.mNativeObject));
-    } else {
-        return nullptr;
-    }
-}
-
 jobject android_hardware_HardwareBuffer_createFromAHardwareBuffer(
         JNIEnv* env, AHardwareBuffer* hardwareBuffer) {
     GraphicBuffer* buffer = AHardwareBuffer_to_GraphicBuffer(hardwareBuffer);
@@ -295,6 +290,7 @@
 
     // --------------- @CriticalNative ----------------------
     { "nEstimateSize", "(J)J",  (void*) android_hardware_HardwareBuffer_estimateSize },
+    { "nGetId", "(J)J",  (void*) android_hardware_HardwareBuffer_getId },
 };
 // clang-format on
 
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index 9501c8d..b9d5ee4 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -1252,20 +1252,6 @@
     return fd;
 }
 
-void android_os_Process_freezeCgroupUID(JNIEnv* env, jobject clazz, jint uid, jboolean freeze) {
-    bool success = true;
-
-    if (freeze) {
-        success = SetUserProfiles(uid, {"Frozen"});
-    } else {
-        success = SetUserProfiles(uid, {"Unfrozen"});
-    }
-
-    if (!success) {
-        jniThrowRuntimeException(env, "Could not apply user profile");
-    }
-}
-
 static const JNINativeMethod methods[] = {
         {"getUidForName", "(Ljava/lang/String;)I", (void*)android_os_Process_getUidForName},
         {"getGidForName", "(Ljava/lang/String;)I", (void*)android_os_Process_getGidForName},
@@ -1307,7 +1293,6 @@
         {"killProcessGroup", "(II)I", (void*)android_os_Process_killProcessGroup},
         {"removeAllProcessGroups", "()V", (void*)android_os_Process_removeAllProcessGroups},
         {"nativePidFdOpen", "(II)I", (void*)android_os_Process_nativePidFdOpen},
-        {"freezeCgroupUid", "(IZ)V", (void*)android_os_Process_freezeCgroupUID},
 };
 
 int register_android_os_Process(JNIEnv* env)
diff --git a/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h b/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h
index dfd8035..964c28f 100644
--- a/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h
+++ b/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h
@@ -28,10 +28,6 @@
 extern AHardwareBuffer* android_hardware_HardwareBuffer_getNativeHardwareBuffer(
         JNIEnv* env, jobject hardwareBufferObj);
 
-/* Gets the underlying GraphicBuffer for a HardwareBuffer. */
-extern GraphicBuffer* android_hardware_HardwareBuffer_getNativeGraphicBuffer(
-        JNIEnv* env, jobject hardwareBufferObj);
-
 /* Returns a HardwareBuffer wrapper for the underlying AHardwareBuffer. */
 extern jobject android_hardware_HardwareBuffer_createFromAHardwareBuffer(
         JNIEnv* env, AHardwareBuffer* hardwareBuffer);
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 322354b..789ceff 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -89,6 +89,14 @@
         // Setting for accessibility magnification for following typing.
         optional SettingProto accessibility_magnification_follow_typing_enabled = 43 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto accessibility_software_cursor_enabled = 44 [ (android.privacy).dest = DEST_AUTOMATIC ];
+
+        message SoftwareCursorSettings {
+            optional SettingProto trigger_hints_enabled = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
+            optional SettingProto keyboard_shift_enabled = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        }
+
+        optional SoftwareCursorSettings accessibility_software_cursor_settings = 45 [ (android.privacy).dest = DEST_AUTOMATIC ];
+
     }
     optional Accessibility accessibility = 2;
 
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index ed6a649..6e59b83 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -236,6 +236,21 @@
     }
 
     @Test
+    public void testSystemDrivenInsetsAnimationLoggingListener_onReady() {
+        prepareControls();
+        // only the original thread that created view hierarchy can touch its views
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            WindowInsetsAnimationControlListener loggingListener =
+                    mock(WindowInsetsAnimationControlListener.class);
+            mController.setSystemDrivenInsetsAnimationLoggingListener(loggingListener);
+            mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
+            // since there is no focused view, forcefully make IME visible.
+            mController.show(Type.ime(), true /* fromIme */);
+            verify(loggingListener).onReady(notNull(), anyInt());
+        });
+    }
+
+    @Test
     public void testAnimationEndState() {
         InsetsSourceControl[] controls = prepareControls();
         InsetsSourceControl navBar = controls[0];
diff --git a/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java b/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java
index 03c8b1b..690b3587 100644
--- a/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java
@@ -213,6 +213,25 @@
     }
 
     @Test
+    public void testSystemDrivenInsetsAnimationLoggingListener() {
+        WindowInsetsAnimationControlListener listener =
+                mock(WindowInsetsAnimationControlListener.class);
+        mPendingInsetsController.setSystemDrivenInsetsAnimationLoggingListener(listener);
+        mPendingInsetsController.replayAndAttach(mReplayedController);
+        verify(mReplayedController).setSystemDrivenInsetsAnimationLoggingListener(eq(listener));
+    }
+
+    @Test
+    public void testSystemDrivenInsetsAnimationLoggingListener_direct() {
+        mPendingInsetsController.replayAndAttach(mReplayedController);
+        WindowInsetsAnimationControlListener listener =
+                mock(WindowInsetsAnimationControlListener.class);
+        mPendingInsetsController.setSystemDrivenInsetsAnimationLoggingListener(listener);
+        verify(mReplayedController).setSystemDrivenInsetsAnimationLoggingListener(
+                eq(listener));
+    }
+
+    @Test
     public void testDetachReattach() {
         mPendingInsetsController.show(systemBars());
         mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index d42fca2..9fb7d19 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -51,28 +51,37 @@
     @VisibleForTesting
     final Map<IBinder, TaskFragmentInfo> mFragmentInfos = new ArrayMap<>();
 
+    @NonNull
     private final TaskFragmentCallback mCallback;
+
     @VisibleForTesting
+    @Nullable
     TaskFragmentAnimationController mAnimationController;
 
     /**
      * Callback that notifies the controller about changes to task fragments.
      */
     interface TaskFragmentCallback {
-        void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo);
-        void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo);
-        void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo);
-        void onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig);
-        void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
-                @NonNull IBinder activityToken);
-        void onTaskFragmentError(@Nullable TaskFragmentInfo taskFragmentInfo, int opType);
+        void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
+                @NonNull TaskFragmentInfo taskFragmentInfo);
+        void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
+                @NonNull TaskFragmentInfo taskFragmentInfo);
+        void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
+                @NonNull TaskFragmentInfo taskFragmentInfo);
+        void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
+                int taskId, @NonNull Configuration parentConfig);
+        void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
+                int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken);
+        void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
+                @Nullable TaskFragmentInfo taskFragmentInfo, int opType);
     }
 
     /**
      * @param executor  callbacks from WM Core are posted on this executor. It should be tied to the
      *                  UI thread that all other calls into methods of this class are also on.
      */
-    JetpackTaskFragmentOrganizer(@NonNull Executor executor, TaskFragmentCallback callback) {
+    JetpackTaskFragmentOrganizer(@NonNull Executor executor,
+            @NonNull TaskFragmentCallback callback) {
         super(executor);
         mCallback = callback;
     }
@@ -147,41 +156,31 @@
      * @param wct WindowContainerTransaction in which the task fragment should be resized.
      * @param fragmentToken token of an existing TaskFragment.
      */
-    void expandTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) {
+    void expandTaskFragment(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken) {
         resizeTaskFragment(wct, fragmentToken, new Rect());
         setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */);
         updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
     }
 
     /**
-     * Expands an existing TaskFragment to fill parent.
-     * @param fragmentToken token of an existing TaskFragment.
-     */
-    void expandTaskFragment(IBinder fragmentToken) {
-        WindowContainerTransaction wct = new WindowContainerTransaction();
-        expandTaskFragment(wct, fragmentToken);
-        applyTransaction(wct);
-    }
-
-    /**
      * Expands an Activity to fill parent by moving it to a new TaskFragment.
      * @param fragmentToken token to create new TaskFragment with.
      * @param activity      activity to move to the fill-parent TaskFragment.
      */
-    void expandActivity(IBinder fragmentToken, Activity activity) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
+    void expandActivity(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
+            @NonNull Activity activity) {
         createTaskFragmentAndReparentActivity(
                 wct, fragmentToken, activity.getActivityToken(), new Rect(),
                 WINDOWING_MODE_UNDEFINED, activity);
-        applyTransaction(wct);
     }
 
     /**
      * @param ownerToken The token of the activity that creates this task fragment. It does not
      *                   have to be a child of this task fragment, but must belong to the same task.
      */
-    void createTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken,
-            IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
+    void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
+            @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
         final TaskFragmentCreationParams fragmentOptions =
                 createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode);
         wct.createTaskFragment(fragmentOptions);
@@ -191,9 +190,9 @@
      * @param ownerToken The token of the activity that creates this task fragment. It does not
      *                   have to be a child of this task fragment, but must belong to the same task.
      */
-    private void createTaskFragmentAndReparentActivity(
-            WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken,
-            @NonNull Rect bounds, @WindowingMode int windowingMode, Activity activity) {
+    private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds,
+            @WindowingMode int windowingMode, @NonNull Activity activity) {
         createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
         wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken());
     }
@@ -202,9 +201,9 @@
      * @param ownerToken The token of the activity that creates this task fragment. It does not
      *                   have to be a child of this task fragment, but must belong to the same task.
      */
-    private void createTaskFragmentAndStartActivity(
-            WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken,
-            @NonNull Rect bounds, @WindowingMode int windowingMode, Intent activityIntent,
+    private void createTaskFragmentAndStartActivity(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds,
+            @WindowingMode int windowingMode, @NonNull Intent activityIntent,
             @Nullable Bundle activityOptions) {
         createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
         wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions);
@@ -225,8 +224,8 @@
         wct.setAdjacentTaskFragments(primary, secondary, adjacentParams);
     }
 
-    TaskFragmentCreationParams createFragmentOptions(IBinder fragmentToken, IBinder ownerToken,
-            Rect bounds, @WindowingMode int windowingMode) {
+    TaskFragmentCreationParams createFragmentOptions(@NonNull IBinder fragmentToken,
+            @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
         if (mFragmentInfos.containsKey(fragmentToken)) {
             throw new IllegalArgumentException(
                     "There is an existing TaskFragment with fragmentToken=" + fragmentToken);
@@ -241,7 +240,7 @@
                 .build();
     }
 
-    void resizeTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken,
+    void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
             @Nullable Rect bounds) {
         if (!mFragmentInfos.containsKey(fragmentToken)) {
             throw new IllegalArgumentException(
@@ -253,8 +252,8 @@
         wct.setBounds(mFragmentInfos.get(fragmentToken).getToken(), bounds);
     }
 
-    void updateWindowingMode(WindowContainerTransaction wct, IBinder fragmentToken,
-            @WindowingMode int windowingMode) {
+    void updateWindowingMode(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken, @WindowingMode int windowingMode) {
         if (!mFragmentInfos.containsKey(fragmentToken)) {
             throw new IllegalArgumentException(
                     "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
@@ -262,7 +261,8 @@
         wct.setWindowingMode(mFragmentInfos.get(fragmentToken).getToken(), windowingMode);
     }
 
-    void deleteTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) {
+    void deleteTaskFragment(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken) {
         if (!mFragmentInfos.containsKey(fragmentToken)) {
             throw new IllegalArgumentException(
                     "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
@@ -272,59 +272,55 @@
 
     @Override
     public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
         final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
         mFragmentInfos.put(fragmentToken, taskFragmentInfo);
-
-        if (mCallback != null) {
-            mCallback.onTaskFragmentAppeared(taskFragmentInfo);
-        }
+        mCallback.onTaskFragmentAppeared(wct, taskFragmentInfo);
+        applyTransaction(wct);
     }
 
     @Override
     public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
         final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
         mFragmentInfos.put(fragmentToken, taskFragmentInfo);
-
-        if (mCallback != null) {
-            mCallback.onTaskFragmentInfoChanged(taskFragmentInfo);
-        }
+        mCallback.onTaskFragmentInfoChanged(wct, taskFragmentInfo);
+        applyTransaction(wct);
     }
 
     @Override
     public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
         mFragmentInfos.remove(taskFragmentInfo.getFragmentToken());
-
-        if (mCallback != null) {
-            mCallback.onTaskFragmentVanished(taskFragmentInfo);
-        }
+        mCallback.onTaskFragmentVanished(wct, taskFragmentInfo);
+        applyTransaction(wct);
     }
 
     @Override
     public void onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig) {
-        if (mCallback != null) {
-            mCallback.onTaskFragmentParentInfoChanged(taskId, parentConfig);
-        }
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mCallback.onTaskFragmentParentInfoChanged(wct, taskId, parentConfig);
+        applyTransaction(wct);
     }
 
     @Override
-    public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
+    public void onActivityReparentedToTask(int taskId, @NonNull Intent activityIntent,
             @NonNull IBinder activityToken) {
-        if (mCallback != null) {
-            mCallback.onActivityReparentToTask(taskId, activityIntent, activityToken);
-        }
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mCallback.onActivityReparentedToTask(wct, taskId, activityIntent, activityToken);
+        applyTransaction(wct);
     }
 
     @Override
     public void onTaskFragmentError(@NonNull IBinder errorCallbackToken,
             @Nullable TaskFragmentInfo taskFragmentInfo,
             int opType, @NonNull Throwable exception) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (taskFragmentInfo != null) {
             final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
             mFragmentInfos.put(fragmentToken, taskFragmentInfo);
         }
-
-        if (mCallback != null) {
-            mCallback.onTaskFragmentError(taskFragmentInfo, opType);
-        }
+        mCallback.onTaskFragmentError(wct, taskFragmentInfo, opType);
+        applyTransaction(wct);
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index f09a910..c8ac0fc 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -16,17 +16,21 @@
 
 package androidx.window.extensions.embedding;
 
-import android.annotation.NonNull;
 import android.app.Activity;
 import android.util.Pair;
 import android.util.Size;
 
+import androidx.annotation.NonNull;
+
 /**
  * Client-side descriptor of a split that holds two containers.
  */
 class SplitContainer {
+    @NonNull
     private final TaskFragmentContainer mPrimaryContainer;
+    @NonNull
     private final TaskFragmentContainer mSecondaryContainer;
+    @NonNull
     private final SplitRule mSplitRule;
 
     SplitContainer(@NonNull TaskFragmentContainer primaryContainer,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index dad0739..0597809f 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -145,35 +145,36 @@
     }
 
     @Override
-    public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
+    public void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         synchronized (mLock) {
             TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
             if (container == null) {
                 return;
             }
 
-            container.setInfo(taskFragmentInfo);
+            container.setInfo(wct, taskFragmentInfo);
             if (container.isFinished()) {
-                mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+                mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
             } else {
                 // Update with the latest Task configuration.
-                mPresenter.updateContainer(container);
+                updateContainer(wct, container);
             }
             updateCallbackIfNecessary();
         }
     }
 
     @Override
-    public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
+    public void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         synchronized (mLock) {
             TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
             if (container == null) {
                 return;
             }
 
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
             final boolean wasInPip = isInPictureInPicture(container);
-            container.setInfo(taskFragmentInfo);
+            container.setInfo(wct, taskFragmentInfo);
             final boolean isInPip = isInPictureInPicture(container);
             // Check if there are no running activities - consider the container empty if there are
             // no non-finishing activities left.
@@ -183,15 +184,15 @@
                     // Instead, the original split should be cleanup, and the dependent may be
                     // expanded to fullscreen.
                     cleanupForEnterPip(wct, container);
-                    mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
+                    mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
                 } else if (taskFragmentInfo.isTaskClearedForReuse()) {
                     // Do not finish the dependents if this TaskFragment was cleared due to
                     // launching activity in the Task.
-                    mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
+                    mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
                 } else if (!container.isWaitingActivityAppear()) {
                     // Do not finish the container before the expected activity appear until
                     // timeout.
-                    mPresenter.cleanupContainer(container, true /* shouldFinishDependent */, wct);
+                    mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */);
                 }
             } else if (wasInPip && isInPip) {
                 // No update until exit PIP.
@@ -208,13 +209,13 @@
                 // needed.
                 updateContainer(wct, container);
             }
-            mPresenter.applyTransaction(wct);
             updateCallbackIfNecessary();
         }
     }
 
     @Override
-    public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
+    public void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         synchronized (mLock) {
             final TaskFragmentContainer container = getContainer(
                     taskFragmentInfo.getFragmentToken());
@@ -225,9 +226,7 @@
                 final TaskFragmentContainer newTopContainer = getTopActiveContainer(
                         container.getTaskId());
                 if (newTopContainer != null) {
-                    final WindowContainerTransaction wct = new WindowContainerTransaction();
                     updateContainer(wct, newTopContainer);
-                    mPresenter.applyTransaction(wct);
                 }
                 updateCallbackIfNecessary();
             }
@@ -236,7 +235,8 @@
     }
 
     @Override
-    public void onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig) {
+    public void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Configuration parentConfig) {
         synchronized (mLock) {
             onTaskConfigurationChanged(taskId, parentConfig);
             if (isInPictureInPicture(parentConfig)) {
@@ -256,7 +256,7 @@
                 final TaskFragmentContainer container = containers.get(i);
                 // Wait until onTaskFragmentAppeared to update new container.
                 if (!container.isFinished() && !container.isWaitingActivityAppear()) {
-                    mPresenter.updateContainer(container);
+                    updateContainer(wct, container);
                 }
             }
             updateCallbackIfNecessary();
@@ -264,7 +264,8 @@
     }
 
     @Override
-    public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
+    public void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Intent activityIntent,
             @NonNull IBinder activityToken) {
         synchronized (mLock) {
             // If the activity belongs to the current app process, we treat it as a new activity
@@ -275,10 +276,10 @@
                 // launching to top. We allow split as primary for activity reparent because the
                 // activity may be split as primary before it is reparented out. In that case, we
                 // want to show it as primary again when it is reparented back.
-                if (!resolveActivityToContainer(activity, true /* isOnReparent */)) {
+                if (!resolveActivityToContainer(wct, activity, true /* isOnReparent */)) {
                     // When there is no embedding rule matched, try to place it in the top container
                     // like a normal launch.
-                    placeActivityInTopContainer(activity);
+                    placeActivityInTopContainer(wct, activity);
                 }
                 updateCallbackIfNecessary();
                 return;
@@ -293,7 +294,6 @@
             // If the activity belongs to a different app process, we treat it as starting new
             // intent, since both actions might result in a new activity that should appear in an
             // organized TaskFragment.
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
             TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId,
                     activityIntent, null /* launchingActivity */);
             if (targetContainer == null) {
@@ -306,14 +306,14 @@
             }
             wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
                     activityToken);
-            mPresenter.applyTransaction(wct);
             // Because the activity does not belong to the organizer process, we wait until
             // onTaskFragmentAppeared to trigger updateCallbackIfNecessary().
         }
     }
 
     @Override
-    public void onTaskFragmentError(@Nullable TaskFragmentInfo taskFragmentInfo, int opType) {
+    public void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
+            @Nullable TaskFragmentInfo taskFragmentInfo, int opType) {
         synchronized (mLock) {
             switch (opType) {
                 case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
@@ -329,10 +329,11 @@
                     }
 
                     // Update the latest taskFragmentInfo and perform necessary clean-up
-                    container.setInfo(taskFragmentInfo);
+                    container.setInfo(wct, taskFragmentInfo);
                     container.clearPendingAppearedActivities();
                     if (container.isEmpty()) {
-                        mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+                        mPresenter.cleanupContainer(wct, container,
+                                false /* shouldFinishDependent */);
                     }
                     break;
                 }
@@ -343,7 +344,7 @@
         }
     }
 
-    /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */
+    /** Called on receiving {@link #onTaskFragmentVanished} for cleanup. */
     private void cleanupTaskFragment(@NonNull IBinder taskFragmentToken) {
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
             final TaskContainer taskContainer = mTaskContainers.valueAt(i);
@@ -422,10 +423,12 @@
     }
 
     @VisibleForTesting
-    void onActivityCreated(@NonNull Activity launchedActivity) {
+    @GuardedBy("mLock")
+    void onActivityCreated(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity launchedActivity) {
         // TODO(b/229680885): we don't support launching into primary yet because we want to always
         // launch the new activity on top.
-        resolveActivityToContainer(launchedActivity, false /* isOnReparent */);
+        resolveActivityToContainer(wct, launchedActivity, false /* isOnReparent */);
         updateCallbackIfNecessary();
     }
 
@@ -440,7 +443,8 @@
      */
     @VisibleForTesting
     @GuardedBy("mLock")
-    boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) {
+    boolean resolveActivityToContainer(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity activity, boolean isOnReparent) {
         if (isInPictureInPicture(activity) || activity.isFinishing()) {
             // We don't embed activity when it is in PIP, or finishing. Return true since we don't
             // want any extra handling.
@@ -472,12 +476,12 @@
 
         // 1. Whether the new launched activity should always expand.
         if (shouldExpand(activity, null /* intent */)) {
-            expandActivity(activity);
+            expandActivity(wct, activity);
             return true;
         }
 
         // 2. Whether the new launched activity should launch a placeholder.
-        if (launchPlaceholderIfNecessary(activity, !isOnReparent)) {
+        if (launchPlaceholderIfNecessary(wct, activity, !isOnReparent)) {
             return true;
         }
 
@@ -492,11 +496,11 @@
             // Can't find any activity below.
             return false;
         }
-        if (putActivitiesIntoSplitIfNecessary(activityBelow, activity)) {
+        if (putActivitiesIntoSplitIfNecessary(wct, activityBelow, activity)) {
             // Have split rule of [ activityBelow | launchedActivity ].
             return true;
         }
-        if (isOnReparent && putActivitiesIntoSplitIfNecessary(activity, activityBelow)) {
+        if (isOnReparent && putActivitiesIntoSplitIfNecessary(wct, activity, activityBelow)) {
             // Have split rule of [ launchedActivity | activityBelow].
             return true;
         }
@@ -519,19 +523,20 @@
             // Can't find the top activity on the other split TaskFragment.
             return false;
         }
-        if (putActivitiesIntoSplitIfNecessary(otherTopActivity, activity)) {
+        if (putActivitiesIntoSplitIfNecessary(wct, otherTopActivity, activity)) {
             // Have split rule of [ otherTopActivity | launchedActivity ].
             return true;
         }
         // Have split rule of [ launchedActivity | otherTopActivity].
-        return isOnReparent && putActivitiesIntoSplitIfNecessary(activity, otherTopActivity);
+        return isOnReparent && putActivitiesIntoSplitIfNecessary(wct, activity, otherTopActivity);
     }
 
     /**
      * Places the given activity to the top most TaskFragment in the task if there is any.
      */
     @VisibleForTesting
-    void placeActivityInTopContainer(@NonNull Activity activity) {
+    void placeActivityInTopContainer(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity activity) {
         if (getContainerWithActivity(activity) != null) {
             // The activity has already been put in a TaskFragment. This is likely to be done by
             // the server when the activity is started.
@@ -547,20 +552,20 @@
             return;
         }
         targetContainer.addPendingAppearedActivity(activity);
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
         wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
                 activity.getActivityToken());
-        mPresenter.applyTransaction(wct);
     }
 
     /**
      * Starts an activity to side of the launchingActivity with the provided split config.
      */
-    private void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent,
+    @GuardedBy("mLock")
+    private void startActivityToSide(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity launchingActivity, @NonNull Intent intent,
             @Nullable Bundle options, @NonNull SplitRule sideRule,
             @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) {
         try {
-            mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule,
+            mPresenter.startActivityToSide(wct, launchingActivity, intent, options, sideRule,
                     isPlaceholder);
         } catch (Exception e) {
             if (failureCallback != null) {
@@ -573,15 +578,17 @@
      * Expands the given activity by either expanding the TaskFragment it is currently in or putting
      * it into a new expanded TaskFragment.
      */
-    private void expandActivity(@NonNull Activity activity) {
+    @GuardedBy("mLock")
+    private void expandActivity(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity activity) {
         final TaskFragmentContainer container = getContainerWithActivity(activity);
         if (shouldContainerBeExpanded(container)) {
             // Make sure that the existing container is expanded.
-            mPresenter.expandTaskFragment(container.getTaskFragmentToken());
+            mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
         } else {
             // Put activity into a new expanded container.
             final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity));
-            mPresenter.expandActivity(newContainer.getTaskFragmentToken(), activity);
+            mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity);
         }
     }
 
@@ -667,8 +674,8 @@
      * and returns {@code true}. Otherwise, returns {@code false}.
      */
     @GuardedBy("mLock")
-    private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity,
-            @NonNull Activity secondaryActivity) {
+    private boolean putActivitiesIntoSplitIfNecessary(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) {
         final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity);
         if (splitRule == null) {
             return false;
@@ -686,23 +693,23 @@
                 return true;
             }
             secondaryContainer.addPendingAppearedActivity(secondaryActivity);
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
             if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
                     secondaryActivity, null /* secondaryIntent */)
                     != RESULT_EXPAND_FAILED_NO_TF_INFO) {
                 wct.reparentActivityToTaskFragment(
                         secondaryContainer.getTaskFragmentToken(),
                         secondaryActivity.getActivityToken());
-                mPresenter.applyTransaction(wct);
                 return true;
             }
         }
         // Create new split pair.
-        mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule);
+        mPresenter.createNewSplitContainer(wct, primaryActivity, secondaryActivity, splitRule);
         return true;
     }
 
-    private void onActivityConfigurationChanged(@NonNull Activity activity) {
+    @GuardedBy("mLock")
+    private void onActivityConfigurationChanged(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity activity) {
         if (activity.isFinishing()) {
             // Do nothing if the activity is currently finishing.
             return;
@@ -721,7 +728,7 @@
         }
 
         // Check if activity requires a placeholder
-        launchPlaceholderIfNecessary(activity, false /* isOnCreated */);
+        launchPlaceholderIfNecessary(wct, activity, false /* isOnCreated */);
     }
 
     @VisibleForTesting
@@ -741,7 +748,22 @@
      * creation.
      */
     void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) {
-        mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+        synchronized (mLock) {
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            onTaskFragmentAppearEmptyTimeout(wct, container);
+            mPresenter.applyTransaction(wct);
+        }
+    }
+
+    /**
+     * Called when we have been waiting too long for the TaskFragment to become non-empty after
+     * creation.
+     */
+    void onTaskFragmentAppearEmptyTimeout(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer container) {
+        synchronized (mLock) {
+            mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
+        }
     }
 
     /**
@@ -971,6 +993,7 @@
     }
 
     /** Cleanups all the dependencies when the TaskFragment is entering PIP. */
+    @GuardedBy("mLock")
     private void cleanupForEnterPip(@NonNull WindowContainerTransaction wct,
             @NonNull TaskFragmentContainer container) {
         final TaskContainer taskContainer = container.getTaskContainer();
@@ -1084,9 +1107,10 @@
      * Updates the presentation of the container. If the container is part of the split or should
      * have a placeholder, it will also update the other part of the split.
      */
+    @GuardedBy("mLock")
     void updateContainer(@NonNull WindowContainerTransaction wct,
             @NonNull TaskFragmentContainer container) {
-        if (launchPlaceholderIfNecessary(container)) {
+        if (launchPlaceholderIfNecessary(wct, container)) {
             // Placeholder was launched, the positions will be updated when the activity is added
             // to the secondary container.
             return;
@@ -1111,7 +1135,7 @@
             // Skip position update - one or both containers are finished.
             return;
         }
-        if (dismissPlaceholderIfNecessary(splitContainer)) {
+        if (dismissPlaceholderIfNecessary(wct, splitContainer)) {
             // Placeholder was finished, the positions will be updated when its container is emptied
             return;
         }
@@ -1173,16 +1197,20 @@
     /**
      * Checks if the container requires a placeholder and launches it if necessary.
      */
-    private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) {
+    @GuardedBy("mLock")
+    private boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer container) {
         final Activity topActivity = container.getTopNonFinishingActivity();
         if (topActivity == null) {
             return false;
         }
 
-        return launchPlaceholderIfNecessary(topActivity, false /* isOnCreated */);
+        return launchPlaceholderIfNecessary(wct, topActivity, false /* isOnCreated */);
     }
 
-    boolean launchPlaceholderIfNecessary(@NonNull Activity activity, boolean isOnCreated) {
+    @GuardedBy("mLock")
+    boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity activity, boolean isOnCreated) {
         if (activity.isFinishing()) {
             return false;
         }
@@ -1216,7 +1244,7 @@
 
         // TODO(b/190433398): Handle failed request
         final Bundle options = getPlaceholderOptions(activity, isOnCreated);
-        startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), options,
+        startActivityToSide(wct, activity, placeholderRule.getPlaceholderIntent(), options,
                 placeholderRule, null /* failureCallback */, true /* isPlaceholder */);
         return true;
     }
@@ -1243,7 +1271,9 @@
     }
 
     @VisibleForTesting
-    boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) {
+    @GuardedBy("mLock")
+    boolean dismissPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct,
+            @NonNull SplitContainer splitContainer) {
         if (!splitContainer.isPlaceholderContainer()) {
             return false;
         }
@@ -1257,7 +1287,7 @@
             return false;
         }
 
-        mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(),
+        mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(),
                 false /* shouldFinishDependent */);
         return true;
     }
@@ -1523,7 +1553,8 @@
     private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter {
 
         @Override
-        public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {
+        public void onActivityPreCreated(@NonNull Activity activity,
+                @Nullable Bundle savedInstanceState) {
             synchronized (mLock) {
                 final IBinder activityToken = activity.getActivityToken();
                 final IBinder initialTaskFragmentToken = getInitialTaskFragmentToken(activity);
@@ -1552,25 +1583,30 @@
         }
 
         @Override
-        public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) {
+        public void onActivityPostCreated(@NonNull Activity activity,
+                @Nullable Bundle savedInstanceState) {
             // Calling after Activity#onCreate is complete to allow the app launch something
             // first. In case of a configured placeholder activity we want to make sure
             // that we don't launch it if an activity itself already requested something to be
             // launched to side.
             synchronized (mLock) {
-                SplitController.this.onActivityCreated(activity);
+                final WindowContainerTransaction wct = new WindowContainerTransaction();
+                SplitController.this.onActivityCreated(wct, activity);
+                mPresenter.applyTransaction(wct);
             }
         }
 
         @Override
-        public void onActivityConfigurationChanged(Activity activity) {
+        public void onActivityConfigurationChanged(@NonNull Activity activity) {
             synchronized (mLock) {
-                SplitController.this.onActivityConfigurationChanged(activity);
+                final WindowContainerTransaction wct = new WindowContainerTransaction();
+                SplitController.this.onActivityConfigurationChanged(wct, activity);
+                mPresenter.applyTransaction(wct);
             }
         }
 
         @Override
-        public void onActivityPostDestroyed(Activity activity) {
+        public void onActivityPostDestroyed(@NonNull Activity activity) {
             synchronized (mLock) {
                 SplitController.this.onActivityDestroyed(activity);
             }
@@ -1582,7 +1618,7 @@
         private final Handler mHandler = new Handler(Looper.getMainLooper());
 
         @Override
-        public void execute(Runnable r) {
+        public void execute(@NonNull Runnable r) {
             mHandler.post(r);
         }
     }
@@ -1662,7 +1698,7 @@
      * If the two rules have the same presentation, we can reuse the same {@link SplitContainer} if
      * there is any.
      */
-    private static boolean canReuseContainer(SplitRule rule1, SplitRule rule2) {
+    private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2) {
         if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) {
             return false;
         }
@@ -1670,7 +1706,8 @@
     }
 
     /** Whether the two rules have the same presentation. */
-    private static boolean haveSamePresentation(SplitPairRule rule1, SplitPairRule rule2) {
+    private static boolean haveSamePresentation(@NonNull SplitPairRule rule1,
+            @NonNull SplitPairRule rule2) {
         // TODO(b/231655482): add util method to do the comparison in SplitPairRule.
         return rule1.getSplitRatio() == rule2.getSplitRatio()
                 && rule1.getLayoutDirection() == rule2.getLayoutDirection()
@@ -1684,7 +1721,7 @@
      * Whether it is ok for other rule to reuse the {@link TaskFragmentContainer} of the given
      * rule.
      */
-    private static boolean isContainerReusableRule(SplitRule rule) {
+    private static boolean isContainerReusableRule(@NonNull SplitRule rule) {
         // We don't expect to reuse the placeholder rule.
         if (!(rule instanceof SplitPairRule)) {
             return false;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index a89847a..2b069d7 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -102,37 +102,18 @@
 
     private final SplitController mController;
 
-    SplitPresenter(@NonNull Executor executor, SplitController controller) {
+    SplitPresenter(@NonNull Executor executor, @NonNull SplitController controller) {
         super(executor, controller);
         mController = controller;
         registerOrganizer();
     }
 
     /**
-     * Updates the presentation of the provided container.
-     */
-    void updateContainer(@NonNull TaskFragmentContainer container) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
-        mController.updateContainer(wct, container);
-        applyTransaction(wct);
-    }
-
-    /**
      * Deletes the specified container and all other associated and dependent containers in the same
      * transaction.
      */
-    void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
-        cleanupContainer(container, shouldFinishDependent, wct);
-        applyTransaction(wct);
-    }
-
-    /**
-     * Deletes the specified container and all other associated and dependent containers in the same
-     * transaction.
-     */
-    void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent,
-            @NonNull WindowContainerTransaction wct) {
+    void cleanupContainer(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
         container.finish(shouldFinishDependent, this, wct, mController);
 
         final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer(
@@ -190,10 +171,9 @@
      *                          created and the activity will be re-parented to it.
      * @param rule The split rule to be applied to the container.
      */
-    void createNewSplitContainer(@NonNull Activity primaryActivity,
-            @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
-
+    void createNewSplitContainer(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity,
+            @NonNull SplitPairRule rule) {
         final Rect parentBounds = getParentContainerBounds(primaryActivity);
         final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity,
                 secondaryActivity);
@@ -219,8 +199,6 @@
                 minDimensionsPair);
 
         mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
-
-        applyTransaction(wct);
     }
 
     /**
@@ -262,7 +240,8 @@
      * @param rule              The split rule to be applied to the container.
      * @param isPlaceholder     Whether the launch is a placeholder.
      */
-    void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent,
+    void startActivityToSide(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity launchingActivity, @NonNull Intent activityIntent,
             @Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) {
         final Rect parentBounds = getParentContainerBounds(launchingActivity);
         final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
@@ -284,7 +263,6 @@
                 launchingActivity, taskId);
         final int windowingMode = mController.getTaskContainer(taskId)
                 .getWindowingModeForSplitTaskFragment(primaryRectBounds);
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
         mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
                 rule);
         startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds,
@@ -294,7 +272,6 @@
             // When placeholder is launched in split, we should keep the focus on the primary.
             wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
         }
-        applyTransaction(wct);
     }
 
     /**
@@ -502,14 +479,14 @@
     }
 
     @NonNull
-    static Pair<Size, Size> getActivitiesMinDimensionsPair(Activity primaryActivity,
-            Activity secondaryActivity) {
+    static Pair<Size, Size> getActivitiesMinDimensionsPair(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity) {
         return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity));
     }
 
     @NonNull
-    static Pair<Size, Size> getActivityIntentMinDimensionsPair(Activity primaryActivity,
-            Intent secondaryIntent) {
+    static Pair<Size, Size> getActivityIntentMinDimensionsPair(@NonNull Activity primaryActivity,
+            @NonNull Intent secondaryIntent) {
         return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent));
     }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 0ea5603..77e26c0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -21,8 +21,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.WindowConfiguration;
 import android.app.WindowConfiguration.WindowingMode;
@@ -31,6 +29,9 @@
 import android.util.ArraySet;
 import android.window.TaskFragmentInfo;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
index f721341..ee2e139 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
@@ -30,6 +30,8 @@
 import android.view.RemoteAnimationDefinition;
 import android.window.TaskFragmentOrganizer;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 /** Controls the TaskFragment remote animations. */
@@ -45,7 +47,7 @@
     /** Task Ids that we have registered for remote animation. */
     private final ArraySet<Integer> mRegisterTasks = new ArraySet<>();
 
-    TaskFragmentAnimationController(TaskFragmentOrganizer organizer) {
+    TaskFragmentAnimationController(@NonNull TaskFragmentOrganizer organizer) {
         mOrganizer = organizer;
         mDefinition = new RemoteAnimationDefinition();
         final RemoteAnimationAdapter animationAdapter =
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
index c4f3709..8af2d9c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -112,6 +112,7 @@
     }
 
     /** Creates the animator given the transition type and windows. */
+    @NonNull
     private Animator createAnimator(@WindowManager.TransitionOldType int transit,
             @NonNull RemoteAnimationTarget[] targets,
             @NonNull IRemoteAnimationFinishedCallback finishedCallback) {
@@ -161,6 +162,7 @@
     }
 
     /** List of {@link TaskFragmentAnimationAdapter} to handle animations on all window targets. */
+    @NonNull
     private List<TaskFragmentAnimationAdapter> createAnimationAdapters(
             @WindowManager.TransitionOldType int transit,
             @NonNull RemoteAnimationTarget[] targets) {
@@ -180,12 +182,14 @@
         }
     }
 
+    @NonNull
     private List<TaskFragmentAnimationAdapter> createOpenAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets) {
         return createOpenCloseAnimationAdapters(targets, true /* isOpening */,
                 mAnimationSpec::loadOpenAnimation);
     }
 
+    @NonNull
     private List<TaskFragmentAnimationAdapter> createCloseAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets) {
         return createOpenCloseAnimationAdapters(targets, false /* isOpening */,
@@ -196,6 +200,7 @@
      * Creates {@link TaskFragmentAnimationAdapter} for OPEN and CLOSE types of transition.
      * @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type.
      */
+    @NonNull
     private List<TaskFragmentAnimationAdapter> createOpenCloseAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets, boolean isOpening,
             @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider) {
@@ -238,6 +243,7 @@
         return adapters;
     }
 
+    @NonNull
     private TaskFragmentAnimationAdapter createOpenCloseAnimationAdapter(
             @NonNull RemoteAnimationTarget target,
             @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider,
@@ -259,6 +265,7 @@
         return new TaskFragmentAnimationAdapter(animation, target);
     }
 
+    @NonNull
     private List<TaskFragmentAnimationAdapter> createChangeAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets) {
         final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
index 5cc496a..97d42391b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
@@ -26,6 +26,7 @@
 import android.os.Handler;
 import android.provider.Settings;
 import android.view.RemoteAnimationTarget;
+import android.view.WindowManager;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
 import android.view.animation.AnimationSet;
@@ -68,16 +69,14 @@
 
         // The transition animation should be adjusted based on the developer option.
         final ContentResolver resolver = mContext.getContentResolver();
-        mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
-                Settings.Global.TRANSITION_ANIMATION_SCALE,
-                mContext.getResources().getFloat(
-                        R.dimen.config_appTransitionAnimationDurationScaleDefault));
+        mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
         resolver.registerContentObserver(
                 Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false,
                 new SettingsObserver(handler));
     }
 
     /** For target that doesn't need to be animated. */
+    @NonNull
     static Animation createNoopAnimation(@NonNull RemoteAnimationTarget target) {
         // Noop but just keep the target showing/hiding.
         final float alpha = target.mode == MODE_CLOSING ? 0f : 1f;
@@ -85,6 +84,7 @@
     }
 
     /** Animation for target that is opening in a change transition. */
+    @NonNull
     Animation createChangeBoundsOpenAnimation(@NonNull RemoteAnimationTarget target) {
         final Rect bounds = target.localBounds;
         // The target will be animated in from left or right depends on its position.
@@ -101,6 +101,7 @@
     }
 
     /** Animation for target that is closing in a change transition. */
+    @NonNull
     Animation createChangeBoundsCloseAnimation(@NonNull RemoteAnimationTarget target) {
         final Rect bounds = target.localBounds;
         // The target will be animated out to left or right depends on its position.
@@ -121,6 +122,7 @@
      * @return the return array always has two elements. The first one is for the start leash, and
      *         the second one is for the end leash.
      */
+    @NonNull
     Animation[] createChangeBoundsChangeAnimations(@NonNull RemoteAnimationTarget target) {
         // Both start bounds and end bounds are in screen coordinates. We will post translate
         // to the local coordinates in TaskFragmentAnimationAdapter#onAnimationUpdate
@@ -177,6 +179,7 @@
         return new Animation[]{startSet, endSet};
     }
 
+    @NonNull
     Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target,
             @NonNull Rect wholeAnimationBounds) {
         final boolean isEnter = target.mode != MODE_CLOSING;
@@ -198,6 +201,7 @@
         return animation;
     }
 
+    @NonNull
     Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target,
             @NonNull Rect wholeAnimationBounds) {
         final boolean isEnter = target.mode != MODE_CLOSING;
@@ -217,6 +221,12 @@
         return animation;
     }
 
+    private float getTransitionAnimationScaleSetting() {
+        return WindowManager.fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
+                                R.dimen.config_appTransitionAnimationDurationScaleDefault)));
+    }
+
     private class SettingsObserver extends ContentObserver {
         SettingsObserver(@NonNull Handler handler) {
             super(handler);
@@ -224,9 +234,7 @@
 
         @Override
         public void onChange(boolean selfChange) {
-            mTransitionAnimationScaleSetting = Settings.Global.getFloat(
-                    mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE,
-                    mTransitionAnimationScaleSetting);
+            mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
         }
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 37f5b6d..11c0db3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -18,8 +18,6 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.WindowConfiguration.WindowingMode;
 import android.content.Intent;
@@ -30,6 +28,9 @@
 import android.window.TaskFragmentInfo;
 import android.window.WindowContainerTransaction;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
@@ -175,6 +176,7 @@
                 && mInfo.getActivities().size() == collectNonFinishingActivities().size();
     }
 
+    @NonNull
     ActivityStack toActivityStack() {
         return new ActivityStack(collectNonFinishingActivities(), isEmpty());
     }
@@ -249,19 +251,22 @@
         return mInfo;
     }
 
-    void setInfo(@NonNull TaskFragmentInfo info) {
+    void setInfo(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentInfo info) {
         if (!mIsFinished && mInfo == null && info.isEmpty()) {
             // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if no
             // pending appeared intent/activities. Otherwise, wait and removing the TaskFragment if
             // it is still empty after timeout.
-            mAppearEmptyTimeout = () -> {
-                mAppearEmptyTimeout = null;
-                mController.onTaskFragmentAppearEmptyTimeout(this);
-            };
             if (mPendingAppearedIntent != null || !mPendingAppearedActivities.isEmpty()) {
+                mAppearEmptyTimeout = () -> {
+                    mAppearEmptyTimeout = null;
+                    // Call without the pass-in wct when timeout. We need to applyWct directly
+                    // in this case.
+                    mController.onTaskFragmentAppearEmptyTimeout(this);
+                };
                 mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS);
             } else {
-                mAppearEmptyTimeout.run();
+                mAppearEmptyTimeout = null;
+                mController.onTaskFragmentAppearEmptyTimeout(wct, this);
             }
         } else if (mAppearEmptyTimeout != null && !info.isEmpty()) {
             mController.getHandler().removeCallbacks(mAppearEmptyTimeout);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index 4d25952..21cf7a6 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -56,6 +56,8 @@
  * Build/Install/Run:
  *  atest WMJetpackUnitTests:JetpackTaskFragmentOrganizerTest
  */
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -119,7 +121,7 @@
                 new Intent(), taskContainer, mSplitController);
         final TaskFragmentInfo info = createMockInfo(container);
         mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info);
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken());
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 4bc5033..07758d2 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -89,6 +89,8 @@
  * Build/Install/Run:
  *  atest WMJetpackUnitTests:SplitControllerTest
  */
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -158,14 +160,14 @@
         final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
         doReturn(new ArrayList<>()).when(info).getActivities();
         doReturn(true).when(info).isEmpty();
-        tf1.setInfo(info);
+        tf1.setInfo(mTransaction, info);
 
         assertWithMessage("Must return tf because we are waiting for tf1 to become non-empty after"
                 + " creation.")
                 .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
 
         doReturn(false).when(info).isEmpty();
-        tf1.setInfo(info);
+        tf1.setInfo(mTransaction, info);
 
         assertWithMessage("Must return null because tf1 becomes empty.")
                 .that(mSplitController.getTopActiveContainer(TASK_ID)).isNull();
@@ -177,7 +179,7 @@
         doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken();
 
         // The TaskFragment has been removed in the server, we only need to cleanup the reference.
-        mSplitController.onTaskFragmentVanished(mInfo);
+        mSplitController.onTaskFragmentVanished(mTransaction, mInfo);
 
         verify(mSplitPresenter, never()).deleteTaskFragment(any(), any());
         verify(mSplitController).removeContainer(tf);
@@ -187,9 +189,10 @@
     @Test
     public void testOnTaskFragmentAppearEmptyTimeout() {
         final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
-        mSplitController.onTaskFragmentAppearEmptyTimeout(tf);
+        mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf);
 
-        verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */);
+        verify(mSplitPresenter).cleanupContainer(mTransaction, tf,
+                false /* shouldFinishDependent */);
     }
 
     @Test
@@ -229,8 +232,8 @@
         spyOn(tf);
         doReturn(mActivity).when(tf).getTopNonFinishingActivity();
         doReturn(true).when(tf).isEmpty();
-        doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mActivity,
-                false /* isOnCreated */);
+        doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mTransaction,
+                mActivity, false /* isOnCreated */);
         doNothing().when(mSplitPresenter).updateSplitContainer(any(), any(), any());
 
         mSplitController.updateContainer(mTransaction, tf);
@@ -250,7 +253,7 @@
 
         mSplitController.updateContainer(mTransaction, tf);
 
-        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any());
+        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any());
 
         // Verify if tf is not in the top splitContainer,
         final SplitContainer splitContainer = mock(SplitContainer.class);
@@ -264,7 +267,7 @@
 
         mSplitController.updateContainer(mTransaction, tf);
 
-        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any());
+        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any());
 
         // Verify if one or both containers in the top SplitContainer are finished,
         // dismissPlaceholder() won't be called.
@@ -273,12 +276,12 @@
 
         mSplitController.updateContainer(mTransaction, tf);
 
-        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any());
+        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any());
 
         // Verify if placeholder should be dismissed, updateSplitContainer() won't be called.
         doReturn(false).when(tf).isFinished();
         doReturn(true).when(mSplitController)
-                .dismissPlaceholderIfNecessary(splitContainer);
+                .dismissPlaceholderIfNecessary(mTransaction, splitContainer);
 
         mSplitController.updateContainer(mTransaction, tf);
 
@@ -286,7 +289,7 @@
 
         // Verify if the top active split is updated if both of its containers are not finished.
         doReturn(false).when(mSplitController)
-                        .dismissPlaceholderIfNecessary(splitContainer);
+                        .dismissPlaceholderIfNecessary(mTransaction, splitContainer);
 
         mSplitController.updateContainer(mTransaction, tf);
 
@@ -315,34 +318,36 @@
 
     @Test
     public void testOnActivityCreated() {
-        mSplitController.onActivityCreated(mActivity);
+        mSplitController.onActivityCreated(mTransaction, mActivity);
 
         // Disallow to split as primary because we want the new launch to be always on top.
-        verify(mSplitController).resolveActivityToContainer(mActivity, false /* isOnReparent */);
+        verify(mSplitController).resolveActivityToContainer(mTransaction, mActivity,
+                false /* isOnReparent */);
     }
 
     @Test
-    public void testOnActivityReparentToTask_sameProcess() {
-        mSplitController.onActivityReparentToTask(TASK_ID, new Intent(),
+    public void testOnActivityReparentedToTask_sameProcess() {
+        mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, new Intent(),
                 mActivity.getActivityToken());
 
         // Treated as on activity created, but allow to split as primary.
-        verify(mSplitController).resolveActivityToContainer(mActivity, true /* isOnReparent */);
+        verify(mSplitController).resolveActivityToContainer(mTransaction,
+                mActivity, true /* isOnReparent */);
         // Try to place the activity to the top TaskFragment when there is no matched rule.
-        verify(mSplitController).placeActivityInTopContainer(mActivity);
+        verify(mSplitController).placeActivityInTopContainer(mTransaction, mActivity);
     }
 
     @Test
-    public void testOnActivityReparentToTask_diffProcess() {
+    public void testOnActivityReparentedToTask_diffProcess() {
         // Create an empty TaskFragment to initialize for the Task.
         mSplitController.newContainer(new Intent(), mActivity, TASK_ID);
         final IBinder activityToken = new Binder();
         final Intent intent = new Intent();
 
-        mSplitController.onActivityReparentToTask(TASK_ID, intent, activityToken);
+        mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken);
 
         // Treated as starting new intent
-        verify(mSplitController, never()).resolveActivityToContainer(any(), anyBoolean());
+        verify(mSplitController, never()).resolveActivityToContainer(any(), any(), anyBoolean());
         verify(mSplitController).resolveStartActivityIntent(any(), eq(TASK_ID), eq(intent),
                 isNull());
     }
@@ -504,26 +509,29 @@
 
     @Test
     public void testPlaceActivityInTopContainer() {
-        mSplitController.placeActivityInTopContainer(mActivity);
+        mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
 
-        verify(mSplitPresenter, never()).applyTransaction(any());
+        verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any());
 
-        mSplitController.newContainer(new Intent(), mActivity, TASK_ID);
-        mSplitController.placeActivityInTopContainer(mActivity);
+        // Place in the top container if there is no other rule matched.
+        final TaskFragmentContainer topContainer = mSplitController
+                .newContainer(new Intent(), mActivity, TASK_ID);
+        mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
 
-        verify(mSplitPresenter).applyTransaction(any());
+        verify(mTransaction).reparentActivityToTaskFragment(topContainer.getTaskFragmentToken(),
+                mActivity.getActivityToken());
 
         // Not reparent if activity is in a TaskFragment.
-        clearInvocations(mSplitPresenter);
+        clearInvocations(mTransaction);
         mSplitController.newContainer(mActivity, TASK_ID);
-        mSplitController.placeActivityInTopContainer(mActivity);
+        mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
 
-        verify(mSplitPresenter, never()).applyTransaction(any());
+        verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any());
     }
 
     @Test
     public void testResolveActivityToContainer_noRuleMatched() {
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
@@ -535,7 +543,7 @@
         setupExpandRule(mActivity);
 
         // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
         final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
                 mActivity);
@@ -543,7 +551,8 @@
         assertTrue(result);
         assertNotNull(container);
         verify(mSplitController).newContainer(mActivity, TASK_ID);
-        verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity);
+        verify(mSplitPresenter).expandActivity(mTransaction, container.getTaskFragmentToken(),
+                mActivity);
     }
 
     @Test
@@ -552,11 +561,11 @@
 
         // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
         final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
-        verify(mSplitPresenter).expandTaskFragment(container.getTaskFragmentToken());
+        verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken());
     }
 
     @Test
@@ -566,14 +575,15 @@
         // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
         final Activity activity = createMockActivity();
         addSplitTaskFragments(activity, mActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
         final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
                 mActivity);
 
         assertTrue(result);
         assertNotNull(container);
-        verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity);
+        verify(mSplitPresenter).expandActivity(mTransaction, container.getTaskFragmentToken(),
+                mActivity);
     }
 
     @Test
@@ -583,11 +593,11 @@
                 (SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
 
         // Launch placeholder if the activity is not in any TaskFragment.
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
-        verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+        verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT,
                 mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */),
                 placeholderRule, true /* isPlaceholder */);
     }
@@ -600,11 +610,11 @@
         final Activity activity = createMockActivity();
         mSplitController.newContainer(mActivity, TASK_ID);
         mSplitController.newContainer(activity, TASK_ID);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
-        verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(),
+        verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(),
                 anyBoolean());
     }
 
@@ -616,11 +626,11 @@
 
         // Launch placeholder if the activity is in the topmost expanded TaskFragment.
         mSplitController.newContainer(mActivity, TASK_ID);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
-        verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+        verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT,
                 mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */),
                 placeholderRule, true /* isPlaceholder */);
     }
@@ -632,11 +642,11 @@
         // Don't launch placeholder if the activity is in primary split.
         final Activity secondaryActivity = createMockActivity();
         addSplitTaskFragments(mActivity, secondaryActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
-        verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(),
+        verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(),
                 anyBoolean());
     }
 
@@ -649,11 +659,11 @@
         // Launch placeholder if the activity is in secondary split.
         final Activity primaryActivity = createMockActivity();
         addSplitTaskFragments(primaryActivity, mActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
-        verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+        verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT,
                 mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */),
                 placeholderRule, true /* isPlaceholder */);
     }
@@ -676,7 +686,7 @@
                 secondaryContainer,
                 splitRule);
         clearInvocations(mSplitController);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
@@ -705,7 +715,7 @@
         final Activity launchedActivity = createMockActivity();
         primaryContainer.addPendingAppearedActivity(launchedActivity);
 
-        assertFalse(mSplitController.resolveActivityToContainer(launchedActivity,
+        assertFalse(mSplitController.resolveActivityToContainer(mTransaction, launchedActivity,
                 false /* isOnReparent */));
     }
 
@@ -717,7 +727,7 @@
         // Activity is already in secondary split, no need to create new split.
         addSplitTaskFragments(primaryActivity, mActivity);
         clearInvocations(mSplitController);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
@@ -735,7 +745,7 @@
         addSplitTaskFragments(primaryActivity, secondaryActivity);
         mSplitController.getContainerWithActivity(secondaryActivity)
                 .addPendingAppearedActivity(mActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
@@ -760,7 +770,7 @@
                 mActivity,
                 secondaryContainer,
                 placeholderRule);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
@@ -774,7 +784,7 @@
         final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
                 TASK_ID);
         container.addPendingAppearedActivity(mActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
@@ -790,14 +800,15 @@
         final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
                 TASK_ID);
         container.addPendingAppearedActivity(mActivity);
-        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
         assertEquals(container, mSplitController.getContainerWithActivity(mActivity));
 
         // Allow to split as primary.
-        result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */);
+        result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
+                true /* isOnReparent */);
 
         assertTrue(result);
         assertSplitPair(mActivity, activityBelow);
@@ -815,7 +826,7 @@
         final TaskFragmentContainer secondaryContainer = mSplitController.getContainerWithActivity(
                 activityBelow);
         secondaryContainer.addPendingAppearedActivity(mActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
         final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
                 mActivity);
@@ -836,14 +847,15 @@
         final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
                 primaryActivity);
         primaryContainer.addPendingAppearedActivity(mActivity);
-        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
         assertEquals(primaryContainer, mSplitController.getContainerWithActivity(mActivity));
 
 
-        result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */);
+        result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
+                true /* isOnReparent */);
 
         assertTrue(result);
         assertSplitPair(mActivity, primaryActivity);
@@ -861,7 +873,7 @@
         container.addPendingAppearedActivity(mActivity);
 
         // Allow to split as primary.
-        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 true /* isOnReparent */);
 
         assertTrue(result);
@@ -879,15 +891,13 @@
                 TASK_ID);
         container.addPendingAppearedActivity(mActivity);
 
-        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
         assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */);
     }
 
-    // Suppress GuardedBy warning on unit tests
-    @SuppressWarnings("GuardedBy")
     @Test
     public void testResolveActivityToContainer_minDimensions_shouldExpandSplitContainer() {
         final Activity primaryActivity = createMockActivity();
@@ -899,14 +909,14 @@
         doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity));
 
         clearInvocations(mSplitPresenter);
-        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
         assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */);
         assertEquals(mSplitController.getContainerWithActivity(secondaryActivity),
                 mSplitController.getContainerWithActivity(mActivity));
-        verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any());
+        verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any(), any());
     }
 
     @Test
@@ -914,7 +924,7 @@
         doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity);
 
         // No need to handle when the new launched activity is in an unknown TaskFragment.
-        assertTrue(mSplitController.resolveActivityToContainer(mActivity,
+        assertTrue(mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */));
     }
 
@@ -993,7 +1003,7 @@
     private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container,
             @NonNull Activity activity) {
         final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity);
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
         mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info);
     }
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index d7931966..3fdf8e5 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -78,6 +78,8 @@
  * Build/Install/Run:
  *  atest WMJetpackUnitTests:SplitPresenterTest
  */
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -226,8 +228,9 @@
                 mTransaction, splitContainer, mActivity, secondaryActivity,
                 null /* secondaryIntent */));
 
-        primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity));
-        secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
+        primaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(primaryTf, mActivity));
+        secondaryTf.setInfo(mTransaction,
+                createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
 
         assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
                 splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 44c7e6c..6cbecff 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -19,6 +19,8 @@
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
 import static org.junit.Assert.assertEquals;
@@ -36,7 +38,6 @@
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Binder;
-import android.os.Handler;
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.window.TaskFragmentInfo;
@@ -62,25 +63,27 @@
  * Build/Install/Run:
  *  atest WMJetpackUnitTests:TaskFragmentContainerTest
  */
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class TaskFragmentContainerTest {
     @Mock
     private SplitPresenter mPresenter;
-    @Mock
     private SplitController mController;
     @Mock
     private TaskFragmentInfo mInfo;
     @Mock
-    private Handler mHandler;
+    private WindowContainerTransaction mTransaction;
     private Activity mActivity;
     private Intent mIntent;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        doReturn(mHandler).when(mController).getHandler();
+        mController = new SplitController();
+        spyOn(mController);
         mActivity = createMockActivity();
         mIntent = new Intent();
     }
@@ -123,7 +126,7 @@
 
         // Remove all references after the container has appeared in server.
         doReturn(new ArrayList<>()).when(mInfo).getActivities();
-        container.setInfo(mInfo);
+        container.setInfo(mTransaction, mInfo);
         container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
 
         verify(mActivity, never()).finish();
@@ -137,7 +140,7 @@
         final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity,
                 null /* pendingAppearedIntent */, taskContainer, mController);
         final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity);
-        container0.setInfo(info);
+        container0.setInfo(mTransaction, info);
         // Request to reparent the activity to a new TaskFragment.
         final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity,
                 null /* pendingAppearedIntent */, taskContainer, mController);
@@ -163,7 +166,7 @@
 
         final TaskFragmentInfo info0 = createMockTaskFragmentInfo(pendingActivityContainer,
                 mActivity);
-        pendingActivityContainer.setInfo(info0);
+        pendingActivityContainer.setInfo(mTransaction, info0);
 
         assertTrue(pendingActivityContainer.mPendingAppearedActivities.isEmpty());
 
@@ -175,7 +178,7 @@
 
         final TaskFragmentInfo info1 = createMockTaskFragmentInfo(pendingIntentContainer,
                 mActivity);
-        pendingIntentContainer.setInfo(info1);
+        pendingIntentContainer.setInfo(mTransaction, info1);
 
         assertNull(pendingIntentContainer.getPendingAppearedIntent());
     }
@@ -191,18 +194,19 @@
         final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
         doReturn(new ArrayList<>()).when(info).getActivities();
         doReturn(true).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         assertTrue(container.isWaitingActivityAppear());
 
         doReturn(false).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         assertFalse(container.isWaitingActivityAppear());
     }
 
     @Test
     public void testAppearEmptyTimeout() {
+        doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any());
         final TaskContainer taskContainer = new TaskContainer(TASK_ID);
         final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
                 mIntent, taskContainer, mController);
@@ -213,20 +217,20 @@
         final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
         container.mInfo = null;
         doReturn(true).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         assertNotNull(container.mAppearEmptyTimeout);
 
         // Not set if it is not appeared empty.
         doReturn(new ArrayList<>()).when(info).getActivities();
         doReturn(false).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         assertNull(container.mAppearEmptyTimeout);
 
         // Remove timeout after the container becomes non-empty.
         doReturn(false).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         assertNull(container.mAppearEmptyTimeout);
 
@@ -234,7 +238,7 @@
         container.mInfo = null;
         container.setPendingAppearedIntent(mIntent);
         doReturn(true).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
         container.mAppearEmptyTimeout.run();
 
         assertNull(container.mAppearEmptyTimeout);
@@ -260,7 +264,7 @@
         final List<IBinder> runningActivities = Lists.newArrayList(activity0.getActivityToken(),
                 activity1.getActivityToken());
         doReturn(runningActivities).when(mInfo).getActivities();
-        container.setInfo(mInfo);
+        container.setInfo(mTransaction, mInfo);
         activities = container.collectNonFinishingActivities();
 
         assertEquals(3, activities.size());
@@ -295,7 +299,7 @@
         final Activity activity = createMockActivity();
         final List<IBinder> runningActivities = Lists.newArrayList(activity.getActivityToken());
         doReturn(runningActivities).when(mInfo).getActivities();
-        container.setInfo(mInfo);
+        container.setInfo(mTransaction, mInfo);
 
         assertEquals(activity, container.getBottomMostActivity());
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
index 764e650..b085b73 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
@@ -16,14 +16,20 @@
 
 package com.android.wm.shell;
 
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+
+import android.app.WindowConfiguration;
 import android.util.SparseArray;
 import android.view.SurfaceControl;
 import android.window.DisplayAreaAppearedInfo;
 import android.window.DisplayAreaInfo;
 import android.window.DisplayAreaOrganizer;
+import android.window.WindowContainerTransaction;
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.protolog.common.ProtoLog;
+
 import java.io.PrintWriter;
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -102,10 +108,44 @@
         mDisplayAreasInfo.put(displayId, displayAreaInfo);
     }
 
+    /**
+     * Create a {@link WindowContainerTransaction} to update display windowing mode.
+     *
+     * @param displayId display id to update windowing mode for
+     * @param windowingMode target {@link WindowConfiguration.WindowingMode}
+     * @return {@link WindowContainerTransaction} with pending operation to set windowing mode
+     */
+    public WindowContainerTransaction prepareWindowingModeChange(int displayId,
+            @WindowConfiguration.WindowingMode int windowingMode) {
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId);
+        if (displayAreaInfo == null) {
+            ProtoLog.e(WM_SHELL_DESKTOP_MODE,
+                    "unable to update windowing mode for display %d display not found", displayId);
+            return wct;
+        }
+
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId,
+                displayAreaInfo.configuration.windowConfiguration.getWindowingMode(),
+                windowingMode);
+
+        wct.setWindowingMode(displayAreaInfo.token, windowingMode);
+        return wct;
+    }
+
     public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
         final String childPrefix = innerPrefix + "  ";
         pw.println(prefix + this);
+
+        for (int i = 0; i < mDisplayAreasInfo.size(); i++) {
+            int displayId = mDisplayAreasInfo.keyAt(i);
+            DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId);
+            int windowingMode =
+                    displayAreaInfo.configuration.windowConfiguration.getWindowingMode();
+            pw.println(innerPrefix + "# displayId=" + displayId + " wmMode=" + windowingMode);
+        }
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 6ae0f9b..d5d4935 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -22,6 +22,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
@@ -46,6 +47,7 @@
 import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
 import android.window.TaskOrganizer;
+import android.window.WindowContainerTransaction;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
@@ -690,6 +692,49 @@
         taskListener.reparentChildSurfaceToTask(taskId, sc, t);
     }
 
+    /**
+     * Create a {@link WindowContainerTransaction} to clear task bounds.
+     *
+     * @param displayId display id for tasks that will have bounds cleared
+     * @return {@link WindowContainerTransaction} with pending operations to clear bounds
+     */
+    public WindowContainerTransaction prepareClearBoundsForTasks(int displayId) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks: displayId=%d", displayId);
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        for (int i = 0; i < mTasks.size(); i++) {
+            RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
+            if (taskInfo.displayId == displayId) {
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s",
+                        taskInfo.token, taskInfo);
+                wct.setBounds(taskInfo.token, null);
+            }
+        }
+        return wct;
+    }
+
+    /**
+     * Create a {@link WindowContainerTransaction} to clear task level freeform setting.
+     *
+     * @param displayId display id for tasks that will have windowing mode reset to {@link
+     *                  WindowConfiguration#WINDOWING_MODE_UNDEFINED}
+     * @return {@link WindowContainerTransaction} with pending operations to clear windowing mode
+     */
+    public WindowContainerTransaction prepareClearFreeformForTasks(int displayId) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks: displayId=%d", displayId);
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        for (int i = 0; i < mTasks.size(); i++) {
+            RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
+            if (taskInfo.displayId == displayId
+                    && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                        "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token,
+                        taskInfo);
+                wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+            }
+        }
+        return wct;
+    }
+
     private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info,
             int event) {
         ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo;
@@ -816,7 +861,14 @@
                 final int key = mTasks.keyAt(i);
                 final TaskAppearedInfo info = mTasks.valueAt(i);
                 final TaskListener listener = getTaskListener(info.getTaskInfo());
-                pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener);
+                final int windowingMode = info.getTaskInfo().getWindowingMode();
+                String pkg = "";
+                if (info.getTaskInfo().baseActivity != null) {
+                    pkg = info.getTaskInfo().baseActivity.getPackageName();
+                }
+                Rect bounds = info.getTaskInfo().getConfiguration().windowConfiguration.getBounds();
+                pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener
+                        + " wmMode=" + windowingMode + " pkg=" + pkg + " bounds=" + bounds);
             }
 
             pw.println();
@@ -826,6 +878,7 @@
                 final TaskListener listener = mLaunchCookieToListener.valueAt(i);
                 pw.println(innerPrefix + "#" + i + " cookie=" + key + " listener=" + listener);
             }
+
         }
     }
 }
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 d5875c0..e270edb 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
@@ -221,8 +221,7 @@
             }
             final Display display = mDisplayController.getDisplay(mDisplayId);
             SurfaceControlViewHost viewRoot =
-                    new SurfaceControlViewHost(
-                            view.getContext(), display, wwm, true /* useSfChoreographer */);
+                    new SurfaceControlViewHost(view.getContext(), display, wwm);
             attrs.flags |= FLAG_HARDWARE_ACCELERATED;
             viewRoot.setView(view, attrs);
             mViewRoots.put(view, viewRoot);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
index 35a309a..0cc545a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
@@ -20,7 +20,6 @@
 import static android.os.Process.THREAD_PRIORITY_DISPLAY;
 import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;
 
-import android.animation.AnimationHandler;
 import android.content.Context;
 import android.os.Build;
 import android.os.Handler;
@@ -31,11 +30,9 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.HandlerExecutor;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ChoreographerSfVsync;
 import com.android.wm.shell.common.annotations.ExternalMainThread;
 import com.android.wm.shell.common.annotations.ShellAnimationThread;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
@@ -195,30 +192,6 @@
     }
 
     /**
-     * Provide a Shell main-thread AnimationHandler.  The AnimationHandler can be set on
-     * {@link android.animation.ValueAnimator}s and will ensure that the animation will run on
-     * the Shell main-thread with the SF vsync.
-     */
-    @WMSingleton
-    @Provides
-    @ChoreographerSfVsync
-    public static AnimationHandler provideShellMainExecutorSfVsyncAnimationHandler(
-            @ShellMainThread ShellExecutor mainExecutor) {
-        try {
-            AnimationHandler handler = new AnimationHandler();
-            mainExecutor.executeBlocking(() -> {
-                // This is called on the animation thread since it calls
-                // Choreographer.getSfInstance() which returns a thread-local Choreographer instance
-                // that uses the SF vsync
-                handler.setProvider(new SfVsyncFrameCallbackProvider());
-            });
-            return handler;
-        } catch (InterruptedException e) {
-            throw new RuntimeException("Failed to initialize SfVsync animation handler in 1s", e);
-        }
-    }
-
-    /**
      * Provides a Shell background thread Handler for low priority background tasks.
      */
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 2ca9c3b..2bcc134 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -27,6 +27,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.TaskViewTransitions;
@@ -48,6 +49,8 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.desktopmode.DesktopModeConstants;
+import com.android.wm.shell.desktopmode.DesktopModeController;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.freeform.FreeformComponents;
 import com.android.wm.shell.freeform.FreeformTaskListener;
@@ -574,6 +577,27 @@
     }
 
     //
+    // Desktop mode (optional feature)
+    //
+
+    @WMSingleton
+    @Provides
+    static Optional<DesktopModeController> provideDesktopModeController(
+            Context context, ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            RootDisplayAreaOrganizer rootDisplayAreaOrganizer,
+            @ShellMainThread Handler mainHandler
+    ) {
+        if (DesktopModeConstants.IS_FEATURE_ENABLED) {
+            return Optional.of(new DesktopModeController(context, shellInit, shellTaskOrganizer,
+                    rootDisplayAreaOrganizer,
+                    mainHandler));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    //
     // Misc
     //
 
@@ -583,7 +607,8 @@
     @ShellCreateTriggerOverride
     @Provides
     static Object provideIndependentShellComponentsToCreate(
-            SplitscreenPipMixedHandler splitscreenPipMixedHandler) {
+            SplitscreenPipMixedHandler splitscreenPipMixedHandler,
+            Optional<DesktopModeController> desktopModeController) {
         return new Object();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeConstants.java
new file mode 100644
index 0000000..e62a63a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeConstants.java
@@ -0,0 +1,31 @@
+/*
+ * 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.wm.shell.desktopmode;
+
+import android.os.SystemProperties;
+
+/**
+ * Constants for desktop mode feature
+ */
+public class DesktopModeConstants {
+
+    /**
+     * Flag to indicate whether desktop mode is available on the device
+     */
+    public static final boolean IS_FEATURE_ENABLED = SystemProperties.getBoolean(
+            "persist.wm.debug.desktop_mode", false);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
new file mode 100644
index 0000000..5849e16
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -0,0 +1,137 @@
+/*
+ * 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.wm.shell.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
+
+/**
+ * Handles windowing changes when desktop mode system setting changes
+ */
+public class DesktopModeController {
+
+    private final Context mContext;
+    private final ShellTaskOrganizer mShellTaskOrganizer;
+    private final RootDisplayAreaOrganizer mRootDisplayAreaOrganizer;
+    private final SettingsObserver mSettingsObserver;
+
+    public DesktopModeController(Context context, ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            RootDisplayAreaOrganizer rootDisplayAreaOrganizer,
+            @ShellMainThread Handler mainHandler) {
+        mContext = context;
+        mShellTaskOrganizer = shellTaskOrganizer;
+        mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer;
+        mSettingsObserver = new SettingsObserver(mContext, mainHandler);
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController");
+        mSettingsObserver.observe();
+    }
+
+    @VisibleForTesting
+    void updateDesktopModeEnabled(boolean enabled) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeState: enabled=%s", enabled);
+
+        int displayId = mContext.getDisplayId();
+
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        // Reset freeform windowing mode that is set per task level (tasks should inherit
+        // container value)
+        wct.merge(mShellTaskOrganizer.prepareClearFreeformForTasks(displayId), true /* transfer */);
+        int targetWindowingMode;
+        if (enabled) {
+            targetWindowingMode = WINDOWING_MODE_FREEFORM;
+        } else {
+            targetWindowingMode = WINDOWING_MODE_FULLSCREEN;
+            // Clear any resized bounds
+            wct.merge(mShellTaskOrganizer.prepareClearBoundsForTasks(displayId),
+                    true /* transfer */);
+        }
+        wct.merge(mRootDisplayAreaOrganizer.prepareWindowingModeChange(displayId,
+                targetWindowingMode), true /* transfer */);
+        mRootDisplayAreaOrganizer.applyTransaction(wct);
+    }
+
+    /**
+     * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE}
+     */
+    private final class SettingsObserver extends ContentObserver {
+
+        private final Uri mDesktopModeSetting = Settings.System.getUriFor(
+                Settings.System.DESKTOP_MODE);
+
+        private final Context mContext;
+
+        SettingsObserver(Context context, Handler handler) {
+            super(handler);
+            mContext = context;
+        }
+
+        public void observe() {
+            // TODO(b/242867463): listen for setting change for all users
+            mContext.getContentResolver().registerContentObserver(mDesktopModeSetting,
+                    false /* notifyForDescendants */, this /* observer */, UserHandle.USER_CURRENT);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, @Nullable Uri uri) {
+            if (mDesktopModeSetting.equals(uri)) {
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Received update for desktop mode setting");
+                desktopModeSettingChanged();
+            }
+        }
+
+        private void desktopModeSettingChanged() {
+            boolean enabled = isDesktopModeEnabled();
+            updateDesktopModeEnabled(enabled);
+        }
+
+        private boolean isDesktopModeEnabled() {
+            try {
+                int result = Settings.System.getIntForUser(mContext.getContentResolver(),
+                        Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT);
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "isDesktopModeEnabled=%s", result);
+                return result != 0;
+            } catch (Settings.SettingNotFoundException e) {
+                ProtoLog.e(WM_SHELL_DESKTOP_MODE, "Failed to read DESKTOP_MODE setting %s", e);
+                return false;
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index 504dc02..a0a8f9f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -31,8 +31,6 @@
 import android.util.Size;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
-import android.view.SyncRtSurfaceTransactionApplier;
-import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
 import android.view.WindowManagerGlobal;
 
 import com.android.internal.protolog.common.ProtoLog;
@@ -42,6 +40,7 @@
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipMediaController.ActionListener;
 import com.android.wm.shell.pip.PipMenuController;
+import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip.PipUiEventLogger;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -115,6 +114,10 @@
     private final ShellExecutor mMainExecutor;
     private final Handler mMainHandler;
 
+    private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+            mSurfaceControlTransactionFactory;
+    private final float[] mTmpTransform = new float[9];
+
     private final ArrayList<Listener> mListeners = new ArrayList<>();
     private final SystemWindows mSystemWindows;
     private final Optional<SplitScreenController> mSplitScreenController;
@@ -124,7 +127,6 @@
     private RemoteAction mCloseAction;
     private List<RemoteAction> mMediaActions;
 
-    private SyncRtSurfaceTransactionApplier mApplier;
     private int mMenuState;
 
     private PipMenuView mPipMenuView;
@@ -150,6 +152,9 @@
         mMainHandler = mainHandler;
         mSplitScreenController = splitScreenOptional;
         mPipUiEventLogger = pipUiEventLogger;
+
+        mSurfaceControlTransactionFactory =
+                new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
     }
 
     public boolean isMenuVisible() {
@@ -194,7 +199,6 @@
             return;
         }
 
-        mApplier = null;
         mSystemWindows.removeView(mPipMenuView);
         mPipMenuView = null;
     }
@@ -289,7 +293,7 @@
                     willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, "    "));
         }
 
-        if (!maybeCreateSyncApplier()) {
+        if (!checkPipMenuState()) {
             return;
         }
 
@@ -312,7 +316,7 @@
             return;
         }
 
-        if (!maybeCreateSyncApplier()) {
+        if (!checkPipMenuState()) {
             return;
         }
 
@@ -328,18 +332,15 @@
         mTmpSourceRectF.set(mTmpSourceBounds);
         mTmpDestinationRectF.set(destinationBounds);
         mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
-        SurfaceControl surfaceControl = getSurfaceControl();
-        SurfaceParams params = new SurfaceParams.Builder(surfaceControl)
-                .withMatrix(mMoveTransform)
-                .build();
+        final SurfaceControl surfaceControl = getSurfaceControl();
+        final SurfaceControl.Transaction menuTx =
+                mSurfaceControlTransactionFactory.getTransaction();
+        menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform);
         if (pipLeash != null && t != null) {
-            SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash)
-                    .withMergeTransaction(t)
-                    .build();
-            mApplier.scheduleApply(params, pipParams);
-        } else {
-            mApplier.scheduleApply(params);
+            // Merge the two transactions, vsyncId has been set on menuTx.
+            menuTx.merge(t);
         }
+        menuTx.apply();
     }
 
     /**
@@ -353,36 +354,29 @@
             return;
         }
 
-        if (!maybeCreateSyncApplier()) {
+        if (!checkPipMenuState()) {
             return;
         }
 
-        SurfaceControl surfaceControl = getSurfaceControl();
-        SurfaceParams params = new SurfaceParams.Builder(surfaceControl)
-                .withWindowCrop(destinationBounds)
-                .build();
+        final SurfaceControl surfaceControl = getSurfaceControl();
+        final SurfaceControl.Transaction menuTx =
+                mSurfaceControlTransactionFactory.getTransaction();
+        menuTx.setCrop(surfaceControl, destinationBounds);
         if (pipLeash != null && t != null) {
-            SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash)
-                    .withMergeTransaction(t)
-                    .build();
-            mApplier.scheduleApply(params, pipParams);
-        } else {
-            mApplier.scheduleApply(params);
+            // Merge the two transactions, vsyncId has been set on menuTx.
+            menuTx.merge(t);
         }
+        menuTx.apply();
     }
 
-    private boolean maybeCreateSyncApplier() {
+    private boolean checkPipMenuState() {
         if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: Not going to move PiP, either menu or its parent is not created.", TAG);
             return false;
         }
 
-        if (mApplier == null) {
-            mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView);
-        }
-
-        return mApplier != null;
+        return true;
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
index 0f3ff36..8e3376f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
@@ -146,11 +146,8 @@
                     "%s: Failed to create input consumer, %s", TAG, e);
         }
         mMainExecutor.execute(() -> {
-            // Choreographer.getSfInstance() must be called on the thread that the input event
-            // receiver should be receiving events
-            // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions
             mInputEventReceiver = new InputEventReceiver(inputChannel,
-                Looper.myLooper(), Choreographer.getSfInstance());
+                Looper.myLooper(), Choreographer.getInstance());
             if (mRegistrationListener != null) {
                 mRegistrationListener.onRegistrationChanged(true /* isRegistered */);
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
index abf1a95..89d85e4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
@@ -625,8 +625,7 @@
 
     class PipResizeInputEventReceiver extends BatchedInputEventReceiver {
         PipResizeInputEventReceiver(InputChannel channel, Looper looper) {
-            // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions
-            super(channel, looper, Choreographer.getSfInstance());
+            super(channel, looper, Choreographer.getInstance());
         }
 
         public void onInputEvent(InputEvent event) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index b296151..93c7529 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -46,6 +46,8 @@
             Consts.TAG_WM_SHELL),
     WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
+    WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+            Consts.TAG_WM_SHELL),
     TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");
 
     private final boolean mEnabled;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 9335438..26d0ec6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -23,6 +23,7 @@
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.view.WindowManager.fixScale;
 import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
@@ -167,10 +168,7 @@
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
 
         ContentResolver resolver = mContext.getContentResolver();
-        mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
-                Settings.Global.TRANSITION_ANIMATION_SCALE,
-                mContext.getResources().getFloat(
-                        R.dimen.config_appTransitionAnimationDurationScaleDefault));
+        mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
         dispatchAnimScaleSetting(mTransitionAnimationScaleSetting);
 
         resolver.registerContentObserver(
@@ -185,6 +183,12 @@
         }
     }
 
+    private float getTransitionAnimationScaleSetting() {
+        return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
+                                R.dimen.config_appTransitionAnimationDurationScaleDefault)));
+    }
+
     public ShellTransitions asRemoteTransitions() {
         return mImpl;
     }
@@ -963,9 +967,7 @@
         @Override
         public void onChange(boolean selfChange) {
             super.onChange(selfChange);
-            mTransitionAnimationScaleSetting = Settings.Global.getFloat(
-                    mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE,
-                    mTransitionAnimationScaleSetting);
+            mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
 
             mMainExecutor.execute(() -> dispatchAnimScaleSetting(mTransitionAnimationScaleSetting));
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 98b5ee9..dc3deb1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -34,6 +34,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.DesktopModeConstants;
 
 /**
  * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
@@ -163,7 +164,13 @@
         View caption = mResult.mRootView.findViewById(R.id.caption);
         caption.setOnTouchListener(mOnCaptionTouchListener);
         View maximize = caption.findViewById(R.id.maximize_window);
-        maximize.setOnClickListener(mOnCaptionButtonClickListener);
+        if (DesktopModeConstants.IS_FEATURE_ENABLED) {
+            // Hide maximize button when desktop mode is available
+            maximize.setVisibility(View.GONE);
+        } else {
+            maximize.setVisibility(View.VISIBLE);
+            maximize.setOnClickListener(mOnCaptionButtonClickListener);
+        }
         View close = caption.findViewById(R.id.close_window);
         close.setOnClickListener(mOnCaptionButtonClickListener);
         View minimize = caption.findViewById(R.id.minimize_window);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 506a4c0..5e64a06 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -248,7 +248,7 @@
         lp.setTrustedOverlay();
         if (mViewHost == null) {
             mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay,
-                    mCaptionWindowManager, true);
+                    mCaptionWindowManager);
             mViewHost.setView(outResult.mRootView, lp);
         } else {
             mViewHost.relayout(lp);
@@ -345,9 +345,8 @@
     }
 
     interface SurfaceControlViewHostFactory {
-        default SurfaceControlViewHost create(
-                Context c, Display d, WindowlessWindowManager wmm, boolean useSfChoreographer) {
-            return new SurfaceControlViewHost(c, d, wmm, useSfChoreographer);
+        default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
+            return new SurfaceControlViewHost(c, d, wmm);
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index f865649..b29c436 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -16,9 +16,11 @@
 
 package com.android.wm.shell;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -30,6 +32,8 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
@@ -38,9 +42,11 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
+import android.app.WindowConfiguration;
 import android.content.LocusId;
 import android.content.pm.ParceledListSlice;
 import android.os.Binder;
@@ -53,6 +59,8 @@
 import android.window.ITaskOrganizerController;
 import android.window.TaskAppearedInfo;
 import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransaction.Change;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -628,6 +636,71 @@
         verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token);
     }
 
+    @Test
+    public void testPrepareClearBoundsForTasks() {
+        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED);
+        task1.displayId = 1;
+        MockToken token1 = new MockToken();
+        task1.token = token1.token();
+        mOrganizer.onTaskAppeared(task1, null);
+
+        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED);
+        task2.displayId = 1;
+        MockToken token2 = new MockToken();
+        task2.token = token2.token();
+        mOrganizer.onTaskAppeared(task2, null);
+
+        RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_UNDEFINED);
+        otherDisplayTask.displayId = 2;
+        MockToken otherDisplayToken = new MockToken();
+        otherDisplayTask.token = otherDisplayToken.token();
+        mOrganizer.onTaskAppeared(otherDisplayTask, null);
+
+        WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForTasks(1);
+
+        assertEquals(wct.getChanges().size(), 2);
+        Change boundsChange1 = wct.getChanges().get(token1.binder());
+        assertNotNull(boundsChange1);
+        assertNotEquals(
+                (boundsChange1.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
+        assertTrue(boundsChange1.getConfiguration().windowConfiguration.getBounds().isEmpty());
+
+        Change boundsChange2 = wct.getChanges().get(token2.binder());
+        assertNotNull(boundsChange2);
+        assertNotEquals(
+                (boundsChange2.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
+        assertTrue(boundsChange2.getConfiguration().windowConfiguration.getBounds().isEmpty());
+    }
+
+    @Test
+    public void testPrepareClearFreeformForTasks() {
+        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM);
+        task1.displayId = 1;
+        MockToken token1 = new MockToken();
+        task1.token = token1.token();
+        mOrganizer.onTaskAppeared(task1, null);
+
+        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
+        task2.displayId = 1;
+        MockToken token2 = new MockToken();
+        task2.token = token2.token();
+        mOrganizer.onTaskAppeared(task2, null);
+
+        RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_FREEFORM);
+        otherDisplayTask.displayId = 2;
+        MockToken otherDisplayToken = new MockToken();
+        otherDisplayTask.token = otherDisplayToken.token();
+        mOrganizer.onTaskAppeared(otherDisplayTask, null);
+
+        WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForTasks(1);
+
+        // Only task with freeform windowing mode and the right display should be updated
+        assertEquals(wct.getChanges().size(), 1);
+        Change wmModeChange1 = wct.getChanges().get(token1.binder());
+        assertNotNull(wmModeChange1);
+        assertEquals(wmModeChange1.getWindowingMode(), WINDOWING_MODE_UNDEFINED);
+    }
+
     private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
@@ -635,4 +708,22 @@
         return taskInfo;
     }
 
+    private static class MockToken {
+        private final WindowContainerToken mToken;
+        private final IBinder mBinder;
+
+        MockToken() {
+            mToken = mock(WindowContainerToken.class);
+            mBinder = mock(IBinder.class);
+            when(mToken.asBinder()).thenReturn(mBinder);
+        }
+
+        WindowContainerToken token() {
+            return mToken;
+        }
+
+        IBinder binder() {
+            return mBinder;
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
new file mode 100644
index 0000000..58f20da
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.wm.shell.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.WindowConfiguration;
+import android.os.Handler;
+import android.os.IBinder;
+import android.testing.AndroidTestingRunner;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransaction.Change;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.RootDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DesktopModeControllerTest extends ShellTestCase {
+
+    @Mock
+    private ShellTaskOrganizer mShellTaskOrganizer;
+    @Mock
+    private RootDisplayAreaOrganizer mRootDisplayAreaOrganizer;
+    @Mock
+    private ShellExecutor mTestExecutor;
+    @Mock
+    private Handler mMockHandler;
+
+    private DesktopModeController mController;
+    private ShellInit mShellInit;
+
+    @Before
+    public void setUp() {
+        mShellInit = Mockito.spy(new ShellInit(mTestExecutor));
+
+        mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer,
+                mRootDisplayAreaOrganizer, mMockHandler);
+
+        mShellInit.init();
+    }
+
+    @Test
+    public void instantiate_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
+    public void testDesktopModeEnabled_taskWmClearedDisplaySetToFreeform() {
+        // Create a fake WCT to simulate setting task windowing mode to undefined
+        WindowContainerTransaction taskWct = new WindowContainerTransaction();
+        MockToken taskMockToken = new MockToken();
+        taskWct.setWindowingMode(taskMockToken.token(), WINDOWING_MODE_UNDEFINED);
+        when(mShellTaskOrganizer.prepareClearFreeformForTasks(mContext.getDisplayId())).thenReturn(
+                taskWct);
+
+        // Create a fake WCT to simulate setting display windowing mode to freeform
+        WindowContainerTransaction displayWct = new WindowContainerTransaction();
+        MockToken displayMockToken = new MockToken();
+        displayWct.setWindowingMode(displayMockToken.token(), WINDOWING_MODE_FREEFORM);
+        when(mRootDisplayAreaOrganizer.prepareWindowingModeChange(mContext.getDisplayId(),
+                WINDOWING_MODE_FREEFORM)).thenReturn(displayWct);
+
+        // The test
+        mController.updateDesktopModeEnabled(true);
+
+        ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+                WindowContainerTransaction.class);
+        verify(mRootDisplayAreaOrganizer).applyTransaction(arg.capture());
+
+        // WCT should have 2 changes - clear task wm mode and set display wm mode
+        WindowContainerTransaction wct = arg.getValue();
+        assertThat(wct.getChanges()).hasSize(2);
+
+        // Verify executed WCT has a change for setting task windowing mode to undefined
+        Change taskWmModeChange = wct.getChanges().get(taskMockToken.binder());
+        assertThat(taskWmModeChange).isNotNull();
+        assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+
+        // Verify executed WCT has a change for setting display windowing mode to freeform
+        Change displayWmModeChange = wct.getChanges().get(displayMockToken.binder());
+        assertThat(displayWmModeChange).isNotNull();
+        assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM);
+    }
+
+    @Test
+    public void testDesktopModeDisabled_taskWmAndBoundsClearedDisplaySetToFullscreen() {
+        // Create a fake WCT to simulate setting task windowing mode to undefined
+        WindowContainerTransaction taskWmWct = new WindowContainerTransaction();
+        MockToken taskWmMockToken = new MockToken();
+        taskWmWct.setWindowingMode(taskWmMockToken.token(), WINDOWING_MODE_UNDEFINED);
+        when(mShellTaskOrganizer.prepareClearFreeformForTasks(mContext.getDisplayId())).thenReturn(
+                taskWmWct);
+
+        // Create a fake WCT to simulate clearing task bounds
+        WindowContainerTransaction taskBoundsWct = new WindowContainerTransaction();
+        MockToken taskBoundsMockToken = new MockToken();
+        taskBoundsWct.setBounds(taskBoundsMockToken.token(), null);
+        when(mShellTaskOrganizer.prepareClearBoundsForTasks(mContext.getDisplayId())).thenReturn(
+                taskBoundsWct);
+
+        // Create a fake WCT to simulate setting display windowing mode to fullscreen
+        WindowContainerTransaction displayWct = new WindowContainerTransaction();
+        MockToken displayMockToken = new MockToken();
+        displayWct.setWindowingMode(displayMockToken.token(), WINDOWING_MODE_FULLSCREEN);
+        when(mRootDisplayAreaOrganizer.prepareWindowingModeChange(mContext.getDisplayId(),
+                WINDOWING_MODE_FULLSCREEN)).thenReturn(displayWct);
+
+        // The test
+        mController.updateDesktopModeEnabled(false);
+
+        ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+                WindowContainerTransaction.class);
+        verify(mRootDisplayAreaOrganizer).applyTransaction(arg.capture());
+
+        // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode
+        WindowContainerTransaction wct = arg.getValue();
+        assertThat(wct.getChanges()).hasSize(3);
+
+        // Verify executed WCT has a change for setting task windowing mode to undefined
+        Change taskWmModeChange = wct.getChanges().get(taskWmMockToken.binder());
+        assertThat(taskWmModeChange).isNotNull();
+        assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+
+        // Verify executed WCT has a change for clearing task bounds
+        Change taskBoundsChange = wct.getChanges().get(taskBoundsMockToken.binder());
+        assertThat(taskBoundsChange).isNotNull();
+        assertThat(taskBoundsChange.getWindowSetMask()
+                & WindowConfiguration.WINDOW_CONFIG_BOUNDS).isNotEqualTo(0);
+        assertThat(taskBoundsChange.getConfiguration().windowConfiguration.getBounds().isEmpty())
+                .isTrue();
+
+        // Verify executed WCT has a change for setting display windowing mode to fullscreen
+        Change displayWmModeChange = wct.getChanges().get(displayMockToken.binder());
+        assertThat(displayWmModeChange).isNotNull();
+        assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
+    }
+
+    private static class MockToken {
+        private final WindowContainerToken mToken;
+        private final IBinder mBinder;
+
+        MockToken() {
+            mToken = mock(WindowContainerToken.class);
+            mBinder = mock(IBinder.class);
+            when(mToken.asBinder()).thenReturn(mBinder);
+        }
+
+        WindowContainerToken token() {
+            return mToken;
+        }
+
+        IBinder binder() {
+            return mBinder;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 226843e..e11be31 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -24,7 +24,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
@@ -107,7 +106,7 @@
         mMockSurfaceControlFinishT = createMockSurfaceControlTransaction();
 
         doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory)
-                .create(any(), any(), any(), anyBoolean());
+                .create(any(), any(), any());
     }
 
     @Test
@@ -148,8 +147,7 @@
 
         verify(decorContainerSurfaceBuilder, never()).build();
         verify(taskBackgroundSurfaceBuilder, never()).build();
-        verify(mMockSurfaceControlViewHostFactory, never())
-                .create(any(), any(), any(), anyBoolean());
+        verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any());
 
         verify(mMockSurfaceControlFinishT).hide(taskSurface);
 
@@ -207,8 +205,7 @@
         verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface, -1);
         verify(mMockSurfaceControlStartT).show(taskBackgroundSurface);
 
-        verify(mMockSurfaceControlViewHostFactory)
-                .create(any(), eq(defaultDisplay), any(), anyBoolean());
+        verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
         verify(mMockSurfaceControlViewHost)
                 .setView(same(mMockView),
                         argThat(lp -> lp.height == 64
@@ -326,8 +323,7 @@
         verify(mMockDisplayController).removeDisplayWindowListener(same(listener));
 
         assertThat(mRelayoutResult.mRootView).isSameInstanceAs(mMockView);
-        verify(mMockSurfaceControlViewHostFactory)
-                .create(any(), eq(mockDisplay), any(), anyBoolean());
+        verify(mMockSurfaceControlViewHostFactory).create(any(), eq(mockDisplay), any());
         verify(mMockSurfaceControlViewHost).setView(same(mMockView), any());
     }
 
diff --git a/location/java/android/location/util/identity/CallerIdentity.java b/location/java/android/location/util/identity/CallerIdentity.java
index ade0ea4..2f8d92b 100644
--- a/location/java/android/location/util/identity/CallerIdentity.java
+++ b/location/java/android/location/util/identity/CallerIdentity.java
@@ -124,15 +124,22 @@
                 packageName, attributionTag, listenerId);
     }
 
+    // in some tests these constants are loaded too early leading to an "incorrect" view of the
+    // current pid and uid. load lazily to prevent this problem in tests.
+    private static class Loader {
+        private static final int MY_UID = Process.myUid();
+        private static final int MY_PID = Process.myPid();
+    }
+
     private final int mUid;
 
     private final int mPid;
 
     private final String mPackageName;
 
-    private final @Nullable String mAttributionTag;
+    @Nullable private final String mAttributionTag;
 
-    private final @Nullable String mListenerId;
+    @Nullable private final String mListenerId;
 
     private CallerIdentity(int uid, int pid, String packageName,
             @Nullable String attributionTag, @Nullable String listenerId) {
@@ -181,6 +188,24 @@
         return mUid == Process.SYSTEM_UID;
     }
 
+    /** Returns true if this identity represents the same user this code is running in. */
+    public boolean isMyUser() {
+        return UserHandle.getUserId(mUid) == UserHandle.getUserId(Loader.MY_UID);
+    }
+
+    /** Returns true if this identity represents the same uid this code is running in. */
+    public boolean isMyUid() {
+        return mUid == Loader.MY_UID;
+    }
+
+    /**
+     * Returns true if this identity represents the same process this code is running in. Returns
+     * false if the identity process is unknown.
+     */
+    public boolean isMyProcess() {
+        return mPid == Loader.MY_PID;
+    }
+
     /**
      * Adds this identity to the worksource supplied, or if not worksource is supplied, creates a
      * new worksource representing this identity.
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index c666140..650f360 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -1299,8 +1299,8 @@
     /** @hide */ public static final String DEVICE_OUT_REMOTE_SUBMIX_NAME = "remote_submix";
     /** @hide */ public static final String DEVICE_OUT_TELEPHONY_TX_NAME = "telephony_tx";
     /** @hide */ public static final String DEVICE_OUT_LINE_NAME = "line";
-    /** @hide */ public static final String DEVICE_OUT_HDMI_ARC_NAME = "hmdi_arc";
-    /** @hide */ public static final String DEVICE_OUT_HDMI_EARC_NAME = "hmdi_earc";
+    /** @hide */ public static final String DEVICE_OUT_HDMI_ARC_NAME = "hdmi_arc";
+    /** @hide */ public static final String DEVICE_OUT_HDMI_EARC_NAME = "hdmi_earc";
     /** @hide */ public static final String DEVICE_OUT_SPDIF_NAME = "spdif";
     /** @hide */ public static final String DEVICE_OUT_FM_NAME = "fm_transmitter";
     /** @hide */ public static final String DEVICE_OUT_AUX_LINE_NAME = "aux_line";
diff --git a/packages/SettingsLib/Spa/.idea/codeStyles/Project.xml b/packages/SettingsLib/Spa/.idea/codeStyles/Project.xml
index 318eeef..cb757d3 100644
--- a/packages/SettingsLib/Spa/.idea/codeStyles/Project.xml
+++ b/packages/SettingsLib/Spa/.idea/codeStyles/Project.xml
@@ -4,6 +4,9 @@
       <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
       <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
     </JetCodeStyleSettings>
+    <XML>
+      <option name="XML_KEEP_LINE_BREAKS" value="true" />
+    </XML>
     <codeStyleSettings language="XML">
       <option name="FORCE_REARRANGE_MODE" value="1" />
       <indentOptions>
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index 914a45b..b5be7cd 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -18,7 +18,8 @@
     package="com.android.settingslib.spa.gallery">
 
     <application
-        android:label="@string/app_name"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_label"
         android:supportsRtl="true">
         <activity
             android:name="com.android.settingslib.spa.gallery.MainActivity"
diff --git a/packages/SettingsLib/Spa/gallery/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/SettingsLib/Spa/gallery/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..623ef56
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@mipmap/ic_launcher_background" />
+    <foreground android:drawable="@mipmap/ic_launcher_foreground" />
+</adaptive-icon>
diff --git a/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher.png b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..c3f1ab9
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_background.png b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..7da1549
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_background.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..187964f
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/gallery/res/values/strings.xml b/packages/SettingsLib/Spa/gallery/res/values/strings.xml
index 510e6c2..0d08d68 100644
--- a/packages/SettingsLib/Spa/gallery/res/values/strings.xml
+++ b/packages/SettingsLib/Spa/gallery/res/values/strings.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   Copyright (C) 2022 The Android Open Source Project
 
@@ -14,6 +15,8 @@
   limitations under the License.
   -->
 <resources>
+    <!-- Gallery App label. [DO NOT TRANSLATE] -->
+    <string name="app_label" translatable="false">Gallery</string>
     <!-- Gallery App name. [DO NOT TRANSLATE] -->
     <string name="app_name" translatable="false">SpaLib Gallery</string>
 </resources>
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/HomePage.kt
index 0d17cd2..ee077f4 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/HomePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/HomePage.kt
@@ -54,6 +54,7 @@
         ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0)
 
         SliderPageProvider.EntryItem()
+        SettingsPagerPageProvider.EntryItem()
         FooterPageProvider.EntryItem()
     }
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PageRepository.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PageRepository.kt
index 8f38d82..6465225 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PageRepository.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PageRepository.kt
@@ -33,6 +33,7 @@
         SwitchPreferencePageProvider,
         ArgumentPageProvider,
         SliderPageProvider,
+        SettingsPagerPageProvider,
         FooterPageProvider,
     ),
     startDestination = Destinations.Home,
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
new file mode 100644
index 0000000..5351ea6
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.settingslib.spa.gallery.page
+
+import android.os.Bundle
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.api.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.SettingsPager
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+
+object SettingsPagerPageProvider : SettingsPageProvider {
+    override val name = "SettingsPager"
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        SettingsPagerPage()
+    }
+
+    @Composable
+    fun EntryItem() {
+        Preference(object : PreferenceModel {
+            override val title = "Sample SettingsPager"
+            override val onClick = navigator(name)
+        })
+    }
+}
+
+@Composable
+private fun SettingsPagerPage() {
+    SettingsPager(listOf("Personal", "Work")) {
+        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+            SettingsTitle(title = "Page $it")
+        }
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingsPagerPagePreview() {
+    SettingsTheme {
+        SettingsPagerPage()
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
new file mode 100644
index 0000000..20020d0
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.settingslib.spa.framework.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.ui.unit.dp
+
+object SettingsShape {
+    val CornerMedium = RoundedCornerShape(12.dp)
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt
new file mode 100644
index 0000000..1ec2390
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.settingslib.spa.widget.scaffold
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.TabRow
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+
+@Composable
+fun SettingsPager(titles: List<String>, content: @Composable (page: Int) -> Unit) {
+    check(titles.isNotEmpty())
+    if (titles.size == 1) {
+        content(0)
+        return
+    }
+
+    Column {
+        var currentPage by rememberSaveable { mutableStateOf(0) }
+
+        TabRow(
+            selectedTabIndex = currentPage,
+            modifier = Modifier.padding(horizontal = SettingsDimension.itemPaddingEnd),
+            containerColor = Color.Transparent,
+            indicator = {},
+            divider = {},
+        ) {
+            titles.forEachIndexed { page, title ->
+                SettingsTab(
+                    title = title,
+                    selected = currentPage == page,
+                    onClick = { currentPage = page },
+                )
+            }
+        }
+
+        content(currentPage)
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt
new file mode 100644
index 0000000..16d8dbc
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.settingslib.spa.widget.scaffold
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Tab
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.theme.SettingsShape
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@Composable
+internal fun SettingsTab(
+    title: String,
+    selected: Boolean,
+    onClick: () -> Unit,
+) {
+    Tab(
+        selected = selected,
+        onClick = onClick,
+        modifier = Modifier
+            .height(48.dp)
+            .padding(horizontal = 4.dp, vertical = 6.dp)
+            .clip(SettingsShape.CornerMedium)
+            .background(color = when {
+                selected -> SettingsTheme.colorScheme.primaryContainer
+                else -> SettingsTheme.colorScheme.surface
+            }),
+    ) {
+        Text(
+            text = title,
+            style = MaterialTheme.typography.labelLarge,
+            color = when {
+                selected -> SettingsTheme.colorScheme.onPrimaryContainer
+                else -> SettingsTheme.colorScheme.secondaryText
+            },
+        )
+    }
+}
+
+@Preview
+@Composable
+private fun SettingsTabPreview() {
+    SettingsTheme {
+        Column {
+            SettingsTab(title = "Personal", selected = true) {}
+            SettingsTab(title = "Work", selected = false) {}
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt
new file mode 100644
index 0000000..f608e10
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.settingslib.spa.widget.scaffold
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsPagerKtTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun twoPage_initialState() {
+        composeTestRule.setContent {
+            TestTwoPage()
+        }
+
+        composeTestRule.onNodeWithText("Personal").assertIsSelected()
+        composeTestRule.onNodeWithText("Page 0").assertIsDisplayed()
+        composeTestRule.onNodeWithText("Work").assertIsNotSelected()
+        composeTestRule.onNodeWithText("Page 1").assertDoesNotExist()
+    }
+
+    @Test
+    fun twoPage_afterSwitch() {
+        composeTestRule.setContent {
+            TestTwoPage()
+        }
+
+        composeTestRule.onNodeWithText("Work").performClick()
+
+        composeTestRule.onNodeWithText("Personal").assertIsNotSelected()
+        composeTestRule.onNodeWithText("Page 0").assertDoesNotExist()
+        composeTestRule.onNodeWithText("Work").assertIsSelected()
+        composeTestRule.onNodeWithText("Page 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun onePage_initialState() {
+        composeTestRule.setContent {
+            SettingsPager(listOf("Personal")) {
+                SettingsTitle(title = "Page $it")
+            }
+        }
+
+        composeTestRule.onNodeWithText("Personal").assertDoesNotExist()
+        composeTestRule.onNodeWithText("Page 0").assertIsDisplayed()
+        composeTestRule.onNodeWithText("Page 1").assertDoesNotExist()
+    }
+}
+
+@Composable
+private fun TestTwoPage() {
+    SettingsPager(listOf("Personal", "Work")) {
+        SettingsTitle(title = "Page $it")
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp
index 48f7ff2..ecbb219 100644
--- a/packages/SettingsLib/SpaPrivileged/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/Android.bp
@@ -29,5 +29,4 @@
         "androidx.compose.runtime_runtime",
     ],
     kotlincflags: ["-Xjvm-default=all"],
-    min_sdk_version: "31",
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/enterprise/EnterpriseRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/enterprise/EnterpriseRepository.kt
new file mode 100644
index 0000000..ae0cb77
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/enterprise/EnterpriseRepository.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.settingslib.spaprivileged.framework.enterprise
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Settings.PERSONAL_CATEGORY_HEADER
+import android.app.admin.DevicePolicyResources.Strings.Settings.WORK_CATEGORY_HEADER
+import android.content.Context
+import com.android.settingslib.spaprivileged.R
+
+class EnterpriseRepository(private val context: Context) {
+    private val resources by lazy {
+        checkNotNull(context.getSystemService(DevicePolicyManager::class.java)).resources
+    }
+
+    fun getEnterpriseString(updatableStringId: String, resId: Int): String =
+        resources.getString(updatableStringId) { context.getString(resId) }
+
+    fun getProfileTitle(isManagedProfile: Boolean): String = if (isManagedProfile) {
+        getEnterpriseString(WORK_CATEGORY_HEADER, R.string.category_work)
+    } else {
+        getEnterpriseString(PERSONAL_CATEGORY_HEADER, R.string.category_personal)
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/WorkProfilePager.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/WorkProfilePager.kt
new file mode 100644
index 0000000..09864a1
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/WorkProfilePager.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.settingslib.spaprivileged.template.scaffold
+
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import com.android.settingslib.spa.widget.scaffold.SettingsPager
+import com.android.settingslib.spaprivileged.framework.enterprise.EnterpriseRepository
+
+@Composable
+fun WorkProfilePager(content: @Composable (userInfo: UserInfo) -> Unit) {
+    val context = LocalContext.current
+    val profiles = remember {
+        val userManager = checkNotNull(context.getSystemService(UserManager::class.java))
+        userManager.getProfiles(UserHandle.myUserId())
+    }
+    val titles = remember {
+        val enterpriseRepository = EnterpriseRepository(context)
+        profiles.map {
+            enterpriseRepository.getProfileTitle(isManagedProfile = it.isManagedProfile)
+        }
+    }
+
+    SettingsPager(titles) { page ->
+        content(profiles[page])
+    }
+}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 42b992f..bc9490f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -212,6 +212,8 @@
         Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
         Settings.Secure.WEAR_TALKBACK_ENABLED,
         Settings.Secure.HBM_SETTING_KEY,
-        Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED
+        Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED,
+        Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED,
+        Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED,
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 14b5855..2c99d71 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -346,5 +346,9 @@
         VALIDATORS.put(Secure.WEAR_TALKBACK_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.HBM_SETTING_KEY, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(
+                Secure.ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(
+                Secure.ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED, BOOLEAN_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index c3b645e..a2ffcf3 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1828,6 +1828,12 @@
         dumpSetting(s, p,
                 Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED,
                 SecureSettingsProto.Accessibility.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED);
+        dumpSetting(s, p,
+                Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED,
+                SecureSettingsProto.Accessibility.SoftwareCursorSettings.TRIGGER_HINTS_ENABLED);
+        dumpSetting(s, p,
+                Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED,
+                SecureSettingsProto.Accessibility.SoftwareCursorSettings.KEYBOARD_SHIFT_ENABLED);
         p.end(accessibilityToken);
 
         final long adaptiveSleepToken = p.start(SecureSettingsProto.ADAPTIVE_SLEEP);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 634df39..a25f567 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -238,6 +238,9 @@
         "com.android.systemui",
     ],
     plugins: ["dagger2-compiler"],
+    lint: {
+        test: true,
+    },
 }
 
 // Opt-in config for optimizing the SystemUI target using R8.
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
index 47ea27f..0839338 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
@@ -30,6 +30,8 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import androidx.core.graphics.ColorUtils;
+
 import com.android.settingslib.Utils;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
@@ -42,7 +44,8 @@
  * @hide
  */
 final class WirelessChargingLayout extends FrameLayout {
-    private static final long RIPPLE_ANIMATION_DURATION = 1500;
+    private static final long CIRCLE_RIPPLE_ANIMATION_DURATION = 1500;
+    private static final long ROUNDED_BOX_RIPPLE_ANIMATION_DURATION = 1750;
     private static final int SCRIM_COLOR = 0x4C000000;
     private static final int SCRIM_FADE_DURATION = 300;
     private RippleView mRippleView;
@@ -131,17 +134,30 @@
                 "backgroundColor", SCRIM_COLOR, Color.TRANSPARENT);
         scrimFadeOutAnimator.setDuration(SCRIM_FADE_DURATION);
         scrimFadeOutAnimator.setInterpolator(Interpolators.LINEAR);
-        scrimFadeOutAnimator.setStartDelay(RIPPLE_ANIMATION_DURATION - SCRIM_FADE_DURATION);
+        scrimFadeOutAnimator.setStartDelay((rippleShape == RippleShape.CIRCLE
+                ? CIRCLE_RIPPLE_ANIMATION_DURATION : ROUNDED_BOX_RIPPLE_ANIMATION_DURATION)
+                - SCRIM_FADE_DURATION);
         AnimatorSet animatorSetScrim = new AnimatorSet();
         animatorSetScrim.playTogether(scrimFadeInAnimator, scrimFadeOutAnimator);
         animatorSetScrim.start();
 
         mRippleView = findViewById(R.id.wireless_charging_ripple);
         mRippleView.setupShader(rippleShape);
+        if (mRippleView.getRippleShape() == RippleShape.ROUNDED_BOX) {
+            mRippleView.setDuration(ROUNDED_BOX_RIPPLE_ANIMATION_DURATION);
+            mRippleView.setSparkleStrength(0.22f);
+            int color = Utils.getColorAttr(mRippleView.getContext(),
+                    android.R.attr.colorAccent).getDefaultColor();
+            mRippleView.setColor(ColorUtils.setAlphaComponent(color, 28));
+        } else {
+            mRippleView.setDuration(CIRCLE_RIPPLE_ANIMATION_DURATION);
+            mRippleView.setColor(Utils.getColorAttr(mRippleView.getContext(),
+                    android.R.attr.colorAccent).getDefaultColor());
+        }
+
         OnAttachStateChangeListener listener = new OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View view) {
-                mRippleView.setDuration(RIPPLE_ANIMATION_DURATION);
                 mRippleView.startRipple();
                 mRippleView.removeOnAttachStateChangeListener(this);
             }
@@ -232,13 +248,13 @@
             int height = getMeasuredHeight();
             mRippleView.setCenter(width * 0.5f, height * 0.5f);
             if (mRippleView.getRippleShape() == RippleShape.ROUNDED_BOX) {
-                mRippleView.setMaxSize(width * 1.5f, height * 1.5f);
+                // Those magic numbers are introduced for visual polish. This aspect ratio maps with
+                // the tablet's docking station.
+                mRippleView.setMaxSize(width * 1.36f, height * 1.46f);
             } else {
                 float maxSize = Math.max(width, height);
                 mRippleView.setMaxSize(maxSize, maxSize);
             }
-            mRippleView.setColor(Utils.getColorAttr(mRippleView.getContext(),
-                    android.R.attr.colorAccent).getDefaultColor());
         }
 
         super.onLayout(changed, left, top, right, bottom);
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index b29176b..42e6b02 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -151,8 +151,8 @@
     public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER =
             new ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip);
 
-    public static final UnreleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE =
-            new UnreleasedFlag(603, false);
+    public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE =
+            new ReleasedFlag(603, false);
 
     public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE = new UnreleasedFlag(604, true);
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index 458ed40..9260020 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.dreams.DreamOverlayStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.NotifPanelEvents
 import com.android.systemui.statusbar.CrossFadeHelper
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
@@ -85,13 +86,22 @@
     private val bypassController: KeyguardBypassController,
     private val mediaCarouselController: MediaCarouselController,
     private val notifLockscreenUserManager: NotificationLockscreenUserManager,
+    private val keyguardViewController: KeyguardViewController,
+    private val dreamOverlayStateController: DreamOverlayStateController,
     configurationController: ConfigurationController,
     wakefulnessLifecycle: WakefulnessLifecycle,
-    private val keyguardViewController: KeyguardViewController,
-    private val dreamOverlayStateController: DreamOverlayStateController
+    panelEventsEvents: NotifPanelEvents,
 ) {
 
     /**
+     * Whether we "skip" QQS during panel expansion.
+     *
+     * This means that when expanding the panel we go directly to QS. Also when we are on QS and
+     * start closing the panel, it fully collapses instead of going to QQS.
+     */
+    private var skipQqsOnExpansion: Boolean = false
+
+    /**
      * The root overlay of the hierarchy. This is where the media notification is attached to
      * whenever the view is transitioning from one host to another. It also make sure that the
      * view is always in its final state when it is attached to a view host.
@@ -504,6 +514,13 @@
         mediaCarouselController.updateUserVisibility = {
             mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
         }
+
+        panelEventsEvents.registerListener(object : NotifPanelEvents.Listener {
+            override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {
+                skipQqsOnExpansion = isExpandImmediateEnabled
+                updateDesiredLocation()
+            }
+        })
     }
 
     private fun updateConfiguration() {
@@ -701,6 +718,9 @@
         if (isCurrentlyInGuidedTransformation()) {
             return false
         }
+        if (skipQqsOnExpansion) {
+            return false
+        }
         // This is an invalid transition, and can happen when using the camera gesture from the
         // lock screen. Disallow.
         if (previousLocation == LOCATION_LOCKSCREEN &&
@@ -852,6 +872,9 @@
      * otherwise
      */
     private fun getTransformationProgress(): Float {
+        if (skipQqsOnExpansion) {
+            return -1.0f
+        }
         val progress = getQSTransformationProgress()
         if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
             return progress
@@ -1042,6 +1065,10 @@
             // reattach it without an animation
             return LOCATION_LOCKSCREEN
         }
+        if (skipQqsOnExpansion) {
+            // When doing an immediate expand or collapse, we want to keep it in QS.
+            return LOCATION_QS
+        }
         return location
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 7e263d8..08cf57c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -296,27 +296,17 @@
         mMediaOutputController.setRefreshing(true);
         // Update header icon
         final int iconRes = getHeaderIconRes();
-        final IconCompat iconCompat = getHeaderIcon();
-        final Drawable appSourceDrawable = getAppSourceIcon();
+        final IconCompat headerIcon = getHeaderIcon();
+        final IconCompat appSourceIcon = getAppSourceIcon();
         boolean colorSetUpdated = false;
         mCastAppLayout.setVisibility(
                 mMediaOutputController.shouldShowLaunchSection()
                         ? View.VISIBLE : View.GONE);
-        if (appSourceDrawable != null) {
-            mAppResourceIcon.setImageDrawable(appSourceDrawable);
-            mAppButton.setCompoundDrawablesWithIntrinsicBounds(resizeDrawable(appSourceDrawable,
-                            mContext.getResources().getDimensionPixelSize(
-                                    R.dimen.media_output_dialog_app_tier_icon_size
-                            )),
-                    null, null, null);
-        } else {
-            mAppResourceIcon.setVisibility(View.GONE);
-        }
         if (iconRes != 0) {
             mHeaderIcon.setVisibility(View.VISIBLE);
             mHeaderIcon.setImageResource(iconRes);
-        } else if (iconCompat != null) {
-            Icon icon = iconCompat.toIcon(mContext);
+        } else if (headerIcon != null) {
+            Icon icon = headerIcon.toIcon(mContext);
             if (icon.getType() != Icon.TYPE_BITMAP && icon.getType() != Icon.TYPE_ADAPTIVE_BITMAP) {
                 // icon doesn't support getBitmap, use default value for color scheme
                 updateButtonBackgroundColorFilter();
@@ -336,6 +326,18 @@
         } else {
             mHeaderIcon.setVisibility(View.GONE);
         }
+        if (appSourceIcon != null) {
+            Icon appIcon = appSourceIcon.toIcon(mContext);
+            mAppResourceIcon.setColorFilter(mMediaOutputController.getColorItemContent());
+            mAppResourceIcon.setImageIcon(appIcon);
+        } else {
+            Drawable appIconDrawable = mMediaOutputController.getAppSourceIconFromPackage();
+            if (appIconDrawable != null) {
+                mAppResourceIcon.setImageDrawable(appIconDrawable);
+            } else {
+                mAppResourceIcon.setVisibility(View.GONE);
+            }
+        }
         if (mHeaderIcon.getVisibility() == View.VISIBLE) {
             final int size = getHeaderIconSize();
             final int padding = mContext.getResources().getDimensionPixelSize(
@@ -480,7 +482,7 @@
         }
     }
 
-    abstract Drawable getAppSourceIcon();
+    abstract IconCompat getAppSourceIcon();
 
     abstract int getHeaderIconRes();
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index 310469d..35baf013 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -19,7 +19,6 @@
 import android.app.AlertDialog;
 import android.content.Context;
 import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.text.method.HideReturnsTransformationMethod;
 import android.text.method.PasswordTransformationMethod;
@@ -116,8 +115,8 @@
     }
 
     @Override
-    Drawable getAppSourceIcon() {
-        return mMediaOutputController.getAppSourceIcon();
+    IconCompat getAppSourceIcon() {
+        return mMediaOutputController.getNotificationSmallIcon();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
index 0fa3265..2b5d6fd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.dialog
 
+import android.app.KeyguardManager
 import android.content.Context
 import android.media.AudioManager
 import android.media.session.MediaSessionManager
@@ -45,7 +46,8 @@
     private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>,
     private val audioManager: AudioManager,
-    private val powerExemptionManager: PowerExemptionManager
+    private val powerExemptionManager: PowerExemptionManager,
+    private val keyGuardManager: KeyguardManager
 ) {
     var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
 
@@ -57,7 +59,7 @@
         val controller = MediaOutputController(context, packageName,
                 mediaSessionManager, lbm, starter, notifCollection,
                 dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
-                powerExemptionManager)
+                powerExemptionManager, keyGuardManager)
         val dialog =
                 MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
         mediaOutputBroadcastDialog = dialog
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 27095b3..f7d80e0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -20,6 +20,7 @@
 
 import android.annotation.CallbackExecutor;
 import android.app.AlertDialog;
+import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.WallpaperColors;
 import android.bluetooth.BluetoothLeBroadcast;
@@ -120,6 +121,7 @@
     final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
     private final AudioManager mAudioManager;
     private final PowerExemptionManager mPowerExemptionManager;
+    private final KeyguardManager mKeyGuardManager;
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
     private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
 
@@ -154,7 +156,8 @@
             DialogLaunchAnimator dialogLaunchAnimator,
             Optional<NearbyMediaDevicesManager> nearbyMediaDevicesManagerOptional,
             AudioManager audioManager,
-            PowerExemptionManager powerExemptionManager) {
+            PowerExemptionManager powerExemptionManager,
+            KeyguardManager keyGuardManager) {
         mContext = context;
         mPackageName = packageName;
         mMediaSessionManager = mediaSessionManager;
@@ -163,6 +166,7 @@
         mNotifCollection = notifCollection;
         mAudioManager = audioManager;
         mPowerExemptionManager = powerExemptionManager;
+        mKeyGuardManager = keyGuardManager;
         InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm);
         mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
         mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
@@ -300,7 +304,7 @@
         }
     }
 
-    Drawable getAppSourceIcon() {
+    Drawable getAppSourceIconFromPackage() {
         if (mPackageName.isEmpty()) {
             return null;
         }
@@ -411,6 +415,24 @@
                 || isSelectedDeviceInGroup;
     }
 
+    IconCompat getNotificationSmallIcon() {
+        if (TextUtils.isEmpty(mPackageName)) {
+            return null;
+        }
+        for (NotificationEntry entry : mNotifCollection.getAllNotifs()) {
+            final Notification notification = entry.getSbn().getNotification();
+            if (notification.isMediaNotification()
+                    && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) {
+                final Icon icon = notification.getSmallIcon();
+                if (icon == null) {
+                    break;
+                }
+                return IconCompat.createFromIcon(icon);
+            }
+        }
+        return null;
+    }
+
     IconCompat getNotificationIcon() {
         if (TextUtils.isEmpty(mPackageName)) {
             return null;
@@ -701,7 +723,8 @@
         ActivityLaunchAnimator.Controller controller =
                 mDialogLaunchAnimator.createActivityLaunchController(view);
 
-        if (controller == null) {
+        if (controller == null || (mKeyGuardManager != null
+                && mKeyGuardManager.isKeyguardLocked())) {
             mCallback.dismissDialog();
         }
 
@@ -753,7 +776,7 @@
         MediaOutputController controller = new MediaOutputController(mContext, mPackageName,
                 mMediaSessionManager, mLocalBluetoothManager, mActivityStarter,
                 mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager),
-                mAudioManager, mPowerExemptionManager);
+                mAudioManager, mPowerExemptionManager, mKeyGuardManager);
         MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true,
                 broadcastSender, controller);
         mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 9fb96b5..cb6f5a7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -17,7 +17,6 @@
 package com.android.systemui.media.dialog;
 
 import android.content.Context;
-import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.view.View;
 import android.view.WindowManager;
@@ -81,8 +80,8 @@
     }
 
     @Override
-    Drawable getAppSourceIcon() {
-        return mMediaOutputController.getAppSourceIcon();
+    IconCompat getAppSourceIcon() {
+        return mMediaOutputController.getNotificationSmallIcon();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
index 8249a7c..543efed 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.dialog
 
+import android.app.KeyguardManager
 import android.content.Context
 import android.media.AudioManager
 import android.media.session.MediaSessionManager
@@ -47,7 +48,8 @@
     private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>,
     private val audioManager: AudioManager,
-    private val powerExemptionManager: PowerExemptionManager
+    private val powerExemptionManager: PowerExemptionManager,
+    private val keyGuardManager: KeyguardManager
 ) {
     companion object {
         private const val INTERACTION_JANK_TAG = "media_output"
@@ -63,7 +65,7 @@
             context, packageName,
             mediaSessionManager, lbm, starter, notifCollection,
             dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
-            powerExemptionManager)
+            powerExemptionManager, keyGuardManager)
         val dialog =
             MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller, uiEventLogger)
         mediaOutputDialog = dialog
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index 833573d..be44202 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -39,6 +39,7 @@
 
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.inject.Provider;
 
 /** Controller for {@link QuickQSPanel}. */
 @QSScope
@@ -52,20 +53,21 @@
                 }
             };
 
-    private final boolean mUsingCollapsedLandscapeMedia;
+    private final Provider<Boolean> mUsingCollapsedLandscapeMediaProvider;
 
     @Inject
     QuickQSPanelController(QuickQSPanel view, QSTileHost qsTileHost,
             QSCustomizerController qsCustomizerController,
             @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer,
             @Named(QUICK_QS_PANEL) MediaHost mediaHost,
-            @Named(QS_USING_COLLAPSED_LANDSCAPE_MEDIA) boolean usingCollapsedLandscapeMedia,
+            @Named(QS_USING_COLLAPSED_LANDSCAPE_MEDIA)
+                    Provider<Boolean> usingCollapsedLandscapeMediaProvider,
             MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
             DumpManager dumpManager
     ) {
         super(view, qsTileHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger,
                 uiEventLogger, qsLogger, dumpManager);
-        mUsingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia;
+        mUsingCollapsedLandscapeMediaProvider = usingCollapsedLandscapeMediaProvider;
     }
 
     @Override
@@ -80,7 +82,8 @@
         int rotation = getRotation();
         boolean isLandscape = rotation == RotationUtils.ROTATION_LANDSCAPE
                 || rotation == RotationUtils.ROTATION_SEASCAPE;
-        if (!mUsingCollapsedLandscapeMedia || !isLandscape) {
+        boolean usingCollapsedLandscapeMedia = mUsingCollapsedLandscapeMediaProvider.get();
+        if (!usingCollapsedLandscapeMedia || !isLandscape) {
             mMediaHost.setExpansion(MediaHost.EXPANDED);
         } else {
             mMediaHost.setExpansion(MediaHost.COLLAPSED);
@@ -126,7 +129,6 @@
         super.setTiles(tiles, /* collapsedView */ true);
     }
 
-    /** */
     public void setContentMargins(int marginStart, int marginEnd) {
         mView.setContentMargins(marginStart, marginEnd, mMediaHost.getHostView());
     }
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
index 56a1874..db7c1fd 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
+++ b/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
@@ -67,7 +67,7 @@
 
                 float rippleInsideAlpha = (1.-inside) * in_fadeFill;
                 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
-                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * 0.45;
+                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
                 vec4 ripple = in_color * rippleAlpha;
                 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
             }
@@ -83,7 +83,7 @@
 
                 float rippleInsideAlpha = (1.-inside) * in_fadeFill;
                 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
-                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * 0.45;
+                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
                 vec4 ripple = in_color * rippleAlpha;
                 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
             }
@@ -99,7 +99,7 @@
 
                 float rippleInsideAlpha = (1.-inside) * in_fadeFill;
                 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
-                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * 0.45;
+                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
                 vec4 ripple = in_color * rippleAlpha;
                 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
index 8b01201..60c8f37 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
@@ -81,6 +81,7 @@
         rippleShader.color = RIPPLE_DEFAULT_COLOR
         rippleShader.progress = 0f
         rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
+        rippleShader.pixelDensity = resources.displayMetrics.density
 
         ripplePaint.shader = rippleShader
     }
@@ -124,6 +125,13 @@
         rippleShader.rippleFill = rippleFill
     }
 
+    /**
+     * Set the intensity of the sparkles.
+     */
+    fun setSparkleStrength(strength: Float) {
+        rippleShader.sparkleStrength = strength
+    }
+
     override fun onDraw(canvas: Canvas?) {
         if (canvas == null || !canvas.isHardwareAccelerated) {
             // Drawing with the ripple shader requires hardware acceleration, so skip
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt b/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt
index ce9d89f..4558061 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt
@@ -29,11 +29,25 @@
     interface Listener {
 
         /** Invoked when the notification panel starts or stops collapsing. */
-        fun onPanelCollapsingChanged(isCollapsing: Boolean)
+        @JvmDefault
+        fun onPanelCollapsingChanged(isCollapsing: Boolean) {}
 
         /**
          * Invoked when the notification panel starts or stops launching an [android.app.Activity].
          */
-        fun onLaunchingActivityChanged(isLaunchingActivity: Boolean)
+        @JvmDefault
+        fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {}
+
+        /**
+         * Invoked when the "expand immediate" attribute changes.
+         *
+         * An example of expanding immediately is when swiping down from the top with two fingers.
+         * Instead of going to QQS, we immediately expand to full QS.
+         *
+         * Another example is when full QS is showing, and we swipe up from the bottom. Instead of
+         * going to QQS, the panel fully collapses.
+         */
+        @JvmDefault
+        fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 37a948d..e8ed00b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1690,12 +1690,17 @@
         }
 
         if (mQsExpanded) {
-            mQsExpandImmediate = true;
+            setQsExpandImmediate(true);
             setShowShelfOnly(true);
         }
         super.collapse(delayed, speedUpFactor);
     }
 
+    private void setQsExpandImmediate(boolean expandImmediate) {
+        mQsExpandImmediate = expandImmediate;
+        mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate);
+    }
+
     private void setShowShelfOnly(boolean shelfOnly) {
         mNotificationStackScrollLayoutController.setShouldShowShelfOnly(
                 shelfOnly && !mSplitShadeEnabled);
@@ -1748,7 +1753,7 @@
 
     public void expandWithQs() {
         if (isQsExpansionEnabled()) {
-            mQsExpandImmediate = true;
+            setQsExpandImmediate(true);
             setShowShelfOnly(true);
         }
         if (mSplitShadeEnabled && isOnKeyguard()) {
@@ -2077,7 +2082,7 @@
         if (mTwoFingerQsExpandPossible && isOpenQsEvent(event) && event.getY(event.getActionIndex())
                 < mStatusBarMinHeight) {
             mMetricsLogger.count(COUNTER_PANEL_OPEN_QS, 1);
-            mQsExpandImmediate = true;
+            setQsExpandImmediate(true);
             setShowShelfOnly(true);
             requestPanelHeightUpdate();
 
@@ -3279,7 +3284,7 @@
         } else {
             setListening(true);
         }
-        mQsExpandImmediate = false;
+        setQsExpandImmediate(false);
         setShowShelfOnly(false);
         mTwoFingerQsExpandPossible = false;
         updateTrackingHeadsUp(null);
@@ -3337,7 +3342,7 @@
         super.onTrackingStarted();
         mScrimController.onTrackingStarted();
         if (mQsFullyExpanded) {
-            mQsExpandImmediate = true;
+            setQsExpandImmediate(true);
             setShowShelfOnly(true);
         }
         mNotificationStackScrollLayoutController.onPanelTrackingStarted();
@@ -4898,7 +4903,7 @@
             // to locked will trigger this event and we're not actually in the process of opening
             // the shade, lockscreen is just always expanded
             if (mSplitShadeEnabled && !isOnKeyguard()) {
-                mQsExpandImmediate = true;
+                setQsExpandImmediate(true);
             }
             mCentralSurfaces.makeExpandedVisible(false);
         }
@@ -4965,5 +4970,11 @@
                 cb.onPanelCollapsingChanged(isCollapsing);
             }
         }
+
+        private void notifyExpandImmediateChange(boolean expandImmediateEnabled) {
+            for (NotifPanelEvents.Listener cb : mListeners) {
+                cb.onExpandImmediateChanged(expandImmediateEnabled);
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index d058b75..53e08ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -10,6 +10,7 @@
 import android.provider.Settings
 import android.view.Surface
 import android.view.View
+import android.view.WindowManager.fixScale
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
@@ -138,8 +139,8 @@
     }
 
     fun updateAnimatorDurationScale() {
-        animatorDurationScale =
-                globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
+        animatorDurationScale = fixScale(
+                globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f))
     }
 
     override fun shouldDelayKeyguardShow(): Boolean =
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 8f2a432..fc20ac2 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -138,7 +138,7 @@
 
         ensureOverlayRemoved()
 
-        val newRoot = SurfaceControlViewHost(context, context.display!!, wwm, false)
+        val newRoot = SurfaceControlViewHost(context, context.display!!, wwm)
         val newView =
             LightRevealScrim(context, null).apply {
                 revealEffect = createLightRevealEffect()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
index d65b6b3..369913d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.media
 
-import org.mockito.Mockito.`when` as whenever
 import android.graphics.Rect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
@@ -30,6 +29,7 @@
 import com.android.systemui.dreams.DreamOverlayStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.testing.FakeNotifPanelEvents
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -50,10 +50,11 @@
 import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
@@ -61,32 +62,19 @@
 @TestableLooper.RunWithLooper
 class MediaHierarchyManagerTest : SysuiTestCase() {
 
-    @Mock
-    private lateinit var lockHost: MediaHost
-    @Mock
-    private lateinit var qsHost: MediaHost
-    @Mock
-    private lateinit var qqsHost: MediaHost
-    @Mock
-    private lateinit var bypassController: KeyguardBypassController
-    @Mock
-    private lateinit var keyguardStateController: KeyguardStateController
-    @Mock
-    private lateinit var statusBarStateController: SysuiStatusBarStateController
-    @Mock
-    private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
-    @Mock
-    private lateinit var mediaCarouselController: MediaCarouselController
-    @Mock
-    private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
-    @Mock
-    private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
-    @Mock
-    private lateinit var keyguardViewController: KeyguardViewController
-    @Mock
-    private lateinit var uniqueObjectHostView: UniqueObjectHostView
-    @Mock
-    private lateinit var dreamOverlayStateController: DreamOverlayStateController
+    @Mock private lateinit var lockHost: MediaHost
+    @Mock private lateinit var qsHost: MediaHost
+    @Mock private lateinit var qqsHost: MediaHost
+    @Mock private lateinit var bypassController: KeyguardBypassController
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
+    @Mock private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
+    @Mock private lateinit var mediaCarouselController: MediaCarouselController
+    @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
+    @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Mock private lateinit var keyguardViewController: KeyguardViewController
+    @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView
+    @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController
     @Captor
     private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
     @Captor
@@ -97,6 +85,7 @@
     private lateinit var mediaHiearchyManager: MediaHierarchyManager
     private lateinit var mediaFrame: ViewGroup
     private val configurationController = FakeConfigurationController()
+    private val notifPanelEvents = FakeNotifPanelEvents()
 
     @Before
     fun setup() {
@@ -111,10 +100,12 @@
                 bypassController,
                 mediaCarouselController,
                 notificationLockscreenUserManager,
+                keyguardViewController,
+                dreamOverlayStateController,
                 configurationController,
                 wakefulnessLifecycle,
-                keyguardViewController,
-                dreamOverlayStateController)
+                notifPanelEvents,
+        )
         verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
         verify(statusBarStateController).addCallback(statusBarCallback.capture())
         setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN, LOCKSCREEN_TOP)
@@ -212,6 +203,25 @@
     }
 
     @Test
+    fun calculateTransformationType_notOnLockscreen_returnsTransition() {
+        expandQS()
+
+        val transformType = mediaHiearchyManager.calculateTransformationType()
+
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
+    }
+
+    @Test
+    fun calculateTransformationType_onLockscreen_returnsTransition() {
+        goToLockscreen()
+        expandQS()
+
+        val transformType = mediaHiearchyManager.calculateTransformationType()
+
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
+    }
+
+    @Test
     fun calculateTransformationType_onLockShade_inSplitShade_goingToFullShade_returnsTransition() {
         enableSplitShade()
         goToLockscreen()
@@ -295,6 +305,18 @@
     }
 
     @Test
+    fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() {
+        notifPanelEvents.changeExpandImmediate(expandImmediate = true)
+        goToLockscreen()
+        enterGuidedTransformation()
+        whenever(lockHost.visible).thenReturn(true)
+        whenever(qsHost.visible).thenReturn(true)
+        whenever(qqsHost.visible).thenReturn(true)
+
+        assertThat(mediaHiearchyManager.isCurrentlyInGuidedTransformation()).isFalse()
+    }
+
+    @Test
     fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue() {
         goToLockscreen()
         enterGuidedTransformation()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 314997d..6173692 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -24,9 +24,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.KeyguardManager;
 import android.content.Context;
 import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
 import android.media.AudioManager;
 import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
@@ -88,6 +88,7 @@
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
     private final AudioManager mAudioManager = mock(AudioManager.class);
     private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
+    private KeyguardManager mKeyguardManager = mock(KeyguardManager.class);
 
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
@@ -119,7 +120,8 @@
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
         mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender,
                 mMediaOutputController);
         mMediaOutputBaseDialogImpl.onCreate(new Bundle());
@@ -276,7 +278,7 @@
         }
 
         @Override
-        Drawable getAppSourceIcon() {
+        IconCompat getAppSourceIcon() {
             return null;
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index 751c895..6dcf802 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -31,6 +31,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.KeyguardManager;
 import android.app.Notification;
 import android.content.Context;
 import android.graphics.drawable.Icon;
@@ -47,6 +48,7 @@
 import android.service.notification.StatusBarNotification;
 import android.testing.AndroidTestingRunner;
 import android.text.TextUtils;
+import android.view.View;
 
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.test.filters.SmallTest;
@@ -57,6 +59,7 @@
 import com.android.settingslib.media.MediaDevice;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
 import com.android.systemui.plugins.ActivityStarter;
@@ -102,11 +105,16 @@
     private RoutingSessionInfo mRemoteSessionInfo = mock(RoutingSessionInfo.class);
     private ActivityStarter mStarter = mock(ActivityStarter.class);
     private AudioManager mAudioManager = mock(AudioManager.class);
+    private KeyguardManager mKeyguardManager = mock(KeyguardManager.class);
     private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
     private CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class);
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
+    private final ActivityLaunchAnimator.Controller mActivityLaunchAnimatorController = mock(
+            ActivityLaunchAnimator.Controller.class);
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
             NearbyMediaDevicesManager.class);
+    private View mDialogLaunchView = mock(View.class);
+    private MediaOutputController.Callback mCallback = mock(MediaOutputController.Callback.class);
 
     private Context mSpyContext;
     private MediaOutputController mMediaOutputController;
@@ -131,7 +139,8 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
         mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         MediaDescription.Builder builder = new MediaDescription.Builder();
@@ -183,7 +192,8 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, null,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
 
         mMediaOutputController.start(mCb);
 
@@ -212,7 +222,8 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, null,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
 
         mMediaOutputController.start(mCb);
 
@@ -461,7 +472,8 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, null,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
 
         assertThat(mMediaOutputController.getNotificationIcon()).isNull();
     }
@@ -557,4 +569,16 @@
         verify(mPowerExemptionManager).addToTemporaryAllowList(anyString(), anyInt(), anyString(),
                 anyLong());
     }
+
+    @Test
+    public void launchBluetoothPairing_isKeyguardLocked_dismissDialog() {
+        when(mDialogLaunchAnimator.createActivityLaunchController(mDialogLaunchView)).thenReturn(
+                mActivityLaunchAnimatorController);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        mMediaOutputController.mCallback = this.mCallback;
+
+        mMediaOutputController.launchBluetoothPairing(mDialogLaunchView);
+
+        verify(mCallback).dismissDialog();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index 4779d32..9557513 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.KeyguardManager;
 import android.media.AudioManager;
 import android.media.MediaRoute2Info;
 import android.media.session.MediaController;
@@ -85,6 +86,7 @@
             NearbyMediaDevicesManager.class);
     private final AudioManager mAudioManager = mock(AudioManager.class);
     private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
+    private KeyguardManager mKeyguardManager = mock(KeyguardManager.class);
 
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private MediaOutputDialog mMediaOutputDialog;
@@ -104,7 +106,8 @@
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         mMediaOutputDialog = new MediaOutputDialog(mContext, false, mBroadcastSender,
                 mMediaOutputController, mUiEventLogger);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index 1f28210..e4f47fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.qs
 
 import android.content.res.Configuration
-import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
@@ -38,38 +38,32 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.any
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 class QuickQSPanelControllerTest : SysuiTestCase() {
 
-    @Mock
-    private lateinit var quickQSPanel: QuickQSPanel
-    @Mock
-    private lateinit var qsTileHost: QSTileHost
-    @Mock
-    private lateinit var qsCustomizerController: QSCustomizerController
-    @Mock
-    private lateinit var mediaHost: MediaHost
-    @Mock
-    private lateinit var metricsLogger: MetricsLogger
+    @Mock private lateinit var quickQSPanel: QuickQSPanel
+    @Mock private lateinit var qsTileHost: QSTileHost
+    @Mock private lateinit var qsCustomizerController: QSCustomizerController
+    @Mock private lateinit var mediaHost: MediaHost
+    @Mock private lateinit var metricsLogger: MetricsLogger
+    @Mock private lateinit var qsLogger: QSLogger
+    @Mock private lateinit var tile: QSTile
+    @Mock private lateinit var tileLayout: TileLayout
+    @Mock private lateinit var tileView: QSTileView
+    @Captor private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener>
+
     private val uiEventLogger = UiEventLoggerFake()
-    @Mock
-    private lateinit var qsLogger: QSLogger
     private val dumpManager = DumpManager()
-    @Mock
-    private lateinit var tile: QSTile
-    @Mock
-    private lateinit var tileLayout: TileLayout
-    @Mock
-    private lateinit var tileView: QSTileView
-    @Captor
-    private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener>
+
+    private var usingCollapsedLandscapeMedia = true
 
     private lateinit var controller: TestQuickQSPanelController
 
@@ -77,24 +71,24 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(quickQSPanel.tileLayout).thenReturn(tileLayout)
-        `when`(quickQSPanel.isAttachedToWindow).thenReturn(true)
-        `when`(quickQSPanel.dumpableTag).thenReturn("")
-        `when`(quickQSPanel.resources).thenReturn(mContext.resources)
-        `when`(qsTileHost.createTileView(any(), any(), anyBoolean())).thenReturn(tileView)
+        whenever(quickQSPanel.tileLayout).thenReturn(tileLayout)
+        whenever(quickQSPanel.isAttachedToWindow).thenReturn(true)
+        whenever(quickQSPanel.dumpableTag).thenReturn("")
+        whenever(quickQSPanel.resources).thenReturn(mContext.resources)
+        whenever(qsTileHost.createTileView(any(), any(), anyBoolean())).thenReturn(tileView)
 
-        controller = TestQuickQSPanelController(
+        controller =
+            TestQuickQSPanelController(
                 quickQSPanel,
                 qsTileHost,
                 qsCustomizerController,
-                false,
+                /* usingMediaPlayer = */ false,
                 mediaHost,
-                true,
+                { usingCollapsedLandscapeMedia },
                 metricsLogger,
                 uiEventLogger,
                 qsLogger,
-                dumpManager
-        )
+                dumpManager)
 
         controller.init()
     }
@@ -106,9 +100,9 @@
 
     @Test
     fun testTileSublistWithFewerTiles_noCrash() {
-        `when`(quickQSPanel.numQuickTiles).thenReturn(3)
+        whenever(quickQSPanel.numQuickTiles).thenReturn(3)
 
-        `when`(qsTileHost.tiles).thenReturn(listOf(tile, tile))
+        whenever(qsTileHost.tiles).thenReturn(listOf(tile, tile))
 
         controller.setTiles()
     }
@@ -116,8 +110,8 @@
     @Test
     fun testTileSublistWithTooManyTiles() {
         val limit = 3
-        `when`(quickQSPanel.numQuickTiles).thenReturn(limit)
-        `when`(qsTileHost.tiles).thenReturn(listOf(tile, tile, tile, tile))
+        whenever(quickQSPanel.numQuickTiles).thenReturn(limit)
+        whenever(qsTileHost.tiles).thenReturn(listOf(tile, tile, tile, tile))
 
         controller.setTiles()
 
@@ -125,39 +119,61 @@
     }
 
     @Test
-    fun testMediaExpansionUpdatedWhenConfigurationChanged() {
+    fun mediaExpansion_afterConfigChange_inLandscape_collapsedInLandscapeTrue_updatesToCollapsed() {
         // times(2) because both controller and base controller are registering their listeners
         verify(quickQSPanel, times(2)).addOnConfigurationChangedListener(captor.capture())
 
         // verify that media starts in the expanded state by default
         verify(mediaHost).expansion = MediaHostState.EXPANDED
 
-        // Rotate device, verify media size updated
+        // Rotate device, verify media size updated to collapsed
+        usingCollapsedLandscapeMedia = true
         controller.setRotation(RotationUtils.ROTATION_LANDSCAPE)
         captor.allValues.forEach { it.onConfigurationChange(Configuration.EMPTY) }
 
         verify(mediaHost).expansion = MediaHostState.COLLAPSED
     }
 
+    @Test
+    fun mediaExpansion_afterConfigChange_landscape_collapsedInLandscapeFalse_remainsExpanded() {
+        // times(2) because both controller and base controller are registering their listeners
+        verify(quickQSPanel, times(2)).addOnConfigurationChangedListener(captor.capture())
+        reset(mediaHost)
+
+        usingCollapsedLandscapeMedia = false
+        controller.setRotation(RotationUtils.ROTATION_LANDSCAPE)
+        captor.allValues.forEach { it.onConfigurationChange(Configuration.EMPTY) }
+
+        verify(mediaHost).expansion = MediaHostState.EXPANDED
+    }
+
     class TestQuickQSPanelController(
         view: QuickQSPanel,
         qsTileHost: QSTileHost,
         qsCustomizerController: QSCustomizerController,
         usingMediaPlayer: Boolean,
         mediaHost: MediaHost,
-        usingCollapsedLandscapeMedia: Boolean,
+        usingCollapsedLandscapeMedia: () -> Boolean,
         metricsLogger: MetricsLogger,
         uiEventLogger: UiEventLoggerFake,
         qsLogger: QSLogger,
         dumpManager: DumpManager
-    ) : QuickQSPanelController(view, qsTileHost, qsCustomizerController, usingMediaPlayer,
-        mediaHost, usingCollapsedLandscapeMedia, metricsLogger, uiEventLogger, qsLogger,
-        dumpManager) {
+    ) :
+        QuickQSPanelController(
+            view,
+            qsTileHost,
+            qsCustomizerController,
+            usingMediaPlayer,
+            mediaHost,
+            usingCollapsedLandscapeMedia,
+            metricsLogger,
+            uiEventLogger,
+            qsLogger,
+            dumpManager) {
 
         private var rotation = RotationUtils.ROTATION_NONE
 
-        @Override
-        override fun getRotation(): Int = rotation
+        @Override override fun getRotation(): Int = rotation
 
         fun setRotation(newRotation: Int) {
             rotation = newRotation
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt
new file mode 100644
index 0000000..d052138
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.shade.testing
+
+import com.android.systemui.shade.NotifPanelEvents
+
+/** Fake implementation of [NotifPanelEvents] for testing. */
+class FakeNotifPanelEvents : NotifPanelEvents {
+
+    private val listeners = mutableListOf<NotifPanelEvents.Listener>()
+
+    override fun registerListener(listener: NotifPanelEvents.Listener) {
+        listeners.add(listener)
+    }
+
+    override fun unregisterListener(listener: NotifPanelEvents.Listener) {
+        listeners.remove(listener)
+    }
+
+    fun changeExpandImmediate(expandImmediate: Boolean) {
+        listeners.forEach { it.onExpandImmediateChanged(expandImmediate) }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
index 309acdf..f539dbd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
@@ -91,6 +91,8 @@
     fun capture(): T = wrapped.capture()
     val value: T
         get() = wrapped.value
+    val allValues: List<T>
+        get() = wrapped.allValues
 }
 
 /**
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 297e6a2..c8519c1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -18414,11 +18414,11 @@
                         attributionSource.getUid()), Process.SHELL_UID);
                 final long identity = Binder.clearCallingIdentity();
                 try {
-                    return superImpl.apply(code, new AttributionSource(shellUid,
+                    return superImpl.apply(code, new AttributionSource(shellUid, /*pid*/ -1,
                             "com.android.shell", attributionSource.getAttributionTag(),
-                            attributionSource.getToken(), attributionSource.getNext()),
-                            shouldCollectAsyncNotedOp, message, shouldCollectMessage,
-                            skiProxyOperation);
+                            attributionSource.getToken(), /*renouncedPermissions*/ null,
+                            attributionSource.getNext()), shouldCollectAsyncNotedOp, message,
+                            shouldCollectMessage, skiProxyOperation);
                 } finally {
                     Binder.restoreCallingIdentity(identity);
                 }
@@ -18465,12 +18465,13 @@
                         attributionSource.getUid()), Process.SHELL_UID);
                 final long identity = Binder.clearCallingIdentity();
                 try {
-                    return superImpl.apply(code, new AttributionSource(shellUid,
+                    return superImpl.apply(code, new AttributionSource(shellUid, /*pid*/ -1,
                             "com.android.shell", attributionSource.getAttributionTag(),
-                            attributionSource.getToken(), attributionSource.getNext()),
-                            startIfModeDefault, shouldCollectAsyncNotedOp, message,
-                            shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
-                            proxiedAttributionFlags, attributionChainId);
+                            attributionSource.getToken(), /*renouncedPermissions*/ null,
+                            attributionSource.getNext()), startIfModeDefault,
+                            shouldCollectAsyncNotedOp, message, shouldCollectMessage,
+                            skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags,
+                            attributionChainId);
                 } finally {
                     Binder.restoreCallingIdentity(identity);
                 }
@@ -18489,10 +18490,10 @@
                         attributionSource.getUid()), Process.SHELL_UID);
                 final long identity = Binder.clearCallingIdentity();
                 try {
-                    superImpl.apply(code, new AttributionSource(shellUid,
+                    superImpl.apply(code, new AttributionSource(shellUid, /*pid*/ -1,
                             "com.android.shell", attributionSource.getAttributionTag(),
-                            attributionSource.getToken(), attributionSource.getNext()),
-                            skipProxyOperation);
+                            attributionSource.getToken(), /*renouncedPermissions*/ null,
+                            attributionSource.getNext()), skipProxyOperation);
                 } finally {
                     Binder.restoreCallingIdentity(identity);
                 }
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index 725d205..31d9f96 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -1969,8 +1969,7 @@
                 info.activityInfo.applicationInfo, true,
                 r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND,
                 new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST, r.curComponent,
-                        r.intent.getAction(), (r.alarm ? HostingRecord.TRIGGER_TYPE_ALARM
-                        : HostingRecord.TRIGGER_TYPE_UNKNOWN)),
+                        r.intent.getAction(), getHostingRecordTriggerType(r)),
                 isActivityCapable ? ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE : ZYGOTE_POLICY_FLAG_EMPTY,
                 (r.intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false);
         if (r.curApp == null) {
@@ -1993,6 +1992,16 @@
         mPendingBroadcastRecvIndex = recIdx;
     }
 
+    private String getHostingRecordTriggerType(BroadcastRecord r) {
+        if (r.alarm) {
+            return HostingRecord.TRIGGER_TYPE_ALARM;
+        } else if (r.pushMessage) {
+            return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE;
+        } else if (r.pushMessageOverQuota) {
+            return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA;
+        }
+        return HostingRecord.TRIGGER_TYPE_UNKNOWN;
+    }
 
     @Nullable
     private String getTargetPackage(BroadcastRecord r) {
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 12bab273..18fbfde 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -72,6 +72,8 @@
     final boolean ordered;  // serialize the send to receivers?
     final boolean sticky;   // originated from existing sticky data?
     final boolean alarm;    // originated from an alarm triggering?
+    final boolean pushMessage; // originated from a push message?
+    final boolean pushMessageOverQuota; // originated from a push message which was over quota?
     final boolean initialSticky; // initial broadcast from register to sticky?
     final int userId;       // user id this broadcast was for
     final String resolvedType; // the resolved data type
@@ -320,6 +322,8 @@
         mBackgroundActivityStartsToken = backgroundActivityStartsToken;
         this.timeoutExempt = timeoutExempt;
         alarm = options != null && options.isAlarmBroadcast();
+        pushMessage = options != null && options.isPushMessagingBroadcast();
+        pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast();
         this.filterExtrasForReceiver = filterExtrasForReceiver;
     }
 
@@ -374,6 +378,8 @@
         mBackgroundActivityStartsToken = from.mBackgroundActivityStartsToken;
         timeoutExempt = from.timeoutExempt;
         alarm = from.alarm;
+        pushMessage = from.pushMessage;
+        pushMessageOverQuota = from.pushMessageOverQuota;
         filterExtrasForReceiver = from.filterExtrasForReceiver;
     }
 
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index b76bad4..653b602 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -110,8 +110,6 @@
 
     private static final String ATRACE_COMPACTION_TRACK = "Compaction";
 
-    private static final int FREEZE_BINDER_TIMEOUT_MS = 100;
-
     // Defaults for phenotype flags.
     @VisibleForTesting static final Boolean DEFAULT_USE_COMPACTION = false;
     @VisibleForTesting static final Boolean DEFAULT_USE_FREEZER = true;
@@ -929,13 +927,11 @@
      * @param pid the target pid for which binder transactions are to be frozen
      * @param freeze specifies whether to flush transactions and then freeze (true) or unfreeze
      * binder for the specificed pid.
-     * @param timeoutMs the timeout in milliseconds to wait for the binder interface to freeze
-     * before giving up.
      *
      * @throws RuntimeException in case a flush/freeze operation could not complete successfully.
      * @return 0 if success, or -EAGAIN indicating there's pending transaction.
      */
-    public static native int freezeBinder(int pid, boolean freeze, int timeoutMs);
+    private static native int freezeBinder(int pid, boolean freeze);
 
     /**
      * Retrieves binder freeze info about a process.
@@ -1302,7 +1298,7 @@
         long freezeTime = opt.getFreezeUnfreezeTime();
 
         try {
-            freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
+            freezeBinder(pid, false);
         } catch (RuntimeException e) {
             Slog.e(TAG_AM, "Unable to unfreeze binder for " + pid + " " + app.processName
                     + ". Killing it");
@@ -1934,7 +1930,7 @@
                 // Freeze binder interface before the process, to flush any
                 // transactions that might be pending.
                 try {
-                    if (freezeBinder(pid, true, FREEZE_BINDER_TIMEOUT_MS) != 0) {
+                    if (freezeBinder(pid, true) != 0) {
                         rescheduleFreeze(proc, "outstanding txns");
                         return;
                     }
diff --git a/services/core/java/com/android/server/am/HostingRecord.java b/services/core/java/com/android/server/am/HostingRecord.java
index efc2a27..2498f76 100644
--- a/services/core/java/com/android/server/am/HostingRecord.java
+++ b/services/core/java/com/android/server/am/HostingRecord.java
@@ -30,6 +30,8 @@
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SERVICE;
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SYSTEM;
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_TOP_ACTIVITY;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_PUSH_MESSAGE;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_PUSH_MESSAGE_OVER_QUOTA;
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_ALARM;
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_UNKNOWN;
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TYPE__UNKNOWN;
@@ -93,6 +95,8 @@
 
     public static final String TRIGGER_TYPE_UNKNOWN = "unknown";
     public static final String TRIGGER_TYPE_ALARM = "alarm";
+    public static final String TRIGGER_TYPE_PUSH_MESSAGE = "push_message";
+    public static final String TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA = "push_message_over_quota";
 
     @NonNull private final String mHostingType;
     private final String mHostingName;
@@ -308,6 +312,10 @@
         switch(triggerType) {
             case TRIGGER_TYPE_ALARM:
                 return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_ALARM;
+            case TRIGGER_TYPE_PUSH_MESSAGE:
+                return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_PUSH_MESSAGE;
+            case TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA:
+                return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_PUSH_MESSAGE_OVER_QUOTA;
             default:
                 return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_UNKNOWN;
         }
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index ee60565..ccbca76 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -31,7 +31,6 @@
 import static android.os.Process.getTotalMemory;
 import static android.os.Process.killProcessQuiet;
 import static android.os.Process.startWebView;
-import static android.system.OsConstants.*;
 
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_LRU;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_NETWORK;
@@ -2706,50 +2705,6 @@
         }
     }
 
-    private static boolean freezePackageCgroup(int packageUID, boolean freeze) {
-        try {
-            Process.freezeCgroupUid(packageUID, freeze);
-        } catch (RuntimeException e) {
-            final String logtxt = freeze ? "freeze" : "unfreeze";
-            Slog.e(TAG, "Unable to " + logtxt + " cgroup uid: " + packageUID + ": " + e);
-            return false;
-        }
-        return true;
-    }
-
-    private static void freezeBinderAndPackageCgroup(ArrayList<Pair<ProcessRecord, Boolean>> procs,
-                                                     int packageUID) {
-        // Freeze all binder processes under the target UID (whose cgroup is about to be frozen).
-        // Since we're going to kill these, we don't need to unfreze them later.
-        // The procs list may not include all processes under the UID cgroup, but unincluded
-        // processes (forks) should not be Binder users.
-        int N = procs.size();
-        for (int i = 0; i < N; i++) {
-            final int uid = procs.get(i).first.uid;
-            final int pid = procs.get(i).first.getPid();
-            int nRetries = 0;
-            // We only freeze the cgroup of the target package, so we do not need to freeze the
-            // Binder interfaces of dependant processes in other UIDs.
-            if (pid > 0 && uid == packageUID) {
-                try {
-                    int rc;
-                    do {
-                        rc = CachedAppOptimizer.freezeBinder(pid, true, 10 /* timeout_ms */);
-                    } while (rc == -EAGAIN && nRetries++ < 1);
-                    if (rc != 0) Slog.e(TAG, "Unable to freeze binder for " + pid + ": " + rc);
-                } catch (RuntimeException e) {
-                    Slog.e(TAG, "Unable to freeze binder for " + pid + ": " + e);
-                }
-            }
-        }
-
-        // We freeze the entire UID (parent) cgroup so that newly-specialized processes also freeze
-        // despite being added to a new child cgroup. The cgroups of package dependant processes are
-        // not frozen, since it's possible this would freeze processes with no dependency on the
-        // package being killed here.
-        freezePackageCgroup(packageUID, true);
-    }
-
     @GuardedBy({"mService", "mProcLock"})
     boolean killPackageProcessesLSP(String packageName, int appId,
             int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart,
@@ -2802,7 +2757,7 @@
                 boolean shouldAllowRestart = false;
 
                 // If no package is specified, we call all processes under the
-                // given user id.
+                // give user id.
                 if (packageName == null) {
                     if (userId != UserHandle.USER_ALL && app.userId != userId) {
                         continue;
@@ -2845,18 +2800,14 @@
             }
         }
 
-        final int packageUID = UserHandle.getUid(userId, appId);
-        freezeBinderAndPackageCgroup(procs, packageUID);
-
         int N = procs.size();
         for (int i=0; i<N; i++) {
             final Pair<ProcessRecord, Boolean> proc = procs.get(i);
             removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second,
-                    reasonCode, subReason, reason, false /* async */);
+                    reasonCode, subReason, reason);
         }
         killAppZygotesLocked(packageName, appId, userId, false /* force */);
         mService.updateOomAdjLocked(OomAdjuster.OOM_ADJ_REASON_PROCESS_END);
-        freezePackageCgroup(packageUID, false);
         return N > 0;
     }
 
@@ -2864,19 +2815,12 @@
     boolean removeProcessLocked(ProcessRecord app,
             boolean callerWillRestart, boolean allowRestart, int reasonCode, String reason) {
         return removeProcessLocked(app, callerWillRestart, allowRestart, reasonCode,
-                ApplicationExitInfo.SUBREASON_UNKNOWN, reason, true);
+                ApplicationExitInfo.SUBREASON_UNKNOWN, reason);
     }
 
     @GuardedBy("mService")
     boolean removeProcessLocked(ProcessRecord app, boolean callerWillRestart,
             boolean allowRestart, int reasonCode, int subReason, String reason) {
-        return removeProcessLocked(app, callerWillRestart, allowRestart, reasonCode, subReason,
-                reason, true);
-    }
-
-    @GuardedBy("mService")
-    boolean removeProcessLocked(ProcessRecord app, boolean callerWillRestart,
-            boolean allowRestart, int reasonCode, int subReason, String reason, boolean async) {
         final String name = app.processName;
         final int uid = app.uid;
         if (DEBUG_PROCESSES) Slog.d(TAG_PROCESSES,
@@ -2913,7 +2857,7 @@
                     needRestart = true;
                 }
             }
-            app.killLocked(reason, reasonCode, subReason, true, async);
+            app.killLocked(reason, reasonCode, subReason, true);
             mService.handleAppDiedLocked(app, pid, willRestart, allowRestart,
                     false /* fromBinderDied */);
             if (willRestart) {
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 2f1c5f6..07b6fcd 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -1057,30 +1057,18 @@
 
     @GuardedBy("mService")
     void killLocked(String reason, @Reason int reasonCode, boolean noisy) {
-        killLocked(reason, reasonCode, ApplicationExitInfo.SUBREASON_UNKNOWN, noisy, true);
+        killLocked(reason, reasonCode, ApplicationExitInfo.SUBREASON_UNKNOWN, noisy);
     }
 
     @GuardedBy("mService")
     void killLocked(String reason, @Reason int reasonCode, @SubReason int subReason,
             boolean noisy) {
-        killLocked(reason, reason, reasonCode, subReason, noisy, true);
+        killLocked(reason, reason, reasonCode, subReason, noisy);
     }
 
     @GuardedBy("mService")
     void killLocked(String reason, String description, @Reason int reasonCode,
             @SubReason int subReason, boolean noisy) {
-        killLocked(reason, description, reasonCode, subReason, noisy, true);
-    }
-
-    @GuardedBy("mService")
-    void killLocked(String reason, @Reason int reasonCode, @SubReason int subReason,
-            boolean noisy, boolean asyncKPG) {
-        killLocked(reason, reason, reasonCode, subReason, noisy, asyncKPG);
-    }
-
-    @GuardedBy("mService")
-    void killLocked(String reason, String description, @Reason int reasonCode,
-            @SubReason int subReason, boolean noisy, boolean asyncKPG) {
         if (!mKilledByAm) {
             Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "kill");
             if (reasonCode == ApplicationExitInfo.REASON_ANR
@@ -1097,8 +1085,7 @@
                 EventLog.writeEvent(EventLogTags.AM_KILL,
                         userId, mPid, processName, mState.getSetAdj(), reason);
                 Process.killProcessQuiet(mPid);
-                if (asyncKPG) ProcessList.killProcessGroup(uid, mPid);
-                else Process.killProcessGroup(uid, mPid);
+                ProcessList.killProcessGroup(uid, mPid);
             } else {
                 mPendingStart = false;
             }
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 1edfabe..a1f3dae 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -71,6 +71,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackagePartitions;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.os.BatteryStats;
 import android.os.Binder;
 import android.os.Bundle;
@@ -1367,7 +1368,8 @@
         List<UserInfo> profilesToStart = new ArrayList<>(profiles.size());
         for (UserInfo user : profiles) {
             if ((user.flags & UserInfo.FLAG_INITIALIZED) == UserInfo.FLAG_INITIALIZED
-                    && user.id != currentUserId && !user.isQuietModeEnabled()) {
+                    && user.id != currentUserId
+                    && shouldStartWithParent(user)) {
                 profilesToStart.add(user);
             }
         }
@@ -1382,6 +1384,13 @@
         }
     }
 
+    private boolean shouldStartWithParent(UserInfo user) {
+        final UserProperties properties = mInjector.getUserManagerInternal()
+                .getUserProperties(user.id);
+        return (properties != null && properties.getStartWithParent())
+                && !user.isQuietModeEnabled();
+    }
+
     // TODO(b/239982558): might need to infer the display id based on parent user
     /**
      * Starts a user only if it's a profile, with a more relaxed permission requirement:
@@ -1468,7 +1477,16 @@
         checkCallingHasOneOfThosePermissions("startUserOnSecondaryDisplay",
                 MANAGE_USERS, CREATE_USERS);
 
-        return startUserNoChecks(userId, displayId, /* foreground= */ false, unlockListener);
+        // DEFAULT_DISPLAY is used for "regular" start user operations
+        Preconditions.checkArgument(displayId != Display.DEFAULT_DISPLAY,
+                "Cannot use DEFAULT_DISPLAY");
+
+        try {
+            return startUserNoChecks(userId, displayId, /* foreground= */ false, unlockListener);
+        } catch (RuntimeException e) {
+            Slogf.w(TAG, "startUserOnSecondaryDisplay(%d, %d) failed: %s", userId, displayId, e);
+            return false;
+        }
     }
 
     private boolean startUserNoChecks(@UserIdInt int userId, int displayId, boolean foreground,
@@ -1492,13 +1510,15 @@
                     foreground ? " in foreground" : "");
         }
 
+        // TODO(b/239982558): move logic below to a different class (like DisplayAssignmentManager)
         if (displayId != Display.DEFAULT_DISPLAY) {
+            // This is called by startUserOnSecondaryDisplay()
             if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) {
-                // TODO(b/239824814): add CTS test and/or unit test
+                // TODO(b/239824814): add CTS test and/or unit test for all exceptional cases
                 throw new UnsupportedOperationException("Not supported by device");
             }
 
-            // TODO(b/239982558): add unit test for the exceptional cases below
+            // TODO(b/239982558): call DisplayManagerInternal to check if display is valid instead
             Preconditions.checkArgument(displayId > 0, "Invalid display id (%d)", displayId);
             Preconditions.checkArgument(userId != UserHandle.USER_SYSTEM, "Cannot start system user"
                     + " on secondary display (%d)", displayId);
@@ -1508,7 +1528,6 @@
             // TODO(b/239982558): for now we're just updating the user's visibility, but most likely
             // we'll need to remove this call and handle that as part of the user state workflow
             // instead.
-            // TODO(b/239982558): check if display is valid
             mInjector.getUserManagerInternal().assignUserToDisplay(userId, displayId);
         }
 
diff --git a/services/core/java/com/android/server/attention/TEST_MAPPING b/services/core/java/com/android/server/attention/TEST_MAPPING
new file mode 100644
index 0000000..35b8165
--- /dev/null
+++ b/services/core/java/com/android/server/attention/TEST_MAPPING
@@ -0,0 +1,24 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsVoiceInteractionTestCases",
+      "options": [
+        {
+          "include-filter": "android.voiceinteraction.cts.AlwaysOnHotwordDetectorTest"
+        },
+        {
+          "include-filter": "android.voiceinteraction.cts.HotwordDetectedResultTest"
+        },
+        {
+          "include-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest"
+        },
+        {
+          "include-filter": "android.voiceinteraction.cts.HotwordDetectionServiceProximityTest"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 8356134..aedbe4e 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -571,7 +571,9 @@
     // There may be different devices with the same device type (aliasing).
     // We always send the full device state info on each change.
     private void logDeviceState(SADeviceState deviceState, String event) {
-        final String deviceName = AudioSystem.getDeviceName(deviceState.mDeviceType);
+        final int deviceType = AudioDeviceInfo.convertDeviceTypeToInternalDevice(
+                deviceState.mDeviceType);
+        final String deviceName = AudioSystem.getDeviceName(deviceType);
         new MediaMetrics.Item(METRICS_DEVICE_PREFIX + deviceName)
             .set(MediaMetrics.Property.ADDRESS, deviceState.mDeviceAddress)
             .set(MediaMetrics.Property.ENABLED, deviceState.mEnabled ? "true" : "false")
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 16a060a..931c692 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -752,7 +752,7 @@
         return true;
     }
 
-    private boolean sendEventToVpnManagerApp(@NonNull String category, int errorClass,
+    private Intent buildVpnManagerEventIntent(@NonNull String category, int errorClass,
             int errorCode, @NonNull final String packageName, @Nullable final String sessionKey,
             @NonNull final VpnProfileState profileState, @Nullable final Network underlyingNetwork,
             @Nullable final NetworkCapabilities nc, @Nullable final LinkProperties lp) {
@@ -771,6 +771,20 @@
             intent.putExtra(VpnManager.EXTRA_ERROR_CODE, errorCode);
         }
 
+        return intent;
+    }
+
+    private boolean sendEventToVpnManagerApp(@NonNull String category, int errorClass,
+            int errorCode, @NonNull final String packageName, @Nullable final String sessionKey,
+            @NonNull final VpnProfileState profileState, @Nullable final Network underlyingNetwork,
+            @Nullable final NetworkCapabilities nc, @Nullable final LinkProperties lp) {
+        final Intent intent = buildVpnManagerEventIntent(category, errorClass, errorCode,
+                packageName, sessionKey, profileState, underlyingNetwork, nc, lp);
+        return sendEventToVpnManagerApp(intent, packageName);
+    }
+
+    private boolean sendEventToVpnManagerApp(@NonNull final Intent intent,
+            @NonNull final String packageName) {
         // Allow VpnManager app to temporarily run background services to handle this error.
         // If an app requires anything beyond this grace period, they MUST either declare
         // themselves as a foreground service, or schedule a job/workitem.
@@ -1182,12 +1196,25 @@
                 mContext.unbindService(mConnection);
                 cleanupVpnStateLocked();
             } else if (mVpnRunner != null) {
-                if (!VpnConfig.LEGACY_VPN.equals(mPackage)) {
-                    notifyVpnManagerVpnStopped(mPackage, mOwnerUID);
+                // Build intent first because the sessionKey will be reset after performing
+                // VpnRunner.exit(). Also, cache mOwnerUID even if ownerUID will not be changed in
+                // VpnRunner.exit() to prevent design being changed in the future.
+                // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
+                //  ConnectivityServiceTest.
+                final int ownerUid = mOwnerUID;
+                Intent intent = null;
+                if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
+                    intent = buildVpnManagerEventIntent(
+                            VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+                            -1 /* errorClass */, -1 /* errorCode*/, mPackage,
+                            getSessionKeyLocked(), makeVpnProfileStateLocked(),
+                            null /* underlyingNetwork */, null /* nc */, null /* lp */);
                 }
-
                 // cleanupVpnStateLocked() is called from mVpnRunner.exit()
                 mVpnRunner.exit();
+                if (intent != null && isVpnApp(mPackage)) {
+                    notifyVpnManagerVpnStopped(mPackage, ownerUid, intent);
+                }
             }
 
             try {
@@ -2886,6 +2913,9 @@
                 final LinkProperties lp;
 
                 synchronized (Vpn.this) {
+                    // Ignore stale runner.
+                    if (mVpnRunner != this) return;
+
                     mInterface = interfaceName;
                     mConfig.mtu = maxMtu;
                     mConfig.interfaze = mInterface;
@@ -2987,6 +3017,9 @@
 
             try {
                 synchronized (Vpn.this) {
+                    // Ignore stale runner.
+                    if (mVpnRunner != this) return;
+
                     mConfig.underlyingNetworks = new Network[] {network};
                     mNetworkCapabilities =
                             new NetworkCapabilities.Builder(mNetworkCapabilities)
@@ -3076,7 +3109,12 @@
 
                 // Clear mInterface to prevent Ikev2VpnRunner being cleared when
                 // interfaceRemoved() is called.
-                mInterface = null;
+                synchronized (Vpn.this) {
+                    // Ignore stale runner.
+                    if (mVpnRunner != this) return;
+
+                    mInterface = null;
+                }
                 // Without MOBIKE, we have no way to seamlessly migrate. Close on old
                 // (non-default) network, and start the new one.
                 resetIkeState();
@@ -3261,6 +3299,9 @@
         /** Marks the state as FAILED, and disconnects. */
         private void markFailedAndDisconnect(Exception exception) {
             synchronized (Vpn.this) {
+                // Ignore stale runner.
+                if (mVpnRunner != this) return;
+
                 updateState(DetailedState.FAILED, exception.getMessage());
             }
 
@@ -3345,6 +3386,9 @@
             }
 
             synchronized (Vpn.this) {
+                // Ignore stale runner.
+                if (mVpnRunner != this) return;
+
                 // TODO(b/230548427): Remove SDK check once VPN related stuff are
                 //  decoupled from ConnectivityServiceTest.
                 if (SdkLevel.isAtLeastT() && category != null && isVpnApp(mPackage)) {
@@ -3371,6 +3415,9 @@
             Log.d(TAG, "Resetting state for token: " + mCurrentToken);
 
             synchronized (Vpn.this) {
+                // Ignore stale runner.
+                if (mVpnRunner != this) return;
+
                 // Since this method handles non-fatal errors only, set mInterface to null to
                 // prevent the NetworkManagementEventObserver from killing this VPN based on the
                 // interface going down (which we expect).
@@ -3993,6 +4040,7 @@
             mConfig.proxyInfo = profile.proxy;
             mConfig.requiresInternetValidation = profile.requiresInternetValidation;
             mConfig.excludeLocalRoutes = profile.excludeLocalRoutes;
+            mConfig.allowBypass = profile.isBypassable;
 
             switch (profile.type) {
                 case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
@@ -4042,13 +4090,23 @@
         // To stop the VPN profile, the caller must be the current prepared package and must be
         // running an Ikev2VpnProfile.
         if (isCurrentIkev2VpnLocked(packageName)) {
-            notifyVpnManagerVpnStopped(packageName, mOwnerUID);
+            // Build intent first because the sessionKey will be reset after performing
+            // VpnRunner.exit(). Also, cache mOwnerUID even if ownerUID will not be changed in
+            // VpnRunner.exit() to prevent design being changed in the future.
+            final int ownerUid = mOwnerUID;
+            final Intent intent = buildVpnManagerEventIntent(
+                    VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+                    -1 /* errorClass */, -1 /* errorCode*/, packageName,
+                    getSessionKeyLocked(), makeVpnProfileStateLocked(),
+                    null /* underlyingNetwork */, null /* nc */, null /* lp */);
 
             mVpnRunner.exit();
+            notifyVpnManagerVpnStopped(packageName, ownerUid, intent);
         }
     }
 
-    private synchronized void notifyVpnManagerVpnStopped(String packageName, int ownerUID) {
+    private synchronized void notifyVpnManagerVpnStopped(String packageName, int ownerUID,
+            Intent intent) {
         mAppOpsManager.finishOp(
                 AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER, ownerUID, packageName, null);
         // The underlying network, NetworkCapabilities and LinkProperties are not
@@ -4057,10 +4115,7 @@
         // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
         //  ConnectivityServiceTest.
         if (SdkLevel.isAtLeastT()) {
-            sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
-                    -1 /* errorClass */, -1 /* errorCode*/, packageName,
-                    getSessionKeyLocked(), makeVpnProfileStateLocked(),
-                    null /* underlyingNetwork */, null /* nc */, null /* lp */);
+            sendEventToVpnManagerApp(intent, packageName);
         }
     }
 
diff --git a/services/core/java/com/android/server/location/LocationPermissions.java b/services/core/java/com/android/server/location/LocationPermissions.java
index ca2ff60..f7da0d8 100644
--- a/services/core/java/com/android/server/location/LocationPermissions.java
+++ b/services/core/java/com/android/server/location/LocationPermissions.java
@@ -26,8 +26,10 @@
 import android.content.Context;
 import android.os.Binder;
 
+import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 /** Utility class for dealing with location permissions. */
 public final class LocationPermissions {
@@ -49,6 +51,7 @@
      */
     public static final int PERMISSION_FINE = 2;
 
+    @Target(ElementType.TYPE_USE)
     @IntDef({PERMISSION_NONE, PERMISSION_COARSE, PERMISSION_FINE})
     @Retention(RetentionPolicy.SOURCE)
     public @interface PermissionLevel {}
diff --git a/services/core/java/com/android/server/location/geofence/GeofenceKey.java b/services/core/java/com/android/server/location/geofence/GeofenceKey.java
deleted file mode 100644
index bbfa68f..0000000
--- a/services/core/java/com/android/server/location/geofence/GeofenceKey.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.location.geofence;
-
-import android.app.PendingIntent;
-import android.location.Geofence;
-
-import com.android.server.location.listeners.PendingIntentListenerRegistration;
-
-import java.util.Objects;
-
-// geofencing unfortunately allows multiple geofences under the same pending intent, even though
-// this makes no real sense. therefore we manufacture an artificial key to use (pendingintent +
-// geofence) instead of (pendingintent).
-final class GeofenceKey  implements PendingIntentListenerRegistration.PendingIntentKey {
-
-    private final PendingIntent mPendingIntent;
-    private final Geofence mGeofence;
-
-    GeofenceKey(PendingIntent pendingIntent, Geofence geofence) {
-        mPendingIntent = Objects.requireNonNull(pendingIntent);
-        mGeofence = Objects.requireNonNull(geofence);
-    }
-
-    @Override
-    public PendingIntent getPendingIntent() {
-        return mPendingIntent;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (!(o instanceof GeofenceKey)) {
-            return false;
-        }
-        GeofenceKey that = (GeofenceKey) o;
-        return mPendingIntent.equals(that.mPendingIntent) && mGeofence.equals(that.mGeofence);
-    }
-
-    @Override
-    public int hashCode() {
-        return mPendingIntent.hashCode();
-    }
-}
diff --git a/services/core/java/com/android/server/location/geofence/GeofenceManager.java b/services/core/java/com/android/server/location/geofence/GeofenceManager.java
index b6342a4..0f5e3d4 100644
--- a/services/core/java/com/android/server/location/geofence/GeofenceManager.java
+++ b/services/core/java/com/android/server/location/geofence/GeofenceManager.java
@@ -59,8 +59,8 @@
  * Manages all geofences.
  */
 public class GeofenceManager extends
-        ListenerMultiplexer<GeofenceKey, PendingIntent, GeofenceManager.GeofenceRegistration,
-                LocationRequest> implements
+        ListenerMultiplexer<GeofenceManager.GeofenceKey, PendingIntent,
+                GeofenceManager.GeofenceRegistration, LocationRequest> implements
         LocationListener {
 
     private static final String TAG = "GeofenceManager";
@@ -73,13 +73,49 @@
     private static final long MAX_LOCATION_AGE_MS = 5 * 60 * 1000L; // five minutes
     private static final long MAX_LOCATION_INTERVAL_MS = 2 * 60 * 60 * 1000; // two hours
 
-    protected final class GeofenceRegistration extends
-            PendingIntentListenerRegistration<Geofence, PendingIntent> {
+    // geofencing unfortunately allows multiple geofences under the same pending intent, even though
+    // this makes no real sense. therefore we manufacture an artificial key to use (pendingintent +
+    // geofence) instead of (pendingintent).
+    static class GeofenceKey {
+
+        private final PendingIntent mPendingIntent;
+        private final Geofence mGeofence;
+
+        GeofenceKey(PendingIntent pendingIntent, Geofence geofence) {
+            mPendingIntent = Objects.requireNonNull(pendingIntent);
+            mGeofence = Objects.requireNonNull(geofence);
+        }
+
+        public PendingIntent getPendingIntent() {
+            return mPendingIntent;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof GeofenceKey) {
+                GeofenceKey that = (GeofenceKey) o;
+                return mPendingIntent.equals(that.mPendingIntent) && mGeofence.equals(
+                        that.mGeofence);
+            }
+
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return mPendingIntent.hashCode();
+        }
+    }
+
+    protected class GeofenceRegistration extends
+            PendingIntentListenerRegistration<GeofenceKey, PendingIntent> {
 
         private static final int STATE_UNKNOWN = 0;
         private static final int STATE_INSIDE = 1;
         private static final int STATE_OUTSIDE = 2;
 
+        private final Geofence mGeofence;
+        private final CallerIdentity mIdentity;
         private final Location mCenter;
         private final PowerManager.WakeLock mWakeLock;
 
@@ -89,13 +125,15 @@
         // spam us, and because checking the values may be more expensive
         private boolean mPermitted;
 
-        private @Nullable Location mCachedLocation;
+        @Nullable private Location mCachedLocation;
         private float mCachedLocationDistanceM;
 
-        protected GeofenceRegistration(Geofence geofence, CallerIdentity identity,
+        GeofenceRegistration(Geofence geofence, CallerIdentity identity,
                 PendingIntent pendingIntent) {
-            super(geofence, identity, pendingIntent);
+            super(pendingIntent);
 
+            mGeofence = geofence;
+            mIdentity = identity;
             mCenter = new Location("");
             mCenter.setLatitude(geofence.getLatitude());
             mCenter.setLongitude(geofence.getLongitude());
@@ -107,16 +145,36 @@
             mWakeLock.setWorkSource(identity.addToWorkSource(null));
         }
 
+        public Geofence getGeofence() {
+            return mGeofence;
+        }
+
+        public CallerIdentity getIdentity() {
+            return mIdentity;
+        }
+
+        @Override
+        public String getTag() {
+            return TAG;
+        }
+
+        @Override
+        protected PendingIntent getPendingIntentFromKey(GeofenceKey geofenceKey) {
+            return geofenceKey.getPendingIntent();
+        }
+
         @Override
         protected GeofenceManager getOwner() {
             return GeofenceManager.this;
         }
 
         @Override
-        protected void onPendingIntentListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             mGeofenceState = STATE_UNKNOWN;
             mPermitted = mLocationPermissionsHelper.hasLocationPermissions(PERMISSION_FINE,
-                    getIdentity());
+                    mIdentity);
         }
 
         @Override
@@ -132,7 +190,7 @@
         }
 
         boolean onLocationPermissionsChanged(@Nullable String packageName) {
-            if (packageName == null || getIdentity().getPackageName().equals(packageName)) {
+            if (packageName == null || mIdentity.getPackageName().equals(packageName)) {
                 return onLocationPermissionsChanged();
             }
 
@@ -140,7 +198,7 @@
         }
 
         boolean onLocationPermissionsChanged(int uid) {
-            if (getIdentity().getUid() == uid) {
+            if (mIdentity.getUid() == uid) {
                 return onLocationPermissionsChanged();
             }
 
@@ -149,7 +207,7 @@
 
         private boolean onLocationPermissionsChanged() {
             boolean permitted = mLocationPermissionsHelper.hasLocationPermissions(PERMISSION_FINE,
-                    getIdentity());
+                    mIdentity);
             if (permitted != mPermitted) {
                 mPermitted = permitted;
                 return true;
@@ -164,12 +222,12 @@
                 mCachedLocationDistanceM = mCenter.distanceTo(mCachedLocation);
             }
 
-            return Math.abs(getRequest().getRadius() - mCachedLocationDistanceM);
+            return Math.abs(mGeofence.getRadius() - mCachedLocationDistanceM);
         }
 
         ListenerOperation<PendingIntent> onLocationChanged(Location location) {
             // remove expired fences
-            if (getRequest().isExpired()) {
+            if (mGeofence.isExpired()) {
                 remove();
                 return null;
             }
@@ -178,7 +236,7 @@
             mCachedLocationDistanceM = mCenter.distanceTo(mCachedLocation);
 
             int oldState = mGeofenceState;
-            float radius = Math.max(getRequest().getRadius(), location.getAccuracy());
+            float radius = Math.max(mGeofence.getRadius(), location.getAccuracy());
             if (mCachedLocationDistanceM <= radius) {
                 mGeofenceState = STATE_INSIDE;
                 if (oldState != STATE_INSIDE) {
@@ -206,14 +264,14 @@
                         null, null, PendingIntentUtils.createDontSendToRestrictedAppsBundle(null));
             } catch (PendingIntent.CanceledException e) {
                 mWakeLock.release();
-                removeRegistration(new GeofenceKey(pendingIntent, getRequest()), this);
+                removeRegistration(new GeofenceKey(pendingIntent, mGeofence), this);
             }
         }
 
         @Override
         public String toString() {
             StringBuilder builder = new StringBuilder();
-            builder.append(getIdentity());
+            builder.append(mIdentity);
 
             ArraySet<String> flags = new ArraySet<>(1);
             if (!mPermitted) {
@@ -223,7 +281,7 @@
                 builder.append(" ").append(flags);
             }
 
-            builder.append(" ").append(getRequest());
+            builder.append(" ").append(mGeofence);
             return builder.toString();
         }
     }
@@ -258,10 +316,10 @@
     protected final LocationUsageLogger mLocationUsageLogger;
 
     @GuardedBy("mLock")
-    private @Nullable LocationManager mLocationManager;
+    @Nullable private LocationManager mLocationManager;
 
     @GuardedBy("mLock")
-    private @Nullable Location mLastLocation;
+    @Nullable private Location mLastLocation;
 
     public GeofenceManager(Context context, Injector injector) {
         mContext = context.createAttributionContext(ATTRIBUTION_TAG);
@@ -271,11 +329,6 @@
         mLocationUsageLogger = injector.getLocationUsageLogger();
     }
 
-    @Override
-    public String getTag() {
-        return TAG;
-    }
-
     private LocationManager getLocationManager() {
         synchronized (mLock) {
             if (mLocationManager == null) {
@@ -375,7 +428,7 @@
                 /* LocationRequest= */ null,
                 /* hasListener= */ false,
                 true,
-                registration.getRequest(), true);
+                registration.getGeofence(), true);
     }
 
     @Override
@@ -389,7 +442,7 @@
                 /* LocationRequest= */ null,
                 /* hasListener= */ false,
                 true,
-                registration.getRequest(), true);
+                registration.getGeofence(), true);
     }
 
     @Override
@@ -417,7 +470,7 @@
         WorkSource workSource = null;
         double minFenceDistanceM = Double.MAX_VALUE;
         for (GeofenceRegistration registration : registrations) {
-            if (registration.getRequest().isExpired(realtimeMs)) {
+            if (registration.getGeofence().isExpired(realtimeMs)) {
                 continue;
             }
 
diff --git a/services/core/java/com/android/server/location/gnss/GnssAntennaInfoProvider.java b/services/core/java/com/android/server/location/gnss/GnssAntennaInfoProvider.java
index e375007..62ab22a 100644
--- a/services/core/java/com/android/server/location/gnss/GnssAntennaInfoProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssAntennaInfoProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.server.location.gnss;
 
+import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
 import static com.android.server.location.gnss.GnssManagerService.TAG;
 
 import android.annotation.Nullable;
@@ -25,6 +26,7 @@
 import android.os.Binder;
 import android.os.IBinder;
 
+import com.android.server.FgThread;
 import com.android.server.location.gnss.hal.GnssNative;
 import com.android.server.location.listeners.BinderListenerRegistration;
 import com.android.server.location.listeners.ListenerMultiplexer;
@@ -45,17 +47,35 @@
      * Registration object for GNSS listeners.
      */
     protected class AntennaInfoListenerRegistration extends
-            BinderListenerRegistration<Void, IGnssAntennaInfoListener> {
+            BinderListenerRegistration<IBinder, IGnssAntennaInfoListener> {
 
-        protected AntennaInfoListenerRegistration(CallerIdentity callerIdentity,
+        private final CallerIdentity mIdentity;
+
+        protected AntennaInfoListenerRegistration(CallerIdentity identity,
                 IGnssAntennaInfoListener listener) {
-            super(null, callerIdentity, listener);
+            super(identity.isMyProcess() ? FgThread.getExecutor() : DIRECT_EXECUTOR, listener);
+            mIdentity = identity;
+        }
+
+        @Override
+        protected String getTag() {
+            return TAG;
         }
 
         @Override
         protected GnssAntennaInfoProvider getOwner() {
             return GnssAntennaInfoProvider.this;
         }
+
+        @Override
+        protected IBinder getBinderFromKey(IBinder key) {
+            return key;
+        }
+
+        @Override
+        public String toString() {
+            return mIdentity.toString();
+        }
     }
 
     private final GnssNative mGnssNative;
@@ -72,11 +92,6 @@
         return mAntennaInfos;
     }
 
-    @Override
-    public String getTag() {
-        return TAG;
-    }
-
     public boolean isSupported() {
         return mGnssNative.isAntennaInfoSupported();
     }
diff --git a/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java b/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java
index a540476..82bcca2 100644
--- a/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java
+++ b/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java
@@ -18,6 +18,7 @@
 
 import static android.location.LocationManager.GPS_PROVIDER;
 
+import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
 import static com.android.server.location.LocationPermissions.PERMISSION_FINE;
 import static com.android.server.location.gnss.GnssManagerService.TAG;
 
@@ -33,6 +34,7 @@
 import android.util.ArraySet;
 
 import com.android.internal.util.Preconditions;
+import com.android.server.FgThread;
 import com.android.server.LocalServices;
 import com.android.server.location.injector.AppForegroundHelper;
 import com.android.server.location.injector.Injector;
@@ -67,16 +69,34 @@
      * Registration object for GNSS listeners.
      */
     protected class GnssListenerRegistration extends
-            BinderListenerRegistration<TRequest, TListener> {
+            BinderListenerRegistration<IBinder, TListener> {
+
+        private final TRequest mRequest;
+        private final CallerIdentity mIdentity;
 
         // we store these values because we don't trust the listeners not to give us dupes, not to
         // spam us, and because checking the values may be more expensive
         private boolean mForeground;
         private boolean mPermitted;
 
-        protected GnssListenerRegistration(@Nullable TRequest request,
-                CallerIdentity callerIdentity, TListener listener) {
-            super(request, callerIdentity, listener);
+        protected GnssListenerRegistration(TRequest request, CallerIdentity identity,
+                TListener listener) {
+            super(identity.isMyProcess() ? FgThread.getExecutor() : DIRECT_EXECUTOR, listener);
+            mRequest = request;
+            mIdentity = identity;
+        }
+
+        public final TRequest getRequest() {
+            return mRequest;
+        }
+
+        public final CallerIdentity getIdentity() {
+            return mIdentity;
+        }
+
+        @Override
+        public String getTag() {
+            return TAG;
         }
 
         @Override
@@ -84,6 +104,11 @@
             return GnssListenerMultiplexer.this;
         }
 
+        @Override
+        protected IBinder getBinderFromKey(IBinder key) {
+            return key;
+        }
+
         /**
          * Returns true if this registration is currently in the foreground.
          */
@@ -96,31 +121,16 @@
         }
 
         @Override
-        protected final void onBinderListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             mPermitted = mLocationPermissionsHelper.hasLocationPermissions(PERMISSION_FINE,
-                    getIdentity());
-            mForeground = mAppForegroundHelper.isAppForeground(getIdentity().getUid());
-
-            onGnssListenerRegister();
+                    mIdentity);
+            mForeground = mAppForegroundHelper.isAppForeground(mIdentity.getUid());
         }
 
-        @Override
-        protected final void onBinderListenerUnregister() {
-            onGnssListenerUnregister();
-        }
-
-        /**
-         * May be overridden in place of {@link #onBinderListenerRegister()}.
-         */
-        protected void onGnssListenerRegister() {}
-
-        /**
-         * May be overridden in place of {@link #onBinderListenerUnregister()}.
-         */
-        protected void onGnssListenerUnregister() {}
-
         boolean onLocationPermissionsChanged(@Nullable String packageName) {
-            if (packageName == null || getIdentity().getPackageName().equals(packageName)) {
+            if (packageName == null || mIdentity.getPackageName().equals(packageName)) {
                 return onLocationPermissionsChanged();
             }
 
@@ -128,7 +138,7 @@
         }
 
         boolean onLocationPermissionsChanged(int uid) {
-            if (getIdentity().getUid() == uid) {
+            if (mIdentity.getUid() == uid) {
                 return onLocationPermissionsChanged();
             }
 
@@ -137,7 +147,7 @@
 
         private boolean onLocationPermissionsChanged() {
             boolean permitted = mLocationPermissionsHelper.hasLocationPermissions(PERMISSION_FINE,
-                    getIdentity());
+                    mIdentity);
             if (permitted != mPermitted) {
                 mPermitted = permitted;
                 return true;
@@ -147,7 +157,7 @@
         }
 
         boolean onForegroundChanged(int uid, boolean foreground) {
-            if (getIdentity().getUid() == uid && foreground != mForeground) {
+            if (mIdentity.getUid() == uid && foreground != mForeground) {
                 mForeground = foreground;
                 return true;
             }
@@ -158,7 +168,7 @@
         @Override
         public String toString() {
             StringBuilder builder = new StringBuilder();
-            builder.append(getIdentity());
+            builder.append(mIdentity);
 
             ArraySet<String> flags = new ArraySet<>(2);
             if (!mForeground) {
@@ -171,8 +181,8 @@
                 builder.append(" ").append(flags);
             }
 
-            if (getRequest() != null) {
-                builder.append(" ").append(getRequest());
+            if (mRequest != null) {
+                builder.append(" ").append(mRequest);
             }
             return builder.toString();
         }
@@ -218,11 +228,6 @@
                 LocalServices.getService(LocationManagerInternal.class));
     }
 
-    @Override
-    public String getTag() {
-        return TAG;
-    }
-
     /**
      * May be overridden by subclasses to return whether the service is supported or not. This value
      * should never change for the lifetime of the multiplexer. If the service is unsupported, all
diff --git a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
index e4e9d01..9f2a9cf 100644
--- a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
@@ -40,10 +40,7 @@
 import java.util.Collection;
 
 /**
- * An base implementation for GNSS measurements provider. It abstracts out the responsibility of
- * handling listeners, while still allowing technology specific implementations to be built.
- *
- * @hide
+ * GNSS measurements HAL module and listener multiplexer.
  */
 public final class GnssMeasurementsProvider extends
         GnssListenerMultiplexer<GnssMeasurementRequest, IGnssMeasurementsListener,
@@ -61,7 +58,9 @@
         }
 
         @Override
-        protected void onGnssListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             executeOperation(listener -> listener.onStatusChanged(
                     GnssMeasurementsEvent.Callback.STATUS_READY));
         }
diff --git a/services/core/java/com/android/server/location/gnss/GnssNavigationMessageProvider.java b/services/core/java/com/android/server/location/gnss/GnssNavigationMessageProvider.java
index e9fce05..63134bb 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNavigationMessageProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNavigationMessageProvider.java
@@ -32,11 +32,7 @@
 import java.util.Collection;
 
 /**
- * An base implementation for GPS navigation messages provider.
- * It abstracts out the responsibility of handling listeners, while still allowing technology
- * specific implementations to be built.
- *
- * @hide
+ * GNSS navigation message HAL module and listener multiplexer.
  */
 public class GnssNavigationMessageProvider extends
         GnssListenerMultiplexer<Void, IGnssNavigationMessageListener, Void> implements
@@ -51,7 +47,9 @@
         }
 
         @Override
-        protected void onGnssListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             executeOperation(listener -> listener.onStatusChanged(
                     GnssNavigationMessage.Callback.STATUS_READY));
         }
diff --git a/services/core/java/com/android/server/location/gnss/GnssNmeaProvider.java b/services/core/java/com/android/server/location/gnss/GnssNmeaProvider.java
index bfef978..d4e38b6a 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNmeaProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNmeaProvider.java
@@ -34,7 +34,7 @@
 import java.util.function.Function;
 
 /**
- * Implementation of a handler for {@link IGnssNmeaListener}.
+ * GNSS NMEA HAL module and listener multiplexer.
  */
 class GnssNmeaProvider extends GnssListenerMultiplexer<Void, IGnssNmeaListener, Void> implements
         GnssNative.BaseCallbacks, GnssNative.NmeaCallbacks {
@@ -97,7 +97,7 @@
                         ListenerExecutor.ListenerOperation<IGnssNmeaListener>>() {
 
                     // only read in the nmea string if we need to
-                    private @Nullable String mNmea;
+                    @Nullable private String mNmea;
 
                     @Override
                     public ListenerExecutor.ListenerOperation<IGnssNmeaListener> apply(
diff --git a/services/core/java/com/android/server/location/gnss/GnssStatusProvider.java b/services/core/java/com/android/server/location/gnss/GnssStatusProvider.java
index 0ce36d6..41fa7a1 100644
--- a/services/core/java/com/android/server/location/gnss/GnssStatusProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssStatusProvider.java
@@ -35,7 +35,7 @@
 import java.util.Collection;
 
 /**
- * Implementation of a handler for {@link IGnssStatusListener}.
+ * GNSS status HAL module and listener multiplexer.
  */
 public class GnssStatusProvider extends
         GnssListenerMultiplexer<Void, IGnssStatusListener, Void> implements
diff --git a/services/core/java/com/android/server/location/listeners/BinderListenerRegistration.java b/services/core/java/com/android/server/location/listeners/BinderListenerRegistration.java
index 709e236..5555aeb 100644
--- a/services/core/java/com/android/server/location/listeners/BinderListenerRegistration.java
+++ b/services/core/java/com/android/server/location/listeners/BinderListenerRegistration.java
@@ -16,71 +16,59 @@
 
 package com.android.server.location.listeners;
 
-import android.annotation.Nullable;
-import android.location.util.identity.CallerIdentity;
-import android.os.Binder;
 import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
 import android.util.Log;
 
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
+
 /**
  * A registration that works with IBinder keys, and registers a DeathListener to automatically
- * remove the registration if the binder dies. The key for this registration must either be an
- * {@link IBinder} or a {@link BinderKey}.
+ * remove the registration if the binder dies.
  *
- * @param <TRequest>  request type
+ * @param <TKey>      key type
  * @param <TListener> listener type
  */
-public abstract class BinderListenerRegistration<TRequest, TListener> extends
-        RemoteListenerRegistration<TRequest, TListener> implements Binder.DeathRecipient {
+public abstract class BinderListenerRegistration<TKey, TListener> extends
+        RemovableListenerRegistration<TKey, TListener> implements DeathRecipient {
 
-    /**
-     * Interface to allow binder retrieval when keys are not themselves IBinders.
-     */
-    public interface BinderKey {
-        /**
-         * Returns the binder associated with this key.
-         */
-        IBinder getBinder();
+    protected BinderListenerRegistration(Executor executor, TListener listener) {
+        super(executor, listener);
     }
 
-    protected BinderListenerRegistration(@Nullable TRequest request, CallerIdentity callerIdentity,
-            TListener listener) {
-        super(request, callerIdentity, listener);
-    }
+    protected abstract IBinder getBinderFromKey(TKey key);
 
     @Override
-    protected final void onRemovableListenerRegister() {
-        IBinder binder = getBinderFromKey(getKey());
+    protected void onRegister() {
+        super.onRegister();
+
         try {
-            binder.linkToDeath(this, 0);
+            getBinderFromKey(getKey()).linkToDeath(this, 0);
         } catch (RemoteException e) {
             remove();
         }
-
-        onBinderListenerRegister();
     }
 
     @Override
-    protected final void onRemovableListenerUnregister() {
-        onBinderListenerUnregister();
-        getBinderFromKey(getKey()).unlinkToDeath(this, 0);
+    protected void onUnregister() {
+        try {
+            getBinderFromKey(getKey()).unlinkToDeath(this, 0);
+        } catch (NoSuchElementException e) {
+            // the only way this exception can occur should be if another exception has been thrown
+            // prior to registration completing, and that exception is currently unwinding the call
+            // stack and causing this cleanup. since that exception should crash us anyways, drop
+            // this exception so we're not hiding the original exception.
+            Log.w(getTag(), "failed to unregister binder death listener", e);
+        }
+
+        super.onUnregister();
     }
 
-    /**
-     * May be overridden in place of {@link #onRemovableListenerRegister()}.
-     */
-    protected void onBinderListenerRegister() {}
-
-    /**
-     * May be overridden in place of {@link #onRemovableListenerUnregister()}.
-     */
-    protected void onBinderListenerUnregister() {}
-
-    @Override
     public void onOperationFailure(ListenerOperation<TListener> operation, Exception e) {
         if (e instanceof RemoteException) {
-            Log.w(getOwner().getTag(), "registration " + this + " removed", e);
+            Log.w(getTag(), "registration " + this + " removed", e);
             remove();
         } else {
             super.onOperationFailure(operation, e);
@@ -90,9 +78,10 @@
     @Override
     public void binderDied() {
         try {
-            if (Log.isLoggable(getOwner().getTag(), Log.DEBUG)) {
-                Log.d(getOwner().getTag(), "binder registration " + getIdentity() + " died");
+            if (Log.isLoggable(getTag(), Log.DEBUG)) {
+                Log.d(getTag(), "binder registration " + this + " died");
             }
+
             remove();
         } catch (RuntimeException e) {
             // the caller may swallow runtime exceptions, so we rethrow as assertion errors to
@@ -100,14 +89,4 @@
             throw new AssertionError(e);
         }
     }
-
-    private static IBinder getBinderFromKey(Object key) {
-        if (key instanceof IBinder) {
-            return (IBinder) key;
-        } else if (key instanceof BinderKey) {
-            return ((BinderKey) key).getBinder();
-        } else {
-            throw new IllegalArgumentException("key must be IBinder or BinderKey");
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java b/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java
index 33b08d4..67ae265 100644
--- a/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java
+++ b/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.os.Build;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
@@ -37,40 +36,48 @@
 import java.util.function.Predicate;
 
 /**
- * A base class to multiplex client listener registrations within system server. Every listener is
+ * A base class to multiplex some event source to multiple listener registrations. Every listener is
  * represented by a registration object which stores all required state for a listener. Keys are
  * used to uniquely identify every registration. Listener operations may be executed on
  * registrations in order to invoke the represented listener.
  *
- * Registrations are divided into two categories, active registrations and inactive registrations,
- * as defined by {@link #isActive(ListenerRegistration)}. If a registration's active state changes,
- * {@link #updateRegistrations(Predicate)} must be invoked and return true for any registration
- * whose active state may have changed. Listeners will only be invoked for active registrations.
+ * <p>Registrations are divided into two categories, active registrations and inactive
+ * registrations, as defined by {@link #isActive(ListenerRegistration)}. The set of active
+ * registrations is combined into a single merged registration, which is submitted to the backing
+ * event source when necessary in order to register with the event source. The merged registration
+ * is updated whenever the set of active registration changes. Listeners will only be invoked for
+ * active registrations.
  *
- * The set of active registrations is combined into a single merged registration, which is submitted
- * to the backing service when necessary in order to register the service. The merged registration
- * is updated whenever the set of active registration changes.
+ * <p>In order to inform the multiplexer of state changes, if a registration's active state changes,
+ * or if the merged registration changes, {@link #updateRegistrations(Predicate)} or {@link
+ * #updateRegistration(Object, Predicate)} must be invoked and return true for any registration
+ * whose state may have changed in such a way that the active state or merged registration state has
+ * changed. It is acceptable to return true from a predicate even if nothing has changed, though
+ * this may result in extra pointless work.
  *
- * Callbacks invoked for various changes will always be ordered according to this lifecycle list:
+ * <p>Callbacks invoked for various changes will always be ordered according to this lifecycle list:
  *
  * <ul>
- * <li>{@link #onRegister()}</li>
- * <li>{@link ListenerRegistration#onRegister(Object)}</li>
- * <li>{@link #onRegistrationAdded(Object, ListenerRegistration)}</li>
- * <li>{@link #onRegistrationReplaced(Object, ListenerRegistration, ListenerRegistration)} (only
- * invoked if this registration is replacing a prior registration)</li>
- * <li>{@link #onActive()}</li>
- * <li>{@link ListenerRegistration#onActive()}</li>
- * <li>{@link ListenerRegistration#onInactive()}</li>
- * <li>{@link #onInactive()}</li>
- * <li>{@link #onRegistrationRemoved(Object, ListenerRegistration)}</li>
- * <li>{@link ListenerRegistration#onUnregister()}</li>
- * <li>{@link #onUnregister()}</li>
+ *   <li>{@link #onRegister()}
+ *   <li>{@link ListenerRegistration#onRegister(Object)}
+ *   <li>{@link #onRegistrationAdded(Object, ListenerRegistration)}
+ *   <li>{@link #onActive()}
+ *   <li>{@link ListenerRegistration#onActive()}
+ *   <li>{@link ListenerRegistration#onInactive()}
+ *   <li>{@link #onInactive()}
+ *   <li>{@link #onRegistrationRemoved(Object, ListenerRegistration)}
+ *   <li>{@link ListenerRegistration#onUnregister()}
+ *   <li>{@link #onUnregister()}
  * </ul>
  *
- * Adding registrations is not allowed to be called re-entrantly (ie, while in the middle of some
- * other operation or callback. Removal is allowed re-entrantly, however only via
- * {@link #removeRegistration(Object, ListenerRegistration)}, not via any other removal method. This
+ * <p>If one registration replaces another, then {@link #onRegistrationReplaced(Object,
+ * ListenerRegistration, Object, ListenerRegistration)} is invoked instead of {@link
+ * #onRegistrationRemoved(Object, ListenerRegistration)} and {@link #onRegistrationAdded(Object,
+ * ListenerRegistration)}.
+ *
+ * <p>Adding registrations is not allowed to be called re-entrantly (ie, while in the middle of some
+ * other operation or callback). Removal is allowed re-entrantly, however only via {@link
+ * #removeRegistration(Object, ListenerRegistration)}, not via any other removal method. This
  * ensures re-entrant removal does not accidentally remove the incorrect registration.
  *
  * @param <TKey>                key type
@@ -81,30 +88,31 @@
 public abstract class ListenerMultiplexer<TKey, TListener,
         TRegistration extends ListenerRegistration<TListener>, TMergedRegistration> {
 
-    @GuardedBy("mRegistrations")
+    /**
+     * The lock object used by the multiplexer. Acquiring this lock allows for multiple operations
+     * on the multiplexer to be completed atomically. Otherwise, it is not required to hold this
+     * lock. This lock is held while invoking all lifecycle callbacks on both the multiplexer and
+     * any registrations.
+     */
+    protected final Object mMultiplexerLock = new Object();
+
+    @GuardedBy("mMultiplexerLock")
     private final ArrayMap<TKey, TRegistration> mRegistrations = new ArrayMap<>();
 
-    @GuardedBy("mRegistrations")
     private final UpdateServiceBuffer mUpdateServiceBuffer = new UpdateServiceBuffer();
 
-    @GuardedBy("mRegistrations")
     private final ReentrancyGuard mReentrancyGuard = new ReentrancyGuard();
 
-    @GuardedBy("mRegistrations")
+    @GuardedBy("mMultiplexerLock")
     private int mActiveRegistrationsCount = 0;
 
-    @GuardedBy("mRegistrations")
+    @GuardedBy("mMultiplexerLock")
     private boolean mServiceRegistered = false;
 
-    @GuardedBy("mRegistrations")
+    @GuardedBy("mMultiplexerLock")
     @Nullable private TMergedRegistration mMerged;
 
     /**
-     * Should be implemented to return a unique identifying tag that may be used for logging, etc...
-     */
-    public abstract @NonNull String getTag();
-
-    /**
      * Should be implemented to register with the backing service with the given merged
      * registration, and should return true if a matching call to {@link #unregisterWithService()}
      * is required to unregister (ie, if registration succeeds). The set of registrations passed in
@@ -120,6 +128,7 @@
      * @see #mergeRegistrations(Collection)
      * @see #reregisterWithService(Object, Object, Collection)
      */
+    @GuardedBy("mMultiplexerLock")
     protected abstract boolean registerWithService(TMergedRegistration merged,
             @NonNull Collection<TRegistration> registrations);
 
@@ -130,6 +139,7 @@
      *
      * @see #registerWithService(Object, Collection)
      */
+    @GuardedBy("mMultiplexerLock")
     protected boolean reregisterWithService(TMergedRegistration oldMerged,
             TMergedRegistration newMerged, @NonNull Collection<TRegistration> registrations) {
         return registerWithService(newMerged, registrations);
@@ -138,6 +148,7 @@
     /**
      * Should be implemented to unregister from the backing service.
      */
+    @GuardedBy("mMultiplexerLock")
     protected abstract void unregisterWithService();
 
     /**
@@ -147,6 +158,7 @@
      * {@link #updateRegistrations(Predicate)} must be invoked with a function that returns true for
      * any registrations that may have changed their active state.
      */
+    @GuardedBy("mMultiplexerLock")
     protected abstract boolean isActive(@NonNull TRegistration registration);
 
     /**
@@ -157,7 +169,8 @@
      * {@link #reregisterWithService(Object, Object, Collection)} will be invoked with the new
      * merged registration so that the backing service can be updated.
      */
-    protected abstract @Nullable TMergedRegistration mergeRegistrations(
+    @GuardedBy("mMultiplexerLock")
+    protected abstract TMergedRegistration mergeRegistrations(
             @NonNull Collection<TRegistration> registrations);
 
     /**
@@ -166,6 +179,7 @@
      * present while there are any registrations. Invoked while holding the multiplexer's internal
      * lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onRegister() {}
 
     /**
@@ -174,28 +188,38 @@
      * present while there are any registrations. Invoked while holding the multiplexer's internal
      * lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onUnregister() {}
 
     /**
      * Invoked when a registration is added. Invoked while holding the multiplexer's internal lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onRegistrationAdded(@NonNull TKey key, @NonNull TRegistration registration) {}
 
     /**
-     * Invoked instead of {@link #onRegistrationAdded(Object, ListenerRegistration)} if a
-     * registration is replacing an old registration. The old registration will have already been
-     * unregistered. Invoked while holding the multiplexer's internal lock. The default behavior is
-     * simply to call into {@link #onRegistrationAdded(Object, ListenerRegistration)}.
+     * Invoked when one registration replaces another (through {@link #replaceRegistration(Object,
+     * Object, ListenerRegistration)}). The old registration has already been unregistered at this
+     * point. Invoked while holding the multiplexer's internal lock.
+     *
+     * <p>The default behavior is simply to call first {@link #onRegistrationRemoved(Object,
+     * ListenerRegistration)} and then {@link #onRegistrationAdded(Object, ListenerRegistration)}.
      */
-    protected void onRegistrationReplaced(@NonNull TKey key, @NonNull TRegistration oldRegistration,
+    @GuardedBy("mMultiplexerLock")
+    protected void onRegistrationReplaced(
+            @NonNull TKey oldKey,
+            @NonNull TRegistration oldRegistration,
+            @NonNull TKey newKey,
             @NonNull TRegistration newRegistration) {
-        onRegistrationAdded(key, newRegistration);
+        onRegistrationRemoved(oldKey, oldRegistration);
+        onRegistrationAdded(newKey, newRegistration);
     }
 
     /**
      * Invoked when a registration is removed. Invoked while holding the multiplexer's internal
      * lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onRegistrationRemoved(@NonNull TKey key, @NonNull TRegistration registration) {}
 
     /**
@@ -204,6 +228,7 @@
      * need to be present while there are active registrations. Invoked while holding the
      * multiplexer's internal lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onActive() {}
 
     /**
@@ -212,6 +237,7 @@
      * need to be present while there are active registrations. Invoked while holding the
      * multiplexer's internal lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onInactive() {}
 
     /**
@@ -224,13 +250,12 @@
 
     /**
      * Atomically removes the registration with the old key and adds a new registration with the
-     * given key. If there was a registration for the old key,
-     * {@link #onRegistrationReplaced(Object, ListenerRegistration, ListenerRegistration)} will be
-     * invoked for the new registration and key instead of
-     * {@link #onRegistrationAdded(Object, ListenerRegistration)}, even though they may not share
-     * the same key. The old key may be the same value as the new key, in which case this function
-     * is equivalent to {@link #putRegistration(Object, ListenerRegistration)}. This method cannot
-     * be called to add a registration re-entrantly.
+     * given key. If there was a registration for the old key, {@link
+     * #onRegistrationReplaced(Object, ListenerRegistration, Object, ListenerRegistration)} will be
+     * invoked instead of {@link #onRegistrationAdded(Object, ListenerRegistration)}, even if they
+     * share the same key. The old key may be the same value as the new key, in which case this
+     * function is equivalent to {@link #putRegistration(Object, ListenerRegistration)}. This method
+     * cannot be called to add a registration re-entrantly.
      */
     protected final void replaceRegistration(@NonNull TKey oldKey, @NonNull TKey key,
             @NonNull TRegistration registration) {
@@ -238,7 +263,7 @@
         Objects.requireNonNull(key);
         Objects.requireNonNull(registration);
 
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             // adding listeners reentrantly is not supported
             Preconditions.checkState(!mReentrancyGuard.isReentrant());
 
@@ -257,12 +282,18 @@
                 boolean wasEmpty = mRegistrations.isEmpty();
 
                 TRegistration oldRegistration = null;
-                int index = mRegistrations.indexOfKey(oldKey);
-                if (index >= 0) {
-                    oldRegistration = removeRegistration(index, oldKey != key);
+                int oldIndex = mRegistrations.indexOfKey(oldKey);
+                if (oldIndex >= 0) {
+                    // remove ourselves instead of using remove(), to balance registration callbacks
+                    oldRegistration = mRegistrations.valueAt(oldIndex);
+                    unregister(oldRegistration);
+                    oldRegistration.onUnregister();
+                    if (oldKey != key) {
+                        mRegistrations.removeAt(oldIndex);
+                    }
                 }
-                if (oldKey == key && index >= 0) {
-                    mRegistrations.setValueAt(index, registration);
+                if (oldKey == key && oldIndex >= 0) {
+                    mRegistrations.setValueAt(oldIndex, registration);
                 } else {
                     mRegistrations.put(key, registration);
                 }
@@ -274,7 +305,7 @@
                 if (oldRegistration == null) {
                     onRegistrationAdded(key, registration);
                 } else {
-                    onRegistrationReplaced(key, oldRegistration, registration);
+                    onRegistrationReplaced(oldKey, oldRegistration, key, registration);
                 }
                 onRegistrationActiveChanged(registration);
             }
@@ -282,29 +313,11 @@
     }
 
     /**
-     * Removes the registration with the given key. This method cannot be called to remove a
-     * registration re-entrantly.
-     */
-    protected final void removeRegistration(@NonNull Object key) {
-        synchronized (mRegistrations) {
-            // this method does not support removing listeners reentrantly
-            Preconditions.checkState(!mReentrancyGuard.isReentrant());
-
-            int index = mRegistrations.indexOfKey(key);
-            if (index < 0) {
-                return;
-            }
-
-            removeRegistration(index, true);
-        }
-    }
-
-    /**
      * Removes all registrations with keys that satisfy the given predicate. This method cannot be
      * called to remove a registration re-entrantly.
      */
     protected final void removeRegistrationIf(@NonNull Predicate<TKey> predicate) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             // this method does not support removing listeners reentrantly
             Preconditions.checkState(!mReentrancyGuard.isReentrant());
 
@@ -329,13 +342,31 @@
     }
 
     /**
+     * Removes the registration with the given key. This method cannot be called to remove a
+     * registration re-entrantly.
+     */
+    protected final void removeRegistration(TKey key) {
+        synchronized (mMultiplexerLock) {
+            // this method does not support removing listeners reentrantly
+            Preconditions.checkState(!mReentrancyGuard.isReentrant());
+
+            int index = mRegistrations.indexOfKey(key);
+            if (index < 0) {
+                return;
+            }
+
+            removeRegistration(index);
+        }
+    }
+
+    /**
      * Removes the given registration with the given key. If the given key has a different
      * registration at the time this method is called, nothing happens. This method allows for
      * re-entrancy, and may be called to remove a registration re-entrantly.
      */
-    protected final void removeRegistration(@NonNull Object key,
+    protected final void removeRegistration(@NonNull TKey key,
             @NonNull ListenerRegistration<?> registration) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             int index = mRegistrations.indexOfKey(key);
             if (index < 0) {
                 return;
@@ -350,17 +381,13 @@
                 unregister(typedRegistration);
                 mReentrancyGuard.markForRemoval(key, typedRegistration);
             } else {
-                removeRegistration(index, true);
+                removeRegistration(index);
             }
         }
     }
 
-    @GuardedBy("mRegistrations")
-    private TRegistration removeRegistration(int index, boolean removeEntry) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mRegistrations));
-        }
-
+    @GuardedBy("mMultiplexerLock")
+    private void removeRegistration(int index) {
         TKey key = mRegistrations.keyAt(index);
         TRegistration registration = mRegistrations.valueAt(index);
 
@@ -376,15 +403,11 @@
             unregister(registration);
             onRegistrationRemoved(key, registration);
             registration.onUnregister();
-            if (removeEntry) {
-                mRegistrations.removeAt(index);
-                if (mRegistrations.isEmpty()) {
-                    onUnregister();
-                }
+            mRegistrations.removeAt(index);
+            if (mRegistrations.isEmpty()) {
+                onUnregister();
             }
         }
-
-        return registration;
     }
 
     /**
@@ -392,14 +415,14 @@
      * registration accordingly.
      */
     protected final void updateService() {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             if (mUpdateServiceBuffer.isBuffered()) {
                 mUpdateServiceBuffer.markUpdateServiceRequired();
                 return;
             }
 
-            ArrayList<TRegistration> actives = new ArrayList<>(mRegistrations.size());
             final int size = mRegistrations.size();
+            ArrayList<TRegistration> actives = new ArrayList<>(size);
             for (int i = 0; i < size; i++) {
                 TRegistration registration = mRegistrations.valueAt(i);
                 if (registration.isActive()) {
@@ -413,17 +436,17 @@
                     mServiceRegistered = false;
                     unregisterWithService();
                 }
-                return;
-            }
-
-            TMergedRegistration merged = mergeRegistrations(actives);
-            if (!mServiceRegistered || !Objects.equals(merged, mMerged)) {
+            } else {
+                TMergedRegistration merged = mergeRegistrations(actives);
                 if (mServiceRegistered) {
-                    mServiceRegistered = reregisterWithService(mMerged, merged, actives);
+                    if (!Objects.equals(merged, mMerged)) {
+                        mServiceRegistered = reregisterWithService(mMerged, merged, actives);
+                        mMerged = mServiceRegistered ? merged : null;
+                    }
                 } else {
                     mServiceRegistered = registerWithService(merged, actives);
+                    mMerged = mServiceRegistered ? merged : null;
                 }
-                mMerged = mServiceRegistered ? merged : null;
             }
         }
     }
@@ -437,7 +460,7 @@
      * reinitialized.
      */
     protected final void resetService() {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             if (mServiceRegistered) {
                 mMerged = null;
                 mServiceRegistered = false;
@@ -453,7 +476,31 @@
      * buffering {@code updateService()} until after multiple adds/removes/updates occur.
      */
     public UpdateServiceLock newUpdateServiceLock() {
-        return new UpdateServiceLock(mUpdateServiceBuffer.acquire());
+        return new UpdateServiceLock(mUpdateServiceBuffer);
+    }
+
+    /**
+     * Evaluates the predicate on all registrations until the predicate returns true, at which point
+     * evaluation will cease. Returns true if the predicate ever returned true, and returns false
+     * otherwise.
+     */
+    protected final boolean findRegistration(Predicate<TRegistration> predicate) {
+        synchronized (mMultiplexerLock) {
+            // we only acquire a reentrancy guard in case of removal while iterating. this method
+            // does not directly affect active state or merged state, so there is no advantage to
+            // acquiring an update source buffer.
+            try (ReentrancyGuard ignored = mReentrancyGuard.acquire()) {
+                final int size = mRegistrations.size();
+                for (int i = 0; i < size; i++) {
+                    TRegistration registration = mRegistrations.valueAt(i);
+                    if (predicate.test(registration)) {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
     }
 
     /**
@@ -463,7 +510,7 @@
      * the resulting changes.
      */
     protected final void updateRegistrations(@NonNull Predicate<TRegistration> predicate) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             // since updating a registration can invoke a variety of callbacks, we need to ensure
             // those callbacks themselves do not re-enter, as this could lead to out-of-order
             // callbacks. note that try-with-resources ordering is meaningful here as well. we want
@@ -492,7 +539,7 @@
      */
     protected final boolean updateRegistration(@NonNull Object key,
             @NonNull Predicate<TRegistration> predicate) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             // since updating a registration can invoke a variety of callbacks, we need to ensure
             // those callbacks themselves do not re-enter, as this could lead to out-of-order
             // callbacks. note that try-with-resources ordering is meaningful here as well. we want
@@ -515,12 +562,8 @@
         }
     }
 
-    @GuardedBy("mRegistrations")
+    @GuardedBy("mMultiplexerLock")
     private void onRegistrationActiveChanged(TRegistration registration) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mRegistrations));
-        }
-
         boolean active = registration.isRegistered() && isActive(registration);
         boolean changed = registration.setActive(active);
         if (changed) {
@@ -547,7 +590,7 @@
      */
     protected final void deliverToListeners(
             @NonNull Function<TRegistration, ListenerOperation<TListener>> function) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             try (ReentrancyGuard ignored = mReentrancyGuard.acquire()) {
                 final int size = mRegistrations.size();
                 for (int i = 0; i < size; i++) {
@@ -571,7 +614,7 @@
      * </pre>
      */
     protected final void deliverToListeners(@NonNull ListenerOperation<TListener> operation) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             try (ReentrancyGuard ignored = mReentrancyGuard.acquire()) {
                 final int size = mRegistrations.size();
                 for (int i = 0; i < size; i++) {
@@ -584,6 +627,7 @@
         }
     }
 
+    @GuardedBy("mMultiplexerLock")
     private void unregister(TRegistration registration) {
         registration.unregisterInternal();
         onRegistrationActiveChanged(registration);
@@ -593,7 +637,7 @@
      * Dumps debug information.
      */
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             pw.print("service: ");
             pw.print(getServiceState());
             pw.println();
@@ -620,6 +664,7 @@
      * May be overridden to provide additional details on service state when dumping the manager
      * state. Invoked while holding the multiplexer's internal lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected String getServiceState() {
         if (mServiceRegistered) {
             if (mMerged != null) {
@@ -643,61 +688,63 @@
      */
     private final class ReentrancyGuard implements AutoCloseable {
 
-        @GuardedBy("mRegistrations")
+        @GuardedBy("mMultiplexerLock")
         private int mGuardCount;
-        @GuardedBy("mRegistrations")
-        private @Nullable ArraySet<Entry<Object, ListenerRegistration<?>>> mScheduledRemovals;
+
+        @GuardedBy("mMultiplexerLock")
+        @Nullable private ArraySet<Entry<TKey, ListenerRegistration<?>>> mScheduledRemovals;
 
         ReentrancyGuard() {
             mGuardCount = 0;
             mScheduledRemovals = null;
         }
 
-        @GuardedBy("mRegistrations")
         boolean isReentrant() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mRegistrations));
+            synchronized (mMultiplexerLock) {
+                return mGuardCount != 0;
             }
-            return mGuardCount != 0;
         }
 
-        @GuardedBy("mRegistrations")
-        void markForRemoval(Object key, ListenerRegistration<?> registration) {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mRegistrations));
-            }
-            Preconditions.checkState(isReentrant());
+        void markForRemoval(TKey key, ListenerRegistration<?> registration) {
+            synchronized (mMultiplexerLock) {
+                Preconditions.checkState(isReentrant());
 
-            if (mScheduledRemovals == null) {
-                mScheduledRemovals = new ArraySet<>(mRegistrations.size());
+                if (mScheduledRemovals == null) {
+                    mScheduledRemovals = new ArraySet<>(mRegistrations.size());
+                }
+                mScheduledRemovals.add(new AbstractMap.SimpleImmutableEntry<>(key, registration));
             }
-            mScheduledRemovals.add(new AbstractMap.SimpleImmutableEntry<>(key, registration));
         }
 
         ReentrancyGuard acquire() {
-            ++mGuardCount;
-            return this;
+            synchronized (mMultiplexerLock) {
+                ++mGuardCount;
+                return this;
+            }
         }
 
         @Override
         public void close() {
-            ArraySet<Entry<Object, ListenerRegistration<?>>> scheduledRemovals = null;
+            synchronized (mMultiplexerLock) {
+                Preconditions.checkState(mGuardCount > 0);
 
-            Preconditions.checkState(mGuardCount > 0);
-            if (--mGuardCount == 0) {
-                scheduledRemovals = mScheduledRemovals;
-                mScheduledRemovals = null;
-            }
+                ArraySet<Entry<TKey, ListenerRegistration<?>>> scheduledRemovals = null;
 
-            if (scheduledRemovals == null) {
-                return;
-            }
+                if (--mGuardCount == 0) {
+                    scheduledRemovals = mScheduledRemovals;
+                    mScheduledRemovals = null;
+                }
 
-            try (UpdateServiceBuffer ignored = mUpdateServiceBuffer.acquire()) {
-                final int size = scheduledRemovals.size();
-                for (int i = 0; i < size; i++) {
-                    Entry<Object, ListenerRegistration<?>> entry = scheduledRemovals.valueAt(i);
-                    removeRegistration(entry.getKey(), entry.getValue());
+                if (scheduledRemovals == null) {
+                    return;
+                }
+
+                try (UpdateServiceBuffer ignored = mUpdateServiceBuffer.acquire()) {
+                    final int size = scheduledRemovals.size();
+                    for (int i = 0; i < size; i++) {
+                        Entry<TKey, ListenerRegistration<?>> entry = scheduledRemovals.valueAt(i);
+                        removeRegistration(entry.getKey(), entry.getValue());
+                    }
                 }
             }
         }
@@ -721,6 +768,7 @@
 
         @GuardedBy("this")
         private int mBufferCount;
+
         @GuardedBy("this")
         private boolean mUpdateServiceRequired;
 
@@ -765,18 +813,18 @@
      * {@link #close()}ed. This can be used to save work by acquiring the lock before multiple calls
      * to updateService() are expected, and closing the lock after.
      */
-    public final class UpdateServiceLock implements AutoCloseable {
+    public static final class UpdateServiceLock implements AutoCloseable {
 
-        private @Nullable UpdateServiceBuffer mUpdateServiceBuffer;
+        @Nullable private ListenerMultiplexer<?, ?, ?, ?>.UpdateServiceBuffer mUpdateServiceBuffer;
 
-        UpdateServiceLock(UpdateServiceBuffer updateServiceBuffer) {
-            mUpdateServiceBuffer = updateServiceBuffer;
+        UpdateServiceLock(ListenerMultiplexer<?, ?, ?, ?>.UpdateServiceBuffer updateServiceBuffer) {
+            mUpdateServiceBuffer = updateServiceBuffer.acquire();
         }
 
         @Override
         public void close() {
             if (mUpdateServiceBuffer != null) {
-                UpdateServiceBuffer buffer = mUpdateServiceBuffer;
+                ListenerMultiplexer<?, ?, ?, ?>.UpdateServiceBuffer buffer = mUpdateServiceBuffer;
                 mUpdateServiceBuffer = null;
                 buffer.close();
             }
diff --git a/services/core/java/com/android/server/location/listeners/ListenerRegistration.java b/services/core/java/com/android/server/location/listeners/ListenerRegistration.java
index 711dde8..fcb2a9b 100644
--- a/services/core/java/com/android/server/location/listeners/ListenerRegistration.java
+++ b/services/core/java/com/android/server/location/listeners/ListenerRegistration.java
@@ -35,7 +35,7 @@
 
     private boolean mActive;
 
-    private volatile @Nullable TListener mListener;
+    @Nullable private volatile TListener mListener;
 
     protected ListenerRegistration(Executor executor, TListener listener) {
         mExecutor = Objects.requireNonNull(executor);
@@ -43,6 +43,13 @@
         mListener = Objects.requireNonNull(listener);
     }
 
+    /**
+     * Returns a tag to use for logging. Should be overridden by subclasses.
+     */
+    protected String getTag() {
+        return "ListenerRegistration";
+    }
+
     protected final Executor getExecutor() {
         return mExecutor;
     }
@@ -50,26 +57,36 @@
     /**
      * May be overridden by subclasses. Invoked when registration occurs. Invoked while holding the
      * owning multiplexer's internal lock.
+     *
+     * <p>If overridden you must ensure the superclass method is invoked (usually as the first thing
+     * in the overridden method).
      */
     protected void onRegister(Object key) {}
 
     /**
      * May be overridden by subclasses. Invoked when unregistration occurs. Invoked while holding
      * the owning multiplexer's internal lock.
+     *
+     * <p>If overridden you must ensure the superclass method is invoked (usually as the last thing
+     * in the overridden method).
      */
     protected void onUnregister() {}
 
     /**
-     * May be overridden by subclasses. Invoked when this registration becomes active. If this
-     * returns a non-null operation, that operation will be invoked for the listener. Invoked
-     * while holding the owning multiplexer's internal lock.
+     * May be overridden by subclasses. Invoked when this registration becomes active. Invoked while
+     * holding the owning multiplexer's internal lock.
+     *
+     * <p>If overridden you must ensure the superclass method is invoked (usually as the first thing
+     * in the overridden method).
      */
     protected void onActive() {}
 
     /**
-     * May be overridden by subclasses. Invoked when registration becomes inactive. If this returns
-     * a non-null operation, that operation will be invoked for the listener. Invoked while holding
-     * the owning multiplexer's internal lock.
+     * May be overridden by subclasses. Invoked when registration becomes inactive. Invoked while
+     * holding the owning multiplexer's internal lock.
+     *
+     * <p>If overridden you must ensure the superclass method is invoked (usually as the last thing
+     * in the overridden method).
      */
     protected void onInactive() {}
 
diff --git a/services/core/java/com/android/server/location/listeners/PendingIntentListenerRegistration.java b/services/core/java/com/android/server/location/listeners/PendingIntentListenerRegistration.java
index 240ac01..c976601 100644
--- a/services/core/java/com/android/server/location/listeners/PendingIntentListenerRegistration.java
+++ b/services/core/java/com/android/server/location/listeners/PendingIntentListenerRegistration.java
@@ -16,63 +16,47 @@
 
 package com.android.server.location.listeners;
 
-import android.annotation.Nullable;
+import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
+
 import android.app.PendingIntent;
-import android.location.util.identity.CallerIdentity;
 import android.util.Log;
 
 /**
  * A registration that works with PendingIntent keys, and registers a CancelListener to
- * automatically remove the registration if the PendingIntent is canceled. The key for this
- * registration must either be a {@link PendingIntent} or a {@link PendingIntentKey}.
+ * automatically remove the registration if the PendingIntent is canceled.
  *
- * @param <TRequest>  request type
+ * @param <TKey>      key type
  * @param <TListener> listener type
  */
-public abstract class PendingIntentListenerRegistration<TRequest, TListener> extends
-        RemoteListenerRegistration<TRequest, TListener> implements PendingIntent.CancelListener {
+public abstract class PendingIntentListenerRegistration<TKey, TListener> extends
+        RemovableListenerRegistration<TKey, TListener> implements PendingIntent.CancelListener {
 
-    /**
-     * Interface to allowed pending intent retrieval when keys are not themselves PendingIntents.
-     */
-    public interface PendingIntentKey {
-        /**
-         * Returns the pending intent associated with this key.
-         */
-        PendingIntent getPendingIntent();
+    protected PendingIntentListenerRegistration(TListener listener) {
+        super(DIRECT_EXECUTOR, listener);
     }
 
-    protected PendingIntentListenerRegistration(@Nullable TRequest request,
-            CallerIdentity callerIdentity, TListener listener) {
-        super(request, callerIdentity, listener);
+    protected abstract PendingIntent getPendingIntentFromKey(TKey key);
+
+    @Override
+    protected void onRegister() {
+        super.onRegister();
+
+        if (!getPendingIntentFromKey(getKey()).addCancelListener(DIRECT_EXECUTOR, this)) {
+            remove();
+        }
     }
 
     @Override
-    protected final void onRemovableListenerRegister() {
-        getPendingIntentFromKey(getKey()).registerCancelListener(this);
-        onPendingIntentListenerRegister();
+    protected void onUnregister() {
+        getPendingIntentFromKey(getKey()).removeCancelListener(this);
+
+        super.onUnregister();
     }
 
     @Override
-    protected final void onRemovableListenerUnregister() {
-        onPendingIntentListenerUnregister();
-        getPendingIntentFromKey(getKey()).unregisterCancelListener(this);
-    }
-
-    /**
-     * May be overridden in place of {@link #onRemovableListenerRegister()}.
-     */
-    protected void onPendingIntentListenerRegister() {}
-
-    /**
-     * May be overridden in place of {@link #onRemovableListenerUnregister()}.
-     */
-    protected void onPendingIntentListenerUnregister() {}
-
-    @Override
     protected void onOperationFailure(ListenerOperation<TListener> operation, Exception e) {
         if (e instanceof PendingIntent.CanceledException) {
-            Log.w(getOwner().getTag(), "registration " + this + " removed", e);
+            Log.w(getTag(), "registration " + this + " removed", e);
             remove();
         } else {
             super.onOperationFailure(operation, e);
@@ -81,21 +65,10 @@
 
     @Override
     public void onCanceled(PendingIntent intent) {
-        if (Log.isLoggable(getOwner().getTag(), Log.DEBUG)) {
-            Log.d(getOwner().getTag(),
-                    "pending intent registration " + getIdentity() + " canceled");
+        if (Log.isLoggable(getTag(), Log.DEBUG)) {
+            Log.d(getTag(), "pending intent registration " + this + " canceled");
         }
 
         remove();
     }
-
-    private PendingIntent getPendingIntentFromKey(Object key) {
-        if (key instanceof PendingIntent) {
-            return (PendingIntent) key;
-        } else if (key instanceof PendingIntentKey) {
-            return ((PendingIntentKey) key).getPendingIntent();
-        } else {
-            throw new IllegalArgumentException("key must be PendingIntent or PendingIntentKey");
-        }
-    }
-}
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/location/listeners/RemoteListenerRegistration.java b/services/core/java/com/android/server/location/listeners/RemoteListenerRegistration.java
deleted file mode 100644
index 4eca577..0000000
--- a/services/core/java/com/android/server/location/listeners/RemoteListenerRegistration.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.location.listeners;
-
-
-import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
-
-import android.annotation.Nullable;
-import android.location.util.identity.CallerIdentity;
-import android.os.Process;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.FgThread;
-
-import java.util.Objects;
-import java.util.concurrent.Executor;
-
-/**
- * A listener registration representing a remote (possibly from a different process) listener.
- * Listeners from a different process will be run on a direct executor, since the x-process listener
- * invocation should already be asynchronous. Listeners from the same process will be run on a
- * normal executor, since in-process listener invocation may be synchronous.
- *
- * @param <TRequest>           request type
- * @param <TListener>          listener type
- */
-public abstract class RemoteListenerRegistration<TRequest, TListener> extends
-        RemovableListenerRegistration<TRequest, TListener> {
-
-    @VisibleForTesting
-    public static final Executor IN_PROCESS_EXECUTOR = FgThread.getExecutor();
-
-    private static Executor chooseExecutor(CallerIdentity identity) {
-        // if a client is in the same process as us, binder calls will execute synchronously and
-        // we shouldn't run callbacks directly since they might be run under lock and deadlock
-        if (identity.getPid() == Process.myPid()) {
-            // there's a slight loophole here for pending intents - pending intent callbacks can
-            // always be run on the direct executor since they're always asynchronous, but honestly
-            // you shouldn't be using pending intent callbacks within the same process anyways
-            return IN_PROCESS_EXECUTOR;
-        } else {
-            return DIRECT_EXECUTOR;
-        }
-    }
-
-    private final CallerIdentity mIdentity;
-
-    protected RemoteListenerRegistration(@Nullable TRequest request, CallerIdentity identity,
-            TListener listener) {
-        super(chooseExecutor(identity), request, listener);
-        mIdentity = Objects.requireNonNull(identity);
-    }
-
-    /**
-     * Returns the listener identity.
-     */
-    public final CallerIdentity getIdentity() {
-        return mIdentity;
-    }
-}
-
diff --git a/services/core/java/com/android/server/location/listeners/RemovableListenerRegistration.java b/services/core/java/com/android/server/location/listeners/RemovableListenerRegistration.java
index 618ff24..3c302fb 100644
--- a/services/core/java/com/android/server/location/listeners/RemovableListenerRegistration.java
+++ b/services/core/java/com/android/server/location/listeners/RemovableListenerRegistration.java
@@ -20,22 +20,23 @@
 
 import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * A listener registration that stores its own key, and thus can remove itself. By default it will
  * remove itself if any checked exception occurs on listener execution.
  *
- * @param <TRequest>           request type
+ * @param <TKey>               key type
  * @param <TListener>          listener type
  */
-public abstract class RemovableListenerRegistration<TRequest, TListener> extends
-        RequestListenerRegistration<TRequest, TListener> {
+public abstract class RemovableListenerRegistration<TKey, TListener> extends
+        ListenerRegistration<TListener> {
 
-    private volatile @Nullable Object mKey;
+    @Nullable private volatile TKey mKey;
+    private final AtomicBoolean mRemoved = new AtomicBoolean(false);
 
-    protected RemovableListenerRegistration(Executor executor, @Nullable TRequest request,
-            TListener listener) {
-        super(executor, request, listener);
+    protected RemovableListenerRegistration(Executor executor, TListener listener) {
+        super(executor, listener);
     }
 
     /**
@@ -43,46 +44,76 @@
      * with. Often this is easiest to accomplish by defining registration subclasses as non-static
      * inner classes of the multiplexer they are to be used with.
      */
-    protected abstract ListenerMultiplexer<?, ? super TListener, ?, ?> getOwner();
+    protected abstract ListenerMultiplexer<TKey, ? super TListener, ?, ?> getOwner();
 
     /**
      * Returns the key associated with this registration. May not be invoked before
      * {@link #onRegister(Object)} or after {@link #onUnregister()}.
      */
-    protected final Object getKey() {
+    protected final TKey getKey() {
         return Objects.requireNonNull(mKey);
     }
 
     /**
-     * Removes this registration. Does nothing if invoked before {@link #onRegister(Object)} or
-     * after {@link #onUnregister()}. It is safe to invoke this from within either function.
+     * Convenience method equivalent to invoking {@link #remove(boolean)} with the
+     * {@code immediately} parameter set to true.
      */
     public final void remove() {
-        Object key = mKey;
-        if (key != null) {
-            getOwner().removeRegistration(key, this);
+        remove(true);
+    }
+
+    /**
+     * Removes this registration. If the {@code immediately} parameter is true, all pending listener
+     * invocations will fail. If the {@code immediately} parameter is false, listener invocations
+     * that were scheduled before remove was invoked (including invocations scheduled within {@link
+     * #onRemove(boolean)}) will continue, but any listener invocations scheduled after remove was
+     * invoked will fail.
+     *
+     * <p>Only the first call to this method will ever go through (and so {@link #onRemove(boolean)}
+     * will only ever be invoked once).
+     *
+     * <p>Does nothing if invoked before {@link #onRegister()} or after {@link #onUnregister()}.
+     */
+    public final void remove(boolean immediately) {
+        TKey key = mKey;
+        if (key != null && !mRemoved.getAndSet(true)) {
+            onRemove(immediately);
+            if (immediately) {
+                getOwner().removeRegistration(key, this);
+            } else {
+                executeOperation(listener -> getOwner().removeRegistration(key, this));
+            }
         }
     }
 
+    /**
+     * Invoked just before this registration is removed due to {@link #remove(boolean)}, on the same
+     * thread as the responsible {@link #remove(boolean)} call.
+     *
+     * <p>This method will only ever be invoked once, no matter how many calls to {@link
+     * #remove(boolean)} are made, as any registration can only be removed once.
+     */
+    protected void onRemove(boolean immediately) {}
+
     @Override
     protected final void onRegister(Object key) {
-        mKey = Objects.requireNonNull(key);
-        onRemovableListenerRegister();
+        super.onRegister(key);
+        mKey = (TKey) Objects.requireNonNull(key);
+        onRegister();
     }
 
+    /**
+     * May be overridden by subclasses. Invoked when registration occurs. Invoked while holding the
+     * owning multiplexer's internal lock.
+     *
+     * <p>If overridden you must ensure the superclass method is invoked (usually as the first thing
+     * in the overridden method).
+     */
+    protected void onRegister() {}
+
     @Override
-    protected final void onUnregister() {
-        onRemovableListenerUnregister();
+    protected void onUnregister() {
         mKey = null;
+        super.onUnregister();
     }
-
-    /**
-     * May be overridden in place of {@link #onRegister(Object)}.
-     */
-    protected void onRemovableListenerRegister() {}
-
-    /**
-     * May be overridden in place of {@link #onUnregister()}.
-     */
-    protected void onRemovableListenerUnregister() {}
 }
diff --git a/services/core/java/com/android/server/location/listeners/RequestListenerRegistration.java b/services/core/java/com/android/server/location/listeners/RequestListenerRegistration.java
deleted file mode 100644
index 0c2fc91..0000000
--- a/services/core/java/com/android/server/location/listeners/RequestListenerRegistration.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.location.listeners;
-
-import java.util.concurrent.Executor;
-
-/**
- * A listener registration object which includes an associated request.
- *
- * @param <TRequest>           request type
- * @param <TListener>          listener type
- */
-public class RequestListenerRegistration<TRequest, TListener> extends
-        ListenerRegistration<TListener> {
-
-    private final TRequest mRequest;
-
-    protected RequestListenerRegistration(Executor executor, TRequest request,
-            TListener listener) {
-        super(executor, listener);
-        mRequest = request;
-    }
-
-    /**
-     * Returns the request associated with this listener, or null if one wasn't supplied.
-     */
-    public TRequest getRequest() {
-        return mRequest;
-    }
-
-    @Override
-    public String toString() {
-        if (mRequest == null) {
-            return "[]";
-        } else {
-            return mRequest.toString();
-        }
-    }
-}
-
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index 549fd49..a69a079 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -67,7 +67,6 @@
 import android.location.provider.ProviderRequest;
 import android.location.util.identity.CallerIdentity;
 import android.os.Binder;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.IBinder;
@@ -115,7 +114,7 @@
 import com.android.server.location.injector.UserInfoHelper;
 import com.android.server.location.injector.UserInfoHelper.UserListener;
 import com.android.server.location.listeners.ListenerMultiplexer;
-import com.android.server.location.listeners.RemoteListenerRegistration;
+import com.android.server.location.listeners.RemovableListenerRegistration;
 import com.android.server.location.settings.LocationSettings;
 import com.android.server.location.settings.LocationUserSettings;
 
@@ -124,8 +123,10 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
 import java.util.function.Predicate;
 
 /**
@@ -354,44 +355,60 @@
         public void deliverOnFlushComplete(int requestCode) {}
     }
 
-    protected abstract class Registration extends RemoteListenerRegistration<LocationRequest,
-            LocationTransport> {
+    protected abstract class Registration extends RemovableListenerRegistration<Object,
+                LocationTransport> {
 
+        private final LocationRequest mBaseRequest;
+        private final CallerIdentity mIdentity;
         private final @PermissionLevel int mPermissionLevel;
 
         // we cache these values because checking/calculating on the fly is more expensive
+        @GuardedBy("mMultiplexerLock")
         private boolean mPermitted;
+        @GuardedBy("mMultiplexerLock")
         private boolean mForeground;
+        @GuardedBy("mMultiplexerLock")
         private LocationRequest mProviderLocationRequest;
+        @GuardedBy("mMultiplexerLock")
         private boolean mIsUsingHighPower;
 
-        private @Nullable Location mLastLocation = null;
+        @Nullable private Location mLastLocation = null;
 
-        protected Registration(LocationRequest request, CallerIdentity identity,
+        protected Registration(LocationRequest request, CallerIdentity identity, Executor executor,
                 LocationTransport transport, @PermissionLevel int permissionLevel) {
-            super(Objects.requireNonNull(request), identity, transport);
+            super(executor, transport);
 
             Preconditions.checkArgument(identity.getListenerId() != null);
             Preconditions.checkArgument(permissionLevel > PERMISSION_NONE);
             Preconditions.checkArgument(!request.getWorkSource().isEmpty());
 
+            mBaseRequest = Objects.requireNonNull(request);
+            mIdentity = Objects.requireNonNull(identity);
             mPermissionLevel = permissionLevel;
             mProviderLocationRequest = request;
         }
 
-        @GuardedBy("mLock")
-        @Override
-        protected final void onRemovableListenerRegister() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
+        public final CallerIdentity getIdentity() {
+            return mIdentity;
+        }
+
+        public final LocationRequest getRequest() {
+            synchronized (mMultiplexerLock) {
+                return mProviderLocationRequest;
             }
+        }
+
+        @GuardedBy("mMultiplexerLock")
+        @Override
+        protected void onRegister() {
+            super.onRegister();
 
             if (D) {
                 Log.d(TAG, mName + " provider added registration from " + getIdentity() + " -> "
                         + getRequest());
             }
 
-            EVENT_LOG.logProviderClientRegistered(mName, getIdentity(), super.getRequest());
+            EVENT_LOG.logProviderClientRegistered(mName, getIdentity(), mBaseRequest);
 
             // initialization order is important as there are ordering dependencies
             mPermitted = mLocationPermissionsHelper.hasLocationPermissions(mPermissionLevel,
@@ -400,110 +417,72 @@
             mProviderLocationRequest = calculateProviderLocationRequest();
             mIsUsingHighPower = isUsingHighPower();
 
-            onProviderListenerRegister();
-
             if (mForeground) {
                 EVENT_LOG.logProviderClientForeground(mName, getIdentity());
             }
         }
 
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onRemovableListenerUnregister() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
-            onProviderListenerUnregister();
-
+        protected void onUnregister() {
             EVENT_LOG.logProviderClientUnregistered(mName, getIdentity());
 
             if (D) {
                 Log.d(TAG, mName + " provider removed registration from " + getIdentity());
             }
+
+            super.onUnregister();
         }
 
-        /**
-         * Subclasses may override this instead of {@link #onRemovableListenerRegister()}.
-         */
-        @GuardedBy("mLock")
-        protected void onProviderListenerRegister() {}
-
-        /**
-         * Subclasses may override this instead of {@link #onRemovableListenerUnregister()}.
-         */
-        @GuardedBy("mLock")
-        protected void onProviderListenerUnregister() {}
-
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onActive() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
+        protected void onActive() {
             EVENT_LOG.logProviderClientActive(mName, getIdentity());
 
             if (!getRequest().isHiddenFromAppOps()) {
                 mAppOpsHelper.startOpNoThrow(OP_MONITOR_LOCATION, getIdentity());
             }
             onHighPowerUsageChanged();
-
-            onProviderListenerActive();
         }
 
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onInactive() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
+        protected void onInactive() {
             onHighPowerUsageChanged();
             if (!getRequest().isHiddenFromAppOps()) {
                 mAppOpsHelper.finishOp(OP_MONITOR_LOCATION, getIdentity());
             }
 
-            onProviderListenerInactive();
-
             EVENT_LOG.logProviderClientInactive(mName, getIdentity());
         }
 
-        /**
-         * Subclasses may override this instead of {@link #onActive()}.
-         */
-        @GuardedBy("mLock")
-        protected void onProviderListenerActive() {}
-
-        /**
-         * Subclasses may override this instead of {@link #onInactive()} ()}.
-         */
-        @GuardedBy("mLock")
-        protected void onProviderListenerInactive() {}
-
-        @Override
-        public final LocationRequest getRequest() {
-            return mProviderLocationRequest;
-        }
-
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         final void setLastDeliveredLocation(@Nullable Location location) {
             mLastLocation = location;
         }
 
-        @GuardedBy("mLock")
         public final Location getLastDeliveredLocation() {
-            return mLastLocation;
+            synchronized (mMultiplexerLock) {
+                return mLastLocation;
+            }
         }
 
         public @PermissionLevel int getPermissionLevel() {
-            return mPermissionLevel;
+            synchronized (mMultiplexerLock) {
+                return mPermissionLevel;
+            }
         }
 
         public final boolean isForeground() {
-            return mForeground;
+            synchronized (mMultiplexerLock) {
+                return mForeground;
+            }
         }
 
         public final boolean isPermitted() {
-            return mPermitted;
+            synchronized (mMultiplexerLock) {
+                return mPermitted;
+            }
         }
 
         public final void flush(int requestCode) {
@@ -519,13 +498,14 @@
             return LocationProviderManager.this;
         }
 
-        @GuardedBy("mLock")
         final boolean onProviderPropertiesChanged() {
-            onHighPowerUsageChanged();
-            return false;
+            synchronized (mMultiplexerLock) {
+                onHighPowerUsageChanged();
+                return false;
+            }
         }
 
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         private void onHighPowerUsageChanged() {
             boolean isUsingHighPower = isUsingHighPower();
             if (isUsingHighPower != mIsUsingHighPower) {
@@ -541,12 +521,7 @@
             }
         }
 
-        @GuardedBy("mLock")
         private boolean isUsingHighPower() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
             ProviderProperties properties = getProperties();
             if (properties == null) {
                 return false;
@@ -557,30 +532,28 @@
                     && properties.getPowerUsage() == ProviderProperties.POWER_USAGE_HIGH;
         }
 
-        @GuardedBy("mLock")
         final boolean onLocationPermissionsChanged(@Nullable String packageName) {
-            if (packageName == null || getIdentity().getPackageName().equals(packageName)) {
-                return onLocationPermissionsChanged();
-            }
+            synchronized (mMultiplexerLock) {
+                if (packageName == null || getIdentity().getPackageName().equals(packageName)) {
+                    return onLocationPermissionsChanged();
+                }
 
-            return false;
+                return false;
+            }
         }
 
-        @GuardedBy("mLock")
         final boolean onLocationPermissionsChanged(int uid) {
-            if (getIdentity().getUid() == uid) {
-                return onLocationPermissionsChanged();
-            }
+            synchronized (mMultiplexerLock) {
+                if (getIdentity().getUid() == uid) {
+                    return onLocationPermissionsChanged();
+                }
 
-            return false;
+                return false;
+            }
         }
 
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         private boolean onLocationPermissionsChanged() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
             boolean permitted = mLocationPermissionsHelper.hasLocationPermissions(mPermissionLevel,
                     getIdentity());
             if (permitted != mPermitted) {
@@ -603,82 +576,73 @@
             return false;
         }
 
-        @GuardedBy("mLock")
         final boolean onAdasGnssLocationEnabledChanged(int userId) {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
-            if (getIdentity().getUserId() == userId) {
-                return onProviderLocationRequestChanged();
-            }
-
-            return false;
-        }
-
-        @GuardedBy("mLock")
-        final boolean onForegroundChanged(int uid, boolean foreground) {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
-            if (getIdentity().getUid() == uid && foreground != mForeground) {
-                if (D) {
-                    Log.v(TAG, mName + " provider uid " + uid + " foreground = " + foreground);
+            synchronized (mMultiplexerLock) {
+                if (getIdentity().getUserId() == userId) {
+                    return onProviderLocationRequestChanged();
                 }
 
-                mForeground = foreground;
-
-                if (mForeground) {
-                    EVENT_LOG.logProviderClientForeground(mName, getIdentity());
-                } else {
-                    EVENT_LOG.logProviderClientBackground(mName, getIdentity());
-                }
-
-                // note that onProviderLocationRequestChanged() is always called
-                return onProviderLocationRequestChanged()
-                        || mLocationPowerSaveModeHelper.getLocationPowerSaveMode()
-                        == LOCATION_MODE_FOREGROUND_ONLY;
-            }
-
-            return false;
-        }
-
-        @GuardedBy("mLock")
-        final boolean onProviderLocationRequestChanged() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
-            LocationRequest newRequest = calculateProviderLocationRequest();
-            if (mProviderLocationRequest.equals(newRequest)) {
                 return false;
             }
-
-            LocationRequest oldRequest = mProviderLocationRequest;
-            mProviderLocationRequest = newRequest;
-            onHighPowerUsageChanged();
-            updateService();
-
-            // if bypass state has changed then the active state may have changed
-            return oldRequest.isBypass() != newRequest.isBypass();
         }
 
+        final boolean onForegroundChanged(int uid, boolean foreground) {
+            synchronized (mMultiplexerLock) {
+                if (getIdentity().getUid() == uid && foreground != mForeground) {
+                    if (D) {
+                        Log.v(TAG, mName + " provider uid " + uid + " foreground = " + foreground);
+                    }
+
+                    mForeground = foreground;
+
+                    if (mForeground) {
+                        EVENT_LOG.logProviderClientForeground(mName, getIdentity());
+                    } else {
+                        EVENT_LOG.logProviderClientBackground(mName, getIdentity());
+                    }
+
+                    // note that onProviderLocationRequestChanged() is always called
+                    return onProviderLocationRequestChanged()
+                            || mLocationPowerSaveModeHelper.getLocationPowerSaveMode()
+                            == LOCATION_MODE_FOREGROUND_ONLY;
+                }
+
+                return false;
+            }
+        }
+
+        final boolean onProviderLocationRequestChanged() {
+            synchronized (mMultiplexerLock) {
+                LocationRequest newRequest = calculateProviderLocationRequest();
+                if (mProviderLocationRequest.equals(newRequest)) {
+                    return false;
+                }
+
+                LocationRequest oldRequest = mProviderLocationRequest;
+                mProviderLocationRequest = newRequest;
+                onHighPowerUsageChanged();
+                updateService();
+
+                // if bypass state has changed then the active state may have changed
+                return oldRequest.isBypass() != newRequest.isBypass();
+            }
+        }
+
+        @GuardedBy("mMultiplexerLock")
         private LocationRequest calculateProviderLocationRequest() {
-            LocationRequest baseRequest = super.getRequest();
-            LocationRequest.Builder builder = new LocationRequest.Builder(baseRequest);
+            LocationRequest.Builder builder = new LocationRequest.Builder(mBaseRequest);
 
             if (mPermissionLevel < PERMISSION_FINE) {
                 builder.setQuality(LocationRequest.QUALITY_LOW_POWER);
-                if (baseRequest.getIntervalMillis() < MIN_COARSE_INTERVAL_MS) {
+                if (mBaseRequest.getIntervalMillis() < MIN_COARSE_INTERVAL_MS) {
                     builder.setIntervalMillis(MIN_COARSE_INTERVAL_MS);
                 }
-                if (baseRequest.getMinUpdateIntervalMillis() < MIN_COARSE_INTERVAL_MS) {
+                if (mBaseRequest.getMinUpdateIntervalMillis() < MIN_COARSE_INTERVAL_MS) {
                     builder.setMinUpdateIntervalMillis(MIN_COARSE_INTERVAL_MS);
                 }
             }
 
-            boolean locationSettingsIgnored = baseRequest.isLocationSettingsIgnored();
+            boolean locationSettingsIgnored = mBaseRequest.isLocationSettingsIgnored();
             if (locationSettingsIgnored) {
                 // if we are not currently allowed use location settings ignored, disable it
                 if (!mSettingsHelper.getIgnoreSettingsAllowlist().contains(
@@ -690,7 +654,7 @@
                 builder.setLocationSettingsIgnored(locationSettingsIgnored);
             }
 
-            boolean adasGnssBypass = baseRequest.isAdasGnssBypass();
+            boolean adasGnssBypass = mBaseRequest.isAdasGnssBypass();
             if (adasGnssBypass) {
                 // if we are not currently allowed use adas gnss bypass, disable it
                 if (!GPS_PROVIDER.equals(mName)) {
@@ -710,7 +674,7 @@
             if (!locationSettingsIgnored && !isThrottlingExempt()) {
                 // throttle in the background
                 if (!mForeground) {
-                    builder.setIntervalMillis(max(baseRequest.getIntervalMillis(),
+                    builder.setIntervalMillis(max(mBaseRequest.getIntervalMillis(),
                             mSettingsHelper.getBackgroundThrottleIntervalMs()));
                 }
             }
@@ -727,8 +691,7 @@
             return mLocationManagerInternal.isProvider(null, getIdentity());
         }
 
-        @GuardedBy("mLock")
-        abstract @Nullable ListenerOperation<LocationTransport> acceptLocationChange(
+        @Nullable abstract ListenerOperation<LocationTransport> acceptLocationChange(
                 LocationResult fineLocationResult);
 
         @Override
@@ -769,13 +732,19 @@
         final ExternalWakeLockReleaser mWakeLockReleaser;
 
         private volatile ProviderTransport mProviderTransport;
+
+        @GuardedBy("mMultiplexerLock")
         private int mNumLocationsDelivered = 0;
+        @GuardedBy("mMultiplexerLock")
         private long mExpirationRealtimeMs = Long.MAX_VALUE;
 
         protected <TTransport extends LocationTransport & ProviderTransport> LocationRegistration(
-                LocationRequest request, CallerIdentity identity, TTransport transport,
+                LocationRequest request,
+                CallerIdentity identity,
+                Executor executor,
+                TTransport transport,
                 @PermissionLevel int permissionLevel) {
-            super(request, identity, transport, permissionLevel);
+            super(request, identity, executor, transport, permissionLevel);
             mProviderTransport = transport;
             mWakeLock = Objects.requireNonNull(mContext.getSystemService(PowerManager.class))
                     .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
@@ -789,9 +758,13 @@
             mProviderTransport = null;
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onProviderListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             long registerTimeMs = SystemClock.elapsedRealtime();
             mExpirationRealtimeMs = getRequest().getExpirationRealtimeMs(registerTimeMs);
 
@@ -810,8 +783,6 @@
             // start listening for provider enabled/disabled events
             addEnabledListener(this);
 
-            onLocationListenerRegister();
-
             // if the provider is currently disabled, let the client know immediately
             int userId = getIdentity().getUserId();
             if (!isEnabled(userId)) {
@@ -819,9 +790,11 @@
             }
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onProviderListenerUnregister() {
+        protected void onUnregister() {
             // stop listening for provider enabled/disabled events
             removeEnabledListener(this);
 
@@ -830,24 +803,16 @@
                 mAlarmHelper.cancel(this);
             }
 
-            onLocationListenerUnregister();
+            super.onUnregister();
         }
 
-        /**
-         * Subclasses may override this instead of {@link #onRemovableListenerRegister()}.
-         */
-        @GuardedBy("mLock")
-        protected void onLocationListenerRegister() {}
-
-        /**
-         * Subclasses may override this instead of {@link #onRemovableListenerUnregister()}.
-         */
-        @GuardedBy("mLock")
-        protected void onLocationListenerUnregister() {}
-
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onProviderListenerActive() {
+        protected void onActive() {
+            super.onActive();
+
             // a new registration may not get a location immediately, the provider request may be
             // delayed. therefore we deliver a historical location if available. since delivering an
             // older location could be considered a breaking change for some applications, we only
@@ -883,21 +848,17 @@
                         + " expired at " + TimeUtils.formatRealtime(mExpirationRealtimeMs));
             }
 
-            synchronized (mLock) {
+            synchronized (mMultiplexerLock) {
                 // no need to remove alarm after it's fired
                 mExpirationRealtimeMs = Long.MAX_VALUE;
                 remove();
             }
         }
 
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         @Override
         @Nullable ListenerOperation<LocationTransport> acceptLocationChange(
                 LocationResult fineLocationResult) {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
             // check expiration time - alarm is not guaranteed to go off at the right time,
             // especially for short intervals
             if (SystemClock.elapsedRealtime() >= mExpirationRealtimeMs) {
@@ -1017,9 +978,7 @@
                                         + " finished after " + mNumLocationsDelivered + " updates");
                             }
 
-                            synchronized (mLock) {
-                                remove();
-                            }
+                            remove();
                         }
                     }
                 }
@@ -1049,12 +1008,18 @@
 
         LocationListenerRegistration(LocationRequest request, CallerIdentity identity,
                 LocationListenerTransport transport, @PermissionLevel int permissionLevel) {
-            super(request, identity, transport, permissionLevel);
+            super(request, identity,
+                    identity.isMyProcess() ? FgThread.getExecutor() : DIRECT_EXECUTOR, transport,
+                    permissionLevel);
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onLocationListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             try {
                 ((IBinder) getKey()).linkToDeath(this, 0);
             } catch (RemoteException e) {
@@ -1062,10 +1027,22 @@
             }
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onLocationListenerUnregister() {
-            ((IBinder) getKey()).unlinkToDeath(this, 0);
+        protected void onUnregister() {
+            try {
+                ((IBinder) getKey()).unlinkToDeath(this, 0);
+            } catch (NoSuchElementException e) {
+                // the only way this exception can occur should be if another exception has been
+                // thrown prior to registration completing, and that exception is currently
+                // unwinding the call stack and causing this cleanup. since that exception should
+                // crash us anyways, drop this exception so we're not hiding the original exception.
+                Log.w(getTag(), "failed to unregister binder death listener", e);
+            }
+
+            super.onUnregister();
         }
 
         @Override
@@ -1083,9 +1060,7 @@
         private void onTransportFailure(Exception e) {
             if (e instanceof RemoteException) {
                 Log.w(TAG, mName + " provider registration " + getIdentity() + " removed", e);
-                synchronized (mLock) {
-                    remove();
-                }
+                remove();
             } else {
                 throw new AssertionError(e);
             }
@@ -1098,9 +1073,7 @@
                     Log.d(TAG, mName + " provider registration " + getIdentity() + " died");
                 }
 
-                synchronized (mLock) {
-                    remove();
-                }
+                remove();
             } catch (RuntimeException e) {
                 // the caller may swallow runtime exceptions, so we rethrow as assertion errors to
                 // ensure the crash is seen
@@ -1115,21 +1088,27 @@
         LocationPendingIntentRegistration(LocationRequest request,
                 CallerIdentity identity, LocationPendingIntentTransport transport,
                 @PermissionLevel int permissionLevel) {
-            super(request, identity, transport, permissionLevel);
+            super(request, identity, DIRECT_EXECUTOR, transport, permissionLevel);
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onLocationListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
             if (!((PendingIntent) getKey()).addCancelListener(DIRECT_EXECUTOR, this)) {
                 remove();
             }
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onLocationListenerUnregister() {
+        protected void onUnregister() {
             ((PendingIntent) getKey()).removeCancelListener(this);
+            super.onUnregister();
         }
 
         @Override
@@ -1147,9 +1126,7 @@
         private void onTransportFailure(Exception e) {
             if (e instanceof PendingIntent.CanceledException) {
                 Log.w(TAG, mName + " provider registration " + getIdentity() + " removed", e);
-                synchronized (mLock) {
-                    remove();
-                }
+                remove();
             } else {
                 throw new AssertionError(e);
             }
@@ -1161,25 +1138,32 @@
                 Log.d(TAG, mName + " provider registration " + getIdentity() + " canceled");
             }
 
-            synchronized (mLock) {
-                remove();
-            }
+            remove();
         }
     }
 
     protected final class GetCurrentLocationListenerRegistration extends Registration implements
             IBinder.DeathRecipient, OnAlarmListener {
 
+        @GuardedBy("mMultiplexerLock")
         private long mExpirationRealtimeMs = Long.MAX_VALUE;
 
         GetCurrentLocationListenerRegistration(LocationRequest request,
                 CallerIdentity identity, LocationTransport transport, int permissionLevel) {
-            super(request, identity, transport, permissionLevel);
+            super(request,
+                    identity,
+                    identity.isMyProcess() ? FgThread.getExecutor() : DIRECT_EXECUTOR,
+                    transport,
+                    permissionLevel);
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onProviderListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             try {
                 ((IBinder) getKey()).linkToDeath(this, 0);
             } catch (RemoteException e) {
@@ -1202,20 +1186,36 @@
             }
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onProviderListenerUnregister() {
+        protected void onUnregister() {
             // remove alarm for expiration
             if (mExpirationRealtimeMs < Long.MAX_VALUE) {
                 mAlarmHelper.cancel(this);
             }
 
-            ((IBinder) getKey()).unlinkToDeath(this, 0);
+            try {
+                ((IBinder) getKey()).unlinkToDeath(this, 0);
+            } catch (NoSuchElementException e) {
+                // the only way this exception can occur should be if another exception has been
+                // thrown prior to registration completing, and that exception is currently
+                // unwinding the call stack and causing this cleanup. since that exception should
+                // crash us anyways, drop this exception so we're not hiding the original exception.
+                Log.w(getTag(), "failed to unregister binder death listener", e);
+            }
+
+            super.onUnregister();
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onProviderListenerActive() {
+        protected void onActive() {
+            super.onActive();
+
             Location lastLocation = getLastLocationUnsafe(
                     getIdentity().getUserId(),
                     getPermissionLevel(),
@@ -1226,17 +1226,19 @@
             }
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onProviderListenerInactive() {
+        protected void onInactive() {
             // if we go inactive for any reason, fail immediately
             executeOperation(acceptLocationChange(null));
+            super.onInactive();
         }
 
+        @GuardedBy("mMultiplexerLock")
         void deliverNull() {
-            synchronized (mLock) {
-                executeOperation(acceptLocationChange(null));
-            }
+            executeOperation(acceptLocationChange(null));
         }
 
         @Override
@@ -1246,21 +1248,17 @@
                         + " expired at " + TimeUtils.formatRealtime(mExpirationRealtimeMs));
             }
 
-            synchronized (mLock) {
+            synchronized (mMultiplexerLock) {
                 // no need to remove alarm after it's fired
                 mExpirationRealtimeMs = Long.MAX_VALUE;
                 executeOperation(acceptLocationChange(null));
             }
         }
 
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         @Override
         @Nullable ListenerOperation<LocationTransport> acceptLocationChange(
                 @Nullable LocationResult fineLocationResult) {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
             // check expiration time - alarm is not guaranteed to go off at the right time,
             // especially for short intervals
             if (SystemClock.elapsedRealtime() >= mExpirationRealtimeMs) {
@@ -1311,9 +1309,7 @@
                     // on failure we're automatically removed anyways, no need to attempt removal
                     // again
                     if (success) {
-                        synchronized (mLock) {
-                            remove();
-                        }
+                        remove();
                     }
                 }
             };
@@ -1324,9 +1320,7 @@
                 Exception e) {
             if (e instanceof RemoteException) {
                 Log.w(TAG, mName + " provider registration " + getIdentity() + " removed", e);
-                synchronized (mLock) {
-                    remove();
-                }
+                remove();
             } else {
                 throw new AssertionError(e);
             }
@@ -1339,9 +1333,7 @@
                     Log.d(TAG, mName + " provider registration " + getIdentity() + " died");
                 }
 
-                synchronized (mLock) {
-                    remove();
-                }
+                remove();
             } catch (RuntimeException e) {
                 // the caller may swallow runtime exceptions, so we rethrow as assertion errors to
                 // ensure the crash is seen
@@ -1350,23 +1342,21 @@
         }
     }
 
-    protected final Object mLock = new Object();
-
     protected final String mName;
-    private final @Nullable PassiveLocationProviderManager mPassiveManager;
+    @Nullable private final PassiveLocationProviderManager mPassiveManager;
 
     protected final Context mContext;
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private @State int mState;
 
     // maps of user id to value
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private final SparseBooleanArray mEnabled; // null or not present means unknown
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private final SparseArray<LastLocation> mLastLocations;
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private final ArrayList<ProviderEnabledListener> mEnabledListeners;
 
     private final CopyOnWriteArrayList<IProviderRequestListener> mProviderRequestListeners;
@@ -1418,14 +1408,14 @@
     private final ScreenInteractiveChangedListener mScreenInteractiveChangedListener =
             this::onScreenInteractiveChanged;
 
-    // acquiring mLock makes operations on mProvider atomic, but is otherwise unnecessary
+    // acquiring mMultiplexerLock makes operations on mProvider atomic, but is otherwise unnecessary
     protected final MockableLocationProvider mProvider;
 
-    @GuardedBy("mLock")
-    private @Nullable OnAlarmListener mDelayedRegister;
+    @GuardedBy("mMultiplexerLock")
+    @Nullable private OnAlarmListener mDelayedRegister;
 
-    @GuardedBy("mLock")
-    private @Nullable StateChangedListener mStateChangedListener;
+    @GuardedBy("mMultiplexerLock")
+    @Nullable private StateChangedListener mStateChangedListener;
 
     public LocationProviderManager(Context context, Injector injector,
             String name, @Nullable PassiveLocationProviderManager passiveManager) {
@@ -1453,19 +1443,14 @@
         mLocationUsageLogger = injector.getLocationUsageLogger();
         mLocationFudger = new LocationFudger(mSettingsHelper.getCoarseLocationAccuracyM());
 
-        mProvider = new MockableLocationProvider(mLock);
+        mProvider = new MockableLocationProvider(mMultiplexerLock);
 
         // set listener last, since this lets our reference escape
         mProvider.getController().setListener(this);
     }
 
-    @Override
-    public String getTag() {
-        return TAG;
-    }
-
     public void startManager(@Nullable StateChangedListener listener) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState == STATE_STOPPED);
             mState = STATE_STARTED;
             mStateChangedListener = listener;
@@ -1485,7 +1470,7 @@
     }
 
     public void stopManager() {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState == STATE_STARTED);
             mState = STATE_STOPPING;
 
@@ -1522,11 +1507,11 @@
         return mProvider.getState();
     }
 
-    public @Nullable CallerIdentity getProviderIdentity() {
+    @Nullable public CallerIdentity getProviderIdentity() {
         return mProvider.getState().identity;
     }
 
-    public @Nullable ProviderProperties getProperties() {
+    @Nullable public ProviderProperties getProperties() {
         return mProvider.getState().properties;
     }
 
@@ -1543,7 +1528,7 @@
 
         Preconditions.checkArgument(userId >= 0);
 
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             int index = mEnabled.indexOfKey(userId);
             if (index < 0) {
                 // this generally shouldn't occur, but might be possible due to race conditions
@@ -1558,14 +1543,14 @@
     }
 
     public void addEnabledListener(ProviderEnabledListener listener) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             mEnabledListeners.add(listener);
         }
     }
 
     public void removeEnabledListener(ProviderEnabledListener listener) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             mEnabledListeners.remove(listener);
         }
@@ -1582,7 +1567,7 @@
     }
 
     public void setRealProvider(@Nullable AbstractLocationProvider provider) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
 
             final long identity = Binder.clearCallingIdentity();
@@ -1595,7 +1580,7 @@
     }
 
     public void setMockProvider(@Nullable MockLocationProvider provider) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
 
             EVENT_LOG.logProviderMocked(mName, provider != null);
@@ -1622,7 +1607,7 @@
     }
 
     public void setMockProviderAllowed(boolean enabled) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             if (!mProvider.isMock()) {
                 throw new IllegalArgumentException(mName + " provider is not a test provider");
             }
@@ -1637,7 +1622,7 @@
     }
 
     public void setMockProviderLocation(Location location) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             if (!mProvider.isMock()) {
                 throw new IllegalArgumentException(mName + " provider is not a test provider");
             }
@@ -1659,7 +1644,7 @@
         }
     }
 
-    public @Nullable Location getLastLocation(LastLocationRequest request,
+    @Nullable public Location getLastLocation(LastLocationRequest request,
             CallerIdentity identity, @PermissionLevel int permissionLevel) {
         request = calculateLastLocationRequest(request, identity);
 
@@ -1732,7 +1717,7 @@
      * location, even if the permissionLevel is coarse. You are responsible for coarsening the
      * location if necessary.
      */
-    public @Nullable Location getLastLocationUnsafe(int userId,
+    @Nullable public Location getLastLocationUnsafe(int userId,
             @PermissionLevel int permissionLevel, boolean isBypass,
             long maximumAgeMs) {
         if (userId == UserHandle.USER_ALL) {
@@ -1756,7 +1741,7 @@
         Preconditions.checkArgument(userId >= 0);
 
         Location location;
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             LastLocation lastLocation = mLastLocations.get(userId);
             if (lastLocation == null) {
@@ -1778,7 +1763,7 @@
     }
 
     public void injectLastLocation(Location location, int userId) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             if (getLastLocationUnsafe(userId, PERMISSION_FINE, false, Long.MAX_VALUE) == null) {
                 setLastLocation(location, userId);
@@ -1800,7 +1785,7 @@
 
         Preconditions.checkArgument(userId >= 0);
 
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             LastLocation lastLocation = mLastLocations.get(userId);
             if (lastLocation == null) {
                 lastLocation = new LastLocation();
@@ -1814,7 +1799,7 @@
         }
     }
 
-    public @Nullable ICancellationSignal getCurrentLocation(LocationRequest request,
+    @Nullable public ICancellationSignal getCurrentLocation(LocationRequest request,
             CallerIdentity identity, int permissionLevel, ILocationCallback callback) {
         if (request.getDurationMillis() > MAX_GET_CURRENT_LOCATION_TIMEOUT_MS) {
             request = new LocationRequest.Builder(request)
@@ -1829,7 +1814,7 @@
                         new GetCurrentLocationTransport(callback),
                         permissionLevel);
 
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -1849,9 +1834,7 @@
                         () -> {
                             final long ident = Binder.clearCallingIdentity();
                             try {
-                                synchronized (mLock) {
-                                    removeRegistration(callback.asBinder(), registration);
-                                }
+                                removeRegistration(callback.asBinder(), registration);
                             } catch (RuntimeException e) {
                                 // since this is within a oneway binder transaction there is nowhere
                                 // for exceptions to go - move onto another thread to crash system
@@ -1885,7 +1868,7 @@
                 new LocationListenerTransport(listener),
                 permissionLevel);
 
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -1904,7 +1887,7 @@
                 new LocationPendingIntentTransport(mContext, pendingIntent),
                 permissionLevel);
 
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             final long identity = Binder.clearCallingIdentity();
             try {
@@ -1916,42 +1899,38 @@
     }
 
     public void flush(ILocationListener listener, int requestCode) {
-        synchronized (mLock) {
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                boolean flushed = updateRegistration(listener.asBinder(), registration -> {
-                    registration.flush(requestCode);
-                    return false;
-                });
-                if (!flushed) {
-                    throw new IllegalArgumentException("unregistered listener cannot be flushed");
-                }
-            } finally {
-                Binder.restoreCallingIdentity(identity);
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            boolean flushed = updateRegistration(listener.asBinder(), registration -> {
+                registration.flush(requestCode);
+                return false;
+            });
+            if (!flushed) {
+                throw new IllegalArgumentException("unregistered listener cannot be flushed");
             }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
         }
     }
 
     public void flush(PendingIntent pendingIntent, int requestCode) {
-        synchronized (mLock) {
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                boolean flushed = updateRegistration(pendingIntent, registration -> {
-                    registration.flush(requestCode);
-                    return false;
-                });
-                if (!flushed) {
-                    throw new IllegalArgumentException(
-                            "unregistered pending intent cannot be flushed");
-                }
-            } finally {
-                Binder.restoreCallingIdentity(identity);
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            boolean flushed = updateRegistration(pendingIntent, registration -> {
+                registration.flush(requestCode);
+                return false;
+            });
+            if (!flushed) {
+                throw new IllegalArgumentException(
+                        "unregistered pending intent cannot be flushed");
             }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
         }
     }
 
     public void unregisterLocationRequest(ILocationListener listener) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             final long identity = Binder.clearCallingIdentity();
             try {
@@ -1963,7 +1942,7 @@
     }
 
     public void unregisterLocationRequest(PendingIntent pendingIntent) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             final long identity = Binder.clearCallingIdentity();
             try {
@@ -1974,13 +1953,9 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected void onRegister() {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         mSettingsHelper.addOnBackgroundThrottleIntervalChangedListener(
                 mBackgroundThrottleIntervalChangedListener);
         mSettingsHelper.addOnBackgroundThrottlePackageWhitelistChangedListener(
@@ -1997,13 +1972,9 @@
         mScreenInteractiveHelper.addListener(mScreenInteractiveChangedListener);
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected void onUnregister() {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         mSettingsHelper.removeOnBackgroundThrottleIntervalChangedListener(
                 mBackgroundThrottleIntervalChangedListener);
         mSettingsHelper.removeOnBackgroundThrottlePackageWhitelistChangedListener(
@@ -2019,13 +1990,9 @@
         mScreenInteractiveHelper.removeListener(mScreenInteractiveChangedListener);
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected void onRegistrationAdded(Object key, Registration registration) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         mLocationUsageLogger.logLocationApiUsage(
                 LocationStatsEnums.USAGE_STARTED,
                 LocationStatsEnums.API_REQUEST_LOCATION_UPDATES,
@@ -2038,23 +2005,21 @@
                 null, registration.isForeground());
     }
 
-    @GuardedBy("mLock")
+    // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+    @SuppressWarnings("GuardedBy")
+    @GuardedBy("mMultiplexerLock")
     @Override
-    protected void onRegistrationReplaced(Object key, Registration oldRegistration,
-            Registration newRegistration) {
+    protected void onRegistrationReplaced(Object oldKey, Registration oldRegistration,
+            Object newKey, Registration newRegistration) {
         // by saving the last delivered location state we are able to potentially delay the
         // resulting provider request longer and save additional power
         newRegistration.setLastDeliveredLocation(oldRegistration.getLastDeliveredLocation());
-        super.onRegistrationReplaced(key, oldRegistration, newRegistration);
+        super.onRegistrationReplaced(oldKey, oldRegistration, newKey, newRegistration);
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected void onRegistrationRemoved(Object key, Registration registration) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         mLocationUsageLogger.logLocationApiUsage(
                 LocationStatsEnums.USAGE_ENDED,
                 LocationStatsEnums.API_REQUEST_LOCATION_UPDATES,
@@ -2067,21 +2032,17 @@
                 null, registration.isForeground());
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected boolean registerWithService(ProviderRequest request,
             Collection<Registration> registrations) {
         return reregisterWithService(ProviderRequest.EMPTY_REQUEST, request, registrations);
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected boolean reregisterWithService(ProviderRequest oldRequest,
             ProviderRequest newRequest, Collection<Registration> registrations) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         // calculate how long the new request should be delayed before sending it off to the
         // provider, under the assumption that once we send the request off, the provider will
         // immediately attempt to deliver a new location satisfying that request.
@@ -2117,7 +2078,7 @@
             mDelayedRegister = new OnAlarmListener() {
                 @Override
                 public void onAlarm() {
-                    synchronized (mLock) {
+                    synchronized (mMultiplexerLock) {
                         if (mDelayedRegister == this) {
                             mDelayedRegister = null;
                             setProviderRequest(newRequest);
@@ -2135,17 +2096,13 @@
         return true;
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected void unregisterWithService() {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         setProviderRequest(ProviderRequest.EMPTY_REQUEST);
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     void setProviderRequest(ProviderRequest request) {
         if (mDelayedRegister != null) {
             mAlarmHelper.cancel(mDelayedRegister);
@@ -2166,13 +2123,9 @@
         });
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected boolean isActive(Registration registration) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         if (!registration.isPermitted()) {
             return false;
         }
@@ -2236,13 +2189,9 @@
         return true;
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected ProviderRequest mergeRegistrations(Collection<Registration> registrations) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         long intervalMs = ProviderRequest.INTERVAL_DISABLED;
         int quality = LocationRequest.QUALITY_LOW_POWER;
         long maxUpdateDelayMs = Long.MAX_VALUE;
@@ -2307,7 +2256,7 @@
                 .build();
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     protected long calculateRequestDelayMillis(long newIntervalMs,
             Collection<Registration> registrations) {
         // calculate the minimum delay across all registrations, ensuring that it is not more than
@@ -2349,7 +2298,7 @@
     }
 
     private void onUserChanged(int userId, int change) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             if (mState == STATE_STOPPED) {
                 return;
             }
@@ -2372,15 +2321,13 @@
     private void onLocationUserSettingsChanged(int userId, LocationUserSettings oldSettings,
             LocationUserSettings newSettings) {
         if (oldSettings.isAdasGnssLocationEnabled() != newSettings.isAdasGnssLocationEnabled()) {
-            synchronized (mLock) {
-                updateRegistrations(
-                        registration -> registration.onAdasGnssLocationEnabledChanged(userId));
-            }
+            updateRegistrations(
+                    registration -> registration.onAdasGnssLocationEnabledChanged(userId));
         }
     }
 
     private void onLocationEnabledChanged(int userId) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             if (mState == STATE_STOPPED) {
                 return;
             }
@@ -2390,88 +2337,64 @@
     }
 
     private void onScreenInteractiveChanged(boolean screenInteractive) {
-        synchronized (mLock) {
-            switch (mLocationPowerSaveModeHelper.getLocationPowerSaveMode()) {
-                case LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF:
-                    if (!GPS_PROVIDER.equals(mName)) {
-                        break;
-                    }
-                    // fall through
-                case LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF:
-                    // fall through
-                case LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF:
-                    updateRegistrations(registration -> true);
+        switch (mLocationPowerSaveModeHelper.getLocationPowerSaveMode()) {
+            case LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF:
+                if (!GPS_PROVIDER.equals(mName)) {
                     break;
-                default:
-                    break;
-            }
+                }
+                // fall through
+            case LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF:
+                // fall through
+            case LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF:
+                updateRegistrations(registration -> true);
+                break;
+            default:
+                break;
         }
     }
 
     private void onBackgroundThrottlePackageWhitelistChanged() {
-        synchronized (mLock) {
-            updateRegistrations(Registration::onProviderLocationRequestChanged);
-        }
+        updateRegistrations(Registration::onProviderLocationRequestChanged);
     }
 
     private void onBackgroundThrottleIntervalChanged() {
-        synchronized (mLock) {
-            updateRegistrations(Registration::onProviderLocationRequestChanged);
-        }
+        updateRegistrations(Registration::onProviderLocationRequestChanged);
     }
 
     private void onLocationPowerSaveModeChanged(@LocationPowerSaveMode int locationPowerSaveMode) {
-        synchronized (mLock) {
-            // this is rare, just assume everything has changed to keep it simple
-            updateRegistrations(registration -> true);
-        }
+        // this is rare, just assume everything has changed to keep it simple
+        updateRegistrations(registration -> true);
     }
 
     private void onAppForegroundChanged(int uid, boolean foreground) {
-        synchronized (mLock) {
-            updateRegistrations(registration -> registration.onForegroundChanged(uid, foreground));
-        }
+        updateRegistrations(registration -> registration.onForegroundChanged(uid, foreground));
     }
 
     private void onAdasAllowlistChanged() {
-        synchronized (mLock) {
-            updateRegistrations(Registration::onProviderLocationRequestChanged);
-        }
+        updateRegistrations(Registration::onProviderLocationRequestChanged);
     }
 
     private void onIgnoreSettingsWhitelistChanged() {
-        synchronized (mLock) {
-            updateRegistrations(Registration::onProviderLocationRequestChanged);
-        }
+        updateRegistrations(Registration::onProviderLocationRequestChanged);
     }
 
     private void onLocationPackageBlacklistChanged(int userId) {
-        synchronized (mLock) {
-            updateRegistrations(registration -> registration.getIdentity().getUserId() == userId);
-        }
+        updateRegistrations(registration -> registration.getIdentity().getUserId() == userId);
     }
 
     private void onLocationPermissionsChanged(@Nullable String packageName) {
-        synchronized (mLock) {
-            updateRegistrations(
-                    registration -> registration.onLocationPermissionsChanged(packageName));
-        }
+        updateRegistrations(
+                registration -> registration.onLocationPermissionsChanged(packageName));
     }
 
     private void onLocationPermissionsChanged(int uid) {
-        synchronized (mLock) {
-            updateRegistrations(registration -> registration.onLocationPermissionsChanged(uid));
-        }
+        updateRegistrations(registration -> registration.onLocationPermissionsChanged(uid));
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     public void onStateChanged(
             AbstractLocationProvider.State oldState, AbstractLocationProvider.State newState) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         if (oldState.allowed != newState.allowed) {
             onEnabledChanged(UserHandle.USER_ALL);
         }
@@ -2487,13 +2410,9 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     public void onReportLocation(LocationResult locationResult) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         LocationResult filtered;
         if (mPassiveManager != null) {
             filtered = locationResult.filter(location -> {
@@ -2549,12 +2468,8 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private void onUserStarted(int userId) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         if (userId == UserHandle.USER_NULL) {
             return;
         }
@@ -2572,12 +2487,8 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private void onUserStopped(int userId) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         if (userId == UserHandle.USER_NULL) {
             return;
         }
@@ -2592,12 +2503,8 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private void onEnabledChanged(int userId) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         if (userId == UserHandle.USER_NULL) {
             // used during initialization - ignore since many lower level operations (checking
             // settings for instance) do not support the null user
@@ -2697,7 +2604,7 @@
     }
 
     public void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             ipw.print(mName);
             ipw.print(" provider");
             if (mProvider.isMock()) {
@@ -2738,10 +2645,10 @@
 
     private static class LastLocation {
 
-        private @Nullable Location mFineLocation;
-        private @Nullable Location mCoarseLocation;
-        private @Nullable Location mFineBypassLocation;
-        private @Nullable Location mCoarseBypassLocation;
+        @Nullable private Location mFineLocation;
+        @Nullable private Location mCoarseLocation;
+        @Nullable private Location mFineBypassLocation;
+        @Nullable private Location mCoarseBypassLocation;
 
         LastLocation() {}
 
@@ -2765,7 +2672,7 @@
             mCoarseLocation = null;
         }
 
-        public @Nullable Location get(@PermissionLevel int permissionLevel,
+        @Nullable public Location get(@PermissionLevel int permissionLevel,
                 boolean isBypass) {
             switch (permissionLevel) {
                 case PERMISSION_FINE:
@@ -2862,7 +2769,7 @@
         private static class GatedCallback implements Runnable {
 
             @GuardedBy("this")
-            private @Nullable Runnable mCallback;
+            @Nullable private Runnable mCallback;
 
             @GuardedBy("this")
             private boolean mGate;
diff --git a/services/core/java/com/android/server/location/provider/PassiveLocationProviderManager.java b/services/core/java/com/android/server/location/provider/PassiveLocationProviderManager.java
index b35af4f..0cb4f9e 100644
--- a/services/core/java/com/android/server/location/provider/PassiveLocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/PassiveLocationProviderManager.java
@@ -54,7 +54,7 @@
      * Reports a new location to passive location provider clients.
      */
     public void updateLocation(LocationResult locationResult) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             PassiveLocationProvider passive = (PassiveLocationProvider) mProvider.getProvider();
             Preconditions.checkState(passive != null);
 
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index c5f73625..8ab3a94 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -1587,7 +1587,7 @@
                 if (!savedCredential.isNone()) {
                     throw new IllegalStateException("Saved credential given, but user has no SP");
                 }
-                initializeSyntheticPasswordLocked(savedCredential, userId);
+                initializeSyntheticPasswordLocked(userId);
             } else if (savedCredential.isNone() && isProfileWithUnifiedLock(userId)) {
                 // get credential from keystore when profile has unified lock
                 try {
@@ -2513,35 +2513,21 @@
     }
 
     /**
-     * Creates the synthetic password (SP) for the given user and protects it with the user's LSKF.
+     * Creates the synthetic password (SP) for the given user and protects it with an empty LSKF.
      * This is called just once in the lifetime of the user: the first time a nonempty LSKF is set,
      * or when an escrow token is activated on a device with an empty LSKF.
-     *
-     * Maintains the SP invariants described in {@link SyntheticPasswordManager}.
      */
     @GuardedBy("mSpManager")
     @VisibleForTesting
-    SyntheticPassword initializeSyntheticPasswordLocked(LockscreenCredential credential,
-            int userId) {
+    SyntheticPassword initializeSyntheticPasswordLocked(int userId) {
         Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId);
         Preconditions.checkState(getCurrentLskfBasedProtectorId(userId) ==
                 SyntheticPasswordManager.NULL_PROTECTOR_ID,
                 "Cannot reinitialize SP");
 
         final SyntheticPassword sp = mSpManager.newSyntheticPassword(userId);
-        long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(), credential,
-                sp, userId);
-        if (!credential.isNone()) {
-            mSpManager.newSidForUser(getGateKeeperService(), sp, userId);
-            mSpManager.verifyChallenge(getGateKeeperService(), sp, 0L, userId);
-            setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
-            setKeystorePassword(sp.deriveKeyStorePassword(), userId);
-        } else {
-            clearUserKeyProtection(userId, null);
-            setKeystorePassword(null, userId);
-            gateKeeperClearSecureUserId(userId);
-        }
-        fixateNewestUserKeyAuth(userId);
+        final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(),
+                LockscreenCredential.createNone(), sp, userId);
         setCurrentLskfBasedProtectorId(protectorId, userId);
         onSyntheticPasswordKnown(userId, sp);
         return sp;
@@ -2818,8 +2804,7 @@
             if (!isUserSecure(userId)) {
                 long protectorId = getCurrentLskfBasedProtectorId(userId);
                 if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
-                    sp = initializeSyntheticPasswordLocked(LockscreenCredential.createNone(),
-                            userId);
+                    sp = initializeSyntheticPasswordLocked(userId);
                 } else {
                     sp = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
                             LockscreenCredential.createNone(), userId, null).syntheticPassword;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 1e86d02..b97aa5f 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -7833,7 +7833,8 @@
                 && (record.getSuppressedVisualEffects() & SUPPRESSED_EFFECT_STATUS_BAR) != 0;
         if (!record.isUpdate
                 && record.getImportance() > IMPORTANCE_MIN
-                && !suppressedByDnd) {
+                && !suppressedByDnd
+                && isNotificationForCurrentUser(record)) {
             sendAccessibilityEvent(record);
             sentAccessibilityEvent = true;
         }
diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java
index 8501c5e..5b3eff9 100644
--- a/services/core/java/com/android/server/pm/AppsFilterBase.java
+++ b/services/core/java/com/android/server/pm/AppsFilterBase.java
@@ -28,6 +28,7 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.Process;
+import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.text.TextUtils;
@@ -340,7 +341,9 @@
                 return !isForceQueryable(targetPkgSetting.getAppId())
                       && !isImplicitlyQueryable(callingAppId, targetPkgSetting.getAppId());
             }
-            if (mCacheReady) { // use cache
+            // use cache
+            if (mCacheReady && SystemProperties.getBoolean("debug.pm.use_app_filter_cache",
+                    true)) {
                 if (!shouldFilterApplicationUsingCache(callingUid,
                         targetPkgSetting.getAppId(),
                         userId)) {
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index d358e43..5731af6 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -21,6 +21,7 @@
 import android.annotation.UserIdInt;
 import android.content.Context;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.os.UserManager;
@@ -306,6 +307,12 @@
      */
     public abstract boolean shouldIgnorePrepareStorageErrors(int userId);
 
+    /**
+     * Returns the {@link UserProperties} of the given user, or {@code null} if it is not found.
+     * NB: The actual object is returned. So do NOT modify it!
+     */
+    public abstract @Nullable UserProperties getUserProperties(@UserIdInt int userId);
+
     /** TODO(b/239982558): add javadoc / mention invalid_id is used to unassing */
     public abstract void assignUserToDisplay(@UserIdInt int userId, int displayId);
 }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index f08bea3..025e973 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -52,6 +52,7 @@
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.UserInfo;
 import android.content.pm.UserInfo.UserInfoFlag;
+import android.content.pm.UserProperties;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -181,7 +182,7 @@
     private static final String TAG_NAME = "name";
     private static final String TAG_ACCOUNT = "account";
     private static final String ATTR_FLAGS = "flags";
-    private static final String ATTR_TYPE = "type";
+    private static final String ATTR_TYPE = "type"; // userType
     private static final String ATTR_ICON_PATH = "icon";
     private static final String ATTR_ID = "id";
     private static final String ATTR_CREATION_TIME = "created";
@@ -216,6 +217,7 @@
     private static final String TAG_ENTRY = "entry";
     private static final String TAG_VALUE = "value";
     private static final String TAG_SEED_ACCOUNT_OPTIONS = "seedAccountOptions";
+    private static final String TAG_USER_PROPERTIES = "userProperties";
     private static final String TAG_LAST_REQUEST_QUIET_MODE_ENABLED_CALL =
             "lastRequestQuietModeEnabledCall";
     private static final String TAG_IGNORE_PREPARE_STORAGE_ERRORS =
@@ -260,7 +262,7 @@
     @VisibleForTesting
     static final int MAX_RECENTLY_REMOVED_IDS_SIZE = 100;
 
-    private static final int USER_VERSION = 9;
+    private static final int USER_VERSION = 10;
 
     private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // ms
 
@@ -326,6 +328,9 @@
         // Whether to perist the seed account information to be available after a boot
         boolean persistSeedData;
 
+        /** Properties of the user whose default values originate from its user type. */
+        UserProperties userProperties;
+
         /** Elapsed realtime since boot when the user started. */
         long startRealtime;
 
@@ -1498,6 +1503,36 @@
         return userTypeDetails != null && userTypeDetails.isSystem();
     }
 
+    /**
+     * Returns a *copy* of the given user's UserProperties, stripping out any information for which
+     * the caller lacks permission.
+     */
+    @Override
+    public @NonNull UserProperties getUserPropertiesCopy(@UserIdInt int userId) {
+        checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "getUserProperties");
+        final UserProperties origProperties = getUserPropertiesInternal(userId);
+        if (origProperties != null) {
+            int callingUid = Binder.getCallingUid();
+            boolean exposeAllFields = callingUid == Process.SYSTEM_UID;
+            boolean hasManage = hasPermissionGranted(Manifest.permission.MANAGE_USERS, callingUid);
+            boolean hasQuery = hasPermissionGranted(Manifest.permission.QUERY_USERS, callingUid);
+            return new UserProperties(origProperties, exposeAllFields, hasManage, hasQuery);
+        }
+        // A non-existent or partial user will reach here.
+        throw new IllegalArgumentException("Cannot access properties for user " + userId);
+    }
+
+    /** Returns the user's actual, canonical UserProperties object. Do not edit it externally. */
+    private @Nullable UserProperties getUserPropertiesInternal(@UserIdInt int userId) {
+        synchronized (mUsersLock) {
+            final UserData userData = getUserDataLU(userId);
+            if (userData != null) {
+                return userData.userProperties;
+            }
+        }
+        return null;
+    }
+
     @Override
     public boolean hasBadge(@UserIdInt int userId) {
         checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "hasBadge");
@@ -1587,6 +1622,10 @@
 
     public boolean isProfile(@UserIdInt int userId) {
         checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "isProfile");
+        return isProfileUnchecked(userId);
+    }
+
+    private boolean isProfileUnchecked(@UserIdInt int userId) {
         synchronized (mUsersLock) {
             UserInfo userInfo = getUserInfoLU(userId);
             return userInfo != null && userInfo.isProfile();
@@ -3311,6 +3350,7 @@
     @VisibleForTesting
     void upgradeIfNecessaryLP(Bundle oldGlobalUserRestrictions, int userVersion,
             int userTypeVersion) {
+        Slog.i(LOG_TAG, "Upgrading users from userVersion " + userVersion + " to " + USER_VERSION);
         Set<Integer> userIdsToWrite = new ArraySet<>();
         final int originalVersion = mUserVersion;
         final int originalUserTypeVersion = mUserTypeVersion;
@@ -3446,6 +3486,27 @@
             userVersion = 9;
         }
 
+        if (userVersion < 10) {
+            // Add UserProperties.
+            synchronized (mUsersLock) {
+                for (int i = 0; i < mUsers.size(); i++) {
+                    final UserData userData = mUsers.valueAt(i);
+                    final UserTypeDetails userTypeDetails = mUserTypes.get(userData.info.userType);
+                    if (userTypeDetails == null) {
+                        throw new IllegalStateException(
+                                "Cannot upgrade user because " + userData.info.userType
+                                        + " isn't defined on this device!");
+                    }
+                    userData.userProperties = new UserProperties(
+                            userTypeDetails.getDefaultUserPropertiesReference());
+                    userIdsToWrite.add(userData.info.id);
+                }
+            }
+            userVersion = 10;
+        }
+
+        // Reminder: If you add another upgrade, make sure to increment USER_VERSION too.
+
         // Done with userVersion changes, moving on to deal with userTypeVersion upgrades
         // Upgrade from previous user type to a new user type
         final int newUserTypeVersion = UserTypeFactory.getUserTypeVersion();
@@ -3460,6 +3521,11 @@
             Slog.w(LOG_TAG, "User version " + mUserVersion + " didn't upgrade as expected to "
                     + USER_VERSION);
         } else {
+            if (userVersion > USER_VERSION) {
+                Slog.wtf(LOG_TAG, "Upgraded user version " + mUserVersion + " is higher the SDK's "
+                        + "one of " + USER_VERSION + ". Someone forgot to update USER_VERSION?");
+            }
+
             mUserVersion = userVersion;
             mUserTypeVersion = newUserTypeVersion;
 
@@ -3579,6 +3645,8 @@
         flags |= mUserTypes.get(systemUserType).getDefaultUserInfoFlags();
         UserInfo system = new UserInfo(UserHandle.USER_SYSTEM, null, null, flags, systemUserType);
         UserData userData = putUserInfo(system);
+        userData.userProperties = new UserProperties(
+                mUserTypes.get(userData.info.userType).getDefaultUserPropertiesReference());
         mNextSerialNumber = MIN_USER_ID;
         mUserVersion = USER_VERSION;
         mUserTypeVersion = UserTypeFactory.getUserTypeVersion();
@@ -3753,6 +3821,12 @@
             serializer.endTag(null, TAG_SEED_ACCOUNT_OPTIONS);
         }
 
+        if (userData.userProperties != null) {
+            serializer.startTag(null, TAG_USER_PROPERTIES);
+            userData.userProperties.writeToXml(serializer);
+            serializer.endTag(null, TAG_USER_PROPERTIES);
+        }
+
         if (userData.getLastRequestQuietModeEnabledMillis() != 0L) {
             serializer.startTag(/* namespace */ null, TAG_LAST_REQUEST_QUIET_MODE_ENABLED_CALL);
             serializer.text(String.valueOf(userData.getLastRequestQuietModeEnabledMillis()));
@@ -3872,6 +3946,7 @@
         String seedAccountName = null;
         String seedAccountType = null;
         PersistableBundle seedAccountOptions = null;
+        UserProperties userProperties = null;
         Bundle baseRestrictions = null;
         Bundle legacyLocalRestrictions = null;
         RestrictionsSet localRestrictions = null;
@@ -3950,6 +4025,17 @@
                 } else if (TAG_SEED_ACCOUNT_OPTIONS.equals(tag)) {
                     seedAccountOptions = PersistableBundle.restoreFromXml(parser);
                     persistSeedData = true;
+                } else if (TAG_USER_PROPERTIES.equals(tag)) {
+                    // We already got the userType above (if it exists), so we can use it.
+                    // And it must exist, since ATTR_TYPE historically predates PROPERTIES.
+                    final UserTypeDetails userTypeDetails = mUserTypes.get(userType);
+                    if (userTypeDetails == null) {
+                        Slog.e(LOG_TAG, "User has properties but no user type!");
+                        return null;
+                    }
+                    final UserProperties defaultProps
+                            = userTypeDetails.getDefaultUserPropertiesReference();
+                    userProperties = new UserProperties(parser, defaultProps);
                 } else if (TAG_LAST_REQUEST_QUIET_MODE_ENABLED_CALL.equals(tag)) {
                     type = parser.next();
                     if (type == XmlPullParser.TEXT) {
@@ -3986,6 +4072,7 @@
         userData.seedAccountType = seedAccountType;
         userData.persistSeedData = persistSeedData;
         userData.seedAccountOptions = seedAccountOptions;
+        userData.userProperties = userProperties;
         userData.setLastRequestQuietModeEnabledMillis(lastRequestQuietModeEnabledTimestamp);
         if (ignorePrepareStorageErrors) {
             userData.setIgnorePrepareStorageErrors();
@@ -4321,6 +4408,8 @@
                     }
                     userData = new UserData();
                     userData.info = userInfo;
+                    userData.userProperties = new UserProperties(
+                            userTypeDetails.getDefaultUserPropertiesReference());
                     mUsers.put(userId, userData);
                 }
                 writeUserLP(userData);
@@ -5647,6 +5736,7 @@
         }
         // If we got here, we probably recycled user ids, so invalidate any caches.
         UserManager.invalidateStaticUserProperties();
+        UserManager.invalidateUserPropertiesCache();
         if (nextId < 0) {
             throw new IllegalStateException("No user id available!");
         }
@@ -6408,6 +6498,10 @@
             }
         }
 
+        if (userData.userProperties != null) {
+            userData.userProperties.println(pw, "    ");
+        }
+
         pw.println("    Ignore errors preparing storage: "
                 + userData.getIgnorePrepareStorageErrors());
     }
@@ -6796,6 +6890,15 @@
         }
 
         @Override
+        public @Nullable UserProperties getUserProperties(@UserIdInt int userId) {
+            final UserProperties props = getUserPropertiesInternal(userId);
+            if (props == null) {
+                Slog.w(LOG_TAG, "A null UserProperties was returned for user " + userId);
+            }
+            return props;
+        }
+
+        @Override
         public void assignUserToDisplay(int userId, int displayId) {
             if (DBG) {
                 Slogf.d(LOG_TAG, "assignUserToDisplay(%d, %d): mUsersOnSecondaryDisplays=%s",
@@ -6827,9 +6930,6 @@
                 return;
             }
 
-            // TODO(b/239982558) check for invalid cases like:
-            // - userId already assigned to another display
-            // - displayId already assigned to another user
             synchronized (mUsersLock) {
                 if (mUsersOnSecondaryDisplays == null) {
                     if (DBG) {
@@ -6841,6 +6941,36 @@
                     Slogf.d(LOG_TAG, "Adding %d->%d to mUsersOnSecondaryDisplays",
                             userId, displayId);
                 }
+
+                if (isProfileUnchecked(userId)) {
+                    // Profile can only start in the same display as parent
+                    int parentUserId = getProfileParentId(userId);
+                    int parentDisplayId = mUsersOnSecondaryDisplays.get(parentUserId);
+                    if (displayId != parentDisplayId) {
+                        throw new IllegalStateException("Cannot assign profile " + userId + " to "
+                                + "display " + displayId + " as its parent (user " + parentUserId
+                                + ") is assigned to display " + parentDisplayId);
+                    }
+                } else {
+                    // Check if display is available
+                    for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
+                        // Make sure display is not used by other users...
+                        // TODO(b/240736142); currently, if a user was started in a display, it
+                        // would need to be stopped first, so "switching" a user on secondary
+                        // diplay requires 2 non-atomic operations (stop and start). Once this logic
+                        // is refactored, it should be atomic.
+                        if (mUsersOnSecondaryDisplays.valueAt(i) == displayId) {
+                            throw new IllegalStateException("Cannot assign " + userId + " to "
+                                    + "display " + displayId + " as it's  already assigned to "
+                                    + "user " + mUsersOnSecondaryDisplays.keyAt(i));
+                        }
+                        // TODO(b/239982558) also check that user is not already assigned to other
+                        // display (including 0). That would be harder to tested under CTS though
+                        // (for example, would need to add a new AM method to start user in bg on
+                        // main display), so it's better to test on unit tests
+                    }
+                }
+
                 mUsersOnSecondaryDisplays.put(userId, displayId);
             }
         }
diff --git a/services/core/java/com/android/server/pm/UserTypeDetails.java b/services/core/java/com/android/server/pm/UserTypeDetails.java
index 2f3ca66..ebb9f98 100644
--- a/services/core/java/com/android/server/pm/UserTypeDetails.java
+++ b/services/core/java/com/android/server/pm/UserTypeDetails.java
@@ -23,6 +23,7 @@
 import android.annotation.StringRes;
 import android.content.pm.UserInfo;
 import android.content.pm.UserInfo.UserInfoFlag;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.UserManager;
@@ -171,6 +172,12 @@
     private final @CrossProfileIntentFilter.AccessControlLevel int
             mCrossProfileIntentFilterAccessControl;
 
+    /**
+     * The default {@link UserProperties} for the user type.
+     * <p> The uninitialized value of each property is implied by {@link UserProperties.Builder}.
+     */
+    private final @NonNull UserProperties mDefaultUserProperties;
+
     private UserTypeDetails(@NonNull String name, boolean enabled, int maxAllowed,
             @UserInfoFlag int baseType, @UserInfoFlag int defaultUserInfoPropertyFlags, int label,
             int maxAllowedPerParent,
@@ -183,7 +190,8 @@
             @Nullable List<DefaultCrossProfileIntentFilter> defaultCrossProfileIntentFilters,
             boolean isMediaSharedWithParent,
             boolean isCredentialSharableWithParent,
-            @CrossProfileIntentFilter.AccessControlLevel int accessControlLevel) {
+            @CrossProfileIntentFilter.AccessControlLevel int accessControlLevel,
+            @NonNull UserProperties defaultUserProperties) {
         this.mName = name;
         this.mEnabled = enabled;
         this.mMaxAllowed = maxAllowed;
@@ -205,6 +213,7 @@
         this.mIsMediaSharedWithParent = isMediaSharedWithParent;
         this.mIsCredentialSharableWithParent = isCredentialSharableWithParent;
         this.mCrossProfileIntentFilterAccessControl = accessControlLevel;
+        this.mDefaultUserProperties = defaultUserProperties;
     }
 
     /**
@@ -310,18 +319,6 @@
         return mDarkThemeBadgeColors[Math.min(badgeIndex, mDarkThemeBadgeColors.length - 1)];
     }
 
-    public boolean isProfile() {
-        return (mBaseType & UserInfo.FLAG_PROFILE) != 0;
-    }
-
-    public boolean isFull() {
-        return (mBaseType & UserInfo.FLAG_FULL) != 0;
-    }
-
-    public boolean isSystem() {
-        return (mBaseType & UserInfo.FLAG_SYSTEM) != 0;
-    }
-
     /**
      * Returns true if the user has shared media with parent user or false otherwise.
      */
@@ -347,6 +344,26 @@
         return mCrossProfileIntentFilterAccessControl;
     }
 
+    /**
+     * Returns the reference to the default {@link UserProperties} for this type of user.
+     * This is not a copy. Do NOT modify this object.
+     */
+    public @NonNull UserProperties getDefaultUserPropertiesReference() {
+        return mDefaultUserProperties;
+    }
+
+    public boolean isProfile() {
+        return (mBaseType & UserInfo.FLAG_PROFILE) != 0;
+    }
+
+    public boolean isFull() {
+        return (mBaseType & UserInfo.FLAG_FULL) != 0;
+    }
+
+    public boolean isSystem() {
+        return (mBaseType & UserInfo.FLAG_SYSTEM) != 0;
+    }
+
     /** Returns a {@link Bundle} representing the default user restrictions. */
     @NonNull Bundle getDefaultRestrictions() {
         return BundleUtils.clone(mDefaultRestrictions);
@@ -384,6 +401,7 @@
         pw.print(prefix); pw.print("mDefaultUserInfoFlags: ");
         pw.println(UserInfo.flagsToString(mDefaultUserInfoPropertyFlags));
         pw.print(prefix); pw.print("mLabel: "); pw.println(mLabel);
+        mDefaultUserProperties.println(pw, prefix);
 
         final String restrictionsPrefix = prefix + "    ";
         if (isSystem()) {
@@ -442,6 +460,9 @@
         private boolean mIsCredentialSharableWithParent = false;
         private @CrossProfileIntentFilter.AccessControlLevel int
                 mCrossProfileIntentFilterAccessControl = CrossProfileIntentFilter.ACCESS_LEVEL_ALL;
+        // Default UserProperties cannot be null but for efficiency we don't initialize it now.
+        // If it isn't set explicitly, {@link UserProperties.Builder#build()} will be used.
+        private @Nullable UserProperties mDefaultUserProperties = null;
 
         public Builder setName(String name) {
             mName = name;
@@ -560,6 +581,23 @@
             return this;
         }
 
+        /**
+         * Sets (replacing if necessary) the default UserProperties object for this user type.
+         * Takes a builder, rather than a built object, to efficiently ensure that a fresh copy of
+         * properties is stored (since it later might be modified by UserProperties#updateFromXml).
+         */
+        public Builder setDefaultUserProperties(UserProperties.Builder userPropertiesBuilder) {
+            mDefaultUserProperties = userPropertiesBuilder.build();
+            return this;
+        }
+
+        public @NonNull UserProperties getDefaultUserProperties() {
+            if (mDefaultUserProperties == null) {
+                mDefaultUserProperties = new UserProperties.Builder().build();
+            }
+            return mDefaultUserProperties;
+        }
+
         @UserInfoFlag int getBaseType() {
             return mBaseType;
         }
@@ -604,7 +642,8 @@
                     mDefaultCrossProfileIntentFilters,
                     mIsMediaSharedWithParent,
                     mIsCredentialSharableWithParent,
-                    mCrossProfileIntentFilterAccessControl);
+                    mCrossProfileIntentFilterAccessControl,
+                    getDefaultUserProperties());
         }
 
         private boolean hasBadge() {
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 857a975..b98d20e 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -37,6 +37,7 @@
 import static com.android.server.pm.UserTypeDetails.UNLIMITED_NUMBER_OF_USERS;
 
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.os.Build;
@@ -124,7 +125,10 @@
                 .setIsMediaSharedWithParent(true)
                 .setCrossProfileIntentFilterAccessControl(
                         CrossProfileIntentFilter.ACCESS_LEVEL_SYSTEM)
-                .setIsCredentialSharableWithParent(true);
+                .setIsCredentialSharableWithParent(true)
+                .setDefaultUserProperties(new UserProperties.Builder()
+                        .setStartWithParent(true)
+                        .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT));
     }
 
     /**
@@ -156,7 +160,10 @@
                 .setDefaultRestrictions(getDefaultManagedProfileRestrictions())
                 .setDefaultSecureSettings(getDefaultManagedProfileSecureSettings())
                 .setDefaultCrossProfileIntentFilters(getDefaultManagedCrossProfileIntentFilter())
-                .setIsCredentialSharableWithParent(true);
+                .setIsCredentialSharableWithParent(true)
+                .setDefaultUserProperties(new UserProperties.Builder()
+                        .setStartWithParent(true)
+                        .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE));
     }
 
     /**
@@ -396,6 +403,9 @@
                         setResAttributeArray(parser, builder::setBadgeColors);
                     } else if (isProfile && "badge-colors-dark".equals(childName)) {
                         setResAttributeArray(parser, builder::setDarkThemeBadgeColors);
+                    } else if ("user-properties".equals(childName)) {
+                        builder.getDefaultUserProperties()
+                                .updateFromXml(XmlUtils.makeTyped(parser));
                     } else {
                         Slog.w(LOG_TAG, "Unrecognized tag " + childName + " in "
                                 + parser.getPositionDescription());
diff --git a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
index fe4aa53..df902c2 100644
--- a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
+++ b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
@@ -657,7 +657,7 @@
 
         // Now that we have finally received all the data, we can tell mStats about it.
         synchronized (mStats) {
-            mStats.addHistoryEventLocked(
+            mStats.recordHistoryEventLocked(
                     elapsedRealtime,
                     uptime,
                     BatteryStats.HistoryItem.EVENT_COLLECT_EXTERNAL_STATS,
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 202beb3..37643c3 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -108,6 +108,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.BatteryStatsHistory;
+import com.android.internal.os.BatteryStatsHistory.HistoryStepDetailsCalculator;
 import com.android.internal.os.BatteryStatsHistoryIterator;
 import com.android.internal.os.BinderCallsStats;
 import com.android.internal.os.BinderTransactionNameResolver;
@@ -173,7 +174,6 @@
     private static final boolean DEBUG_ENERGY_CPU = DEBUG_ENERGY;
     private static final boolean DEBUG_BINDER_STATS = false;
     private static final boolean DEBUG_MEMORY = false;
-    private static final boolean DEBUG_HISTORY = false;
 
     // TODO: remove "tcp" from network methods, since we measure total stats.
 
@@ -322,6 +322,11 @@
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     protected Queue<UidToRemove> mPendingRemovedUids = new LinkedList<>();
 
+    @NonNull
+    BatteryStatsHistory copyHistory() {
+        return mHistory.copy();
+    }
+
     @VisibleForTesting
     public final class UidToRemove {
         private final int mStartUid;
@@ -413,7 +418,7 @@
                 if (changed) {
                     final long uptimeMs = mClock.uptimeMillis();
                     final long elapsedRealtimeMs = mClock.elapsedRealtime();
-                    addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+                    mHistory.writeHistoryItem(elapsedRealtimeMs, uptimeMs);
                 }
             }
         }
@@ -668,16 +673,16 @@
     /**
      * Mapping isolated uids to the actual owning app uid.
      */
-    final SparseIntArray mIsolatedUids = new SparseIntArray();
+    private final SparseIntArray mIsolatedUids = new SparseIntArray();
     /**
      * Internal reference count of isolated uids.
      */
-    final SparseIntArray mIsolatedUidRefCounts = new SparseIntArray();
+    private final SparseIntArray mIsolatedUidRefCounts = new SparseIntArray();
 
     /**
      * The statistics we have collected organized by uids.
      */
-    final SparseArray<BatteryStatsImpl.Uid> mUidStats = new SparseArray<>();
+    private final SparseArray<BatteryStatsImpl.Uid> mUidStats = new SparseArray<>();
 
     // A set of pools of currently active timers.  When a timer is queried, we will divide the
     // elapsed time by the number of active timers to arrive at that timer's share of the time.
@@ -685,20 +690,21 @@
     // changes.
     @VisibleForTesting
     protected ArrayList<StopwatchTimer> mPartialTimers = new ArrayList<>();
-    final ArrayList<StopwatchTimer> mFullTimers = new ArrayList<>();
-    final ArrayList<StopwatchTimer> mWindowTimers = new ArrayList<>();
-    final ArrayList<StopwatchTimer> mDrawTimers = new ArrayList<>();
-    final SparseArray<ArrayList<StopwatchTimer>> mSensorTimers = new SparseArray<>();
-    final ArrayList<StopwatchTimer> mWifiRunningTimers = new ArrayList<>();
-    final ArrayList<StopwatchTimer> mFullWifiLockTimers = new ArrayList<>();
-    final ArrayList<StopwatchTimer> mWifiMulticastTimers = new ArrayList<>();
-    final ArrayList<StopwatchTimer> mWifiScanTimers = new ArrayList<>();
-    final SparseArray<ArrayList<StopwatchTimer>> mWifiBatchedScanTimers = new SparseArray<>();
-    final ArrayList<StopwatchTimer> mAudioTurnedOnTimers = new ArrayList<>();
-    final ArrayList<StopwatchTimer> mVideoTurnedOnTimers = new ArrayList<>();
-    final ArrayList<StopwatchTimer> mFlashlightTurnedOnTimers = new ArrayList<>();
-    final ArrayList<StopwatchTimer> mCameraTurnedOnTimers = new ArrayList<>();
-    final ArrayList<StopwatchTimer> mBluetoothScanOnTimers = new ArrayList<>();
+    private final ArrayList<StopwatchTimer> mFullTimers = new ArrayList<>();
+    private final ArrayList<StopwatchTimer> mWindowTimers = new ArrayList<>();
+    private final ArrayList<StopwatchTimer> mDrawTimers = new ArrayList<>();
+    private final SparseArray<ArrayList<StopwatchTimer>> mSensorTimers = new SparseArray<>();
+    private final ArrayList<StopwatchTimer> mWifiRunningTimers = new ArrayList<>();
+    private final ArrayList<StopwatchTimer> mFullWifiLockTimers = new ArrayList<>();
+    private final ArrayList<StopwatchTimer> mWifiMulticastTimers = new ArrayList<>();
+    private final ArrayList<StopwatchTimer> mWifiScanTimers = new ArrayList<>();
+    private final SparseArray<ArrayList<StopwatchTimer>> mWifiBatchedScanTimers =
+            new SparseArray<>();
+    private final ArrayList<StopwatchTimer> mAudioTurnedOnTimers = new ArrayList<>();
+    private final ArrayList<StopwatchTimer> mVideoTurnedOnTimers = new ArrayList<>();
+    private final ArrayList<StopwatchTimer> mFlashlightTurnedOnTimers = new ArrayList<>();
+    private final ArrayList<StopwatchTimer> mCameraTurnedOnTimers = new ArrayList<>();
+    private final ArrayList<StopwatchTimer> mBluetoothScanOnTimers = new ArrayList<>();
 
     // Last partial timers we use for distributing CPU usage.
     @VisibleForTesting
@@ -713,69 +719,24 @@
     protected final TimeBase mOnBatteryScreenOffTimeBase = new TimeBase(true);
 
     private boolean mSystemReady;
-    boolean mShuttingDown;
+    private boolean mShuttingDown;
 
-    final HistoryEventTracker mActiveEvents = new HistoryEventTracker();
+    private final HistoryEventTracker mActiveEvents = new HistoryEventTracker();
+    private final HistoryStepDetailsCalculatorImpl mStepDetailsCalculator =
+            new HistoryStepDetailsCalculatorImpl();
 
-    long mHistoryBaseTimeMs;
-    protected boolean mHaveBatteryLevel = false;
-    protected boolean mRecordingHistory = false;
-    int mNumHistoryItems;
-
-    private static final int HISTORY_TAG_INDEX_LIMIT = 0x7ffe;
-    private static final int MAX_HISTORY_TAG_STRING_LENGTH = 1024;
-
-    final HashMap<HistoryTag, Integer> mHistoryTagPool = new HashMap<>();
-    private SparseArray<HistoryTag> mHistoryTags;
-    final Parcel mHistoryBuffer = Parcel.obtain();
-    final HistoryItem mHistoryLastWritten = new HistoryItem();
-    final HistoryItem mHistoryLastLastWritten = new HistoryItem();
-    final HistoryItem mHistoryAddTmp = new HistoryItem();
-    int mNextHistoryTagIdx = 0;
-    int mNumHistoryTagChars = 0;
-    int mHistoryBufferLastPos = -1;
-    int mActiveHistoryStates = 0xffffffff;
-    int mActiveHistoryStates2 = 0xffffffff;
-    long mLastHistoryElapsedRealtimeMs = 0;
-    long mTrackRunningHistoryElapsedRealtimeMs = 0;
-    long mTrackRunningHistoryUptimeMs = 0;
+    private boolean mHaveBatteryLevel = false;
+    private boolean mBatteryPluggedIn;
+    private int mBatteryStatus;
+    private int mBatteryLevel;
+    private int mBatteryPlugType;
+    private int mBatteryChargeUah;
+    private int mBatteryHealth;
+    private int mBatteryTemperature;
+    private int mBatteryVoltageMv = -1;
 
     @NonNull
-    final BatteryStatsHistory mBatteryStatsHistory;
-
-    final HistoryItem mHistoryCur = new HistoryItem();
-
-    // Used by computeHistoryStepDetails
-    HistoryStepDetails mLastHistoryStepDetails = null;
-    byte mLastHistoryStepLevel = 0;
-    final HistoryStepDetails mCurHistoryStepDetails = new HistoryStepDetails();
-    final HistoryStepDetails mTmpHistoryStepDetails = new HistoryStepDetails();
-
-    /**
-     * Total time (in milliseconds) spent executing in user code.
-     */
-    long mLastStepCpuUserTimeMs;
-    long mCurStepCpuUserTimeMs;
-    /**
-     * Total time (in milliseconds) spent executing in kernel code.
-     */
-    long mLastStepCpuSystemTimeMs;
-    long mCurStepCpuSystemTimeMs;
-    /**
-     * Times from /proc/stat (but measured in milliseconds).
-     */
-    long mLastStepStatUserTimeMs;
-    long mLastStepStatSystemTimeMs;
-    long mLastStepStatIOWaitTimeMs;
-    long mLastStepStatIrqTimeMs;
-    long mLastStepStatSoftIrqTimeMs;
-    long mLastStepStatIdleTimeMs;
-    long mCurStepStatUserTimeMs;
-    long mCurStepStatSystemTimeMs;
-    long mCurStepStatIOWaitTimeMs;
-    long mCurStepStatIrqTimeMs;
-    long mCurStepStatSoftIrqTimeMs;
-    long mCurStepStatIdleTimeMs;
+    private final BatteryStatsHistory mHistory;
 
     private BatteryStatsHistoryIterator mBatteryStatsHistoryIterator;
 
@@ -1391,7 +1352,6 @@
     int mDischargeUnplugLevel;
     int mDischargePlugLevel;
     int mDischargeCurrentLevel;
-    int mCurrentBatteryLevel;
     int mLowDischargeAmountSinceCharge;
     int mHighDischargeAmountSinceCharge;
     int mDischargeScreenOnUnplugLevel;
@@ -1443,7 +1403,6 @@
 
     private int mNumConnectivityChange;
 
-    private int mBatteryVoltageMv = -1;
     private int mEstimatedBatteryCapacityMah = -1;
 
     private int mLastLearnedBatteryCapacityUah = -1;
@@ -1627,28 +1586,27 @@
     }
 
     public BatteryStatsImpl(Clock clock) {
-        this(clock, (File) null);
+        this(clock, null);
     }
 
     public BatteryStatsImpl(Clock clock, File historyDirectory) {
         init(clock);
+        mHandler = null;
+        mConstants = new Constants(mHandler);
         mStartClockTimeMs = clock.currentTimeMillis();
         mCheckinFile = null;
         mDailyFile = null;
         if (historyDirectory == null) {
             mStatsFile = null;
-            mBatteryStatsHistory = new BatteryStatsHistory(mHistoryBuffer);
+            mHistory = new BatteryStatsHistory(mStepDetailsCalculator, mClock);
         } else {
             mStatsFile = new AtomicFile(new File(historyDirectory, "batterystats.bin"));
-            mBatteryStatsHistory = new BatteryStatsHistory(mHistoryBuffer, historyDirectory,
-                    this::getMaxHistoryFiles);
+            mHistory = new BatteryStatsHistory(historyDirectory, mConstants.MAX_HISTORY_FILES,
+                    mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock);
         }
-        mHandler = null;
         mPlatformIdleStateCallback = null;
         mMeasuredEnergyRetriever = null;
         mUserInfoProvider = null;
-        mConstants = new Constants(mHandler);
-        clearHistoryLocked();
     }
 
     private void init(Clock clock) {
@@ -3911,406 +3869,188 @@
         return kmt;
     }
 
-    /**
-     * Returns the index for the specified tag. If this is the first time the tag is encountered
-     * while writing the current history buffer, the method returns
-     * <code>(index | TAG_FIRST_OCCURRENCE_FLAG)</code>
-     */
-    private int writeHistoryTag(HistoryTag tag) {
-        if (tag.string == null) {
-            Slog.wtfStack(TAG, "writeHistoryTag called with null name");
-        }
+    private class HistoryStepDetailsCalculatorImpl implements HistoryStepDetailsCalculator {
+        private final HistoryStepDetails mDetails = new HistoryStepDetails();
 
-        final int stringLength = tag.string.length();
-        if (stringLength > MAX_HISTORY_TAG_STRING_LENGTH) {
-            Slog.e(TAG, "Long battery history tag: " + tag.string);
-            tag.string = tag.string.substring(0, MAX_HISTORY_TAG_STRING_LENGTH);
-        }
+        private boolean mHasHistoryStepDetails;
 
-        Integer idxObj = mHistoryTagPool.get(tag);
-        int idx;
-        if (idxObj != null) {
-            idx = idxObj;
-            if ((idx & BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG) != 0) {
-                mHistoryTagPool.put(tag, idx & ~BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG);
+        private int mLastHistoryStepLevel;
+
+        /**
+         * Total time (in milliseconds) spent executing in user code.
+         */
+        private long mLastStepCpuUserTimeMs;
+        private long mCurStepCpuUserTimeMs;
+        /**
+         * Total time (in milliseconds) spent executing in kernel code.
+         */
+        private long mLastStepCpuSystemTimeMs;
+        private long mCurStepCpuSystemTimeMs;
+        /**
+         * Times from /proc/stat (but measured in milliseconds).
+         */
+        private long mLastStepStatUserTimeMs;
+        private long mLastStepStatSystemTimeMs;
+        private long mLastStepStatIOWaitTimeMs;
+        private long mLastStepStatIrqTimeMs;
+        private long mLastStepStatSoftIrqTimeMs;
+        private long mLastStepStatIdleTimeMs;
+        private long mCurStepStatUserTimeMs;
+        private long mCurStepStatSystemTimeMs;
+        private long mCurStepStatIOWaitTimeMs;
+        private long mCurStepStatIrqTimeMs;
+        private long mCurStepStatSoftIrqTimeMs;
+        private long mCurStepStatIdleTimeMs;
+
+        @Override
+        public HistoryStepDetails getHistoryStepDetails() {
+            if (mBatteryLevel >= mLastHistoryStepLevel && mHasHistoryStepDetails) {
+                mLastHistoryStepLevel = mBatteryLevel;
+                return null;
             }
-            return idx;
-        } else if (mNextHistoryTagIdx < HISTORY_TAG_INDEX_LIMIT) {
-            idx = mNextHistoryTagIdx;
-            HistoryTag key = new HistoryTag();
-            key.setTo(tag);
-            tag.poolIdx = idx;
-            mHistoryTagPool.put(key, idx);
-            mNextHistoryTagIdx++;
 
-            mNumHistoryTagChars += stringLength + 1;
-            if (mHistoryTags != null) {
-                mHistoryTags.put(idx, key);
-            }
-            return idx | BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG;
-        } else {
-            // Tag pool overflow: include the tag itself in the parcel
-            return HISTORY_TAG_INDEX_LIMIT | BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG;
-        }
-    }
+            // Perform a CPU update right after we do this collection, so we have started
+            // collecting good data for the next step.
+            requestImmediateCpuUpdate();
 
-    /*
-        The history delta format uses flags to denote further data in subsequent ints in the parcel.
-
-        There is always the first token, which may contain the delta time, or an indicator of
-        the length of the time (int or long) following this token.
-
-        First token: always present,
-        31              23              15               7             0
-        â–ˆM|L|K|J|I|H|G|Fâ–ˆE|D|C|B|A|T|T|Tâ–ˆT|T|T|T|T|T|T|Tâ–ˆT|T|T|T|T|T|T|Tâ–ˆ
-
-        T: the delta time if it is <= 0x7fffd. Otherwise 0x7fffe indicates an int immediately
-           follows containing the time, and 0x7ffff indicates a long immediately follows with the
-           delta time.
-        A: battery level changed and an int follows with battery data.
-        B: state changed and an int follows with state change data.
-        C: state2 has changed and an int follows with state2 change data.
-        D: wakelock/wakereason has changed and an wakelock/wakereason struct follows.
-        E: event data has changed and an event struct follows.
-        F: battery charge in coulombs has changed and an int with the charge follows.
-        G: state flag denoting that the mobile radio was active.
-        H: state flag denoting that the wifi radio was active.
-        I: state flag denoting that a wifi scan occurred.
-        J: state flag denoting that a wifi full lock was held.
-        K: state flag denoting that the gps was on.
-        L: state flag denoting that a wakelock was held.
-        M: state flag denoting that the cpu was running.
-
-        Time int/long: if T in the first token is 0x7ffff or 0x7fffe, then an int or long follows
-        with the time delta.
-
-        Battery level int: if A in the first token is set,
-        31              23              15               7             0
-        â–ˆL|L|L|L|L|L|L|Tâ–ˆT|T|T|T|T|T|T|Tâ–ˆT|V|V|V|V|V|V|Vâ–ˆV|V|V|V|V|V|V|Dâ–ˆ
-
-        D: indicates that extra history details follow.
-        V: the battery voltage.
-        T: the battery temperature.
-        L: the battery level (out of 100).
-
-        State change int: if B in the first token is set,
-        31              23              15               7             0
-        â–ˆS|S|S|H|H|H|P|Pâ–ˆF|E|D|C|B| | |Aâ–ˆ | | | | | | | â–ˆ | | | | | | | â–ˆ
-
-        A: wifi multicast was on.
-        B: battery was plugged in.
-        C: screen was on.
-        D: phone was scanning for signal.
-        E: audio was on.
-        F: a sensor was active.
-
-        State2 change int: if C in the first token is set,
-        31              23              15               7             0
-        â–ˆM|L|K|J|I|H|H|Gâ–ˆF|E|D|C| | | | â–ˆ | | | | | | | â–ˆ |B|B|B|A|A|A|Aâ–ˆ
-
-        A: 4 bits indicating the wifi supplicant state: {@link BatteryStats#WIFI_SUPPL_STATE_NAMES}.
-        B: 3 bits indicating the wifi signal strength: 0, 1, 2, 3, 4.
-        C: a bluetooth scan was active.
-        D: the camera was active.
-        E: bluetooth was on.
-        F: a phone call was active.
-        G: the device was charging.
-        H: 2 bits indicating the device-idle (doze) state: off, light, full
-        I: the flashlight was on.
-        J: wifi was on.
-        K: wifi was running.
-        L: video was playing.
-        M: power save mode was on.
-
-        Wakelock/wakereason struct: if D in the first token is set,
-        TODO(adamlesinski): describe wakelock/wakereason struct.
-
-        Event struct: if E in the first token is set,
-        TODO(adamlesinski): describe the event struct.
-
-        History step details struct: if D in the battery level int is set,
-        TODO(adamlesinski): describe the history step details struct.
-
-        Battery charge int: if F in the first token is set, an int representing the battery charge
-        in coulombs follows.
-     */
-
-    @GuardedBy("this")
-    public void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) {
-        if (last == null || cur.cmd != HistoryItem.CMD_UPDATE) {
-            dest.writeInt(BatteryStatsHistory.DELTA_TIME_ABS);
-            cur.writeToParcel(dest, 0);
-            return;
-        }
-
-        final long deltaTime = cur.time - last.time;
-        final int lastBatteryLevelInt = buildBatteryLevelInt(last);
-        final int lastStateInt = buildStateInt(last);
-
-        int deltaTimeToken;
-        if (deltaTime < 0 || deltaTime > Integer.MAX_VALUE) {
-            deltaTimeToken = BatteryStatsHistory.DELTA_TIME_LONG;
-        } else if (deltaTime >= BatteryStatsHistory.DELTA_TIME_ABS) {
-            deltaTimeToken = BatteryStatsHistory.DELTA_TIME_INT;
-        } else {
-            deltaTimeToken = (int)deltaTime;
-        }
-        int firstToken = deltaTimeToken | (cur.states & BatteryStatsHistory.DELTA_STATE_MASK);
-        final int includeStepDetails = mLastHistoryStepLevel > cur.batteryLevel
-                ? BatteryStatsHistory.BATTERY_DELTA_LEVEL_FLAG : 0;
-        final boolean computeStepDetails = includeStepDetails != 0
-                || mLastHistoryStepDetails == null;
-        final int batteryLevelInt = buildBatteryLevelInt(cur) | includeStepDetails;
-        final boolean batteryLevelIntChanged = batteryLevelInt != lastBatteryLevelInt;
-        if (batteryLevelIntChanged) {
-            firstToken |= BatteryStatsHistory.DELTA_BATTERY_LEVEL_FLAG;
-        }
-        final int stateInt = buildStateInt(cur);
-        final boolean stateIntChanged = stateInt != lastStateInt;
-        if (stateIntChanged) {
-            firstToken |= BatteryStatsHistory.DELTA_STATE_FLAG;
-        }
-        final boolean state2IntChanged = cur.states2 != last.states2;
-        if (state2IntChanged) {
-            firstToken |= BatteryStatsHistory.DELTA_STATE2_FLAG;
-        }
-        if (cur.wakelockTag != null || cur.wakeReasonTag != null) {
-            firstToken |= BatteryStatsHistory.DELTA_WAKELOCK_FLAG;
-        }
-        if (cur.eventCode != HistoryItem.EVENT_NONE) {
-            firstToken |= BatteryStatsHistory.DELTA_EVENT_FLAG;
-        }
-
-        final boolean batteryChargeChanged = cur.batteryChargeUah != last.batteryChargeUah;
-        if (batteryChargeChanged) {
-            firstToken |= BatteryStatsHistory.DELTA_BATTERY_CHARGE_FLAG;
-        }
-        dest.writeInt(firstToken);
-        if (DEBUG) Slog.i(TAG, "WRITE DELTA: firstToken=0x" + Integer.toHexString(firstToken)
-                + " deltaTime=" + deltaTime);
-
-        if (deltaTimeToken >= BatteryStatsHistory.DELTA_TIME_INT) {
-            if (deltaTimeToken == BatteryStatsHistory.DELTA_TIME_INT) {
-                if (DEBUG) Slog.i(TAG, "WRITE DELTA: int deltaTime=" + (int)deltaTime);
-                dest.writeInt((int)deltaTime);
-            } else {
-                if (DEBUG) Slog.i(TAG, "WRITE DELTA: long deltaTime=" + deltaTime);
-                dest.writeLong(deltaTime);
-            }
-        }
-        if (batteryLevelIntChanged) {
-            dest.writeInt(batteryLevelInt);
-            if (DEBUG) Slog.i(TAG, "WRITE DELTA: batteryToken=0x"
-                    + Integer.toHexString(batteryLevelInt)
-                    + " batteryLevel=" + cur.batteryLevel
-                    + " batteryTemp=" + cur.batteryTemperature
-                    + " batteryVolt=" + (int)cur.batteryVoltage);
-        }
-        if (stateIntChanged) {
-            dest.writeInt(stateInt);
-            if (DEBUG) Slog.i(TAG, "WRITE DELTA: stateToken=0x"
-                    + Integer.toHexString(stateInt)
-                    + " batteryStatus=" + cur.batteryStatus
-                    + " batteryHealth=" + cur.batteryHealth
-                    + " batteryPlugType=" + cur.batteryPlugType
-                    + " states=0x" + Integer.toHexString(cur.states));
-        }
-        if (state2IntChanged) {
-            dest.writeInt(cur.states2);
-            if (DEBUG) Slog.i(TAG, "WRITE DELTA: states2=0x"
-                    + Integer.toHexString(cur.states2));
-        }
-        if (cur.wakelockTag != null || cur.wakeReasonTag != null) {
-            int wakeLockIndex;
-            int wakeReasonIndex;
-            if (cur.wakelockTag != null) {
-                wakeLockIndex = writeHistoryTag(cur.wakelockTag);
-                if (DEBUG) Slog.i(TAG, "WRITE DELTA: wakelockTag=#" + cur.wakelockTag.poolIdx
-                    + " " + cur.wakelockTag.uid + ":" + cur.wakelockTag.string);
-            } else {
-                wakeLockIndex = 0xffff;
-            }
-            if (cur.wakeReasonTag != null) {
-                wakeReasonIndex = writeHistoryTag(cur.wakeReasonTag);
-                if (DEBUG) Slog.i(TAG, "WRITE DELTA: wakeReasonTag=#" + cur.wakeReasonTag.poolIdx
-                    + " " + cur.wakeReasonTag.uid + ":" + cur.wakeReasonTag.string);
-            } else {
-                wakeReasonIndex = 0xffff;
-            }
-            dest.writeInt((wakeReasonIndex<<16) | wakeLockIndex);
-            if (cur.wakelockTag != null
-                    && (wakeLockIndex & BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG) != 0) {
-                cur.wakelockTag.writeToParcel(dest, 0);
-                cur.tagsFirstOccurrence = true;
-            }
-            if (cur.wakeReasonTag != null
-                    && (wakeReasonIndex & BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG) != 0) {
-                cur.wakeReasonTag.writeToParcel(dest, 0);
-                cur.tagsFirstOccurrence = true;
-            }
-        }
-        if (cur.eventCode != HistoryItem.EVENT_NONE) {
-            final int index = writeHistoryTag(cur.eventTag);
-            final int codeAndIndex = (cur.eventCode & 0xffff) | (index << 16);
-            dest.writeInt(codeAndIndex);
-            if ((index & BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG) != 0) {
-                cur.eventTag.writeToParcel(dest, 0);
-                cur.tagsFirstOccurrence = true;
-            }
-            if (DEBUG) Slog.i(TAG, "WRITE DELTA: event=" + cur.eventCode + " tag=#"
-                    + cur.eventTag.poolIdx + " " + cur.eventTag.uid + ":"
-                    + cur.eventTag.string);
-        }
-        if (computeStepDetails) {
             if (mPlatformIdleStateCallback != null) {
-                mCurHistoryStepDetails.statSubsystemPowerState =
+                mDetails.statSubsystemPowerState =
                         mPlatformIdleStateCallback.getSubsystemLowPowerStats();
                 if (DEBUG) Slog.i(TAG, "WRITE SubsystemPowerState:" +
-                        mCurHistoryStepDetails.statSubsystemPowerState);
-
+                        mDetails.statSubsystemPowerState);
             }
-            computeHistoryStepDetails(mCurHistoryStepDetails, mLastHistoryStepDetails);
-            if (includeStepDetails != 0) {
-                mCurHistoryStepDetails.writeToParcel(dest);
-            }
-            cur.stepDetails = mCurHistoryStepDetails;
-            mLastHistoryStepDetails = mCurHistoryStepDetails;
-        } else {
-            cur.stepDetails = null;
-        }
-        if (mLastHistoryStepLevel < cur.batteryLevel) {
-            mLastHistoryStepDetails = null;
-        }
-        mLastHistoryStepLevel = cur.batteryLevel;
 
-        if (batteryChargeChanged) {
-            if (DEBUG) Slog.i(TAG, "WRITE DELTA: batteryChargeUah=" + cur.batteryChargeUah);
-            dest.writeInt(cur.batteryChargeUah);
-        }
-        dest.writeDouble(cur.modemRailChargeMah);
-        dest.writeDouble(cur.wifiRailChargeMah);
-    }
-
-    private int buildBatteryLevelInt(HistoryItem h) {
-        return ((((int)h.batteryLevel)<<25)&0xfe000000)
-                | ((((int)h.batteryTemperature)<<15)&0x01ff8000)
-                | ((((int)h.batteryVoltage)<<1)&0x00007ffe);
-    }
-
-    private int buildStateInt(HistoryItem h) {
-        int plugType = 0;
-        if ((h.batteryPlugType&BatteryManager.BATTERY_PLUGGED_AC) != 0) {
-            plugType = 1;
-        } else if ((h.batteryPlugType&BatteryManager.BATTERY_PLUGGED_USB) != 0) {
-            plugType = 2;
-        } else if ((h.batteryPlugType&BatteryManager.BATTERY_PLUGGED_WIRELESS) != 0) {
-            plugType = 3;
-        }
-        return ((h.batteryStatus & BatteryStatsHistory.STATE_BATTERY_STATUS_MASK)
-                << BatteryStatsHistory.STATE_BATTERY_STATUS_SHIFT)
-                | ((h.batteryHealth & BatteryStatsHistory.STATE_BATTERY_HEALTH_MASK)
-                << BatteryStatsHistory.STATE_BATTERY_HEALTH_SHIFT)
-                | ((plugType & BatteryStatsHistory.STATE_BATTERY_PLUG_MASK)
-                << BatteryStatsHistory.STATE_BATTERY_PLUG_SHIFT)
-                | (h.states & (~BatteryStatsHistory.STATE_BATTERY_MASK));
-    }
-
-    private void computeHistoryStepDetails(final HistoryStepDetails out,
-            final HistoryStepDetails last) {
-        final HistoryStepDetails tmp = last != null ? mTmpHistoryStepDetails : out;
-
-        // Perform a CPU update right after we do this collection, so we have started
-        // collecting good data for the next step.
-        requestImmediateCpuUpdate();
-
-        if (last == null) {
-            // We are not generating a delta, so all we need to do is reset the stats
-            // we will later be doing a delta from.
-            final int NU = mUidStats.size();
-            for (int i=0; i<NU; i++) {
-                final BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
-                uid.mLastStepUserTimeMs = uid.mCurStepUserTimeMs;
-                uid.mLastStepSystemTimeMs = uid.mCurStepSystemTimeMs;
-            }
-            mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs;
-            mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs;
-            mLastStepStatUserTimeMs = mCurStepStatUserTimeMs;
-            mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs;
-            mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs;
-            mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs;
-            mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs;
-            mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs;
-            tmp.clear();
-            return;
-        }
-        if (DEBUG) {
-            Slog.d(TAG, "Step stats last: user=" + mLastStepCpuUserTimeMs + " sys="
-                    + mLastStepStatSystemTimeMs + " io=" + mLastStepStatIOWaitTimeMs
-                    + " irq=" + mLastStepStatIrqTimeMs + " sirq="
-                    + mLastStepStatSoftIrqTimeMs + " idle=" + mLastStepStatIdleTimeMs);
-            Slog.d(TAG, "Step stats cur: user=" + mCurStepCpuUserTimeMs + " sys="
-                    + mCurStepStatSystemTimeMs + " io=" + mCurStepStatIOWaitTimeMs
-                    + " irq=" + mCurStepStatIrqTimeMs + " sirq="
-                    + mCurStepStatSoftIrqTimeMs + " idle=" + mCurStepStatIdleTimeMs);
-        }
-        out.userTime = (int) (mCurStepCpuUserTimeMs - mLastStepCpuUserTimeMs);
-        out.systemTime = (int) (mCurStepCpuSystemTimeMs - mLastStepCpuSystemTimeMs);
-        out.statUserTime = (int) (mCurStepStatUserTimeMs - mLastStepStatUserTimeMs);
-        out.statSystemTime = (int) (mCurStepStatSystemTimeMs - mLastStepStatSystemTimeMs);
-        out.statIOWaitTime = (int) (mCurStepStatIOWaitTimeMs - mLastStepStatIOWaitTimeMs);
-        out.statIrqTime = (int) (mCurStepStatIrqTimeMs - mLastStepStatIrqTimeMs);
-        out.statSoftIrqTime = (int) (mCurStepStatSoftIrqTimeMs - mLastStepStatSoftIrqTimeMs);
-        out.statIdlTime = (int) (mCurStepStatIdleTimeMs - mLastStepStatIdleTimeMs);
-        out.appCpuUid1 = out.appCpuUid2 = out.appCpuUid3 = -1;
-        out.appCpuUTime1 = out.appCpuUTime2 = out.appCpuUTime3 = 0;
-        out.appCpuSTime1 = out.appCpuSTime2 = out.appCpuSTime3 = 0;
-        final int NU = mUidStats.size();
-        for (int i=0; i<NU; i++) {
-            final BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
-            final int totalUTimeMs = (int) (uid.mCurStepUserTimeMs - uid.mLastStepUserTimeMs);
-            final int totalSTimeMs = (int) (uid.mCurStepSystemTimeMs - uid.mLastStepSystemTimeMs);
-            final int totalTimeMs = totalUTimeMs + totalSTimeMs;
-            uid.mLastStepUserTimeMs = uid.mCurStepUserTimeMs;
-            uid.mLastStepSystemTimeMs = uid.mCurStepSystemTimeMs;
-            if (totalTimeMs <= (out.appCpuUTime3 + out.appCpuSTime3)) {
-                continue;
-            }
-            if (totalTimeMs <= (out.appCpuUTime2 + out.appCpuSTime2)) {
-                out.appCpuUid3 = uid.mUid;
-                out.appCpuUTime3 = totalUTimeMs;
-                out.appCpuSTime3 = totalSTimeMs;
-            } else {
-                out.appCpuUid3 = out.appCpuUid2;
-                out.appCpuUTime3 = out.appCpuUTime2;
-                out.appCpuSTime3 = out.appCpuSTime2;
-                if (totalTimeMs <= (out.appCpuUTime1 + out.appCpuSTime1)) {
-                    out.appCpuUid2 = uid.mUid;
-                    out.appCpuUTime2 = totalUTimeMs;
-                    out.appCpuSTime2 = totalSTimeMs;
-                } else {
-                    out.appCpuUid2 = out.appCpuUid1;
-                    out.appCpuUTime2 = out.appCpuUTime1;
-                    out.appCpuSTime2 = out.appCpuSTime1;
-                    out.appCpuUid1 = uid.mUid;
-                    out.appCpuUTime1 = totalUTimeMs;
-                    out.appCpuSTime1 = totalSTimeMs;
+            if (!mHasHistoryStepDetails) {
+                // We are not generating a delta, so all we need to do is reset the stats
+                // we will later be doing a delta from.
+                final int uidCount = mUidStats.size();
+                for (int i = 0; i < uidCount; i++) {
+                    final BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+                    uid.mLastStepUserTimeMs = uid.mCurStepUserTimeMs;
+                    uid.mLastStepSystemTimeMs = uid.mCurStepSystemTimeMs;
                 }
+                mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs;
+                mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs;
+                mLastStepStatUserTimeMs = mCurStepStatUserTimeMs;
+                mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs;
+                mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs;
+                mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs;
+                mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs;
+                mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs;
+                mDetails.clear();
+            } else {
+                if (DEBUG) {
+                    Slog.d(TAG, "Step stats last: user=" + mLastStepCpuUserTimeMs + " sys="
+                            + mLastStepStatSystemTimeMs + " io=" + mLastStepStatIOWaitTimeMs
+                            + " irq=" + mLastStepStatIrqTimeMs + " sirq="
+                            + mLastStepStatSoftIrqTimeMs + " idle=" + mLastStepStatIdleTimeMs);
+                    Slog.d(TAG, "Step stats cur: user=" + mCurStepCpuUserTimeMs + " sys="
+                            + mCurStepStatSystemTimeMs + " io=" + mCurStepStatIOWaitTimeMs
+                            + " irq=" + mCurStepStatIrqTimeMs + " sirq="
+                            + mCurStepStatSoftIrqTimeMs + " idle=" + mCurStepStatIdleTimeMs);
+                }
+                mDetails.userTime = (int) (mCurStepCpuUserTimeMs - mLastStepCpuUserTimeMs);
+                mDetails.systemTime = (int) (mCurStepCpuSystemTimeMs - mLastStepCpuSystemTimeMs);
+                mDetails.statUserTime = (int) (mCurStepStatUserTimeMs - mLastStepStatUserTimeMs);
+                mDetails.statSystemTime =
+                        (int) (mCurStepStatSystemTimeMs - mLastStepStatSystemTimeMs);
+                mDetails.statIOWaitTime =
+                        (int) (mCurStepStatIOWaitTimeMs - mLastStepStatIOWaitTimeMs);
+                mDetails.statIrqTime = (int) (mCurStepStatIrqTimeMs - mLastStepStatIrqTimeMs);
+                mDetails.statSoftIrqTime =
+                        (int) (mCurStepStatSoftIrqTimeMs - mLastStepStatSoftIrqTimeMs);
+                mDetails.statIdlTime = (int) (mCurStepStatIdleTimeMs - mLastStepStatIdleTimeMs);
+                mDetails.appCpuUid1 = mDetails.appCpuUid2 = mDetails.appCpuUid3 = -1;
+                mDetails.appCpuUTime1 = mDetails.appCpuUTime2 = mDetails.appCpuUTime3 = 0;
+                mDetails.appCpuSTime1 = mDetails.appCpuSTime2 = mDetails.appCpuSTime3 = 0;
+                final int uidCount = mUidStats.size();
+                for (int i = 0; i < uidCount; i++) {
+                    final BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
+                    final int totalUTimeMs =
+                            (int) (uid.mCurStepUserTimeMs - uid.mLastStepUserTimeMs);
+                    final int totalSTimeMs =
+                            (int) (uid.mCurStepSystemTimeMs - uid.mLastStepSystemTimeMs);
+                    final int totalTimeMs = totalUTimeMs + totalSTimeMs;
+                    uid.mLastStepUserTimeMs = uid.mCurStepUserTimeMs;
+                    uid.mLastStepSystemTimeMs = uid.mCurStepSystemTimeMs;
+                    if (totalTimeMs <= (mDetails.appCpuUTime3 + mDetails.appCpuSTime3)) {
+                        continue;
+                    }
+                    if (totalTimeMs <= (mDetails.appCpuUTime2 + mDetails.appCpuSTime2)) {
+                        mDetails.appCpuUid3 = uid.mUid;
+                        mDetails.appCpuUTime3 = totalUTimeMs;
+                        mDetails.appCpuSTime3 = totalSTimeMs;
+                    } else {
+                        mDetails.appCpuUid3 = mDetails.appCpuUid2;
+                        mDetails.appCpuUTime3 = mDetails.appCpuUTime2;
+                        mDetails.appCpuSTime3 = mDetails.appCpuSTime2;
+                        if (totalTimeMs <= (mDetails.appCpuUTime1 + mDetails.appCpuSTime1)) {
+                            mDetails.appCpuUid2 = uid.mUid;
+                            mDetails.appCpuUTime2 = totalUTimeMs;
+                            mDetails.appCpuSTime2 = totalSTimeMs;
+                        } else {
+                            mDetails.appCpuUid2 = mDetails.appCpuUid1;
+                            mDetails.appCpuUTime2 = mDetails.appCpuUTime1;
+                            mDetails.appCpuSTime2 = mDetails.appCpuSTime1;
+                            mDetails.appCpuUid1 = uid.mUid;
+                            mDetails.appCpuUTime1 = totalUTimeMs;
+                            mDetails.appCpuSTime1 = totalSTimeMs;
+                        }
+                    }
+                }
+                mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs;
+                mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs;
+                mLastStepStatUserTimeMs = mCurStepStatUserTimeMs;
+                mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs;
+                mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs;
+                mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs;
+                mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs;
+                mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs;
             }
+
+            mHasHistoryStepDetails = mBatteryLevel <= mLastHistoryStepLevel;
+            mLastHistoryStepLevel = mBatteryLevel;
+
+            return mDetails;
         }
-        mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs;
-        mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs;
-        mLastStepStatUserTimeMs = mCurStepStatUserTimeMs;
-        mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs;
-        mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs;
-        mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs;
-        mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs;
-        mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs;
+
+        public void addCpuStats(int totalUTimeMs, int totalSTimeMs, int statUserTimeMs,
+                int statSystemTimeMs, int statIOWaitTimeMs, int statIrqTimeMs,
+                int statSoftIrqTimeMs, int statIdleTimeMs) {
+            if (DEBUG) {
+                Slog.d(TAG, "Adding cpu: tuser=" + totalUTimeMs + " tsys=" + totalSTimeMs
+                        + " user=" + statUserTimeMs + " sys=" + statSystemTimeMs
+                        + " io=" + statIOWaitTimeMs + " irq=" + statIrqTimeMs
+                        + " sirq=" + statSoftIrqTimeMs + " idle=" + statIdleTimeMs);
+            }
+            mCurStepCpuUserTimeMs += totalUTimeMs;
+            mCurStepCpuSystemTimeMs += totalSTimeMs;
+            mCurStepStatUserTimeMs += statUserTimeMs;
+            mCurStepStatSystemTimeMs += statSystemTimeMs;
+            mCurStepStatIOWaitTimeMs += statIOWaitTimeMs;
+            mCurStepStatIrqTimeMs += statIrqTimeMs;
+            mCurStepStatSoftIrqTimeMs += statSoftIrqTimeMs;
+            mCurStepStatIdleTimeMs += statIdleTimeMs;
+        }
+
+        @Override
+        public void clear() {
+            mHasHistoryStepDetails = false;
+            mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs = 0;
+            mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs = 0;
+            mLastStepStatUserTimeMs = mCurStepStatUserTimeMs = 0;
+            mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs = 0;
+            mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs = 0;
+            mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs = 0;
+            mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs = 0;
+            mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs = 0;
+        }
     }
 
     @GuardedBy("this")
     @Override
     public void commitCurrentHistoryBatchLocked() {
-        mHistoryLastWritten.cmd = HistoryItem.CMD_NULL;
+        mHistory.commitCurrentHistoryBatchLocked();
     }
 
     @GuardedBy("this")
@@ -4326,191 +4066,9 @@
     }
 
     @GuardedBy("this")
-    void addHistoryBufferLocked(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) {
-        if (!mHaveBatteryLevel || !mRecordingHistory) {
-            return;
-        }
-
-        final long timeDiffMs = (mHistoryBaseTimeMs + elapsedRealtimeMs) - mHistoryLastWritten.time;
-        final int diffStates = mHistoryLastWritten.states^(cur.states&mActiveHistoryStates);
-        final int diffStates2 = mHistoryLastWritten.states2^(cur.states2&mActiveHistoryStates2);
-        final int lastDiffStates = mHistoryLastWritten.states^mHistoryLastLastWritten.states;
-        final int lastDiffStates2 = mHistoryLastWritten.states2^mHistoryLastLastWritten.states2;
-        if (DEBUG) {
-            Slog.i(TAG, "ADD: tdelta=" + timeDiffMs + " diff="
-                    + Integer.toHexString(diffStates) + " lastDiff="
-                    + Integer.toHexString(lastDiffStates) + " diff2="
-                    + Integer.toHexString(diffStates2) + " lastDiff2="
-                    + Integer.toHexString(lastDiffStates2));
-        }
-        if (mHistoryBufferLastPos >= 0 && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE
-                && timeDiffMs < 1000 && (diffStates & lastDiffStates) == 0
-                && (diffStates2&lastDiffStates2) == 0
-                && (!mHistoryLastWritten.tagsFirstOccurrence && !cur.tagsFirstOccurrence)
-                && (mHistoryLastWritten.wakelockTag == null || cur.wakelockTag == null)
-                && (mHistoryLastWritten.wakeReasonTag == null || cur.wakeReasonTag == null)
-                && mHistoryLastWritten.stepDetails == null
-                && (mHistoryLastWritten.eventCode == HistoryItem.EVENT_NONE
-                        || cur.eventCode == HistoryItem.EVENT_NONE)
-                && mHistoryLastWritten.batteryLevel == cur.batteryLevel
-                && mHistoryLastWritten.batteryStatus == cur.batteryStatus
-                && mHistoryLastWritten.batteryHealth == cur.batteryHealth
-                && mHistoryLastWritten.batteryPlugType == cur.batteryPlugType
-                && mHistoryLastWritten.batteryTemperature == cur.batteryTemperature
-                && mHistoryLastWritten.batteryVoltage == cur.batteryVoltage) {
-            // We can merge this new change in with the last one.  Merging is
-            // allowed as long as only the states have changed, and within those states
-            // as long as no bit has changed both between now and the last entry, as
-            // well as the last entry and the one before it (so we capture any toggles).
-            if (DEBUG) Slog.i(TAG, "ADD: rewinding back to " + mHistoryBufferLastPos);
-            mHistoryBuffer.setDataSize(mHistoryBufferLastPos);
-            mHistoryBuffer.setDataPosition(mHistoryBufferLastPos);
-            mHistoryBufferLastPos = -1;
-            elapsedRealtimeMs = mHistoryLastWritten.time - mHistoryBaseTimeMs;
-            // If the last written history had a wakelock tag, we need to retain it.
-            // Note that the condition above made sure that we aren't in a case where
-            // both it and the current history item have a wakelock tag.
-            if (mHistoryLastWritten.wakelockTag != null) {
-                cur.wakelockTag = cur.localWakelockTag;
-                cur.wakelockTag.setTo(mHistoryLastWritten.wakelockTag);
-            }
-            // If the last written history had a wake reason tag, we need to retain it.
-            // Note that the condition above made sure that we aren't in a case where
-            // both it and the current history item have a wakelock tag.
-            if (mHistoryLastWritten.wakeReasonTag != null) {
-                cur.wakeReasonTag = cur.localWakeReasonTag;
-                cur.wakeReasonTag.setTo(mHistoryLastWritten.wakeReasonTag);
-            }
-            // If the last written history had an event, we need to retain it.
-            // Note that the condition above made sure that we aren't in a case where
-            // both it and the current history item have an event.
-            if (mHistoryLastWritten.eventCode != HistoryItem.EVENT_NONE) {
-                cur.eventCode = mHistoryLastWritten.eventCode;
-                cur.eventTag = cur.localEventTag;
-                cur.eventTag.setTo(mHistoryLastWritten.eventTag);
-            }
-            mHistoryLastWritten.setTo(mHistoryLastLastWritten);
-        }
-        final int dataSize = mHistoryBuffer.dataSize();
-
-        if (dataSize >= mConstants.MAX_HISTORY_BUFFER) {
-            //open a new history file.
-            final long start = SystemClock.uptimeMillis();
-            writeHistoryLocked();
-            if (DEBUG) {
-                Slog.d(TAG, "addHistoryBufferLocked writeHistoryLocked takes ms:"
-                        + (SystemClock.uptimeMillis() - start));
-            }
-            mBatteryStatsHistory.startNextFile();
-            mHistoryBuffer.setDataSize(0);
-            mHistoryBuffer.setDataPosition(0);
-            mHistoryBuffer.setDataCapacity(mConstants.MAX_HISTORY_BUFFER / 2);
-            mHistoryBufferLastPos = -1;
-            mHistoryLastWritten.clear();
-            mHistoryLastLastWritten.clear();
-
-            // Mark every entry in the pool with a flag indicating that the tag
-            // has not yet been encountered while writing the current history buffer.
-            for (Map.Entry<HistoryTag, Integer> entry: mHistoryTagPool.entrySet()) {
-                entry.setValue(entry.getValue() | BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG);
-            }
-            // Make a copy of mHistoryCur.
-            HistoryItem copy = new HistoryItem();
-            copy.setTo(cur);
-            // startRecordingHistory will reset mHistoryCur.
-            startRecordingHistory(elapsedRealtimeMs, uptimeMs, false);
-            // Add the copy into history buffer.
-            addHistoryBufferLocked(elapsedRealtimeMs, HistoryItem.CMD_UPDATE, copy);
-            return;
-        }
-
-        if (dataSize == 0) {
-            // The history is currently empty; we need it to start with a time stamp.
-            cur.currentTime = mClock.currentTimeMillis();
-            addHistoryBufferLocked(elapsedRealtimeMs, HistoryItem.CMD_RESET, cur);
-        }
-        addHistoryBufferLocked(elapsedRealtimeMs, HistoryItem.CMD_UPDATE, cur);
-    }
-
-    @GuardedBy("this")
-    private void addHistoryBufferLocked(long elapsedRealtimeMs, byte cmd, HistoryItem cur) {
-        if (mBatteryStatsHistoryIterator != null) {
-            throw new IllegalStateException("Can't do this while iterating history!");
-        }
-        mHistoryBufferLastPos = mHistoryBuffer.dataPosition();
-        mHistoryLastLastWritten.setTo(mHistoryLastWritten);
-        final boolean hasTags = mHistoryLastWritten.tagsFirstOccurrence || cur.tagsFirstOccurrence;
-        mHistoryLastWritten.setTo(mHistoryBaseTimeMs + elapsedRealtimeMs, cmd, cur);
-        mHistoryLastWritten.tagsFirstOccurrence = hasTags;
-        mHistoryLastWritten.states &= mActiveHistoryStates;
-        mHistoryLastWritten.states2 &= mActiveHistoryStates2;
-        writeHistoryDelta(mHistoryBuffer, mHistoryLastWritten, mHistoryLastLastWritten);
-        mLastHistoryElapsedRealtimeMs = elapsedRealtimeMs;
-        cur.wakelockTag = null;
-        cur.wakeReasonTag = null;
-        cur.eventCode = HistoryItem.EVENT_NONE;
-        cur.eventTag = null;
-        cur.tagsFirstOccurrence = false;
-        if (DEBUG_HISTORY) Slog.i(TAG, "Writing history buffer: was " + mHistoryBufferLastPos
-                + " now " + mHistoryBuffer.dataPosition()
-                + " size is now " + mHistoryBuffer.dataSize());
-    }
-
-    @GuardedBy("this")
-    void addHistoryRecordLocked(long elapsedRealtimeMs, long uptimeMs) {
-        if (mTrackRunningHistoryElapsedRealtimeMs != 0) {
-            final long diffElapsedMs = elapsedRealtimeMs - mTrackRunningHistoryElapsedRealtimeMs;
-            final long diffUptimeMs = uptimeMs - mTrackRunningHistoryUptimeMs;
-            if (diffUptimeMs < (diffElapsedMs - 20)) {
-                final long wakeElapsedTimeMs = elapsedRealtimeMs - (diffElapsedMs - diffUptimeMs);
-                mHistoryAddTmp.setTo(mHistoryLastWritten);
-                mHistoryAddTmp.wakelockTag = null;
-                mHistoryAddTmp.wakeReasonTag = null;
-                mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE;
-                mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG;
-                addHistoryRecordInnerLocked(wakeElapsedTimeMs, uptimeMs, mHistoryAddTmp);
-            }
-        }
-        mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG;
-        mTrackRunningHistoryElapsedRealtimeMs = elapsedRealtimeMs;
-        mTrackRunningHistoryUptimeMs = uptimeMs;
-        addHistoryRecordInnerLocked(elapsedRealtimeMs, uptimeMs, mHistoryCur);
-    }
-
-    @GuardedBy("this")
-    void addHistoryRecordInnerLocked(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) {
-        addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, cur);
-    }
-
-    @GuardedBy("this")
-    public void addHistoryEventLocked(long elapsedRealtimeMs, long uptimeMs, int code,
+    public void recordHistoryEventLocked(long elapsedRealtimeMs, long uptimeMs, int code,
             String name, int uid) {
-        mHistoryCur.eventCode = code;
-        mHistoryCur.eventTag = mHistoryCur.localEventTag;
-        mHistoryCur.eventTag.string = name;
-        mHistoryCur.eventTag.uid = uid;
-        addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
-    }
-
-    @GuardedBy("this")
-    void clearHistoryLocked() {
-        if (DEBUG_HISTORY) Slog.i(TAG, "********** CLEARING HISTORY!");
-        mHistoryBaseTimeMs = 0;
-        mLastHistoryElapsedRealtimeMs = 0;
-        mTrackRunningHistoryElapsedRealtimeMs = 0;
-        mTrackRunningHistoryUptimeMs = 0;
-
-        mHistoryBuffer.setDataSize(0);
-        mHistoryBuffer.setDataPosition(0);
-        mHistoryBuffer.setDataCapacity(mConstants.MAX_HISTORY_BUFFER / 2);
-        mHistoryLastLastWritten.clear();
-        mHistoryLastWritten.clear();
-        mHistoryTagPool.clear();
-        mNextHistoryTagIdx = 0;
-        mNumHistoryTagChars = 0;
-        mHistoryBufferLastPos = -1;
-        mActiveHistoryStates = 0xffffffff;
-        mActiveHistoryStates2 = 0xffffffff;
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, code, name, uid);
     }
 
     @GuardedBy("this")
@@ -4663,13 +4221,13 @@
         if (!mActiveEvents.updateState(code, name, uid, 0)) {
             return;
         }
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, code, name, uid);
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, code, name, uid);
     }
 
     @GuardedBy("this")
     public void noteCurrentTimeChangedLocked(long currentTimeMs,
             long elapsedRealtimeMs, long uptimeMs) {
-        recordCurrentTimeChangeLocked(currentTimeMs, elapsedRealtimeMs, uptimeMs);
+        mHistory.recordCurrentTimeChange(elapsedRealtimeMs, uptimeMs, currentTimeMs);
     }
 
     @GuardedBy("this")
@@ -4686,7 +4244,7 @@
         if (!mRecordAllHistory) {
             return;
         }
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_PROC_START, name, uid);
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_PROC_START, name, uid);
     }
 
     @GuardedBy("this")
@@ -4744,8 +4302,7 @@
         if (!mRecordAllHistory) {
             return;
         }
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_PROC_FINISH,
-                name, uid);
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_PROC_FINISH, name, uid);
     }
 
     @GuardedBy("this")
@@ -4761,7 +4318,7 @@
         if (!mActiveEvents.updateState(HistoryItem.EVENT_SYNC_START, name, uid, 0)) {
             return;
         }
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_SYNC_START, name, uid);
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_SYNC_START, name, uid);
     }
 
     @GuardedBy("this")
@@ -4777,8 +4334,7 @@
         if (!mActiveEvents.updateState(HistoryItem.EVENT_SYNC_FINISH, name, uid, 0)) {
             return;
         }
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_SYNC_FINISH,
-                name, uid);
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_SYNC_FINISH, name, uid);
     }
 
     @GuardedBy("this")
@@ -4794,7 +4350,7 @@
         if (!mActiveEvents.updateState(HistoryItem.EVENT_JOB_START, name, uid, 0)) {
             return;
         }
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_JOB_START, name, uid);
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_JOB_START, name, uid);
     }
 
     @GuardedBy("this")
@@ -4812,7 +4368,7 @@
         if (!mActiveEvents.updateState(HistoryItem.EVENT_JOB_FINISH, name, uid, 0)) {
             return;
         }
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_JOB_FINISH, name, uid);
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_JOB_FINISH, name, uid);
     }
 
     @GuardedBy("this")
@@ -4860,7 +4416,7 @@
             for (int i = 0; i < workSource.size(); ++i) {
                 uid = mapUid(workSource.getUid(i));
                 if (mActiveEvents.updateState(historyItem, name, uid, 0)) {
-                    addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, historyItem, name, uid);
+                    mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, historyItem, name, uid);
                 }
             }
 
@@ -4869,7 +4425,7 @@
                 for (int i = 0; i < workChains.size(); ++i) {
                     uid = mapUid(workChains.get(i).getAttributionUid());
                     if (mActiveEvents.updateState(historyItem, name, uid, 0)) {
-                        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, historyItem, name, uid);
+                        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, historyItem, name, uid);
                     }
                 }
             }
@@ -4877,7 +4433,7 @@
             uid = mapUid(uid);
 
             if (mActiveEvents.updateState(historyItem, name, uid, 0)) {
-                addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, historyItem, name, uid);
+                mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, historyItem, name, uid);
             }
         }
     }
@@ -4952,7 +4508,7 @@
                 for (HashMap.Entry<String, SparseIntArray> ent : active.entrySet()) {
                     SparseIntArray uids = ent.getValue();
                     for (int j=0; j<uids.size(); j++) {
-                        addHistoryEventLocked(mSecRealtime, mSecUptime,
+                        mHistory.recordEvent(mSecRealtime, mSecUptime,
                                 HistoryItem.EVENT_PROC_FINISH, ent.getKey(), uids.keyAt(j));
                     }
                 }
@@ -4967,8 +4523,8 @@
                 for (HashMap.Entry<String, SparseIntArray> ent : active.entrySet()) {
                     SparseIntArray uids = ent.getValue();
                     for (int j=0; j<uids.size(); j++) {
-                        addHistoryEventLocked(mSecRealtime, mSecUptime,
-                                HistoryItem.EVENT_PROC_START, ent.getKey(), uids.keyAt(j));
+                        mHistory.recordEvent(mSecRealtime, mSecUptime, HistoryItem.EVENT_PROC_START,
+                                ent.getKey(), uids.keyAt(j));
                     }
                 }
             }
@@ -5011,30 +4567,19 @@
             if (mRecordAllHistory) {
                 if (mActiveEvents.updateState(HistoryItem.EVENT_WAKE_LOCK_START, historyName,
                         mappedUid, 0)) {
-                    addHistoryEventLocked(elapsedRealtimeMs, uptimeMs,
+                    mHistory.recordEvent(elapsedRealtimeMs, uptimeMs,
                             HistoryItem.EVENT_WAKE_LOCK_START, historyName, mappedUid);
                 }
             }
             if (mWakeLockNesting == 0) {
-                mHistoryCur.states |= HistoryItem.STATE_WAKE_LOCK_FLAG;
-                if (DEBUG_HISTORY) Slog.v(TAG, "Start wake lock to: "
-                        + Integer.toHexString(mHistoryCur.states));
-                mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
-                mHistoryCur.wakelockTag.string = historyName;
-                mHistoryCur.wakelockTag.uid = mappedUid;
                 mWakeLockImportant = !unimportantForLogging;
-                addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
-            } else if (!mWakeLockImportant && !unimportantForLogging
-                    && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE) {
-                if (mHistoryLastWritten.wakelockTag != null) {
-                    // We'll try to update the last tag.
-                    mHistoryLastWritten.wakelockTag = null;
-                    mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
-                    mHistoryCur.wakelockTag.string = historyName;
-                    mHistoryCur.wakelockTag.uid = mappedUid;
-                    addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+                mHistory.recordWakelockStartEvent(elapsedRealtimeMs, uptimeMs, historyName,
+                        mappedUid);
+            } else if (!mWakeLockImportant && !unimportantForLogging) {
+                if (mHistory.maybeUpdateWakelockTag(elapsedRealtimeMs, uptimeMs, historyName,
+                        mappedUid)) {
+                    mWakeLockImportant = true;
                 }
-                mWakeLockImportant = true;
             }
             mWakeLockNesting++;
         }
@@ -5087,15 +4632,13 @@
                 }
                 if (mActiveEvents.updateState(HistoryItem.EVENT_WAKE_LOCK_FINISH, historyName,
                         mappedUid, 0)) {
-                    addHistoryEventLocked(elapsedRealtimeMs, uptimeMs,
+                    mHistory.recordEvent(elapsedRealtimeMs, uptimeMs,
                             HistoryItem.EVENT_WAKE_LOCK_FINISH, historyName, mappedUid);
                 }
             }
             if (mWakeLockNesting == 0) {
-                mHistoryCur.states &= ~HistoryItem.STATE_WAKE_LOCK_FLAG;
-                if (DEBUG_HISTORY) Slog.v(TAG, "Stop wake lock to: "
-                        + Integer.toHexString(mHistoryCur.states));
-                addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+                mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                        HistoryItem.STATE_WAKE_LOCK_FLAG);
             }
         }
         if (mappedUid >= 0) {
@@ -5286,7 +4829,7 @@
                 mappedUid, 0)) {
             return;
         }
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_LONG_WAKE_LOCK_START,
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_LONG_WAKE_LOCK_START,
                 historyName, mappedUid);
         if (mappedUid != uid) {
             // Prevent the isolated uid mapping from being removed while the wakelock is
@@ -5339,7 +4882,7 @@
                 mappedUid, 0)) {
             return;
         }
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH,
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH,
                 historyName, mappedUid);
         if (mappedUid != uid) {
             // Decrement the ref count for the isolated uid and delete the mapping if uneeded.
@@ -5361,15 +4904,10 @@
 
     @GuardedBy("this")
     public void noteWakeupReasonLocked(String reason, long elapsedRealtimeMs, long uptimeMs) {
-        if (DEBUG_HISTORY) Slog.v(TAG, "Wakeup reason \"" + reason +"\": "
-                + Integer.toHexString(mHistoryCur.states));
         aggregateLastWakeupUptimeLocked(elapsedRealtimeMs, uptimeMs);
-        mHistoryCur.wakeReasonTag = mHistoryCur.localWakeReasonTag;
-        mHistoryCur.wakeReasonTag.string = reason;
-        mHistoryCur.wakeReasonTag.uid = 0;
+        mHistory.recordWakeupEvent(elapsedRealtimeMs, uptimeMs, reason);
         mLastWakeupReason = reason;
         mLastWakeupUptimeMs = uptimeMs;
-        addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
     }
 
     @GuardedBy("this")
@@ -5380,22 +4918,11 @@
 
     @GuardedBy("this")
     public void finishAddingCpuLocked(int totalUTimeMs, int totalSTimeMs, int statUserTimeMs,
-                                      int statSystemTimeMs, int statIOWaitTimeMs, int statIrqTimeMs,
-                                      int statSoftIrqTimeMs, int statIdleTimeMs) {
-        if (DEBUG) {
-            Slog.d(TAG, "Adding cpu: tuser=" + totalUTimeMs + " tsys=" + totalSTimeMs
-                    + " user=" + statUserTimeMs + " sys=" + statSystemTimeMs
-                    + " io=" + statIOWaitTimeMs + " irq=" + statIrqTimeMs
-                    + " sirq=" + statSoftIrqTimeMs + " idle=" + statIdleTimeMs);
-        }
-        mCurStepCpuUserTimeMs += totalUTimeMs;
-        mCurStepCpuSystemTimeMs += totalSTimeMs;
-        mCurStepStatUserTimeMs += statUserTimeMs;
-        mCurStepStatSystemTimeMs += statSystemTimeMs;
-        mCurStepStatIOWaitTimeMs += statIOWaitTimeMs;
-        mCurStepStatIrqTimeMs += statIrqTimeMs;
-        mCurStepStatSoftIrqTimeMs += statSoftIrqTimeMs;
-        mCurStepStatIdleTimeMs += statIdleTimeMs;
+            int statSystemTimeMs, int statIOWaitTimeMs, int statIrqTimeMs,
+            int statSoftIrqTimeMs, int statIdleTimeMs) {
+        mStepDetailsCalculator.addCpuStats(totalUTimeMs, totalSTimeMs, statUserTimeMs,
+                statSystemTimeMs, statIOWaitTimeMs, statIrqTimeMs,
+                statSoftIrqTimeMs, statIdleTimeMs);
     }
 
     public void noteProcessDiedLocked(int uid, int pid) {
@@ -5425,10 +4952,8 @@
     public void noteStartSensorLocked(int uid, int sensor, long elapsedRealtimeMs, long uptimeMs) {
         uid = mapUid(uid);
         if (mSensorNesting == 0) {
-            mHistoryCur.states |= HistoryItem.STATE_SENSOR_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Start sensor to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_SENSOR_ON_FLAG);
         }
         mSensorNesting++;
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
@@ -5445,10 +4970,8 @@
         uid = mapUid(uid);
         mSensorNesting--;
         if (mSensorNesting == 0) {
-            mHistoryCur.states &= ~HistoryItem.STATE_SENSOR_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Stop sensor to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_SENSOR_ON_FLAG);
         }
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
                 .noteStopSensor(sensor, elapsedRealtimeMs);
@@ -5498,10 +5021,8 @@
         }
         final int mappedUid = mapUid(uid);
         if (mGpsNesting == 0) {
-            mHistoryCur.states |= HistoryItem.STATE_GPS_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Start GPS to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_GPS_ON_FLAG);
         }
         mGpsNesting++;
 
@@ -5526,10 +5047,8 @@
         final int mappedUid = mapUid(uid);
         mGpsNesting--;
         if (mGpsNesting == 0) {
-            mHistoryCur.states &= ~HistoryItem.STATE_GPS_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Stop GPS to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_GPS_ON_FLAG);
             stopAllGpsSignalQualityTimersLocked(-1, elapsedRealtimeMs);
             mGpsSignalQualityBin = -1;
         }
@@ -5562,12 +5081,9 @@
             if(!mGpsSignalQualityTimer[signalLevel].isRunningLocked()) {
                 mGpsSignalQualityTimer[signalLevel].startRunningLocked(elapsedRealtimeMs);
             }
-            mHistoryCur.states2 = (mHistoryCur.states2&~HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK)
-                    | (signalLevel << HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT);
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordGpsSignalQualityEvent(elapsedRealtimeMs, uptimeMs, signalLevel);
             mGpsSignalQualityBin = signalLevel;
         }
-        return;
     }
 
     @GuardedBy("this")
@@ -5740,41 +5256,33 @@
                 }
             }
 
-            boolean updateHistory = false;
+            int startStates = 0;
+            int stopStates = 0;
             if (Display.isDozeState(state) && !Display.isDozeState(oldState)) {
-                mHistoryCur.states |= HistoryItem.STATE_SCREEN_DOZE_FLAG;
+                startStates |= HistoryItem.STATE_SCREEN_DOZE_FLAG;
                 mScreenDozeTimer.startRunningLocked(elapsedRealtimeMs);
-                updateHistory = true;
             } else if (Display.isDozeState(oldState) && !Display.isDozeState(state)) {
-                mHistoryCur.states &= ~HistoryItem.STATE_SCREEN_DOZE_FLAG;
+                stopStates |= HistoryItem.STATE_SCREEN_DOZE_FLAG;
                 mScreenDozeTimer.stopRunningLocked(elapsedRealtimeMs);
-                updateHistory = true;
             }
             if (Display.isOnState(state)) {
-                mHistoryCur.states |= HistoryItem.STATE_SCREEN_ON_FLAG;
-                if (DEBUG_HISTORY) Slog.v(TAG, "Screen on to: "
-                        + Integer.toHexString(mHistoryCur.states));
+                startStates |= HistoryItem.STATE_SCREEN_ON_FLAG;
                 mScreenOnTimer.startRunningLocked(elapsedRealtimeMs);
                 if (mScreenBrightnessBin >= 0) {
                     mScreenBrightnessTimer[mScreenBrightnessBin]
                             .startRunningLocked(elapsedRealtimeMs);
                 }
-                updateHistory = true;
             } else if (Display.isOnState(oldState)) {
-                mHistoryCur.states &= ~HistoryItem.STATE_SCREEN_ON_FLAG;
-                if (DEBUG_HISTORY) Slog.v(TAG, "Screen off to: "
-                        + Integer.toHexString(mHistoryCur.states));
+                stopStates |= HistoryItem.STATE_SCREEN_ON_FLAG;
                 mScreenOnTimer.stopRunningLocked(elapsedRealtimeMs);
                 if (mScreenBrightnessBin >= 0) {
                     mScreenBrightnessTimer[mScreenBrightnessBin]
                             .stopRunningLocked(elapsedRealtimeMs);
                 }
-                updateHistory = true;
             }
-            if (updateHistory) {
-                if (DEBUG_HISTORY) Slog.v(TAG, "Screen state to: "
-                        + Display.stateToString(state));
-                addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            if (startStates != 0 || stopStates != 0) {
+                mHistory.recordStateChangeEvent(elapsedRealtimeMs, uptimeMs, startStates,
+                        stopStates);
             }
 
             // Per screen state Cpu stats needed. Prepare to schedule an external sync.
@@ -5888,13 +5396,7 @@
             long uptimeMs) {
         if (mScreenBrightnessBin != overallBin) {
             if (overallBin >= 0) {
-                mHistoryCur.states = (mHistoryCur.states & ~HistoryItem.STATE_BRIGHTNESS_MASK)
-                        | (overallBin << HistoryItem.STATE_BRIGHTNESS_SHIFT);
-                if (DEBUG_HISTORY) {
-                    Slog.v(TAG, "Screen brightness " + overallBin + " to: "
-                            + Integer.toHexString(mHistoryCur.states));
-                }
-                addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+                mHistory.recordScreenBrightnessEvent(elapsedRealtimeMs, uptimeMs, overallBin);
             }
             if (mScreenState == Display.STATE_ON) {
                 if (mScreenBrightnessBin >= 0) {
@@ -5922,8 +5424,8 @@
     @GuardedBy("this")
     public void noteWakeUpLocked(String reason, int reasonUid,
             long elapsedRealtimeMs, long uptimeMs) {
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_SCREEN_WAKE_UP,
-                reason, reasonUid);
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_SCREEN_WAKE_UP, reason,
+                reasonUid);
     }
 
     @GuardedBy("this")
@@ -5942,7 +5444,7 @@
     @GuardedBy("this")
     public void noteConnectivityChangedLocked(int type, String extra,
             long elapsedRealtimeMs, long uptimeMs) {
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_CONNECTIVITY_CHANGED,
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_CONNECTIVITY_CHANGED,
                 extra, type);
         mNumConnectivityChange++;
     }
@@ -5951,7 +5453,7 @@
     private void noteMobileRadioApWakeupLocked(final long elapsedRealtimeMillis,
             final long uptimeMillis, int uid) {
         uid = mapUid(uid);
-        addHistoryEventLocked(elapsedRealtimeMillis, uptimeMillis, HistoryItem.EVENT_WAKEUP_AP, "",
+        mHistory.recordEvent(elapsedRealtimeMillis, uptimeMillis, HistoryItem.EVENT_WAKEUP_AP, "",
                 uid);
         getUidStatsLocked(uid, elapsedRealtimeMillis, uptimeMillis).noteMobileRadioApWakeupLocked();
     }
@@ -5977,7 +5479,8 @@
                 }
 
                 mMobileRadioActiveStartTimeMs = realElapsedRealtimeMs = timestampNs / (1000 * 1000);
-                mHistoryCur.states |= HistoryItem.STATE_MOBILE_RADIO_ACTIVE_FLAG;
+                mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs,
+                        HistoryItem.STATE_MOBILE_RADIO_ACTIVE_FLAG);
             } else {
                 realElapsedRealtimeMs = timestampNs / (1000*1000);
                 long lastUpdateTimeMs = mMobileRadioActiveStartTimeMs;
@@ -5989,11 +5492,9 @@
                     mMobileRadioActiveAdjustedTime.addCountLocked(elapsedRealtimeMs
                             - realElapsedRealtimeMs);
                 }
-                mHistoryCur.states &= ~HistoryItem.STATE_MOBILE_RADIO_ACTIVE_FLAG;
+                mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                        HistoryItem.STATE_MOBILE_RADIO_ACTIVE_FLAG);
             }
-            if (DEBUG_HISTORY) Slog.v(TAG, "Mobile network active " + active + " to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
             mMobileRadioPowerState = powerState;
 
             // Inform current RatBatteryStats that the modem active state might have changed.
@@ -6043,17 +5544,14 @@
             mCurStepMode = (mCurStepMode&~STEP_LEVEL_MODE_POWER_SAVE) | stepState;
             mPowerSaveModeEnabled = enabled;
             if (enabled) {
-                mHistoryCur.states2 |= HistoryItem.STATE2_POWER_SAVE_FLAG;
-                if (DEBUG_HISTORY) Slog.v(TAG, "Power save mode enabled to: "
-                        + Integer.toHexString(mHistoryCur.states2));
+                mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
+                        HistoryItem.STATE2_POWER_SAVE_FLAG);
                 mPowerSaveModeEnabledTimer.startRunningLocked(elapsedRealtimeMs);
             } else {
-                mHistoryCur.states2 &= ~HistoryItem.STATE2_POWER_SAVE_FLAG;
-                if (DEBUG_HISTORY) Slog.v(TAG, "Power save mode disabled to: "
-                        + Integer.toHexString(mHistoryCur.states2));
+                mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                        HistoryItem.STATE2_POWER_SAVE_FLAG);
                 mPowerSaveModeEnabledTimer.stopRunningLocked(elapsedRealtimeMs);
             }
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
             FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED,
                     enabled
                         ? FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__ON
@@ -6077,7 +5575,7 @@
             nowLightIdling = true;
         }
         if (activeReason != null && (mDeviceIdling || mDeviceLightIdling)) {
-            addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_ACTIVE,
+            mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_ACTIVE,
                     activeReason, activeUid);
         }
         if (mDeviceIdling != nowIdling || mDeviceLightIdling != nowLightIdling) {
@@ -6107,11 +5605,7 @@
             }
         }
         if (mDeviceIdleMode != mode) {
-            mHistoryCur.states2 = (mHistoryCur.states2 & ~HistoryItem.STATE2_DEVICE_IDLE_MASK)
-                    | (mode << HistoryItem.STATE2_DEVICE_IDLE_SHIFT);
-            if (DEBUG_HISTORY) Slog.v(TAG, "Device idle mode changed to: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordDeviceIdleEvent(elapsedRealtimeMs, uptimeMs, mode);
             long lastDuration = elapsedRealtimeMs - mLastIdleTimeStartMs;
             mLastIdleTimeStartMs = elapsedRealtimeMs;
             if (mDeviceIdleMode == DEVICE_IDLE_MODE_LIGHT) {
@@ -6139,7 +5633,7 @@
     public void notePackageInstalledLocked(String pkgName, long versionCode,
             long elapsedRealtimeMs, long uptimeMs) {
         // XXX need to figure out what to do with long version codes.
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_PACKAGE_INSTALLED,
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_PACKAGE_INSTALLED,
                 pkgName, (int)versionCode);
         PackageChange pc = new PackageChange();
         pc.mPackageName = pkgName;
@@ -6151,8 +5645,8 @@
     @GuardedBy("this")
     public void notePackageUninstalledLocked(String pkgName,
             long elapsedRealtimeMs, long uptimeMs) {
-        addHistoryEventLocked(elapsedRealtimeMs, uptimeMs,
-                HistoryItem.EVENT_PACKAGE_UNINSTALLED, pkgName, 0);
+        mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.EVENT_PACKAGE_UNINSTALLED,
+                pkgName, 0);
         PackageChange pc = new PackageChange();
         pc.mPackageName = pkgName;
         pc.mUpdate = true;
@@ -6181,10 +5675,8 @@
     @GuardedBy("this")
     public void notePhoneOnLocked(long elapsedRealtimeMs, long uptimeMs) {
         if (!mPhoneOn) {
-            mHistoryCur.states2 |= HistoryItem.STATE2_PHONE_IN_CALL_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Phone on to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_PHONE_IN_CALL_FLAG);
             mPhoneOn = true;
             mPhoneOnTimer.startRunningLocked(elapsedRealtimeMs);
         }
@@ -6193,10 +5685,8 @@
     @GuardedBy("this")
     public void notePhoneOffLocked(long elapsedRealtimeMs, long uptimeMs) {
         if (mPhoneOn) {
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_PHONE_IN_CALL_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Phone off to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_PHONE_IN_CALL_FLAG);
             mPhoneOn = false;
             mPhoneOnTimer.stopRunningLocked(elapsedRealtimeMs);
         }
@@ -6234,11 +5724,12 @@
         if (mUsbDataState != newState) {
             mUsbDataState = newState;
             if (connected) {
-                mHistoryCur.states2 |= HistoryItem.STATE2_USB_DATA_LINK_FLAG;
+                mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
+                        HistoryItem.STATE2_USB_DATA_LINK_FLAG);
             } else {
-                mHistoryCur.states2 &= ~HistoryItem.STATE2_USB_DATA_LINK_FLAG;
+                mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                        HistoryItem.STATE2_USB_DATA_LINK_FLAG);
             }
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
         }
     }
 
@@ -6259,6 +5750,10 @@
             long elapsedRealtimeMs, long uptimeMs) {
         boolean scanning = false;
         boolean newHistory = false;
+        int addStateFlag = 0;
+        int removeStateFlag = 0;
+        int newState = -1;
+        int newSignalStrength = -1;
 
         mPhoneServiceStateRaw = state;
         mPhoneSimStateRaw = simState;
@@ -6287,10 +5782,8 @@
             scanning = true;
             strengthBin = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
             if (!mPhoneSignalScanningTimer.isRunningLocked()) {
-                mHistoryCur.states |= HistoryItem.STATE_PHONE_SCANNING_FLAG;
+                addStateFlag = HistoryItem.STATE_PHONE_SCANNING_FLAG;
                 newHistory = true;
-                if (DEBUG_HISTORY) Slog.v(TAG, "Phone started scanning to: "
-                        + Integer.toHexString(mHistoryCur.states));
                 mPhoneSignalScanningTimer.startRunningLocked(elapsedRealtimeMs);
                 FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state,
                         simState, strengthBin);
@@ -6300,9 +5793,7 @@
         if (!scanning) {
             // If we are no longer scanning, then stop the scanning timer.
             if (mPhoneSignalScanningTimer.isRunningLocked()) {
-                mHistoryCur.states &= ~HistoryItem.STATE_PHONE_SCANNING_FLAG;
-                if (DEBUG_HISTORY) Slog.v(TAG, "Phone stopped scanning to: "
-                        + Integer.toHexString(mHistoryCur.states));
+                removeStateFlag = HistoryItem.STATE_PHONE_SCANNING_FLAG;
                 newHistory = true;
                 mPhoneSignalScanningTimer.stopRunningLocked(elapsedRealtimeMs);
                 FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state,
@@ -6311,10 +5802,7 @@
         }
 
         if (mPhoneServiceState != state) {
-            mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_PHONE_STATE_MASK)
-                    | (state << HistoryItem.STATE_PHONE_STATE_SHIFT);
-            if (DEBUG_HISTORY) Slog.v(TAG, "Phone state " + state + " to: "
-                    + Integer.toHexString(mHistoryCur.states));
+            newState = state;
             newHistory = true;
             mPhoneServiceState = state;
         }
@@ -6328,11 +5816,7 @@
                 if (!mPhoneSignalStrengthsTimer[strengthBin].isRunningLocked()) {
                     mPhoneSignalStrengthsTimer[strengthBin].startRunningLocked(elapsedRealtimeMs);
                 }
-                mHistoryCur.states =
-                        (mHistoryCur.states & ~HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK)
-                        | (strengthBin << HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT);
-                if (DEBUG_HISTORY) Slog.v(TAG, "Signal strength " + strengthBin + " to: "
-                        + Integer.toHexString(mHistoryCur.states));
+                newSignalStrength = strengthBin;
                 newHistory = true;
                 FrameworkStatsLog.write(
                         FrameworkStatsLog.PHONE_SIGNAL_STRENGTH_CHANGED, strengthBin);
@@ -6343,7 +5827,8 @@
         }
 
         if (newHistory) {
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordPhoneStateChangeEvent(elapsedRealtimeMs, uptimeMs,
+                    addStateFlag, removeStateFlag, newState, newSignalStrength);
         }
     }
 
@@ -6467,11 +5952,7 @@
 
         if (DEBUG) Log.i(TAG, "Phone Data Connection -> " + dataType + " = " + hasData);
         if (mPhoneDataConnectionType != bin) {
-            mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_DATA_CONNECTION_MASK)
-                    | (bin << HistoryItem.STATE_DATA_CONNECTION_SHIFT);
-            if (DEBUG_HISTORY) Slog.v(TAG, "Data connection " + bin + " to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordDataConnectionTypeChangeEvent(elapsedRealtimeMs, uptimeMs, bin);
             if (mPhoneDataConnectionType >= 0) {
                 mPhoneDataConnectionsTimer[mPhoneDataConnectionType].stopRunningLocked(
                         elapsedRealtimeMs);
@@ -6544,10 +6025,8 @@
     @GuardedBy("this")
     public void noteWifiOnLocked(long elapsedRealtimeMs, long uptimeMs) {
         if (!mWifiOn) {
-            mHistoryCur.states2 |= HistoryItem.STATE2_WIFI_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI on to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_WIFI_ON_FLAG);
             mWifiOn = true;
             mWifiOnTimer.startRunningLocked(elapsedRealtimeMs);
             scheduleSyncExternalStatsLocked("wifi-off", ExternalStatsSync.UPDATE_WIFI);
@@ -6557,10 +6036,8 @@
     @GuardedBy("this")
     public void noteWifiOffLocked(long elapsedRealtimeMs, long uptimeMs) {
         if (mWifiOn) {
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_WIFI_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI off to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_WIFI_ON_FLAG);
             mWifiOn = false;
             mWifiOnTimer.stopRunningLocked(elapsedRealtimeMs);
             scheduleSyncExternalStatsLocked("wifi-on", ExternalStatsSync.UPDATE_WIFI);
@@ -6571,10 +6048,8 @@
     public void noteAudioOnLocked(int uid, long elapsedRealtimeMs, long uptimeMs) {
         uid = mapUid(uid);
         if (mAudioOnNesting == 0) {
-            mHistoryCur.states |= HistoryItem.STATE_AUDIO_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Audio on to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_AUDIO_ON_FLAG);
             mAudioOnTimer.startRunningLocked(elapsedRealtimeMs);
         }
         mAudioOnNesting++;
@@ -6589,10 +6064,8 @@
         }
         uid = mapUid(uid);
         if (--mAudioOnNesting == 0) {
-            mHistoryCur.states &= ~HistoryItem.STATE_AUDIO_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Audio off to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_AUDIO_ON_FLAG);
             mAudioOnTimer.stopRunningLocked(elapsedRealtimeMs);
         }
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
@@ -6603,10 +6076,8 @@
     public void noteVideoOnLocked(int uid, long elapsedRealtimeMs, long uptimeMs) {
         uid = mapUid(uid);
         if (mVideoOnNesting == 0) {
-            mHistoryCur.states2 |= HistoryItem.STATE2_VIDEO_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Video on to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_VIDEO_ON_FLAG);
             mVideoOnTimer.startRunningLocked(elapsedRealtimeMs);
         }
         mVideoOnNesting++;
@@ -6621,10 +6092,8 @@
         }
         uid = mapUid(uid);
         if (--mVideoOnNesting == 0) {
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_VIDEO_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Video off to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_VIDEO_ON_FLAG);
             mVideoOnTimer.stopRunningLocked(elapsedRealtimeMs);
         }
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
@@ -6635,10 +6104,8 @@
     public void noteResetAudioLocked(long elapsedRealtimeMs, long uptimeMs) {
         if (mAudioOnNesting > 0) {
             mAudioOnNesting = 0;
-            mHistoryCur.states &= ~HistoryItem.STATE_AUDIO_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Audio off to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_AUDIO_ON_FLAG);
             mAudioOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
             for (int i=0; i<mUidStats.size(); i++) {
                 BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
@@ -6651,10 +6118,8 @@
     public void noteResetVideoLocked(long elapsedRealtimeMs, long uptimeMs) {
         if (mVideoOnNesting > 0) {
             mVideoOnNesting = 0;
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_VIDEO_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Video off to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_VIDEO_ON_FLAG);
             mVideoOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
             for (int i=0; i<mUidStats.size(); i++) {
                 BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
@@ -6706,10 +6171,8 @@
     public void noteFlashlightOnLocked(int uid, long elapsedRealtimeMs, long uptimeMs) {
         uid = mapUid(uid);
         if (mFlashlightOnNesting++ == 0) {
-            mHistoryCur.states2 |= HistoryItem.STATE2_FLASHLIGHT_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Flashlight on to: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_FLASHLIGHT_FLAG);
             mFlashlightOnTimer.startRunningLocked(elapsedRealtimeMs);
         }
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
@@ -6723,10 +6186,8 @@
         }
         uid = mapUid(uid);
         if (--mFlashlightOnNesting == 0) {
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_FLASHLIGHT_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Flashlight off to: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_FLASHLIGHT_FLAG);
             mFlashlightOnTimer.stopRunningLocked(elapsedRealtimeMs);
         }
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
@@ -6737,10 +6198,8 @@
     public void noteCameraOnLocked(int uid, long elapsedRealtimeMs, long uptimeMs) {
         uid = mapUid(uid);
         if (mCameraOnNesting++ == 0) {
-            mHistoryCur.states2 |= HistoryItem.STATE2_CAMERA_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Camera on to: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_CAMERA_FLAG);
             mCameraOnTimer.startRunningLocked(elapsedRealtimeMs);
         }
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
@@ -6754,10 +6213,8 @@
         }
         uid = mapUid(uid);
         if (--mCameraOnNesting == 0) {
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_CAMERA_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Camera off to: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_CAMERA_FLAG);
             mCameraOnTimer.stopRunningLocked(elapsedRealtimeMs);
         }
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
@@ -6768,10 +6225,8 @@
     public void noteResetCameraLocked(long elapsedRealtimeMs, long uptimeMs) {
         if (mCameraOnNesting > 0) {
             mCameraOnNesting = 0;
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_CAMERA_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Camera off to: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_CAMERA_FLAG);
             mCameraOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
             for (int i=0; i<mUidStats.size(); i++) {
                 BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
@@ -6784,10 +6239,8 @@
     public void noteResetFlashlightLocked(long elapsedRealtimeMs, long uptimeMs) {
         if (mFlashlightOnNesting > 0) {
             mFlashlightOnNesting = 0;
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_FLASHLIGHT_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Flashlight off to: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_FLASHLIGHT_FLAG);
             mFlashlightOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
             for (int i=0; i<mUidStats.size(); i++) {
                 BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
@@ -6804,10 +6257,8 @@
         }
         uid = mapUid(uid);
         if (mBluetoothScanNesting == 0) {
-            mHistoryCur.states2 |= HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "BLE scan started for: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG);
             mBluetoothScanTimer.startRunningLocked(elapsedRealtimeMs);
         }
         mBluetoothScanNesting++;
@@ -6848,10 +6299,8 @@
         uid = mapUid(uid);
         mBluetoothScanNesting--;
         if (mBluetoothScanNesting == 0) {
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "BLE scan stopped for: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG);
             mBluetoothScanTimer.stopRunningLocked(elapsedRealtimeMs);
         }
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
@@ -6886,10 +6335,8 @@
     public void noteResetBluetoothScanLocked(long elapsedRealtimeMs, long uptimeMs) {
         if (mBluetoothScanNesting > 0) {
             mBluetoothScanNesting = 0;
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "BLE can stopped for: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG);
             mBluetoothScanTimer.stopAllRunningLocked(elapsedRealtimeMs);
             for (int i=0; i<mUidStats.size(); i++) {
                 BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
@@ -6929,7 +6376,7 @@
     private void noteWifiRadioApWakeupLocked(final long elapsedRealtimeMillis,
             final long uptimeMillis, int uid) {
         uid = mapUid(uid);
-        addHistoryEventLocked(elapsedRealtimeMillis, uptimeMillis, HistoryItem.EVENT_WAKEUP_AP, "",
+        mHistory.recordEvent(elapsedRealtimeMillis, uptimeMillis, HistoryItem.EVENT_WAKEUP_AP, "",
                 uid);
         getUidStatsLocked(uid, elapsedRealtimeMillis, uptimeMillis).noteWifiRadioApWakeupLocked();
     }
@@ -6945,15 +6392,14 @@
                 if (uid > 0) {
                     noteWifiRadioApWakeupLocked(elapsedRealtimeMs, uptimeMs, uid);
                 }
-                mHistoryCur.states |= HistoryItem.STATE_WIFI_RADIO_ACTIVE_FLAG;
+                mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs,
+                        HistoryItem.STATE_WIFI_RADIO_ACTIVE_FLAG);
                 mWifiActiveTimer.startRunningLocked(elapsedRealtimeMs);
             } else {
-                mHistoryCur.states &= ~HistoryItem.STATE_WIFI_RADIO_ACTIVE_FLAG;
+                mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                        HistoryItem.STATE_WIFI_RADIO_ACTIVE_FLAG);
                 mWifiActiveTimer.stopRunningLocked(timestampNs / (1000 * 1000));
             }
-            if (DEBUG_HISTORY) Slog.v(TAG, "Wifi network active " + active + " to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
             mWifiRadioPowerState = powerState;
         }
     }
@@ -6961,10 +6407,8 @@
     @GuardedBy("this")
     public void noteWifiRunningLocked(WorkSource ws, long elapsedRealtimeMs, long uptimeMs) {
         if (!mGlobalWifiRunning) {
-            mHistoryCur.states2 |= HistoryItem.STATE2_WIFI_RUNNING_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI running to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_WIFI_RUNNING_FLAG);
             mGlobalWifiRunning = true;
             mGlobalWifiRunningTimer.startRunningLocked(elapsedRealtimeMs);
             int N = ws.size();
@@ -7032,10 +6476,8 @@
     @GuardedBy("this")
     public void noteWifiStoppedLocked(WorkSource ws, long elapsedRealtimeMs, long uptimeMs) {
         if (mGlobalWifiRunning) {
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_WIFI_RUNNING_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI stopped to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_WIFI_RUNNING_FLAG);
             mGlobalWifiRunning = false;
             mGlobalWifiRunningTimer.stopRunningLocked(elapsedRealtimeMs);
             int N = ws.size();
@@ -7083,12 +6525,7 @@
             }
             mWifiSupplState = supplState;
             mWifiSupplStateTimer[supplState].startRunningLocked(elapsedRealtimeMs);
-            mHistoryCur.states2 =
-                    (mHistoryCur.states2&~HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK)
-                    | (supplState << HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT);
-            if (DEBUG_HISTORY) Slog.v(TAG, "Wifi suppl state " + supplState + " to: "
-                    + Integer.toHexString(mHistoryCur.states2));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordWifiSupplicantStateChangeEvent(elapsedRealtimeMs, uptimeMs, supplState);
         }
     }
 
@@ -7117,12 +6554,8 @@
                 if (!mWifiSignalStrengthsTimer[strengthBin].isRunningLocked()) {
                     mWifiSignalStrengthsTimer[strengthBin].startRunningLocked(elapsedRealtimeMs);
                 }
-                mHistoryCur.states2 =
-                        (mHistoryCur.states2&~HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK)
-                        | (strengthBin << HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT);
-                if (DEBUG_HISTORY) Slog.v(TAG, "Wifi signal strength " + strengthBin + " to: "
-                        + Integer.toHexString(mHistoryCur.states2));
-                addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+                mHistory.recordWifiSignalStrengthChangeEvent(elapsedRealtimeMs, uptimeMs,
+                        strengthBin);
             } else {
                 stopAllWifiSignalStrengthTimersLocked(-1, elapsedRealtimeMs);
             }
@@ -7135,10 +6568,8 @@
     @GuardedBy("this")
     public void noteFullWifiLockAcquiredLocked(int uid, long elapsedRealtimeMs, long uptimeMs) {
         if (mWifiFullLockNesting == 0) {
-            mHistoryCur.states |= HistoryItem.STATE_WIFI_FULL_LOCK_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI full lock on to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_WIFI_FULL_LOCK_FLAG);
         }
         mWifiFullLockNesting++;
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
@@ -7149,10 +6580,8 @@
     public void noteFullWifiLockReleasedLocked(int uid, long elapsedRealtimeMs, long uptimeMs) {
         mWifiFullLockNesting--;
         if (mWifiFullLockNesting == 0) {
-            mHistoryCur.states &= ~HistoryItem.STATE_WIFI_FULL_LOCK_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI full lock off to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_WIFI_FULL_LOCK_FLAG);
         }
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
                 .noteFullWifiLockReleasedLocked(elapsedRealtimeMs);
@@ -7168,10 +6597,8 @@
     @GuardedBy("this")
     public void noteWifiScanStartedLocked(int uid, long elapsedRealtimeMs, long uptimeMs) {
         if (mWifiScanNesting == 0) {
-            mHistoryCur.states |= HistoryItem.STATE_WIFI_SCAN_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI scan started for: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_WIFI_SCAN_FLAG);
         }
         mWifiScanNesting++;
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
@@ -7187,10 +6614,8 @@
     public void noteWifiScanStoppedLocked(int uid, long elapsedRealtimeMs, long uptimeMs) {
         mWifiScanNesting--;
         if (mWifiScanNesting == 0) {
-            mHistoryCur.states &= ~HistoryItem.STATE_WIFI_SCAN_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI scan stopped for: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_WIFI_SCAN_FLAG);
         }
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
                 .noteWifiScanStoppedLocked(elapsedRealtimeMs);
@@ -7215,14 +6640,10 @@
     public void noteWifiMulticastEnabledLocked(int uid, long elapsedRealtimeMs, long uptimeMs) {
         uid = mapUid(uid);
         if (mWifiMulticastNesting == 0) {
-            mHistoryCur.states |= HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI multicast on to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
-
+            mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG);
             // Start Wifi Multicast overall timer
             if (!mWifiMulticastWakelockTimer.isRunningLocked()) {
-                if (DEBUG_HISTORY) Slog.v(TAG, "WiFi Multicast Overall Timer Started");
                 mWifiMulticastWakelockTimer.startRunningLocked(elapsedRealtimeMs);
             }
         }
@@ -7236,14 +6657,12 @@
         uid = mapUid(uid);
         mWifiMulticastNesting--;
         if (mWifiMulticastNesting == 0) {
-            mHistoryCur.states &= ~HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "WIFI multicast off to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG);
 
             // Stop Wifi Multicast overall timer
             if (mWifiMulticastWakelockTimer.isRunningLocked()) {
-                if (DEBUG_HISTORY) Slog.v(TAG, "Multicast Overall Timer Stopped");
+                if (DEBUG) Slog.v(TAG, "Multicast Overall Timer Stopped");
                 mWifiMulticastWakelockTimer.stopRunningLocked(elapsedRealtimeMs);
             }
         }
@@ -7995,8 +7414,9 @@
             // If the start clock time has changed by more than a year, then presumably
             // the previous time was completely bogus.  So we are going to figure out a
             // new time based on how much time has elapsed since we started counting.
-            recordCurrentTimeChangeLocked(currentTimeMs, mClock.elapsedRealtime(),
-                    mClock.uptimeMillis());
+            mHistory.recordCurrentTimeChange(mClock.elapsedRealtime(), mClock.uptimeMillis(),
+                    currentTimeMs
+            );
             return currentTimeMs - (mClock.elapsedRealtime() - (mRealtimeStartUs / 1000));
         }
         return mStartClockTimeMs;
@@ -11228,18 +10648,19 @@
             UserInfoProvider userInfoProvider) {
         init(clock);
 
+        mHandler = new MyHandler(handler.getLooper());
+        mConstants = new Constants(mHandler);
+
         if (systemDir == null) {
             mStatsFile = null;
-            mBatteryStatsHistory = new BatteryStatsHistory(mHistoryBuffer);
+            mHistory = new BatteryStatsHistory(mStepDetailsCalculator, mClock);
         } else {
             mStatsFile = new AtomicFile(new File(systemDir, "batterystats.bin"));
-            mBatteryStatsHistory = new BatteryStatsHistory(mHistoryBuffer, systemDir,
-                    this::getMaxHistoryFiles);
+            mHistory = new BatteryStatsHistory(systemDir, mConstants.MAX_HISTORY_FILES,
+                    mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock);
         }
         mCheckinFile = new AtomicFile(new File(systemDir, "batterystats-checkin.bin"));
         mDailyFile = new AtomicFile(new File(systemDir, "batterystats-daily.xml"));
-        mHandler = new MyHandler(handler.getLooper());
-        mConstants = new Constants(mHandler);
         mStartCount++;
         initTimersAndCounters();
         mOnBattery = mOnBatteryInternal = false;
@@ -11248,7 +10669,6 @@
         initTimes(uptimeUs, realtimeUs);
         mStartPlatformVersion = mEndPlatformVersion = Build.ID;
         initDischarge(realtimeUs);
-        clearHistoryLocked();
         updateDailyDeadlineLocked();
         mPlatformIdleStateCallback = cb;
         mMeasuredEnergyRetriever = energyStatsCb;
@@ -11259,12 +10679,6 @@
         FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mDeviceIdleMode);
     }
 
-    private int getMaxHistoryFiles() {
-        synchronized (this) {
-            return mConstants.MAX_HISTORY_FILES;
-        }
-    }
-
     @VisibleForTesting
     protected void initTimersAndCounters() {
         mScreenOnTimer = new StopwatchTimer(mClock, null, -1, null, mOnBatteryTimeBase);
@@ -11346,7 +10760,7 @@
         mDischargeUnplugLevel = 0;
         mDischargePlugLevel = -1;
         mDischargeCurrentLevel = 0;
-        mCurrentBatteryLevel = 0;
+        mBatteryLevel = 0;
     }
 
     public void setPowerProfileLocked(PowerProfile profile) {
@@ -11733,7 +11147,7 @@
     }
 
     public int getHistoryUsedSize() {
-        return mBatteryStatsHistory.getHistoryUsedSize();
+        return mHistory.getHistoryUsedSize();
     }
 
     @Override
@@ -11747,43 +11161,27 @@
      */
     @VisibleForTesting
     public BatteryStatsHistoryIterator createBatteryStatsHistoryIterator() {
-        return new BatteryStatsHistoryIterator(mBatteryStatsHistory);
+        return mHistory.iterate();
     }
 
     @Override
     public int getHistoryStringPoolSize() {
-        return mHistoryTagPool.size();
+        return mHistory.getHistoryStringPoolSize();
     }
 
     @Override
     public int getHistoryStringPoolBytes() {
-        return mNumHistoryTagChars;
+        return mHistory.getHistoryStringPoolBytes();
     }
 
     @Override
     public String getHistoryTagPoolString(int index) {
-        ensureHistoryTagArray();
-        HistoryTag historyTag = mHistoryTags.get(index);
-        return historyTag != null ? historyTag.string : null;
+        return mHistory.getHistoryTagPoolString(index);
     }
 
     @Override
     public int getHistoryTagPoolUid(int index) {
-        ensureHistoryTagArray();
-        HistoryTag historyTag = mHistoryTags.get(index);
-        return historyTag != null ? historyTag.uid : Process.INVALID_UID;
-    }
-
-    private void ensureHistoryTagArray() {
-        if (mHistoryTags != null) {
-            return;
-        }
-
-        mHistoryTags = new SparseArray<>(mHistoryTagPool.size());
-        for (Map.Entry<HistoryTag, Integer> entry: mHistoryTagPool.entrySet()) {
-            mHistoryTags.put(entry.getValue() & ~BatteryStatsHistory.TAG_FIRST_OCCURRENCE_FLAG,
-                    entry.getKey());
-        }
+        return mHistory.getHistoryTagPoolUid(index);
     }
 
     @Override
@@ -11793,15 +11191,11 @@
 
     @Override
     public void finishIteratingHistoryLocked() {
+        mBatteryStatsHistoryIterator.close();
         mBatteryStatsHistoryIterator = null;
     }
 
     @Override
-    public long getHistoryBaseTime() {
-        return mHistoryBaseTimeMs;
-    }
-
-    @Override
     public int getStartCount() {
         return mStartCount;
     }
@@ -11854,24 +11248,23 @@
         long realtimeUs = mSecRealtime * 1000;
         resetAllStatsLocked(mSecUptime, mSecRealtime, RESET_REASON_ADB_COMMAND);
         pullPendingStateUpdatesLocked();
-        addHistoryRecordLocked(mSecRealtime, mSecUptime);
-        mDischargeCurrentLevel = mDischargeUnplugLevel = mDischargePlugLevel
-                = mCurrentBatteryLevel = mHistoryCur.batteryLevel;
+        mHistory.writeHistoryItem(mSecRealtime, mSecUptime);
+        mDischargeCurrentLevel = mDischargeUnplugLevel = mDischargePlugLevel = mBatteryLevel;
         mOnBatteryTimeBase.reset(uptimeUs, realtimeUs);
         mOnBatteryScreenOffTimeBase.reset(uptimeUs, realtimeUs);
-        if ((mHistoryCur.states&HistoryItem.STATE_BATTERY_PLUGGED_FLAG) == 0) {
+        if (!mBatteryPluggedIn) {
             if (Display.isOnState(mScreenState)) {
-                mDischargeScreenOnUnplugLevel = mHistoryCur.batteryLevel;
+                mDischargeScreenOnUnplugLevel = mBatteryLevel;
                 mDischargeScreenDozeUnplugLevel = 0;
                 mDischargeScreenOffUnplugLevel = 0;
             } else if (Display.isDozeState(mScreenState)) {
                 mDischargeScreenOnUnplugLevel = 0;
-                mDischargeScreenDozeUnplugLevel = mHistoryCur.batteryLevel;
+                mDischargeScreenDozeUnplugLevel = mBatteryLevel;
                 mDischargeScreenOffUnplugLevel = 0;
             } else {
                 mDischargeScreenOnUnplugLevel = 0;
                 mDischargeScreenDozeUnplugLevel = 0;
-                mDischargeScreenOffUnplugLevel = mHistoryCur.batteryLevel;
+                mDischargeScreenOffUnplugLevel = mBatteryLevel;
             }
             mDischargeAmountScreenOn = 0;
             mDischargeAmountScreenOff = 0;
@@ -12015,27 +11408,12 @@
 
         resetIfNotNull(mBinderThreadCpuTimesUs, false, elapsedRealtimeUs);
 
-        mLastHistoryStepDetails = null;
-        mLastStepCpuUserTimeMs = mLastStepCpuSystemTimeMs = 0;
-        mCurStepCpuUserTimeMs = mCurStepCpuSystemTimeMs = 0;
-        mLastStepCpuUserTimeMs = mCurStepCpuUserTimeMs = 0;
-        mLastStepCpuSystemTimeMs = mCurStepCpuSystemTimeMs = 0;
-        mLastStepStatUserTimeMs = mCurStepStatUserTimeMs = 0;
-        mLastStepStatSystemTimeMs = mCurStepStatSystemTimeMs = 0;
-        mLastStepStatIOWaitTimeMs = mCurStepStatIOWaitTimeMs = 0;
-        mLastStepStatIrqTimeMs = mCurStepStatIrqTimeMs = 0;
-        mLastStepStatSoftIrqTimeMs = mCurStepStatSoftIrqTimeMs = 0;
-        mLastStepStatIdleTimeMs = mCurStepStatIdleTimeMs = 0;
-
         mNumAllUidCpuTimeReads = 0;
         mNumUidsRemoved = 0;
 
         initDischarge(elapsedRealtimeUs);
 
-        clearHistoryLocked();
-        if (mBatteryStatsHistory != null) {
-            mBatteryStatsHistory.resetAllFiles();
-        }
+        mHistory.reset();
 
         // Flush external data, gathering snapshots, but don't process it since it is pre-reset data
         mIgnoreNextExternalStats = true;
@@ -12058,7 +11436,7 @@
             for (HashMap.Entry<String, SparseIntArray> ent : active.entrySet()) {
                 SparseIntArray uids = ent.getValue();
                 for (int j=0; j<uids.size(); j++) {
-                    addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, i, ent.getKey(),
+                    mHistory.recordEvent(elapsedRealtimeMs, uptimeMs, i, ent.getKey(),
                             uids.keyAt(j));
                 }
             }
@@ -12483,9 +11861,8 @@
                         (long) (mTmpRailStats.getWifiTotalEnergyUseduWs() / opVolt);
                 mWifiActivity.getMonitoredRailChargeConsumedMaMs().addCountLocked(
                         monitoredRailChargeConsumedMaMs);
-                mHistoryCur.wifiRailChargeMah +=
-                        (monitoredRailChargeConsumedMaMs / MILLISECONDS_IN_HOUR);
-                addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+                mHistory.recordWifiConsumedCharge(elapsedRealtimeMs, uptimeMs,
+                        (monitoredRailChargeConsumedMaMs / MILLISECONDS_IN_HOUR));
                 mTmpRailStats.resetWifiTotalEnergyUsed();
 
                 if (uidEstimatedConsumptionMah != null) {
@@ -12598,9 +11975,8 @@
                             (long) (mTmpRailStats.getCellularTotalEnergyUseduWs() / opVolt);
                     mModemActivity.getMonitoredRailChargeConsumedMaMs().addCountLocked(
                             monitoredRailChargeConsumedMaMs);
-                    mHistoryCur.modemRailChargeMah +=
-                            (monitoredRailChargeConsumedMaMs / MILLISECONDS_IN_HOUR);
-                    addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+                    mHistory.recordWifiConsumedCharge(elapsedRealtimeMs, uptimeMs,
+                            (monitoredRailChargeConsumedMaMs / MILLISECONDS_IN_HOUR));
                     mTmpRailStats.resetCellularTotalEnergyUsed();
                 }
 
@@ -12868,8 +12244,8 @@
             }
         }
         if (levelMaxTimeSpent == ModemActivityInfo.getNumTxPowerLevels() - 1) {
-            mHistoryCur.states2 |= HistoryItem.STATE2_CELLULAR_HIGH_TX_POWER_FLAG;
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs,
+                    HistoryItem.STATE2_CELLULAR_HIGH_TX_POWER_FLAG);
         }
     }
 
@@ -14302,11 +13678,7 @@
         mHandler.removeCallbacks(mDeferSetCharging);
         if (mCharging != charging) {
             mCharging = charging;
-            if (charging) {
-                mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG;
-            } else {
-                mHistoryCur.states2 &= ~HistoryItem.STATE2_CHARGING_FLAG;
-            }
+            mHistory.setChargingState(charging);
             mHandler.sendEmptyMessage(MSG_REPORT_CHARGING);
             return true;
         }
@@ -14320,6 +13692,15 @@
         mSystemReady = true;
     }
 
+    /**
+     * Force recording of all history events regardless of the "charging" state.
+     */
+    @VisibleForTesting
+    public void forceRecordAllHistory() {
+        mHistory.forceRecordAllHistory();
+        mRecordAllHistory = true;
+    }
+
     @GuardedBy("this")
     protected void setOnBatteryLocked(final long mSecRealtime, final long mSecUptime,
             final boolean onBattery, final int oldStatus, final int level, final int chargeUah) {
@@ -14403,15 +13784,12 @@
             mInitStepMode = mCurStepMode;
             mModStepMode = 0;
             pullPendingStateUpdatesLocked();
-            mHistoryCur.batteryLevel = (byte)level;
-            mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Battery unplugged to: "
-                    + Integer.toHexString(mHistoryCur.states));
             if (reset) {
-                mRecordingHistory = true;
-                startRecordingHistory(mSecRealtime, mSecUptime, reset);
+                mHistory.startRecordingHistory(mSecRealtime, mSecUptime, reset);
+                initActiveHistoryEventsLocked(mSecRealtime, mSecUptime);
             }
-            addHistoryRecordLocked(mSecRealtime, mSecUptime);
+            mBatteryPluggedIn = false;
+            mHistory.recordBatteryState(mSecRealtime, mSecUptime, level, mBatteryPluggedIn);
             mDischargeCurrentLevel = mDischargeUnplugLevel = level;
             if (Display.isOnState(screenState)) {
                 mDischargeScreenOnUnplugLevel = level;
@@ -14433,11 +13811,8 @@
         } else {
             mOnBattery = mOnBatteryInternal = false;
             pullPendingStateUpdatesLocked();
-            mHistoryCur.batteryLevel = (byte)level;
-            mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
-            if (DEBUG_HISTORY) Slog.v(TAG, "Battery plugged to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(mSecRealtime, mSecUptime);
+            mBatteryPluggedIn = true;
+            mHistory.recordBatteryState(mSecRealtime, mSecUptime, level, mBatteryPluggedIn);
             mDischargeCurrentLevel = mDischargePlugLevel = level;
             if (level < mDischargeUnplugLevel) {
                 mLowDischargeAmountSinceCharge += mDischargeUnplugLevel-level-1;
@@ -14452,45 +13827,12 @@
             mModStepMode = 0;
         }
         if (doWrite || (mLastWriteTimeMs + (60 * 1000)) < mSecRealtime) {
-            if (mStatsFile != null && mBatteryStatsHistory.getActiveFile() != null) {
+            if (mStatsFile != null && !mHistory.isReadOnly()) {
                 writeAsyncLocked();
             }
         }
     }
 
-    @GuardedBy("this")
-    private void startRecordingHistory(final long elapsedRealtimeMs, final long uptimeMs,
-            boolean reset) {
-        mRecordingHistory = true;
-        mHistoryCur.currentTime = mClock.currentTimeMillis();
-        addHistoryBufferLocked(elapsedRealtimeMs,
-                reset ? HistoryItem.CMD_RESET : HistoryItem.CMD_CURRENT_TIME,
-                mHistoryCur);
-        mHistoryCur.currentTime = 0;
-        if (reset) {
-            initActiveHistoryEventsLocked(elapsedRealtimeMs, uptimeMs);
-        }
-    }
-
-    @GuardedBy("this")
-    private void recordCurrentTimeChangeLocked(final long currentTimeMs,
-            final long elapsedRealtimeMs, final long uptimeMs) {
-        if (mRecordingHistory) {
-            mHistoryCur.currentTime = currentTimeMs;
-            addHistoryBufferLocked(elapsedRealtimeMs, HistoryItem.CMD_CURRENT_TIME, mHistoryCur);
-            mHistoryCur.currentTime = 0;
-        }
-    }
-
-    @GuardedBy("this")
-    private void recordShutdownLocked(final long currentTimeMs, final long elapsedRealtimeMs) {
-        if (mRecordingHistory) {
-            mHistoryCur.currentTime = currentTimeMs;
-            addHistoryBufferLocked(elapsedRealtimeMs, HistoryItem.CMD_SHUTDOWN, mHistoryCur);
-            mHistoryCur.currentTime = 0;
-        }
-    }
-
     private void scheduleSyncExternalStatsLocked(String reason, int updateFlags) {
         if (mExternalSync != null) {
             mExternalSync.scheduleSync(reason, updateFlags);
@@ -14508,8 +13850,7 @@
         // Temperature is encoded without the signed bit, so clamp any negative temperatures to 0.
         temp = Math.max(0, temp);
 
-        reportChangesToStatsLog(mHaveBatteryLevel ? mHistoryCur : null,
-                status, plugType, level);
+        reportChangesToStatsLog(status, plugType, level);
 
         final boolean onBattery = isOnBattery(plugType, status);
         if (!mHaveBatteryLevel) {
@@ -14519,52 +13860,47 @@
             // plugged in, then twiddle our state to correctly reflect that
             // since we won't be going through the full setOnBattery().
             if (onBattery == mOnBattery) {
-                if (onBattery) {
-                    mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
-                } else {
-                    mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
-                }
+                mHistory.setPluggedInState(!onBattery);
             }
+            mBatteryStatus = status;
+            mBatteryLevel = level;
+            mBatteryChargeUah = chargeUah;
+
             // Always start out assuming charging, that will be updated later.
-            mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG;
-            mHistoryCur.batteryStatus = (byte)status;
-            mHistoryCur.batteryLevel = (byte)level;
-            mHistoryCur.batteryChargeUah = chargeUah;
+            mHistory.setBatteryState(true /* charging */, status, level, chargeUah);
+
             mMaxChargeStepLevel = mMinDischargeStepLevel =
                     mLastChargeStepLevel = mLastDischargeStepLevel = level;
-        } else if (mCurrentBatteryLevel != level || mOnBattery != onBattery) {
+        } else if (mBatteryLevel != level || mOnBattery != onBattery) {
             recordDailyStatsIfNeededLocked(level >= 100 && onBattery, currentTimeMs);
         }
-        int oldStatus = mHistoryCur.batteryStatus;
+        int oldStatus = mBatteryStatus;
         if (onBattery) {
             mDischargeCurrentLevel = level;
-            if (!mRecordingHistory) {
-                mRecordingHistory = true;
-                startRecordingHistory(elapsedRealtimeMs, uptimeMs, true);
+            if (!mHistory.isRecordingHistory()) {
+                mHistory.startRecordingHistory(elapsedRealtimeMs, uptimeMs, true);
             }
         } else if (level < 96 &&
                 status != BatteryManager.BATTERY_STATUS_UNKNOWN) {
-            if (!mRecordingHistory) {
-                mRecordingHistory = true;
-                startRecordingHistory(elapsedRealtimeMs, uptimeMs, true);
+            if (!mHistory.isRecordingHistory()) {
+                mHistory.startRecordingHistory(elapsedRealtimeMs, uptimeMs, true);
             }
         }
-        mBatteryVoltageMv = voltageMv;
-        mCurrentBatteryLevel = level;
         if (mDischargePlugLevel < 0) {
             mDischargePlugLevel = level;
         }
 
         if (onBattery != mOnBattery) {
-            mHistoryCur.batteryLevel = (byte)level;
-            mHistoryCur.batteryStatus = (byte)status;
-            mHistoryCur.batteryHealth = (byte)health;
-            mHistoryCur.batteryPlugType = (byte)plugType;
-            mHistoryCur.batteryTemperature = (short)temp;
-            mHistoryCur.batteryVoltage = (char) voltageMv;
-            if (chargeUah < mHistoryCur.batteryChargeUah) {
+            mBatteryLevel = level;
+            mBatteryStatus = status;
+            mBatteryHealth = health;
+            mBatteryPlugType = plugType;
+            mBatteryTemperature = temp;
+            mBatteryVoltageMv = voltageMv;
+            mHistory.setBatteryState(status, level, health, plugType, temp, voltageMv, chargeUah);
+            if (chargeUah < mBatteryChargeUah) {
                 // Only record discharges
-                final long chargeDiff = mHistoryCur.batteryChargeUah - chargeUah;
+                final long chargeDiff = (long) mBatteryChargeUah - chargeUah;
                 mDischargeCounter.addCountLocked(chargeDiff);
                 mDischargeScreenOffCounter.addCountLocked(chargeDiff);
                 if (Display.isDozeState(mScreenState)) {
@@ -14576,12 +13912,12 @@
                     mDischargeDeepDozeCounter.addCountLocked(chargeDiff);
                 }
             }
-            mHistoryCur.batteryChargeUah = chargeUah;
+            mBatteryChargeUah = chargeUah;
             setOnBatteryLocked(elapsedRealtimeMs, uptimeMs, onBattery, oldStatus, level, chargeUah);
         } else {
             boolean changed = false;
-            if (mHistoryCur.batteryLevel != level) {
-                mHistoryCur.batteryLevel = (byte)level;
+            if (mBatteryLevel != level) {
+                mBatteryLevel = level;
                 changed = true;
 
                 // TODO(adamlesinski): Schedule the creation of a HistoryStepDetails record
@@ -14589,33 +13925,33 @@
                 mExternalSync.scheduleSyncDueToBatteryLevelChange(
                         mConstants.BATTERY_LEVEL_COLLECTION_DELAY_MS);
             }
-            if (mHistoryCur.batteryStatus != status) {
-                mHistoryCur.batteryStatus = (byte)status;
+            if (mBatteryStatus != status) {
+                mBatteryStatus = status;
                 changed = true;
             }
-            if (mHistoryCur.batteryHealth != health) {
-                mHistoryCur.batteryHealth = (byte)health;
+            if (mBatteryHealth != health) {
+                mBatteryHealth = health;
                 changed = true;
             }
-            if (mHistoryCur.batteryPlugType != plugType) {
-                mHistoryCur.batteryPlugType = (byte)plugType;
+            if (mBatteryPlugType != plugType) {
+                mBatteryPlugType = plugType;
                 changed = true;
             }
-            if (temp >= (mHistoryCur.batteryTemperature+10)
-                    || temp <= (mHistoryCur.batteryTemperature-10)) {
-                mHistoryCur.batteryTemperature = (short)temp;
+            if (temp >= (mBatteryTemperature + 10)
+                    || temp <= (mBatteryTemperature - 10)) {
+                mBatteryTemperature = temp;
                 changed = true;
             }
-            if (voltageMv > (mHistoryCur.batteryVoltage + 20)
-                    || voltageMv < (mHistoryCur.batteryVoltage - 20)) {
-                mHistoryCur.batteryVoltage = (char) voltageMv;
+            if (voltageMv > (mBatteryVoltageMv + 20)
+                    || voltageMv < (mBatteryVoltageMv - 20)) {
+                mBatteryVoltageMv = voltageMv;
                 changed = true;
             }
-            if (chargeUah >= (mHistoryCur.batteryChargeUah + 10)
-                    || chargeUah <= (mHistoryCur.batteryChargeUah - 10)) {
-                if (chargeUah < mHistoryCur.batteryChargeUah) {
+            if (chargeUah >= (mBatteryChargeUah + 10)
+                    || chargeUah <= (mBatteryChargeUah - 10)) {
+                if (chargeUah < mBatteryChargeUah) {
                     // Only record discharges
-                    final long chargeDiff = mHistoryCur.batteryChargeUah - chargeUah;
+                    final long chargeDiff = (long) mBatteryChargeUah - chargeUah;
                     mDischargeCounter.addCountLocked(chargeDiff);
                     mDischargeScreenOffCounter.addCountLocked(chargeDiff);
                     if (Display.isDozeState(mScreenState)) {
@@ -14627,9 +13963,10 @@
                         mDischargeDeepDozeCounter.addCountLocked(chargeDiff);
                     }
                 }
-                mHistoryCur.batteryChargeUah = chargeUah;
+                mBatteryChargeUah = chargeUah;
                 changed = true;
             }
+
             long modeBits = (((long)mInitStepMode) << STEP_LEVEL_INITIAL_MODE_SHIFT)
                     | (((long)mModStepMode) << STEP_LEVEL_MODIFIED_MODE_SHIFT)
                     | (((long)(level&0xff)) << STEP_LEVEL_LEVEL_SHIFT);
@@ -14687,7 +14024,10 @@
                 mLastChargeStepLevel = level;
             }
             if (changed) {
-                addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+                mHistory.setBatteryState(mBatteryStatus, mBatteryLevel, mBatteryHealth,
+                        mBatteryPlugType, mBatteryTemperature, mBatteryVoltageMv,
+                        mBatteryChargeUah);
+                mHistory.writeHistoryItem(elapsedRealtimeMs, uptimeMs);
             }
         }
         if (!onBattery &&
@@ -14696,7 +14036,7 @@
             // We don't record history while we are plugged in and fully charged
             // (or when battery is not present).  The next time we are
             // unplugged, history will be cleared.
-            mRecordingHistory = DEBUG;
+            mHistory.setHistoryRecordingEnabled(DEBUG);
         }
 
         mLastLearnedBatteryCapacityUah = chargeFullUah;
@@ -14715,17 +14055,18 @@
     }
 
     // Inform StatsLog of setBatteryState changes.
-    // If this is the first reporting, pass in recentPast == null.
-    private void reportChangesToStatsLog(HistoryItem recentPast,
-            final int status, final int plugType, final int level) {
+    private void reportChangesToStatsLog(final int status, final int plugType, final int level) {
+        if (!mHaveBatteryLevel) {
+            return;
+        }
 
-        if (recentPast == null || recentPast.batteryStatus != status) {
+        if (mBatteryStatus != status) {
             FrameworkStatsLog.write(FrameworkStatsLog.CHARGING_STATE_CHANGED, status);
         }
-        if (recentPast == null || recentPast.batteryPlugType != plugType) {
+        if (mBatteryPlugType != plugType) {
             FrameworkStatsLog.write(FrameworkStatsLog.PLUGGED_STATE_CHANGED, plugType);
         }
-        if (recentPast == null || recentPast.batteryLevel != level) {
+        if (mBatteryLevel != level) {
             FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_LEVEL_CHANGED, level);
         }
     }
@@ -14795,7 +14136,7 @@
         if (msPerLevel <= 0) {
             return -1;
         }
-        return (msPerLevel * mCurrentBatteryLevel) * 1000;
+        return (msPerLevel * mBatteryLevel) * 1000;
     }
 
     @Override
@@ -14825,7 +14166,7 @@
         if (msPerLevel <= 0) {
             return -1;
         }
-        return (msPerLevel * (100 - mCurrentBatteryLevel)) * 1000;
+        return (msPerLevel * (100 - mBatteryLevel)) * 1000;
     }
 
     /*@hide */
@@ -15256,7 +14597,8 @@
 
     @GuardedBy("this")
     public void shutdownLocked() {
-        recordShutdownLocked(mClock.currentTimeMillis(), mClock.elapsedRealtime());
+        mHistory.recordShutdownEvent(mClock.elapsedRealtime(), mClock.uptimeMillis(),
+                mClock.currentTimeMillis());
         writeSyncLocked();
         mShuttingDown = true;
     }
@@ -15464,7 +14806,6 @@
                 PROC_STATE_CHANGE_COLLECTION_DELAY_MS = mParser.getLong(
                         KEY_PROC_STATE_CHANGE_COLLECTION_DELAY_MS,
                         DEFAULT_PROC_STATE_CHANGE_COLLECTION_DELAY_MS);
-
                 MAX_HISTORY_FILES = mParser.getInt(KEY_MAX_HISTORY_FILES,
                         ActivityManager.isLowRamDeviceStatic() ?
                                 DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE
@@ -15475,9 +14816,20 @@
                                 : DEFAULT_MAX_HISTORY_BUFFER_KB)
                         * 1024;
                 updateBatteryChargedDelayMsLocked();
+
+                onChange();
             }
         }
 
+        /**
+         * Propagates changes in constant values.
+         */
+        @VisibleForTesting
+        public void onChange() {
+            mHistory.setMaxHistoryFiles(MAX_HISTORY_FILES);
+            mHistory.setMaxHistoryBufferSize(MAX_HISTORY_BUFFER);
+        }
+
         private void updateBatteryChargedDelayMsLocked() {
             // a negative value indicates that we should ignore this override
             final int delay = Settings.Global.getInt(mResolver,
@@ -15698,27 +15050,11 @@
     }
 
     private void writeHistoryLocked() {
-        if (mBatteryStatsHistory.getActiveFile() == null) {
-            Slog.w(TAG, "writeHistoryLocked: no history file associated with this instance");
-            return;
-        }
-
         if (mShuttingDown) {
             return;
         }
 
-        Parcel p = Parcel.obtain();
-        try {
-            final long start = SystemClock.uptimeMillis();
-            writeHistoryBuffer(p, true);
-            if (DEBUG) {
-                Slog.d(TAG, "writeHistoryBuffer duration ms:"
-                        + (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize());
-            }
-            writeParcelToFileLocked(p, mBatteryStatsHistory.getActiveFile());
-        } finally {
-            p.recycle();
-        }
+        mHistory.writeHistory();
     }
 
     private final ReentrantLock mWriteLock = new ReentrantLock();
@@ -15757,13 +15093,6 @@
             return;
         }
 
-        final AtomicFile activeHistoryFile = mBatteryStatsHistory.getActiveFile();
-        if (activeHistoryFile == null) {
-            Slog.w(TAG,
-                    "readLocked: no history file associated with this instance");
-            return;
-        }
-
         mUidStats.clear();
 
         Parcel stats = Parcel.obtain();
@@ -15776,7 +15105,7 @@
                 readSummaryFromParcel(stats);
                 if (DEBUG) {
                     Slog.d(TAG, "readLocked stats file:" + mStatsFile.getBaseFile().getPath()
-                            + " bytes:" + raw.length + " takes ms:" + (SystemClock.uptimeMillis()
+                            + " bytes:" + raw.length + " took ms:" + (SystemClock.uptimeMillis()
                             - start));
                 }
             }
@@ -15788,126 +15117,19 @@
             stats.recycle();
         }
 
-        Parcel history = Parcel.obtain();
-        try {
-            final long start = SystemClock.uptimeMillis();
-            if (activeHistoryFile.exists()) {
-                byte[] raw = activeHistoryFile.readFully();
-                if (raw.length > 0) {
-                    history.unmarshall(raw, 0, raw.length);
-                    history.setDataPosition(0);
-                    readHistoryBuffer(history);
-                }
-                if (DEBUG) {
-                    Slog.d(TAG, "readLocked history file::"
-                            + activeHistoryFile.getBaseFile().getPath()
-                            + " bytes:" + raw.length + " takes ms:" + (SystemClock.uptimeMillis()
-                            - start));
-                }
-            }
-        } catch (Exception e) {
-            Slog.e(TAG, "Error reading battery history", e);
-            clearHistoryLocked();
-            mBatteryStatsHistory.resetAllFiles();
-        } finally {
-            history.recycle();
+        if (!mHistory.readSummary()) {
+            resetAllStatsLocked(SystemClock.uptimeMillis(), SystemClock.elapsedRealtime(),
+                    RESET_REASON_CORRUPT_FILE);
         }
 
         mEndPlatformVersion = Build.ID;
 
-        if (mHistoryBuffer.dataPosition() > 0
-                || mBatteryStatsHistory.getFilesNumbers().size() > 1) {
-            mRecordingHistory = true;
-            final long elapsedRealtimeMs = mClock.elapsedRealtime();
-            final long uptimeMs = mClock.uptimeMillis();
-            addHistoryBufferLocked(elapsedRealtimeMs, HistoryItem.CMD_START, mHistoryCur);
-            startRecordingHistory(elapsedRealtimeMs, uptimeMs, false);
-        }
+        mHistory.continueRecordingHistory();
 
         recordDailyStatsIfNeededLocked(false, mClock.currentTimeMillis());
     }
 
     @GuardedBy("this")
-    void  readHistoryBuffer(Parcel in) throws ParcelFormatException {
-        final int version = in.readInt();
-        if (version != BatteryStatsHistory.VERSION) {
-            Slog.w("BatteryStats", "readHistoryBuffer: version got " + version
-                    + ", expected " + BatteryStatsHistory.VERSION + "; erasing old stats");
-            return;
-        }
-
-        final long historyBaseTime = in.readLong();
-
-        mHistoryBuffer.setDataSize(0);
-        mHistoryBuffer.setDataPosition(0);
-
-        int bufSize = in.readInt();
-        int curPos = in.dataPosition();
-        if (bufSize >= (mConstants.MAX_HISTORY_BUFFER*100)) {
-            throw new ParcelFormatException("File corrupt: history data buffer too large " +
-                    bufSize);
-        } else if ((bufSize&~3) != bufSize) {
-            throw new ParcelFormatException("File corrupt: history data buffer not aligned " +
-                    bufSize);
-        } else {
-            if (DEBUG_HISTORY) Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize
-                    + " bytes at " + curPos);
-            mHistoryBuffer.appendFrom(in, curPos, bufSize);
-            in.setDataPosition(curPos + bufSize);
-        }
-
-        if (DEBUG_HISTORY) {
-            StringBuilder sb = new StringBuilder(128);
-            sb.append("****************** OLD mHistoryBaseTimeMs: ");
-            TimeUtils.formatDuration(mHistoryBaseTimeMs, sb);
-            Slog.i(TAG, sb.toString());
-        }
-        mHistoryBaseTimeMs = historyBaseTime;
-        if (DEBUG_HISTORY) {
-            StringBuilder sb = new StringBuilder(128);
-            sb.append("****************** NEW mHistoryBaseTimeMs: ");
-            TimeUtils.formatDuration(mHistoryBaseTimeMs, sb);
-            Slog.i(TAG, sb.toString());
-        }
-
-        // We are just arbitrarily going to insert 1 minute from the sample of
-        // the last run until samples in this run.
-        if (mHistoryBaseTimeMs > 0) {
-            long oldnow = mClock.elapsedRealtime();
-            mHistoryBaseTimeMs = mHistoryBaseTimeMs - oldnow + 1;
-            if (DEBUG_HISTORY) {
-                StringBuilder sb = new StringBuilder(128);
-                sb.append("****************** ADJUSTED mHistoryBaseTimeMs: ");
-                TimeUtils.formatDuration(mHistoryBaseTimeMs, sb);
-                Slog.i(TAG, sb.toString());
-            }
-        }
-    }
-
-    void writeHistoryBuffer(Parcel out, boolean inclData) {
-        if (DEBUG_HISTORY) {
-            StringBuilder sb = new StringBuilder(128);
-            sb.append("****************** WRITING mHistoryBaseTimeMs: ");
-            TimeUtils.formatDuration(mHistoryBaseTimeMs, sb);
-            sb.append(" mLastHistoryElapsedRealtimeMs: ");
-            TimeUtils.formatDuration(mLastHistoryElapsedRealtimeMs, sb);
-            Slog.i(TAG, sb.toString());
-        }
-        out.writeInt(BatteryStatsHistory.VERSION);
-        out.writeLong(mHistoryBaseTimeMs + mLastHistoryElapsedRealtimeMs);
-        if (!inclData) {
-            out.writeInt(0);
-            out.writeInt(0);
-            return;
-        }
-
-        out.writeInt(mHistoryBuffer.dataSize());
-        if (DEBUG_HISTORY) Slog.i(TAG, "***************** WRITING HISTORY: "
-                + mHistoryBuffer.dataSize() + " bytes at " + out.dataPosition());
-        out.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
-    }
-
-    @GuardedBy("this")
     public void readSummaryFromParcel(Parcel in) throws ParcelFormatException {
         final int version = in.readInt();
 
@@ -15917,31 +15139,7 @@
             return;
         }
 
-        boolean inclHistory = in.readBoolean();
-        if (inclHistory) {
-            readHistoryBuffer(in);
-            mBatteryStatsHistory.readFromParcel(in);
-        }
-
-        mHistoryTagPool.clear();
-        mNextHistoryTagIdx = 0;
-        mNumHistoryTagChars = 0;
-
-        int numTags = in.readInt();
-        for (int i=0; i<numTags; i++) {
-            int idx = in.readInt();
-            String str = in.readString();
-            int uid = in.readInt();
-            HistoryTag tag = new HistoryTag();
-            tag.string = str;
-            tag.uid = uid;
-            tag.poolIdx = idx;
-            mHistoryTagPool.put(tag, idx);
-            if (idx >= mNextHistoryTagIdx) {
-                mNextHistoryTagIdx = idx+1;
-            }
-            mNumHistoryTagChars += tag.string.length() + 1;
-        }
+        mHistory.readSummaryFromParcel(in);
 
         mStartCount = in.readInt();
         mUptimeUs = in.readLong();
@@ -15954,7 +15152,7 @@
         mDischargeUnplugLevel = in.readInt();
         mDischargePlugLevel = in.readInt();
         mDischargeCurrentLevel = in.readInt();
-        mCurrentBatteryLevel = in.readInt();
+        mBatteryLevel = in.readInt();
         mEstimatedBatteryCapacityMah = in.readInt();
         mLastLearnedBatteryCapacityUah = in.readInt();
         mMinLearnedBatteryCapacityUah = in.readInt();
@@ -16457,19 +15655,7 @@
 
         out.writeInt(VERSION);
 
-        out.writeBoolean(inclHistory);
-        if (inclHistory) {
-            writeHistoryBuffer(out, true);
-            mBatteryStatsHistory.writeToParcel(out);
-        }
-
-        out.writeInt(mHistoryTagPool.size());
-        for (HashMap.Entry<HistoryTag, Integer> ent : mHistoryTagPool.entrySet()) {
-            HistoryTag tag = ent.getKey();
-            out.writeInt(ent.getValue());
-            out.writeString(tag.string);
-            out.writeInt(tag.uid);
-        }
+        mHistory.writeSummaryToParcel(out, inclHistory);
 
         out.writeInt(mStartCount);
         out.writeLong(computeUptime(nowUptime, STATS_SINCE_CHARGED));
@@ -16482,7 +15668,7 @@
         out.writeInt(mDischargeUnplugLevel);
         out.writeInt(mDischargePlugLevel);
         out.writeInt(mDischargeCurrentLevel);
-        out.writeInt(mCurrentBatteryLevel);
+        out.writeInt(mBatteryLevel);
         out.writeInt(mEstimatedBatteryCapacityMah);
         out.writeInt(mLastLearnedBatteryCapacityUah);
         out.writeInt(mMinLearnedBatteryCapacityUah);
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index 0cdd4d1..c36d950 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -22,7 +22,6 @@
 import android.os.BatteryStats;
 import android.os.BatteryUsageStats;
 import android.os.BatteryUsageStatsQuery;
-import android.os.Parcel;
 import android.os.Process;
 import android.os.SystemClock;
 import android.os.UidBatteryConsumer;
@@ -32,10 +31,8 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.os.BatteryStatsHistory;
 import com.android.internal.os.PowerProfile;
 
-import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -220,18 +217,7 @@
             }
 
             BatteryStatsImpl batteryStatsImpl = (BatteryStatsImpl) mStats;
-
-            // Make a copy of battery history to avoid concurrent modification.
-            Parcel historyBuffer = Parcel.obtain();
-            historyBuffer.appendFrom(batteryStatsImpl.mHistoryBuffer, 0,
-                    batteryStatsImpl.mHistoryBuffer.dataSize());
-
-            final File systemDir =
-                    batteryStatsImpl.mBatteryStatsHistory.getHistoryDirectory().getParentFile();
-            final BatteryStatsHistory batteryStatsHistory =
-                    new BatteryStatsHistory(historyBuffer, systemDir, null);
-
-            batteryUsageStatsBuilder.setBatteryHistory(batteryStatsHistory);
+            batteryUsageStatsBuilder.setBatteryHistory(batteryStatsImpl.copyHistory());
         }
 
         BatteryUsageStats stats = batteryUsageStatsBuilder.build();
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d11adc1..55175c5 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -1566,7 +1566,7 @@
         if (task == mLastParentBeforePip && task != null) {
             // Notify the TaskFragmentOrganizer that the activity is reparented back from pip.
             mAtmService.mWindowOrganizerController.mTaskFragmentOrganizerController
-                    .onActivityReparentToTask(this);
+                    .onActivityReparentedToTask(this);
             // Activity's reparented back from pip, clear the links once established
             clearLastParentBeforePip();
         }
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 5221072..096ebe2 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2371,18 +2371,19 @@
             return;
         }
 
-        // The immersive mode confirmation should never affect the system bar visibility, otherwise
+        // Immersive mode confirmation should never affect the system bar visibility, otherwise
         // it will unhide the navigation bar and hide itself.
         if (winCandidate.getAttrs().token == mImmersiveModeConfirmation.getWindowToken()) {
-
-            // The immersive mode confirmation took the focus from mLastFocusedWindow which was
-            // controlling the system ui visibility. So if mLastFocusedWindow can still receive
-            // keys, we let it keep controlling the visibility.
-            final boolean lastFocusCanReceiveKeys =
-                    (mLastFocusedWindow != null && mLastFocusedWindow.canReceiveKeys());
-            winCandidate = isKeyguardShowing() && !isKeyguardOccluded() ? mNotificationShade
-                    : lastFocusCanReceiveKeys ? mLastFocusedWindow
-                            : mTopFullscreenOpaqueWindowState;
+            if (mNotificationShade != null && mNotificationShade.canReceiveKeys()) {
+                // Let notification shade control the system bar visibility.
+                winCandidate = mNotificationShade;
+            } else if (mLastFocusedWindow != null && mLastFocusedWindow.canReceiveKeys()) {
+                // Immersive mode confirmation took the focus from mLastFocusedWindow which was
+                // controlling the system bar visibility. Let it keep controlling the visibility.
+                winCandidate = mLastFocusedWindow;
+            } else {
+                winCandidate = mTopFullscreenOpaqueWindowState;
+            }
             if (winCandidate == null) {
                 return;
             }
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 7a9e6a9..f3bd1a1 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -761,7 +761,7 @@
 
         InsetsPolicyAnimationControlListener(boolean show, Runnable finishCallback, int types) {
             super(show, false /* hasCallbacks */, types, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
-                    false /* disable */, 0 /* floatingImeBottomInsets */);
+                    false /* disable */, 0 /* floatingImeBottomInsets */, null);
             mFinishCallback = finishCallback;
             mControlCallbacks = new InsetsPolicyAnimationControlCallbacks(this);
         }
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index c1f06e4..44b5b88 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2340,7 +2340,10 @@
             return false;
         }
 
-        return !startBounds.equals(getBounds());
+        // Only take snapshot if the bounds are resized.
+        final Rect endBounds = getConfiguration().windowConfiguration.getBounds();
+        return endBounds.width() != startBounds.width()
+                || endBounds.height() != startBounds.height();
     }
 
     boolean canHaveEmbeddingActivityTransition(@NonNull ActivityRecord child) {
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 6ca5648..88059e1 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -18,7 +18,7 @@
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.window.TaskFragmentOrganizer.putErrorInfoInBundle;
-import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENT_TO_TASK;
+import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
@@ -277,7 +277,7 @@
         }
 
         @Nullable
-        TaskFragmentTransaction.Change prepareActivityReparentToTask(
+        TaskFragmentTransaction.Change prepareActivityReparentedToTask(
                 @NonNull ActivityRecord activity) {
             if (activity.finishing) {
                 Slog.d(TAG, "Reparent activity=" + activity.token + " is finishing");
@@ -315,7 +315,7 @@
             }
             ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Activity=%s reparent to taskId=%d",
                     activity.token, task.mTaskId);
-            return new TaskFragmentTransaction.Change(TYPE_ACTIVITY_REPARENT_TO_TASK)
+            return new TaskFragmentTransaction.Change(TYPE_ACTIVITY_REPARENTED_TO_TASK)
                     .setTaskId(task.mTaskId)
                     .setActivityIntent(activity.intent)
                     .setActivityToken(activityToken);
@@ -521,7 +521,7 @@
         mAtmService.mWindowManager.mWindowPlacerLocked.requestTraversal();
     }
 
-    void onActivityReparentToTask(@NonNull ActivityRecord activity) {
+    void onActivityReparentedToTask(@NonNull ActivityRecord activity) {
         final ITaskFragmentOrganizer organizer;
         if (activity.mLastTaskFragmentOrganizerBeforePip != null) {
             // If the activity is previously embedded in an organized TaskFragment.
@@ -547,7 +547,7 @@
             return;
         }
         addPendingEvent(new PendingTaskFragmentEvent.Builder(
-                PendingTaskFragmentEvent.EVENT_ACTIVITY_REPARENT_TO_TASK, organizer)
+                PendingTaskFragmentEvent.EVENT_ACTIVITY_REPARENTED_TO_TASK, organizer)
                 .setActivity(activity)
                 .build());
     }
@@ -601,7 +601,7 @@
         static final int EVENT_INFO_CHANGED = 2;
         static final int EVENT_PARENT_INFO_CHANGED = 3;
         static final int EVENT_ERROR = 4;
-        static final int EVENT_ACTIVITY_REPARENT_TO_TASK = 5;
+        static final int EVENT_ACTIVITY_REPARENTED_TO_TASK = 5;
 
         @IntDef(prefix = "EVENT_", value = {
                 EVENT_APPEARED,
@@ -609,7 +609,7 @@
                 EVENT_INFO_CHANGED,
                 EVENT_PARENT_INFO_CHANGED,
                 EVENT_ERROR,
-                EVENT_ACTIVITY_REPARENT_TO_TASK
+                EVENT_ACTIVITY_REPARENTED_TO_TASK
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface EventType {}
@@ -900,8 +900,8 @@
             case PendingTaskFragmentEvent.EVENT_ERROR:
                 return state.prepareTaskFragmentError(event.mErrorCallbackToken, taskFragment,
                         event.mOpType, event.mException);
-            case PendingTaskFragmentEvent.EVENT_ACTIVITY_REPARENT_TO_TASK:
-                return state.prepareActivityReparentToTask(event.mActivity);
+            case PendingTaskFragmentEvent.EVENT_ACTIVITY_REPARENTED_TO_TASK:
+                return state.prepareActivityReparentedToTask(event.mActivity);
             default:
                 throw new IllegalArgumentException("Unknown TaskFragmentEvent=" + event.mEventType);
         }
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 584a40e..fa2ab31 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1205,7 +1205,14 @@
             return false;
         }
 
-        final @TransitionInfo.TransitionMode int mode = changes.get(target).getTransitMode(target);
+        final ChangeInfo change = changes.get(target);
+        if (change.mStartParent != null && target.getParent() != change.mStartParent) {
+            // When a window is reparented, the state change won't fit into any of the parents.
+            // Don't promote such change so that we can animate the reparent if needed.
+            return false;
+        }
+
+        final @TransitionInfo.TransitionMode int mode = change.getTransitMode(target);
         for (int i = parent.getChildCount() - 1; i >= 0; --i) {
             final WindowContainer<?> sibling = parent.getChildAt(i);
             if (target == sibling) continue;
@@ -1345,14 +1352,14 @@
                     // Intermediate parents must be those that has window to be managed by Shell.
                     continue;
                 }
-                if (parentChange.mParent != null && !skipIntermediateReports) {
-                    changes.get(wc).mParent = p;
+                if (parentChange.mEndParent != null && !skipIntermediateReports) {
+                    changes.get(wc).mEndParent = p;
                     // The chain above the parent was processed.
                     break;
                 }
                 if (targetList.contains(p)) {
                     if (skipIntermediateReports) {
-                        changes.get(wc).mParent = p;
+                        changes.get(wc).mEndParent = p;
                     } else {
                         intermediates.add(p);
                     }
@@ -1364,10 +1371,10 @@
             }
             if (!foundParentInTargets || intermediates.isEmpty()) continue;
             // Add any always-report parents along the way.
-            changes.get(wc).mParent = intermediates.get(0);
+            changes.get(wc).mEndParent = intermediates.get(0);
             for (int j = 0; j < intermediates.size() - 1; j++) {
                 final WindowContainer<?> intermediate = intermediates.get(j);
-                changes.get(intermediate).mParent = intermediates.get(j + 1);
+                changes.get(intermediate).mEndParent = intermediates.get(j + 1);
                 targets.add(intermediate);
             }
         }
@@ -1480,8 +1487,8 @@
                     target.mRemoteToken != null ? target.mRemoteToken.toWindowContainerToken()
                             : null, getLeashSurface(target, startT));
             // TODO(shell-transitions): Use leash for non-organized windows.
-            if (info.mParent != null) {
-                change.setParent(info.mParent.mRemoteToken.toWindowContainerToken());
+            if (info.mEndParent != null) {
+                change.setParent(info.mEndParent.mRemoteToken.toWindowContainerToken());
             }
             change.setMode(info.getTransitMode(target));
             change.setStartAbsBounds(info.mAbsoluteBounds);
@@ -1664,7 +1671,9 @@
         @interface Flag {}
 
         // Usually "post" change state.
-        WindowContainer mParent;
+        WindowContainer mEndParent;
+        // Parent before change state.
+        WindowContainer mStartParent;
 
         // State tracking
         boolean mExistenceChanged = false;
@@ -1685,6 +1694,7 @@
             mAbsoluteBounds.set(origState.getBounds());
             mShowWallpaper = origState.showWallpaper();
             mRotation = origState.getWindowConfiguration().getRotation();
+            mStartParent = origState.getParent();
         }
 
         @VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 3ee4be0..22411bb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -88,6 +88,7 @@
 import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED;
 import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_RELAUNCH;
+import static android.view.WindowManager.fixScale;
 import static android.view.WindowManagerGlobal.ADD_OKAY;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED;
@@ -1326,15 +1327,10 @@
         }, UserHandle.ALL, suspendPackagesFilter, null, null);
 
         // Get persisted window scale setting
-        mWindowAnimationScaleSetting = Settings.Global.getFloat(resolver,
-                Settings.Global.WINDOW_ANIMATION_SCALE, mWindowAnimationScaleSetting);
-        mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
-                Settings.Global.TRANSITION_ANIMATION_SCALE,
-                context.getResources().getFloat(
-                        R.dimen.config_appTransitionAnimationDurationScaleDefault));
+        mWindowAnimationScaleSetting = getWindowAnimationScaleSetting();
+        mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
 
-        setAnimatorDurationScale(Settings.Global.getFloat(resolver,
-                Settings.Global.ANIMATOR_DURATION_SCALE, mAnimatorDurationScaleSetting));
+        setAnimatorDurationScale(getAnimatorDurationScaleSetting());
 
         mForceDesktopModeOnExternalDisplays = Settings.Global.getInt(resolver,
                 DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0;
@@ -1408,6 +1404,22 @@
                 lightRadius);
     }
 
+    private float getTransitionAnimationScaleSetting() {
+        return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
+                                R.dimen.config_appTransitionAnimationDurationScaleDefault)));
+    }
+
+    private float getAnimatorDurationScaleSetting() {
+        return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.ANIMATOR_DURATION_SCALE, mAnimatorDurationScaleSetting));
+    }
+
+    private float getWindowAnimationScaleSetting() {
+        return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.WINDOW_ANIMATION_SCALE, mWindowAnimationScaleSetting));
+    }
+
     /**
      * Called after all entities (such as the {@link ActivityManagerService}) have been set up and
      * associated with the {@link WindowManagerService}.
@@ -3407,11 +3419,6 @@
         }
     }
 
-    static float fixScale(float scale) {
-        if (scale < 0) scale = 0;
-        else if (scale > 20) scale = 20;
-        return Math.abs(scale);
-    }
 
     @Override
     public void setAnimationScale(int which, float scale) {
@@ -5339,24 +5346,16 @@
                     final int mode = msg.arg1;
                     switch (mode) {
                         case WINDOW_ANIMATION_SCALE: {
-                            mWindowAnimationScaleSetting = Settings.Global.getFloat(
-                                    mContext.getContentResolver(),
-                                    Settings.Global.WINDOW_ANIMATION_SCALE,
-                                    mWindowAnimationScaleSetting);
+                            mWindowAnimationScaleSetting = getWindowAnimationScaleSetting();
                             break;
                         }
                         case TRANSITION_ANIMATION_SCALE: {
-                            mTransitionAnimationScaleSetting = Settings.Global.getFloat(
-                                    mContext.getContentResolver(),
-                                    Settings.Global.TRANSITION_ANIMATION_SCALE,
-                                    mTransitionAnimationScaleSetting);
+                            mTransitionAnimationScaleSetting =
+                                    getTransitionAnimationScaleSetting();
                             break;
                         }
                         case ANIMATION_DURATION_SCALE: {
-                            mAnimatorDurationScaleSetting = Settings.Global.getFloat(
-                                    mContext.getContentResolver(),
-                                    Settings.Global.ANIMATOR_DURATION_SCALE,
-                                    mAnimatorDurationScaleSetting);
+                            mAnimatorDurationScaleSetting = getAnimatorDurationScaleSetting();
                             dispatchNewAnimatorScaleLocked(null);
                             break;
                         }
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index d3d69ae..1d56078 100644
--- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
+++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
@@ -491,10 +491,10 @@
     compactProcessOrFallback(pid, compactionFlags);
 }
 
-static jint com_android_server_am_CachedAppOptimizer_freezeBinder(JNIEnv* env, jobject clazz,
-                                                                  jint pid, jboolean freeze,
-                                                                  jint timeout_ms) {
-    jint retVal = IPCThreadState::freeze(pid, freeze, timeout_ms);
+static jint com_android_server_am_CachedAppOptimizer_freezeBinder(
+        JNIEnv *env, jobject clazz, jint pid, jboolean freeze) {
+
+    jint retVal = IPCThreadState::freeze(pid, freeze, 100 /* timeout [ms] */);
     if (retVal != 0 && retVal != -EAGAIN) {
         jniThrowException(env, "java/lang/RuntimeException", "Unable to freeze/unfreeze binder");
     }
@@ -548,7 +548,7 @@
          (void*)com_android_server_am_CachedAppOptimizer_getMemoryFreedCompaction},
         {"compactSystem", "()V", (void*)com_android_server_am_CachedAppOptimizer_compactSystem},
         {"compactProcess", "(II)V", (void*)com_android_server_am_CachedAppOptimizer_compactProcess},
-        {"freezeBinder", "(IZI)I", (void*)com_android_server_am_CachedAppOptimizer_freezeBinder},
+        {"freezeBinder", "(IZ)I", (void*)com_android_server_am_CachedAppOptimizer_freezeBinder},
         {"getBinderFreezeInfo", "(I)I",
          (void*)com_android_server_am_CachedAppOptimizer_getBinderFreezeInfo},
         {"getFreezerCheckPath", "()Ljava/lang/String;",
diff --git a/services/core/xsd/display-device-config/autobrightness.xsd b/services/core/xsd/display-device-config/autobrightness.xsd
new file mode 100644
index 0000000..477625a
--- /dev/null
+++ b/services/core/xsd/display-device-config/autobrightness.xsd
@@ -0,0 +1,33 @@
+<!--
+    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.
+-->
+<xs:schema version="2.0"
+           elementFormDefault="qualified"
+           xmlns:xs="http://www.w3.org/2001/XMLSchema">
+    <xs:complexType name="autoBrightness">
+        <xs:sequence>
+            <!-- Sets the debounce for autoBrightness brightening in millis-->
+            <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger"
+                        minOccurs="0" maxOccurs="1">
+                <xs:annotation name="final"/>
+            </xs:element>
+            <!-- Sets the debounce for autoBrightness darkening in millis-->
+            <xs:element name="darkeningLightDebounceMillis" type="xs:nonNegativeInteger"
+                        minOccurs="0" maxOccurs="1">
+                <xs:annotation name="final"/>
+            </xs:element>
+        </xs:sequence>
+    </xs:complexType>
+</xs:schema>
\ No newline at end of file
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 073b131c..bea5e2c 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -23,6 +23,7 @@
 <xs:schema version="2.0"
            elementFormDefault="qualified"
            xmlns:xs="http://www.w3.org/2001/XMLSchema">
+    <xs:include schemaLocation="autobrightness.xsd" />
     <xs:element name="displayConfiguration">
         <xs:complexType>
             <xs:sequence>
@@ -102,22 +103,6 @@
     </xs:element>
 
     <!-- Type definitions -->
-
-    <xs:complexType name="autoBrightness">
-        <xs:sequence>
-            <!-- Sets the debounce for autoBrightness brightening in millis-->
-            <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger"
-                        minOccurs="0" maxOccurs="1">
-                <xs:annotation name="final"/>
-            </xs:element>
-            <!-- Sets the debounce for autoBrightness darkening in millis-->
-            <xs:element name="darkeningLightDebounceMillis" type="xs:nonNegativeInteger"
-                        minOccurs="0" maxOccurs="1">
-                <xs:annotation name="final"/>
-            </xs:element>
-        </xs:sequence>
-    </xs:complexType>
-
     <xs:complexType name="displayQuirks">
         <xs:sequence>
             <xs:element name="quirk" type="xs:string" minOccurs="0" maxOccurs="unbounded" />
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/listeners/ListenerMultiplexerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/listeners/ListenerMultiplexerTest.java
index d7fef60..5b92706 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/listeners/ListenerMultiplexerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/listeners/ListenerMultiplexerTest.java
@@ -58,8 +58,10 @@
         void onRegistrationAdded(Consumer<TestListenerRegistration> consumer,
                 TestListenerRegistration registration);
 
-        void onRegistrationReplaced(Consumer<TestListenerRegistration> consumer,
-                TestListenerRegistration oldRegistration, TestListenerRegistration newRegistration);
+        void onRegistrationReplaced(Consumer<TestListenerRegistration> oldConsumer,
+                TestListenerRegistration oldRegistration,
+                Consumer<TestListenerRegistration> newConsumer,
+                TestListenerRegistration newRegistration);
 
         void onRegistrationRemoved(Consumer<TestListenerRegistration> consumer,
                 TestListenerRegistration registration);
@@ -93,10 +95,10 @@
         assertThat(mMultiplexer.mMergedRequest).isEqualTo(0);
 
         mMultiplexer.addListener(1, consumer);
-        mInOrder.verify(mCallbacks).onRegistrationRemoved(eq(consumer),
-                any(TestListenerRegistration.class));
         mInOrder.verify(mCallbacks).onRegistrationReplaced(eq(consumer),
-                any(TestListenerRegistration.class), any(TestListenerRegistration.class));
+                any(TestListenerRegistration.class),
+                eq(consumer),
+                any(TestListenerRegistration.class));
         assertThat(mMultiplexer.mRegistered).isTrue();
         assertThat(mMultiplexer.mMergedRequest).isEqualTo(1);
 
@@ -115,10 +117,10 @@
                 any(TestListenerRegistration.class));
         mInOrder.verify(mCallbacks).onActive();
         mMultiplexer.replaceListener(1, oldConsumer, consumer);
-        mInOrder.verify(mCallbacks).onRegistrationRemoved(eq(oldConsumer),
+        mInOrder.verify(mCallbacks).onRegistrationReplaced(eq(oldConsumer),
+                any(TestListenerRegistration.class),
+                eq(consumer),
                 any(TestListenerRegistration.class));
-        mInOrder.verify(mCallbacks).onRegistrationReplaced(eq(consumer),
-                any(TestListenerRegistration.class), any(TestListenerRegistration.class));
         assertThat(mMultiplexer.mRegistered).isTrue();
         assertThat(mMultiplexer.mMergedRequest).isEqualTo(1);
 
@@ -352,13 +354,19 @@
     }
 
     private static class TestListenerRegistration extends
-            RequestListenerRegistration<Integer, Consumer<TestListenerRegistration>> {
+            ListenerRegistration<Consumer<TestListenerRegistration>> {
 
+        private final Integer mInteger;
         boolean mActive = true;
 
         protected TestListenerRegistration(Integer integer,
                 Consumer<TestListenerRegistration> consumer) {
-            super(DIRECT_EXECUTOR, integer, consumer);
+            super(DIRECT_EXECUTOR, consumer);
+            mInteger = integer;
+        }
+
+        public Integer getInteger() {
+            return mInteger;
         }
     }
 
@@ -375,11 +383,6 @@
             mCallbacks = callbacks;
         }
 
-        @Override
-        public String getTag() {
-            return "TestMultiplexer";
-        }
-
         public void addListener(Integer request, Consumer<TestListenerRegistration> consumer) {
             putRegistration(consumer, new TestListenerRegistration(request, consumer));
         }
@@ -399,9 +402,9 @@
             removeRegistration(consumer, registration);
         }
 
-        public void setActive(Integer request, boolean active) {
+        public void setActive(Integer integer, boolean active) {
             updateRegistrations(testRegistration -> {
-                if (testRegistration.getRequest().equals(request)) {
+                if (testRegistration.getInteger().equals(integer)) {
                     testRegistration.mActive = active;
                     return true;
                 }
@@ -458,10 +461,11 @@
         }
 
         @Override
-        protected void onRegistrationReplaced(Consumer<TestListenerRegistration> consumer,
+        protected void onRegistrationReplaced(Consumer<TestListenerRegistration> oldKey,
                 TestListenerRegistration oldRegistration,
+                Consumer<TestListenerRegistration> newKey,
                 TestListenerRegistration newRegistration) {
-            mCallbacks.onRegistrationReplaced(consumer, oldRegistration, newRegistration);
+            mCallbacks.onRegistrationReplaced(oldKey, oldRegistration, newKey, newRegistration);
         }
 
         @Override
@@ -475,8 +479,8 @@
                 Collection<TestListenerRegistration> testRegistrations) {
             int max = Integer.MIN_VALUE;
             for (TestListenerRegistration registration : testRegistrations) {
-                if (registration.getRequest() > max) {
-                    max = registration.getRequest();
+                if (registration.getInteger() > max) {
+                    max = registration.getInteger();
                 }
             }
             mMergeCount++;
@@ -493,7 +497,7 @@
         @Override
         protected void onRegistrationAdded(Consumer<TestListenerRegistration> consumer,
                 TestListenerRegistration registration) {
-            addListener(registration.getRequest(), consumer);
+            addListener(registration.getInteger(), consumer);
         }
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
index 71cc65b..0ac1443 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
@@ -32,7 +32,6 @@
 import static com.android.server.location.LocationPermissions.PERMISSION_FINE;
 import static com.android.server.location.LocationUtils.createLocation;
 import static com.android.server.location.LocationUtils.createLocationResult;
-import static com.android.server.location.listeners.RemoteListenerRegistration.IN_PROCESS_EXECUTOR;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -534,7 +533,7 @@
                 listener);
 
         CountDownLatch blocker = new CountDownLatch(1);
-        IN_PROCESS_EXECUTOR.execute(() -> {
+        FgThread.getExecutor().execute(() -> {
             try {
                 blocker.await();
             } catch (InterruptedException e) {
@@ -661,7 +660,7 @@
                 listener);
 
         CountDownLatch blocker = new CountDownLatch(1);
-        IN_PROCESS_EXECUTOR.execute(() -> {
+        FgThread.getExecutor().execute(() -> {
             try {
                 blocker.await();
             } catch (InterruptedException e) {
diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
index daa7d7b..b27f49d 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_profile.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
@@ -30,6 +30,10 @@
             <item res='@*android:color/profile_badge_2' />
         </badge-colors>
         <default-restrictions no_remove_user='true' no_bluetooth='true' />
+        <user-properties
+            showInLauncher='2020'
+            startWithParent='false'
+        />
     </profile-type>
     <profile-type name='custom.test.1' max-allowed-per-parent='14' />
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index a4cccb3..3477288 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -369,6 +369,8 @@
                     throws Exception {
         final LockscreenCredential parentPassword = newPassword("parentPassword");
         final LockscreenCredential profilePassword = newPattern("12345");
+        mService.setSeparateProfileChallengeEnabled(
+                MANAGED_PROFILE_USER_ID, true, profilePassword);
         initializeStorageWithCredential(PRIMARY_USER_ID, parentPassword);
         // Create and verify separate profile credentials.
         testCreateCredential(MANAGED_PROFILE_USER_ID, profilePassword);
@@ -550,11 +552,12 @@
             throws RemoteException {
         assertEquals(0, mGateKeeperService.getSecureUserId(userId));
         synchronized (mService.mSpManager) {
-            mService.initializeSyntheticPasswordLocked(credential, userId);
+            mService.initializeSyntheticPasswordLocked(userId);
         }
         if (credential.isNone()) {
             assertEquals(0, mGateKeeperService.getSecureUserId(userId));
         } else {
+            assertTrue(mService.setLockCredential(credential, nonePassword(), userId));
             assertNotEquals(0, mGateKeeperService.getSecureUserId(userId));
         }
     }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
new file mode 100644
index 0000000..13a7a3e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.server.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.pm.UserProperties;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+import android.util.TypedXmlPullParser;
+import android.util.TypedXmlSerializer;
+import android.util.Xml;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Supplier;
+
+/**
+ * Tests for UserManager's {@link UserProperties}.
+ *
+ * Additional test coverage (that actually exercises the functionality) can be found in
+ * {@link UserManagerTest} and
+ * {@link UserManagerServiceUserTypeTest} (for {@link UserProperties#updateFromXml}).
+ *
+ * <p>Run with: atest UserManagerServiceUserPropertiesTest
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class UserManagerServiceUserPropertiesTest {
+
+    /** Test that UserProperties can properly read the xml information that it writes. */
+    @Test
+    public void testWriteReadXml() throws Exception {
+        final UserProperties defaultProps = new UserProperties.Builder()
+                .setShowInLauncher(21)
+                .setStartWithParent(false)
+                .build();
+        final UserProperties actualProps = new UserProperties(defaultProps);
+        actualProps.setShowInLauncher(14);
+
+        // Write the properties to xml.
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        final TypedXmlSerializer out = Xml.newFastSerializer();
+        out.setOutput(baos, StandardCharsets.UTF_8.name());
+        out.startDocument(null, true);
+        out.startTag(null, "testTag");
+        actualProps.writeToXml(out);
+        out.endTag(null, "testTag");
+        out.endDocument();
+
+        // Now read those properties from xml.
+        final ByteArrayInputStream input = new ByteArrayInputStream(baos.toByteArray());
+        final TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(input, StandardCharsets.UTF_8.name());
+        parser.nextTag();
+        final UserProperties readProps = new UserProperties(parser, defaultProps);
+
+        assertUserPropertiesEquals(actualProps, readProps);
+    }
+
+    /** Tests parcelling an object in which all properties are present. */
+    @Test
+    public void testParcelUnparcel() throws Exception {
+        final UserProperties originalProps = new UserProperties.Builder()
+                .setShowInLauncher(2145)
+                .build();
+        final UserProperties readProps = parcelThenUnparcel(originalProps);
+        assertUserPropertiesEquals(originalProps, readProps);
+    }
+
+    /** Tests copying a UserProperties object varying permissions. */
+    @Test
+    public void testCopyLacksPermissions() throws Exception {
+        final UserProperties defaultProps = new UserProperties.Builder()
+                .setShowInLauncher(2145)
+                .setStartWithParent(true)
+                .build();
+        final UserProperties orig = new UserProperties(defaultProps);
+        orig.setShowInLauncher(2841);
+        orig.setStartWithParent(false);
+
+        // Test every permission level. (Currently, it's linear so it's easy.)
+        for (int permLevel = 0; permLevel < 4; permLevel++) {
+            final boolean exposeAll = permLevel >= 3;
+            final boolean hasManage = permLevel >= 2;
+            final boolean hasQuery = permLevel >= 1;
+
+            // Make a possibly-not-full-permission (i.e. partial) copy and check that it is correct.
+            final UserProperties copy = new UserProperties(orig, exposeAll, hasManage, hasQuery);
+            verifyTestCopyLacksPermissions(orig, copy, exposeAll, hasManage, hasQuery);
+            if (permLevel < 1) {
+                // PropertiesPresent should definitely be different since not all items were copied.
+                assertThat(orig.getPropertiesPresent()).isNotEqualTo(copy.getPropertiesPresent());
+            }
+
+            // Now, just like in the SystemServer, parcel/unparcel the copy and make sure that the
+            // unparcelled version behaves just like the partial copy did.
+            final UserProperties readProps = parcelThenUnparcel(copy);
+            verifyTestCopyLacksPermissions(orig, readProps, exposeAll, hasManage, hasQuery);
+        }
+    }
+
+    /**
+     * Verifies that the copy of orig has the expected properties
+     * for the test {@link #testCopyLacksPermissions}.
+     */
+    private void verifyTestCopyLacksPermissions(
+            UserProperties orig,
+            UserProperties copy,
+            boolean exposeAll,
+            boolean hasManagePermission,
+            boolean hasQueryPermission) {
+
+        // Items requiring exposeAll.
+        assertEqualGetterOrThrows(orig::getStartWithParent, copy::getStartWithParent, exposeAll);
+
+        // Items requiring hasManagePermission - put them here using hasManagePermission.
+        // Items requiring hasQueryPermission - put them here using hasQueryPermission.
+
+        // Items with no permission requirements.
+        assertEqualGetterOrThrows(orig::getShowInLauncher, copy::getShowInLauncher, true);
+    }
+
+    /**
+     * If hasPerm, then asserts that value of actualGetter equals value of expectedGetter.
+     * If !hasPerm, then asserts that actualGetter throws a SecurityException.
+     */
+    @SuppressWarnings("ReturnValueIgnored")
+    private void assertEqualGetterOrThrows(
+            Supplier expectedGetter,
+            Supplier actualGetter,
+            boolean hasPerm) {
+        if (hasPerm) {
+            assertThat(expectedGetter.get()).isEqualTo(actualGetter.get());
+        } else {
+            assertThrows(SecurityException.class, actualGetter::get);
+        }
+    }
+
+    private UserProperties parcelThenUnparcel(UserProperties originalProps) {
+        final Parcel out = Parcel.obtain();
+        originalProps.writeToParcel(out, 0);
+        final byte[] data = out.marshall();
+        out.recycle();
+
+        final Parcel in = Parcel.obtain();
+        in.unmarshall(data, 0, data.length);
+        in.setDataPosition(0);
+        final UserProperties readProps = UserProperties.CREATOR.createFromParcel(in);
+        in.recycle();
+
+        return readProps;
+    }
+
+    /** Checks that two UserProperties get the same values. */
+    private void assertUserPropertiesEquals(UserProperties expected, UserProperties actual) {
+        assertThat(expected.getPropertiesPresent()).isEqualTo(actual.getPropertiesPresent());
+        assertThat(expected.getShowInLauncher()).isEqualTo(actual.getShowInLauncher());
+        assertThat(expected.getStartWithParent()).isEqualTo(actual.getStartWithParent());
+    }
+}
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 971b036..5f48004 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -35,6 +35,7 @@
 import static org.testng.Assert.assertThrows;
 
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.os.Bundle;
@@ -81,6 +82,8 @@
                 DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
                 /* flags= */0,
                 /* letsPersonalDataIntoProfile= */false).build());
+        final UserProperties.Builder userProps = new UserProperties.Builder()
+                .setShowInLauncher(17);
         final UserTypeDetails type = new UserTypeDetails.Builder()
                 .setName("a.name")
                 .setEnabled(1)
@@ -98,6 +101,7 @@
                 .setDefaultSystemSettings(systemSettings)
                 .setDefaultSecureSettings(secureSettings)
                 .setDefaultCrossProfileIntentFilters(filters)
+                .setDefaultUserProperties(userProps)
                 .createUserTypeDetails();
 
         assertEquals("a.name", type.getName());
@@ -135,6 +139,8 @@
             assertEquals(filters.get(i), type.getDefaultCrossProfileIntentFilters().get(i));
         }
 
+        assertEquals(17, type.getDefaultUserPropertiesReference().getShowInLauncher());
+
         assertEquals(23, type.getBadgeLabel(0));
         assertEquals(24, type.getBadgeLabel(1));
         assertEquals(25, type.getBadgeLabel(2));
@@ -173,6 +179,11 @@
         assertTrue(type.getDefaultSecureSettings().isEmpty());
         assertTrue(type.getDefaultCrossProfileIntentFilters().isEmpty());
 
+        final UserProperties props = type.getDefaultUserPropertiesReference();
+        assertNotNull(props);
+        assertFalse(props.getStartWithParent());
+        assertEquals(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT, props.getShowInLauncher());
+
         assertFalse(type.hasBadge());
     }
 
@@ -250,19 +261,24 @@
 
         // Mock some "AOSP defaults".
         final Bundle restrictions = makeRestrictionsBundle("no_config_vpn", "no_config_tethering");
+        final UserProperties.Builder props = new UserProperties.Builder()
+                .setShowInLauncher(19)
+                .setStartWithParent(true);
         final ArrayMap<String, UserTypeDetails.Builder> builders = new ArrayMap<>();
         builders.put(userTypeAosp1, new UserTypeDetails.Builder()
                 .setName(userTypeAosp1)
                 .setBaseType(FLAG_PROFILE)
                 .setMaxAllowedPerParent(31)
-                .setDefaultRestrictions(restrictions));
+                .setDefaultRestrictions(restrictions)
+                .setDefaultUserProperties(props));
         builders.put(userTypeAosp2, new UserTypeDetails.Builder()
                 .setName(userTypeAosp1)
                 .setBaseType(FLAG_PROFILE)
                 .setMaxAllowedPerParent(32)
                 .setIconBadge(401)
                 .setBadgeColors(402, 403, 404)
-                .setDefaultRestrictions(restrictions));
+                .setDefaultRestrictions(restrictions)
+                .setDefaultUserProperties(props));
 
         final XmlResourceParser parser = mResources.getXml(R.xml.usertypes_test_profile);
         UserTypeFactory.customizeBuilders(builders, parser);
@@ -272,6 +288,8 @@
         assertEquals(31, aospType.getMaxAllowedPerParent());
         assertEquals(Resources.ID_NULL, aospType.getIconBadge());
         assertTrue(UserRestrictionsUtils.areEqual(restrictions, aospType.getDefaultRestrictions()));
+        assertEquals(19, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
+        assertEquals(true, aospType.getDefaultUserPropertiesReference().getStartWithParent());
 
         // userTypeAosp2 should be modified.
         aospType = builders.get(userTypeAosp2).createUserTypeDetails();
@@ -300,6 +318,8 @@
         assertTrue(UserRestrictionsUtils.areEqual(
                 makeRestrictionsBundle("no_remove_user", "no_bluetooth"),
                 aospType.getDefaultRestrictions()));
+        assertEquals(2020, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
+        assertEquals(false, aospType.getDefaultUserPropertiesReference().getStartWithParent());
 
         // 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 5d48501..f567e80 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -31,6 +31,7 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.UserHandle;
@@ -564,6 +565,33 @@
         assertThat(userManagerForUser.isProfile()).isEqualTo(userTypeDetails.isProfile());
     }
 
+    /** Test that UserManager returns the correct UserProperties for a new managed profile. */
+    @MediumTest
+    @Test
+    public void testUserProperties() throws Exception {
+        assumeManagedUsersSupported();
+
+        // Get the default properties for a user type.
+        final UserTypeDetails userTypeDetails =
+                UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_PROFILE_MANAGED);
+        assertWithMessage("No %s type on device", UserManager.USER_TYPE_PROFILE_MANAGED)
+                .that(userTypeDetails).isNotNull();
+        final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference();
+
+        // Create an actual user (of this user type) and get its properties.
+        final int primaryUserId = mUserManager.getPrimaryUser().id;
+        final UserInfo userInfo = createProfileForUser("Managed",
+                UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId);
+        assertThat(userInfo).isNotNull();
+        final int userId = userInfo.id;
+        final UserProperties userProps = mUserManager.getUserProperties(UserHandle.of(userId));
+
+        // Check that this new user has the expected properties (relative to the defaults)
+        // provided that the test caller has the necessary permissions.
+        assertThat(userProps.getShowInLauncher()).isEqualTo(typeProps.getShowInLauncher());
+        assertThrows(SecurityException.class, userProps::getStartWithParent);
+    }
+
     // Make sure only max managed profiles can be created
     @MediumTest
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index 61a7f38..5c934852 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -28,6 +28,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.os.BatteryStatsHistory;
+import com.android.internal.os.Clock;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -49,13 +50,14 @@
     private final Parcel mHistoryBuffer = Parcel.obtain();
     private File mSystemDir;
     private File mHistoryDir;
+    private final Clock mClock = new MockClock();
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         Context context = InstrumentationRegistry.getContext();
         mSystemDir = context.getDataDir();
-        mHistoryDir = new File(mSystemDir, BatteryStatsHistory.HISTORY_DIR);
+        mHistoryDir = new File(mSystemDir, "battery-history");
         String[] files = mHistoryDir.list();
         if (files != null) {
             for (int i = 0; i < files.length; i++) {
@@ -67,8 +69,8 @@
 
     @Test
     public void testConstruct() {
-        BatteryStatsHistory history =
-                new BatteryStatsHistory(mHistoryBuffer, mSystemDir, () -> 32);
+        BatteryStatsHistory history = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
+                null, mClock);
         createActiveFile(history);
         verifyFileNumbers(history, Arrays.asList(0));
         verifyActiveFile(history, "0.bin");
@@ -76,8 +78,8 @@
 
     @Test
     public void testStartNextFile() {
-        BatteryStatsHistory history =
-                new BatteryStatsHistory(mHistoryBuffer, mSystemDir, () -> 32);
+        BatteryStatsHistory history = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
+                null, mClock);
         List<Integer> fileList = new ArrayList<>();
         fileList.add(0);
         createActiveFile(history);
@@ -114,13 +116,13 @@
         assertEquals(0, history.getHistoryUsedSize());
 
         // create a new BatteryStatsHistory object, it will pick up existing history files.
-        BatteryStatsHistory history2 =
-                new BatteryStatsHistory(mHistoryBuffer, mSystemDir, () -> 32);
-        // verify construct can pick up all files from file system.
+        BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
+                null, mClock);
+        // verify constructor can pick up all files from file system.
         verifyFileNumbers(history2, fileList);
         verifyActiveFile(history2, "33.bin");
 
-        history2.resetAllFiles();
+        history2.reset();
         createActiveFile(history2);
         // verify all existing files are deleted.
         for (int i = 2; i < 33; ++i) {
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/servicestests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index 713e786..570b2ee 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -63,6 +63,7 @@
     MockBatteryStatsImpl(Clock clock, File historyDirectory) {
         super(clock, historyDirectory);
         initTimersAndCounters();
+        setMaxHistoryBuffer(128 * 1024);
 
         setExternalStatsSyncLocked(mExternalStatsSync);
         informThatAllExternalStatsAreFlushed();
@@ -104,12 +105,6 @@
         return mForceOnBattery ? true : super.isOnBattery();
     }
 
-    public void forceRecordAllHistory() {
-        mHaveBatteryLevel = true;
-        mRecordingHistory = true;
-        mRecordAllHistory = true;
-    }
-
     public TimeBase getOnBatteryBackgroundTimeBase(int uid) {
         return getUidStatsLocked(uid).mOnBatteryBackgroundTimeBase;
     }
@@ -201,12 +196,14 @@
     @GuardedBy("this")
     public MockBatteryStatsImpl setMaxHistoryFiles(int maxHistoryFiles) {
         mConstants.MAX_HISTORY_FILES = maxHistoryFiles;
+        mConstants.onChange();
         return this;
     }
 
     @GuardedBy("this")
     public MockBatteryStatsImpl setMaxHistoryBuffer(int maxHistoryBuffer) {
         mConstants.MAX_HISTORY_BUFFER = maxHistoryBuffer;
+        mConstants.onChange();
         return this;
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
index 911fb6a..08c2c6e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
@@ -1301,6 +1301,21 @@
     }
 
     @Test
+    public void testA11yCrossUserEventNotSent() throws Exception {
+        final Notification n = new Builder(getContext(), "test")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+        int userId = mUser.getIdentifier() + 1;
+        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid,
+                mPid, n, UserHandle.of(userId), null, System.currentTimeMillis());
+        NotificationRecord r = new NotificationRecord(getContext(), sbn,
+                new NotificationChannel("test", "test", IMPORTANCE_HIGH));
+
+        mService.buzzBeepBlinkLocked(r);
+
+        verify(mAccessibilityService, never()).sendAccessibilityEvent(any(), anyInt());
+    }
+
+    @Test
     public void testLightsScreenOn() {
         mService.mScreenOn = true;
         NotificationRecord r = getLightsNotification();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index 7415460..f61effa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -474,7 +474,7 @@
     }
 
     @Test
-    public void testActivityRecordReparentToTaskFragment() {
+    public void testActivityRecordReparentedToTaskFragment() {
         final ActivityRecord activity = createActivityRecord(mDc);
         final SurfaceControl activityLeash = mock(SurfaceControl.class);
         doNothing().when(activity).setDropInputMode(anyInt());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 8b3cff8..8332cb4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -204,7 +204,7 @@
                 mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentInfoChanged(mTaskFragmentInfo);
+        verify(mOrganizer).onTaskFragmentInfoChanged(eq(mTaskFragmentInfo));
     }
 
     @Test
@@ -231,7 +231,7 @@
         verify(mOrganizer, never()).onTaskFragmentAppeared(any());
         verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
         verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(anyInt(), any());
-        verify(mOrganizer).onTaskFragmentVanished(mTaskFragmentInfo);
+        verify(mOrganizer).onTaskFragmentVanished(eq(mTaskFragmentInfo));
 
         // Not trigger onTaskFragmentInfoChanged.
         // Call onTaskFragmentAppeared before calling onTaskFragmentInfoChanged.
@@ -247,7 +247,7 @@
         verify(mOrganizer, never()).onTaskFragmentAppeared(any());
         verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
         verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(anyInt(), any());
-        verify(mOrganizer).onTaskFragmentVanished(mTaskFragmentInfo);
+        verify(mOrganizer).onTaskFragmentVanished(eq(mTaskFragmentInfo));
     }
 
     @Test
@@ -298,11 +298,12 @@
                 mErrorToken, null /* taskFragment */, -1 /* opType */, exception);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), eq(null), eq(-1), eq(exception));
+        verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), eq(null), eq(-1),
+                eq(exception));
     }
 
     @Test
-    public void testOnActivityReparentToTask_activityInOrganizerProcess_useActivityToken() {
+    public void testOnActivityReparentedToTask_activityInOrganizerProcess_useActivityToken() {
         // Make sure the activity pid/uid is the same as the organizer caller.
         final int pid = Binder.getCallingPid();
         final int uid = Binder.getCallingUid();
@@ -314,17 +315,18 @@
         task.effectiveUid = uid;
 
         // No need to notify organizer if it is not embedded.
-        mController.onActivityReparentToTask(activity);
+        mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onActivityReparentToTask(anyInt(), any(), any());
+        verify(mOrganizer, never()).onActivityReparentedToTask(anyInt(), any(), any());
 
         // Notify organizer if it was embedded before entered Pip.
         activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer;
-        mController.onActivityReparentToTask(activity);
+        mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onActivityReparentToTask(task.mTaskId, activity.intent, activity.token);
+        verify(mOrganizer).onActivityReparentedToTask(eq(task.mTaskId), eq(activity.intent),
+                eq(activity.token));
 
         // Notify organizer if there is any embedded in the Task.
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
@@ -335,15 +337,16 @@
                 DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME);
         activity.reparent(taskFragment, POSITION_TOP);
         activity.mLastTaskFragmentOrganizerBeforePip = null;
-        mController.onActivityReparentToTask(activity);
+        mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
         verify(mOrganizer, times(2))
-                .onActivityReparentToTask(task.mTaskId, activity.intent, activity.token);
+                .onActivityReparentedToTask(eq(task.mTaskId), eq(activity.intent),
+                        eq(activity.token));
     }
 
     @Test
-    public void testOnActivityReparentToTask_activityNotInOrganizerProcess_useTemporaryToken() {
+    public void testOnActivityReparentedToTask_activityNotInOrganizerProcess_useTemporaryToken() {
         final int pid = Binder.getCallingPid();
         final int uid = Binder.getCallingUid();
         mTaskFragment.setTaskFragmentOrganizer(mOrganizer.getOrganizerToken(), uid,
@@ -364,11 +367,11 @@
         // Notify organizer if it was embedded before entered Pip.
         // Create a temporary token since the activity doesn't belong to the same process.
         activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer;
-        mController.onActivityReparentToTask(activity);
+        mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
         // Allow organizer to reparent activity in other process using the temporary token.
-        verify(mOrganizer).onActivityReparentToTask(eq(task.mTaskId), eq(activity.intent),
+        verify(mOrganizer).onActivityReparentedToTask(eq(task.mTaskId), eq(activity.intent),
                 token.capture());
         final IBinder temporaryToken = token.getValue();
         assertNotEquals(activity.token, temporaryToken);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 88eadfc..83f1789 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -134,6 +134,23 @@
     }
 
     @Test
+    public void testStartChangeTransition_doNotFreezeWhenOnlyMoved() {
+        final Rect startBounds = new Rect(0, 0, 1000, 1000);
+        final Rect endBounds = new Rect(startBounds);
+        endBounds.offset(500, 0);
+        mTaskFragment.setBounds(startBounds);
+        doReturn(true).when(mTaskFragment).isVisible();
+        doReturn(true).when(mTaskFragment).isVisibleRequested();
+
+        clearInvocations(mTransaction);
+        mTaskFragment.setBounds(endBounds);
+
+        // No change transition, but update the organized surface position.
+        verify(mTaskFragment, never()).initializeChangeTransition(any(), any());
+        verify(mTransaction).setPosition(mLeash, endBounds.left, endBounds.top);
+    }
+
+    @Test
     public void testNotOkToAnimate_doNotStartChangeTransition() {
         mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer);
         final Rect startBounds = new Rect(0, 0, 1000, 1000);
@@ -323,7 +340,7 @@
         activity.reparent(task, POSITION_TOP);
 
         // Notify the organizer about the reparent.
-        verify(mAtm.mTaskFragmentOrganizerController).onActivityReparentToTask(activity);
+        verify(mAtm.mTaskFragmentOrganizerController).onActivityReparentedToTask(activity);
         assertNull(activity.mLastTaskFragmentOrganizerBeforePip);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 5a2d456..85ac7bd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -36,8 +36,10 @@
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 import static android.window.TransitionInfo.isIndependent;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -1082,6 +1084,39 @@
         assertTrue((info.getChanges().get(1).getFlags() & FLAG_IS_EMBEDDED) != 0);
     }
 
+    @Test
+    public void testIncludeEmbeddedActivityReparent() {
+        final Transition transition = createTestTransition(TRANSIT_OPEN);
+        final Task task = createTask(mDisplayContent);
+        task.setBounds(new Rect(0, 0, 2000, 1000));
+        final ActivityRecord activity = createActivityRecord(task);
+        activity.mVisibleRequested = true;
+        // Skip manipulate the SurfaceControl.
+        doNothing().when(activity).setDropInputMode(anyInt());
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        mAtm.mTaskFragmentOrganizerController.registerOrganizer(
+                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()));
+        final TaskFragment embeddedTf = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setOrganizer(organizer)
+                .build();
+        // TaskFragment with different bounds from Task.
+        embeddedTf.setBounds(new Rect(0, 0, 1000, 1000));
+
+        // Start states.
+        transition.collect(activity);
+        transition.collectExistenceChange(embeddedTf);
+
+        // End states.
+        activity.reparent(embeddedTf, POSITION_TOP);
+
+        // Verify that both activity and TaskFragment are included.
+        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+                transition.mParticipants, transition.mChanges);
+        assertTrue(targets.contains(embeddedTf));
+        assertTrue(targets.contains(activity));
+    }
+
     private static void makeTaskOrganized(Task... tasks) {
         final ITaskOrganizer organizer = mock(ITaskOrganizer.class);
         for (Task t : tasks) {
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index f28408e1a..4fb6587 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -139,24 +139,19 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static final Uri CONTENT_URI = SimInfo.CONTENT_URI;
 
-    /** @hide */
-    public static final String CACHE_KEY_DEFAULT_SUB_ID_PROPERTY =
+    private static final String CACHE_KEY_DEFAULT_SUB_ID_PROPERTY =
             "cache_key.telephony.get_default_sub_id";
 
-    /** @hide */
-    public static final String CACHE_KEY_DEFAULT_DATA_SUB_ID_PROPERTY =
+    private static final String CACHE_KEY_DEFAULT_DATA_SUB_ID_PROPERTY =
             "cache_key.telephony.get_default_data_sub_id";
 
-    /** @hide */
-    public static final String CACHE_KEY_DEFAULT_SMS_SUB_ID_PROPERTY =
+    private static final String CACHE_KEY_DEFAULT_SMS_SUB_ID_PROPERTY =
             "cache_key.telephony.get_default_sms_sub_id";
 
-    /** @hide */
-    public static final String CACHE_KEY_ACTIVE_DATA_SUB_ID_PROPERTY =
+    private static final String CACHE_KEY_ACTIVE_DATA_SUB_ID_PROPERTY =
             "cache_key.telephony.get_active_data_sub_id";
 
-    /** @hide */
-    public static final String CACHE_KEY_SLOT_INDEX_PROPERTY =
+    private static final String CACHE_KEY_SLOT_INDEX_PROPERTY =
             "cache_key.telephony.get_slot_index";
 
     /** @hide */