Merge "Make "ime reset --user <userId>" consistent among users" into main
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index cbabb02..90de7ab 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -2214,6 +2214,9 @@
         notifyVoiceInteractionManagerServiceActivityEvent(
                 VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_RESUME);
 
+        // Notify autofill
+        getAutofillClientController().onActivityPostResumed();
+
         mCalled = true;
     }
 
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 4952af3..8696e0f 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -4623,7 +4623,7 @@
 
     private void scheduleResume(ActivityClientRecord r) {
         final ClientTransaction transaction = new ClientTransaction(mAppThread);
-        final ResumeActivityItem resumeActivityItem = ResumeActivityItem.obtain(r.token,
+        final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(r.token,
                 /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
         transaction.addTransactionItem(resumeActivityItem);
         executeTransaction(transaction);
diff --git a/core/java/android/app/servertransaction/ActivityLifecycleItem.java b/core/java/android/app/servertransaction/ActivityLifecycleItem.java
index 48db18f..d07ad46 100644
--- a/core/java/android/app/servertransaction/ActivityLifecycleItem.java
+++ b/core/java/android/app/servertransaction/ActivityLifecycleItem.java
@@ -18,6 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.os.IBinder;
 import android.os.Parcel;
 
 import java.lang.annotation.Retention;
@@ -25,6 +26,7 @@
 
 /**
  * Request for lifecycle state that an activity should reach.
+ *
  * @hide
  */
 public abstract class ActivityLifecycleItem extends ActivityTransactionItem {
@@ -52,8 +54,19 @@
     public static final int ON_DESTROY = 6;
     public static final int ON_RESTART = 7;
 
-    ActivityLifecycleItem() {}
+    ActivityLifecycleItem(@NonNull IBinder activityToken) {
+        super(activityToken);
+    }
 
+    // TODO(b/311089192): Remove this method once no subclasses obtain from the object pool.
+    @Deprecated
+    ActivityLifecycleItem() {
+        super();
+    }
+
+    // Parcelable implementation
+
+    /** Reads from Parcel. */
     ActivityLifecycleItem(@NonNull Parcel in) {
         super(in);
     }
diff --git a/core/java/android/app/servertransaction/ActivityTransactionItem.java b/core/java/android/app/servertransaction/ActivityTransactionItem.java
index b4ff476f..8ef86ee 100644
--- a/core/java/android/app/servertransaction/ActivityTransactionItem.java
+++ b/core/java/android/app/servertransaction/ActivityTransactionItem.java
@@ -49,9 +49,29 @@
 public abstract class ActivityTransactionItem extends ClientTransactionItem {
 
     /** Target client activity. */
+    // TODO(b/311089192): Mark this with @NonNull and final.
     private IBinder mActivityToken;
 
-    ActivityTransactionItem() {}
+    public ActivityTransactionItem(@NonNull IBinder activityToken) {
+        mActivityToken = requireNonNull(activityToken);
+    }
+
+    // TODO(b/311089192): Remove this method once no subclasses obtain from the object pool.
+    @Deprecated
+    ActivityTransactionItem() {
+    }
+
+    /**
+     * Sets the activity token after the instance is obtained from the object pool.
+     *
+     * @deprecated This method is deprecated. The object pool is no longer used.
+     *     Instead, directly create a new object with the activity token.
+     * TODO(b/311089192): Remove this method once no subclasses obtain from the object pool.
+     */
+    @Deprecated
+    void setActivityToken(@NonNull IBinder activityToken) {
+        mActivityToken = requireNonNull(activityToken);
+    }
 
     @Override
     public final void execute(@NonNull ClientTransactionHandler client,
@@ -94,26 +114,18 @@
         return mActivityToken;
     }
 
-    void setActivityToken(@NonNull IBinder activityToken) {
-        mActivityToken = requireNonNull(activityToken);
-    }
+    // Parcelable implementation
 
-    // To be overridden
-
-    ActivityTransactionItem(@NonNull Parcel in) {
-        mActivityToken = in.readStrongBinder();
-    }
-
+    /** Writes to Parcel. */
     @CallSuper
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeStrongBinder(mActivityToken);
     }
 
-    @CallSuper
-    @Override
-    public void recycle() {
-        mActivityToken = null;
+    /** Reads from Parcel. */
+    ActivityTransactionItem(@NonNull Parcel in) {
+        this(in.readStrongBinder());
     }
 
     @Override
diff --git a/core/java/android/app/servertransaction/DestroyActivityItem.java b/core/java/android/app/servertransaction/DestroyActivityItem.java
index b0213d7..8a81143 100644
--- a/core/java/android/app/servertransaction/DestroyActivityItem.java
+++ b/core/java/android/app/servertransaction/DestroyActivityItem.java
@@ -28,11 +28,17 @@
 
 /**
  * Request to destroy an activity.
+ *
  * @hide
  */
 public class DestroyActivityItem extends ActivityLifecycleItem {
 
-    private boolean mFinished;
+    private final boolean mFinished;
+
+    public DestroyActivityItem(@NonNull IBinder activityToken, boolean finished) {
+        super(activityToken);
+        mFinished = finished;
+    }
 
     @Override
     public void preExecute(@NonNull ClientTransactionHandler client) {
@@ -60,40 +66,16 @@
         return ON_DESTROY;
     }
 
-    // ObjectPoolItem implementation
-
-    private DestroyActivityItem() {}
-
-    /** Obtain an instance initialized with provided params. */
-    @NonNull
-    public static DestroyActivityItem obtain(@NonNull IBinder activityToken, boolean finished) {
-        DestroyActivityItem instance = ObjectPool.obtain(DestroyActivityItem.class);
-        if (instance == null) {
-            instance = new DestroyActivityItem();
-        }
-        instance.setActivityToken(activityToken);
-        instance.mFinished = finished;
-
-        return instance;
-    }
-
-    @Override
-    public void recycle() {
-        super.recycle();
-        mFinished = false;
-        ObjectPool.recycle(this);
-    }
-
     // Parcelable implementation
 
-    /** Write to Parcel. */
+    /** Writes to Parcel. */
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         super.writeToParcel(dest, flags);
         dest.writeBoolean(mFinished);
     }
 
-    /** Read from Parcel. */
+    /** Reads from Parcel. */
     private DestroyActivityItem(@NonNull Parcel in) {
         super(in);
         mFinished = in.readBoolean();
diff --git a/core/java/android/app/servertransaction/ResumeActivityItem.java b/core/java/android/app/servertransaction/ResumeActivityItem.java
index 4a0ea98..a28791f 100644
--- a/core/java/android/app/servertransaction/ResumeActivityItem.java
+++ b/core/java/android/app/servertransaction/ResumeActivityItem.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.app.ActivityClient;
 import android.app.ActivityManager;
+import android.app.ActivityManager.ProcessState;
 import android.app.ActivityThread.ActivityClientRecord;
 import android.app.ClientTransactionHandler;
 import android.os.IBinder;
@@ -30,22 +31,37 @@
 
 /**
  * Request to move an activity to resumed state.
+ *
  * @hide
  */
 public class ResumeActivityItem extends ActivityLifecycleItem {
 
-    private static final String TAG = "ResumeActivityItem";
+    @ProcessState
+    private final int mProcState;
 
-    private int mProcState;
-    private boolean mUpdateProcState;
-    private boolean mIsForward;
+    private final boolean mIsForward;
+
     // Whether we should send compat fake focus when the activity is resumed. This is needed
     // because some game engines wait to get focus before drawing the content of the app.
-    private boolean mShouldSendCompatFakeFocus;
+    private final boolean mShouldSendCompatFakeFocus;
+
+    public ResumeActivityItem(@NonNull IBinder activityToken, boolean isForward,
+            boolean shouldSendCompatFakeFocus) {
+        this(activityToken, ActivityManager.PROCESS_STATE_UNKNOWN, isForward,
+                shouldSendCompatFakeFocus);
+    }
+
+    public ResumeActivityItem(@NonNull IBinder activityToken, @ProcessState int procState,
+            boolean isForward, boolean shouldSendCompatFakeFocus) {
+        super(activityToken);
+        mProcState = procState;
+        mIsForward = isForward;
+        mShouldSendCompatFakeFocus = shouldSendCompatFakeFocus;
+    }
 
     @Override
     public void preExecute(@NonNull ClientTransactionHandler client) {
-        if (mUpdateProcState) {
+        if (mProcState != ActivityManager.PROCESS_STATE_UNKNOWN) {
             client.updateProcessState(mProcState, false);
         }
     }
@@ -72,71 +88,21 @@
         return ON_RESUME;
     }
 
-    // ObjectPoolItem implementation
-
-    private ResumeActivityItem() {}
-
-    /** Obtain an instance initialized with provided params. */
-    @NonNull
-    public static ResumeActivityItem obtain(@NonNull IBinder activityToken, int procState,
-            boolean isForward, boolean shouldSendCompatFakeFocus) {
-        ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class);
-        if (instance == null) {
-            instance = new ResumeActivityItem();
-        }
-        instance.setActivityToken(activityToken);
-        instance.mProcState = procState;
-        instance.mUpdateProcState = true;
-        instance.mIsForward = isForward;
-        instance.mShouldSendCompatFakeFocus = shouldSendCompatFakeFocus;
-
-        return instance;
-    }
-
-    /** Obtain an instance initialized with provided params. */
-    @NonNull
-    public static ResumeActivityItem obtain(@NonNull IBinder activityToken, boolean isForward,
-            boolean shouldSendCompatFakeFocus) {
-        ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class);
-        if (instance == null) {
-            instance = new ResumeActivityItem();
-        }
-        instance.setActivityToken(activityToken);
-        instance.mProcState = ActivityManager.PROCESS_STATE_UNKNOWN;
-        instance.mUpdateProcState = false;
-        instance.mIsForward = isForward;
-        instance.mShouldSendCompatFakeFocus = shouldSendCompatFakeFocus;
-
-        return instance;
-    }
-
-    @Override
-    public void recycle() {
-        super.recycle();
-        mProcState = ActivityManager.PROCESS_STATE_UNKNOWN;
-        mUpdateProcState = false;
-        mIsForward = false;
-        mShouldSendCompatFakeFocus = false;
-        ObjectPool.recycle(this);
-    }
-
     // Parcelable implementation
 
-    /** Write to Parcel. */
+    /** Writes to Parcel. */
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         super.writeToParcel(dest, flags);
         dest.writeInt(mProcState);
-        dest.writeBoolean(mUpdateProcState);
         dest.writeBoolean(mIsForward);
         dest.writeBoolean(mShouldSendCompatFakeFocus);
     }
 
-    /** Read from Parcel. */
+    /** Reads from Parcel. */
     private ResumeActivityItem(@NonNull Parcel in) {
         super(in);
         mProcState = in.readInt();
-        mUpdateProcState = in.readBoolean();
         mIsForward = in.readBoolean();
         mShouldSendCompatFakeFocus = in.readBoolean();
     }
@@ -160,7 +126,7 @@
             return false;
         }
         final ResumeActivityItem other = (ResumeActivityItem) o;
-        return mProcState == other.mProcState && mUpdateProcState == other.mUpdateProcState
+        return mProcState == other.mProcState
                 && mIsForward == other.mIsForward
                 && mShouldSendCompatFakeFocus == other.mShouldSendCompatFakeFocus;
     }
@@ -170,7 +136,6 @@
         int result = 17;
         result = 31 * result + super.hashCode();
         result = 31 * result + mProcState;
-        result = 31 * result + (mUpdateProcState ? 1 : 0);
         result = 31 * result + (mIsForward ? 1 : 0);
         result = 31 * result + (mShouldSendCompatFakeFocus ? 1 : 0);
         return result;
@@ -180,7 +145,6 @@
     public String toString() {
         return "ResumeActivityItem{" + super.toString()
                 + ",procState=" + mProcState
-                + ",updateProcState=" + mUpdateProcState
                 + ",isForward=" + mIsForward
                 + ",shouldSendCompatFakeFocus=" + mShouldSendCompatFakeFocus + "}";
     }
diff --git a/core/java/android/app/servertransaction/TransactionExecutorHelper.java b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
index 9f622e9..8ba9076 100644
--- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java
+++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
@@ -42,6 +42,7 @@
 
 /**
  * Helper class for {@link TransactionExecutor} that contains utils for lifecycle path resolution.
+ *
  * @hide
  */
 public class TransactionExecutorHelper {
@@ -203,7 +204,7 @@
                 lifecycleItem = StopActivityItem.obtain(r.token);
                 break;
             default:
-                lifecycleItem = ResumeActivityItem.obtain(r.token, false /* isForward */,
+                lifecycleItem = new ResumeActivityItem(r.token, false /* isForward */,
                         false /* shouldSendCompatFakeFocus */);
                 break;
         }
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 5a39702..ce241c1 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -337,3 +337,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "private_space_search_illustration_config"
+  namespace: "profile_experiences"
+  description: "Check config to show/hide the private space search illustration and search tile content in Hide Private Space settings page"
+  bug: "346612477"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 811a834..7353dde 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -37,6 +37,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -470,6 +471,9 @@
         // Set to PowerManager.BRIGHTNESS_INVALID if there's no override.
         public float screenBrightnessOverride;
 
+        // Tag used to identify the app window requesting the brightness override.
+        public CharSequence screenBrightnessOverrideTag;
+
         // An override of the screen auto-brightness adjustment factor in the range -1 (dimmer) to
         // 1 (brighter). Set to Float.NaN if there's no override.
         public float screenAutoBrightnessAdjustmentOverride;
@@ -524,6 +528,7 @@
             policy = other.policy;
             useProximitySensor = other.useProximitySensor;
             screenBrightnessOverride = other.screenBrightnessOverride;
+            screenBrightnessOverrideTag = other.screenBrightnessOverrideTag;
             screenAutoBrightnessAdjustmentOverride = other.screenAutoBrightnessAdjustmentOverride;
             screenLowPowerBrightnessFactor = other.screenLowPowerBrightnessFactor;
             blockScreenOn = other.blockScreenOn;
@@ -544,8 +549,9 @@
             return other != null
                     && policy == other.policy
                     && useProximitySensor == other.useProximitySensor
-                    && floatEquals(screenBrightnessOverride,
-                            other.screenBrightnessOverride)
+                    && floatEquals(screenBrightnessOverride, other.screenBrightnessOverride)
+                    && Objects.equals(screenBrightnessOverrideTag,
+                            other.screenBrightnessOverrideTag)
                     && floatEquals(screenAutoBrightnessAdjustmentOverride,
                             other.screenAutoBrightnessAdjustmentOverride)
                     && screenLowPowerBrightnessFactor
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index df353e5..ce3156e 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -104,8 +104,10 @@
      * This method must only be called by the window manager.
      *
      * @param brightness The overridden brightness, or Float.NaN to disable the override.
+     * @param tag Source identifier of the app window that requests the override.
      */
-    public abstract void setScreenBrightnessOverrideFromWindowManager(float brightness);
+    public abstract void setScreenBrightnessOverrideFromWindowManager(
+            float brightness, CharSequence tag);
 
     /**
      * Used by the window manager to override the user activity timeout based on the
diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java
index b1ef05a..6a2daea 100644
--- a/core/java/android/os/ZygoteProcess.java
+++ b/core/java/android/os/ZygoteProcess.java
@@ -1102,7 +1102,6 @@
     /**
      * Instructs the zygote to pre-load the application code for the given Application.
      * Only the app zygote supports this function.
-     * TODO preloadPackageForAbi() can probably be removed and the callers an use this instead.
      */
     public boolean preloadApp(ApplicationInfo appInfo, String abi)
             throws ZygoteStartFailedEx, IOException {
@@ -1130,39 +1129,6 @@
     }
 
     /**
-     * Instructs the zygote to pre-load the classes and native libraries at the given paths
-     * for the specified abi. Not all zygotes support this function.
-     */
-    public boolean preloadPackageForAbi(
-            String packagePath, String libsPath, String libFileName, String cacheKey, String abi)
-            throws ZygoteStartFailedEx, IOException {
-        synchronized (mLock) {
-            ZygoteState state = openZygoteSocketIfNeeded(abi);
-            state.mZygoteOutputWriter.write("5");
-            state.mZygoteOutputWriter.newLine();
-
-            state.mZygoteOutputWriter.write("--preload-package");
-            state.mZygoteOutputWriter.newLine();
-
-            state.mZygoteOutputWriter.write(packagePath);
-            state.mZygoteOutputWriter.newLine();
-
-            state.mZygoteOutputWriter.write(libsPath);
-            state.mZygoteOutputWriter.newLine();
-
-            state.mZygoteOutputWriter.write(libFileName);
-            state.mZygoteOutputWriter.newLine();
-
-            state.mZygoteOutputWriter.write(cacheKey);
-            state.mZygoteOutputWriter.newLine();
-
-            state.mZygoteOutputWriter.flush();
-
-            return (state.mZygoteInputStream.readInt() == 0);
-        }
-    }
-
-    /**
      * Instructs the zygote to preload the default set of classes and resources. Returns
      * {@code true} if a preload was performed as a result of this call, and {@code false}
      * otherwise. The latter usually means that the zygote eagerly preloaded at startup
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ff38920..850b979 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2630,7 +2630,7 @@
     /**
      * Activity Action: Show screen that let user select its Autofill Service.
      * <p>
-     * Input: Intent's data URI set with an application name, using the
+     * Input: Intent's data URI set with an application package name, using the
      * "package" schema (like "package:com.my.app").
      *
      * <p>
@@ -2650,7 +2650,7 @@
     /**
      * Activity Action: Show screen that let user enable a Credential Manager provider.
      * <p>
-     * Input: Intent's data URI set with an application name, using the
+     * Input: Intent's data URI set with an application package name, using the
      * "package" schema (like "package:com.my.app").
      *
      * <p>
diff --git a/core/java/android/service/notification/SystemZenRules.java b/core/java/android/service/notification/SystemZenRules.java
index 22234a9..1d18643 100644
--- a/core/java/android/service/notification/SystemZenRules.java
+++ b/core/java/android/service/notification/SystemZenRules.java
@@ -129,10 +129,7 @@
         }
         sb.append(daysSummary);
         sb.append(context.getString(R.string.zen_mode_trigger_summary_divider_text));
-        sb.append(context.getString(
-                R.string.zen_mode_trigger_summary_range_symbol_combination,
-                timeString(context, schedule.startHour, schedule.startMinute),
-                timeString(context, schedule.endHour, schedule.endMinute)));
+        sb.append(getTimeSummary(context, schedule));
 
         return sb.toString();
     }
@@ -142,7 +139,7 @@
      * adjacent days grouped together ("Sun-Wed" instead of "Sun,Mon,Tue,Wed").
      */
     @Nullable
-    private static String getShortDaysSummary(Context context, @NonNull ScheduleInfo schedule) {
+    public static String getShortDaysSummary(Context context, @NonNull ScheduleInfo schedule) {
         // Compute a list of days with contiguous days grouped together, for example: "Sun-Thu" or
         // "Sun-Mon,Wed,Fri"
         final int[] days = schedule.days;
@@ -224,6 +221,14 @@
         return null;
     }
 
+    /** Returns the time part of a {@link ScheduleInfo}, e.g. {@code 9:00-17:00}. */
+    public static String getTimeSummary(Context context, @NonNull ScheduleInfo schedule) {
+        return context.getString(
+                R.string.zen_mode_trigger_summary_range_symbol_combination,
+                timeString(context, schedule.startHour, schedule.startMinute),
+                timeString(context, schedule.endHour, schedule.endMinute));
+    }
+
     /**
      * Convenience method for representing the specified time in string format.
      */
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index a23e383..64c7766 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -11004,6 +11004,11 @@
             ? afm.isAutofillable(this) : false;
     }
 
+    /**
+     * Returns whether the view is autofillable.
+     *
+     * @return whether the view is autofillable, and should send out autofill request to provider.
+     */
     private boolean isAutofillable() {
         if (DBG) {
             Log.d(VIEW_LOG_TAG, "isAutofillable() entered.");
@@ -27631,6 +27636,23 @@
         return null;
     }
 
+
+    /**
+     * Performs the traversal to find views that are autofillable.
+     * Autofillable views are added to the provided list.
+     *
+     * <strong>Note:</strong>This method does not stop at the root namespace
+     * boundary.
+     *
+     * @param autofillableViews The output list of autofillable Views.
+     * @hide
+     */
+    public void findAutofillableViewsByTraversal(@NonNull List<View> autofillableViews) {
+        if (isAutofillable()) {
+            autofillableViews.add(this);
+        }
+    }
+
     /**
      * Look for a child view with the given tag.  If this view has the given
      * tag, return this view.
@@ -30597,6 +30619,8 @@
         }
     }
 
+    // Note that if the function returns true, it indicates aapt did not generate this id.
+    // However false value does not indicate that aapt did generated this id.
     private static boolean isViewIdGenerated(int id) {
         return (id & 0xFF000000) == 0 && (id & 0x00FFFFFF) != 0;
     }
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index ceaca22..6f88386 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -1500,6 +1500,19 @@
         return null;
     }
 
+    /** @hide */
+    @Override
+    public void findAutofillableViewsByTraversal(@NonNull List<View> autofillableViews) {
+        super.findAutofillableViewsByTraversal(autofillableViews);
+
+        final int childrenCount = mChildrenCount;
+        final View[] children = mChildren;
+        for (int i = 0; i < childrenCount; i++) {
+            View child = children[i];
+            child.findAutofillableViewsByTraversal(autofillableViews);
+        }
+    }
+
     @Override
     public void dispatchWindowFocusChanged(boolean hasFocus) {
         super.dispatchWindowFocusChanged(hasFocus);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 37d5220..9e52a14 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -12900,11 +12900,6 @@
                     mFrameRateCategoryHighCount = FRAME_RATE_CATEGORY_COUNT;
         }
 
-        // If it's currently an intermittent update,
-        // we should keep mPreferredFrameRateCategory as NORMAL
-        if (intermittentUpdateState() == INTERMITTENT_STATE_INTERMITTENT) {
-            return;
-        }
 
         if (mFrameRateCategoryHighCount > 0) {
             mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_HIGH;
diff --git a/core/java/android/view/autofill/AutofillClientController.java b/core/java/android/view/autofill/AutofillClientController.java
index 2f7adaa..d505c733 100644
--- a/core/java/android/view/autofill/AutofillClientController.java
+++ b/core/java/android/view/autofill/AutofillClientController.java
@@ -39,6 +39,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 /**
  * A controller to manage the autofill requests for the {@link Activity}.
@@ -71,6 +72,7 @@
     private AutofillPopupWindow mAutofillPopupWindow;
     private boolean mAutoFillResetNeeded;
     private boolean mAutoFillIgnoreFirstResumePause;
+    private Boolean mRelayoutFix;
 
     /**
      * AutofillClientController constructor.
@@ -86,6 +88,18 @@
         return mAutofillManager;
     }
 
+    /**
+     * Whether to apply relayout fixes.
+     *
+     * @hide
+     */
+    public boolean isRelayoutFixEnabled() {
+        if (mRelayoutFix == null) {
+            mRelayoutFix = getAutofillManager().isRelayoutFixEnabled();
+        }
+        return mRelayoutFix;
+    }
+
     // ------------------ Called for Activity events ------------------
 
     /**
@@ -119,7 +133,54 @@
      * Called when the {@link Activity#onResume()} is called.
      */
     public void onActivityResumed() {
+        if (Helper.sVerbose) {
+            Log.v(TAG, "onActivityResumed()");
+        }
+        if (isRelayoutFixEnabled()) {
+            // Do nothing here. We'll handle it in onActivityPostResumed()
+            return;
+        }
+        if (Helper.sVerbose) {
+            Log.v(TAG, "onActivityResumed(): Relayout fix not enabled");
+        }
+        forResume();
+    }
+
+    /**
+     * Called when the {@link Activity#onPostResume()} is called.
+     */
+    public void onActivityPostResumed() {
+        if (Helper.sVerbose) {
+            Log.v(TAG, "onActivityPostResumed()");
+        }
+        if (!isRelayoutFixEnabled()) {
+            return;
+        }
+        if (Helper.sVerbose) {
+            Log.v(TAG, "onActivityPostResumed(): Relayout fix enabled");
+        }
+        forResume();
+    }
+
+    /**
+     * Code to execute when an app has resumed (or is about to resume)
+     */
+    private void forResume() {
         enableAutofillCompatibilityIfNeeded();
+        boolean relayoutFix = isRelayoutFixEnabled();
+        if (relayoutFix) {
+            if (getAutofillManager().shouldRetryFill()) {
+                if (Helper.sVerbose) {
+                    Log.v(TAG, "forResume(): Autofill potential relayout. Retrying fill.");
+                }
+                getAutofillManager().attemptRefill();
+            } else {
+                if (Helper.sVerbose) {
+                    Log.v(TAG, "forResume(): Not attempting refill.");
+                }
+            }
+        }
+
         if (mAutoFillResetNeeded) {
             if (!mAutoFillIgnoreFirstResumePause) {
                 View focus = mActivity.getCurrentFocus();
@@ -131,7 +192,16 @@
                     // ViewRootImpl.performTraversals() changes window visibility to VISIBLE.
                     // So we cannot call View.notifyEnterOrExited() which will do nothing
                     // when View.isVisibleToUser() is false.
-                    getAutofillManager().notifyViewEntered(focus);
+                    if (relayoutFix && getAutofillManager().isAuthenticationPending()) {
+                        if (Helper.sVerbose) {
+                            Log.v(TAG, "forResume(): ignoring focus due to auth pending");
+                        }
+                    } else {
+                        if (Helper.sVerbose) {
+                            Log.v(TAG, "forResume(): notifyViewEntered");
+                        }
+                        getAutofillManager().notifyViewEntered(focus);
+                    }
                 }
             }
         }
@@ -427,6 +497,22 @@
     }
 
     @Override
+    public List<View> autofillClientFindAutofillableViewsByTraversal() {
+        final ArrayList<View> views = new ArrayList<>();
+        final ArrayList<ViewRootImpl> roots =
+                WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
+
+        for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
+            final View rootView = roots.get(rootNum).getView();
+
+            if (rootView != null) {
+                rootView.findAutofillableViewsByTraversal(views);
+            }
+        }
+        return views;
+    }
+
+    @Override
     public boolean autofillClientIsFillUiShowing() {
         return mAutofillPopupWindow != null && mAutofillPopupWindow.isShowing();
     }
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 515ed0e..02a86c9 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -874,6 +874,13 @@
         @Nullable View autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId);
 
         /**
+         * Finds all the autofillable views on the screen.
+         *
+         * @return The list of views that are autofillable.
+         */
+        List<View> autofillClientFindAutofillableViewsByTraversal();
+
+        /**
          * Runs the specified action on the UI thread.
          */
         void autofillClientRunOnUiThread(Runnable action);
@@ -1498,6 +1505,37 @@
     }
 
     /**
+     * Called to know whether authentication was pending.
+     * @hide
+     */
+    public boolean isAuthenticationPending() {
+        return mState == STATE_PENDING_AUTHENTICATION;
+    }
+
+    /**
+     * Called to check if we should retry fill.
+     * Useful for knowing whether to attempt refill after relayout.
+     *
+     * @hide
+     */
+    public boolean shouldRetryFill() {
+        // TODO: Implement in follow-up cl
+        return false;
+    }
+
+    /**
+     * Called when a potential relayout may have occurred.
+     *
+     * @return whether refill was done. True if refill was done partially or fully.
+     * @hide
+     */
+    public boolean attemptRefill() {
+        Log.i(TAG, "Attempting refill");
+        // TODO: Implement in follow-up cl
+        return false;
+    }
+
+    /**
      * Called when a {@link View} that supports autofill is entered.
      *
      * @param view {@link View} that was entered.
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 0dadbe3..eb35817 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -8360,7 +8360,7 @@
         }
     }
 
-    private interface PendingResources<T> {
+    interface PendingResources<T> {
         T create(Context context, Resources appResources, HierarchyRootData rootData, int depth)
                 throws Exception;
     }
diff --git a/core/java/android/widget/RemoteViewsSerializers.java b/core/java/android/widget/RemoteViewsSerializers.java
new file mode 100644
index 0000000..600fea4
--- /dev/null
+++ b/core/java/android/widget/RemoteViewsSerializers.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.widget;
+
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BlendMode;
+import android.graphics.drawable.Icon;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
+
+import androidx.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.util.function.Function;
+
+/**
+ * This class provides serialization for certain types used within RemoteViews.
+ *
+ * @hide
+ */
+public class RemoteViewsSerializers {
+    private static final String TAG = "RemoteViews";
+
+    /**
+     * Write Icon to proto.
+     */
+    public static void writeIconToProto(@NonNull ProtoOutputStream out,
+            @NonNull Resources appResources, @NonNull Icon icon) {
+        if (icon.getTintList() != null) {
+            final long token = out.start(RemoteViewsProto.Icon.TINT_LIST);
+            icon.getTintList().writeToProto(out);
+            out.end(token);
+        }
+        out.write(RemoteViewsProto.Icon.BLEND_MODE, BlendMode.toValue(icon.getTintBlendMode()));
+        switch (icon.getType()) {
+            case Icon.TYPE_BITMAP:
+                final ByteArrayOutputStream bitmapBytes = new ByteArrayOutputStream();
+                icon.getBitmap().compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 100, bitmapBytes);
+                out.write(RemoteViewsProto.Icon.BITMAP, bitmapBytes.toByteArray());
+                break;
+            case Icon.TYPE_ADAPTIVE_BITMAP:
+                final ByteArrayOutputStream adaptiveBitmapBytes = new ByteArrayOutputStream();
+                icon.getBitmap().compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 100,
+                        adaptiveBitmapBytes);
+                out.write(RemoteViewsProto.Icon.ADAPTIVE_BITMAP, adaptiveBitmapBytes.toByteArray());
+                break;
+            case Icon.TYPE_RESOURCE:
+                out.write(RemoteViewsProto.Icon.RESOURCE,
+                        appResources.getResourceName(icon.getResId()));
+                break;
+            case Icon.TYPE_DATA:
+                out.write(RemoteViewsProto.Icon.DATA, icon.getDataBytes());
+                break;
+            case Icon.TYPE_URI:
+                out.write(RemoteViewsProto.Icon.URI, icon.getUriString());
+                break;
+            case Icon.TYPE_URI_ADAPTIVE_BITMAP:
+                out.write(RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP, icon.getUriString());
+                break;
+            default:
+                Log.e(TAG, "Tried to serialize unknown Icon type " + icon.getType());
+        }
+    }
+
+    /**
+     * Create Icon from proto.
+     */
+    @NonNull
+    public static Function<Resources, Icon> createIconFromProto(@NonNull ProtoInputStream in)
+            throws Exception {
+        final LongSparseArray<Object> values = new LongSparseArray<>();
+        while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            switch (in.getFieldNumber()) {
+                case (int) RemoteViewsProto.Icon.BLEND_MODE:
+                    values.put(RemoteViewsProto.Icon.BLEND_MODE,
+                            in.readInt(RemoteViewsProto.Icon.BLEND_MODE));
+                    break;
+                case (int) RemoteViewsProto.Icon.TINT_LIST:
+                    final long tintListToken = in.start(RemoteViewsProto.Icon.TINT_LIST);
+                    values.put(RemoteViewsProto.Icon.TINT_LIST, ColorStateList.createFromProto(in));
+                    in.end(tintListToken);
+                    break;
+                case (int) RemoteViewsProto.Icon.BITMAP:
+                    byte[] bitmapData = in.readBytes(RemoteViewsProto.Icon.BITMAP);
+                    values.put(RemoteViewsProto.Icon.BITMAP,
+                            BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length));
+                    break;
+                case (int) RemoteViewsProto.Icon.ADAPTIVE_BITMAP:
+                    final byte[] bitmapAdaptiveData = in.readBytes(
+                            RemoteViewsProto.Icon.ADAPTIVE_BITMAP);
+                    values.put(RemoteViewsProto.Icon.ADAPTIVE_BITMAP,
+                            BitmapFactory.decodeByteArray(bitmapAdaptiveData, 0,
+                                    bitmapAdaptiveData.length));
+                    break;
+                case (int) RemoteViewsProto.Icon.RESOURCE:
+                    values.put(RemoteViewsProto.Icon.RESOURCE,
+                            in.readString(RemoteViewsProto.Icon.RESOURCE));
+                    break;
+                case (int) RemoteViewsProto.Icon.DATA:
+                    values.put(RemoteViewsProto.Icon.DATA,
+                            in.readBytes(RemoteViewsProto.Icon.DATA));
+                    break;
+                case (int) RemoteViewsProto.Icon.URI:
+                    values.put(RemoteViewsProto.Icon.URI, in.readString(RemoteViewsProto.Icon.URI));
+                    break;
+                case (int) RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP:
+                    values.put(RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP,
+                            in.readString(RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP));
+                    break;
+                default:
+                    Log.w(TAG, "Unhandled field while reading Icon proto!\n"
+                            + ProtoUtils.currentFieldToString(in));
+            }
+        }
+
+        return (resources) -> {
+            final int blendMode = (int) values.get(RemoteViewsProto.Icon.BLEND_MODE, -1);
+            final ColorStateList tintList = (ColorStateList) values.get(
+                    RemoteViewsProto.Icon.TINT_LIST);
+            final Bitmap bitmap = (Bitmap) values.get(RemoteViewsProto.Icon.BITMAP);
+            final Bitmap bitmapAdaptive = (Bitmap) values.get(
+                    RemoteViewsProto.Icon.ADAPTIVE_BITMAP);
+            final String resName = (String) values.get(RemoteViewsProto.Icon.RESOURCE);
+            final int resource = resName != null ? resources.getIdentifier(resName, /* defType= */
+                    null,
+                    /* defPackage= */ null) : -1;
+            final byte[] data = (byte[]) values.get(RemoteViewsProto.Icon.DATA);
+            final String uri = (String) values.get(RemoteViewsProto.Icon.URI);
+            final String uriAdaptive = (String) values.get(
+                    RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP);
+            Icon icon;
+            if (bitmap != null) {
+                icon = Icon.createWithBitmap(bitmap);
+            } else if (bitmapAdaptive != null) {
+                icon = Icon.createWithAdaptiveBitmap(bitmapAdaptive);
+            } else if (resource != -1) {
+                icon = Icon.createWithResource(resources, resource);
+            } else if (data != null) {
+                icon = Icon.createWithData(data, 0, data.length);
+            } else if (uri != null) {
+                icon = Icon.createWithContentUri(uri);
+            } else if (uriAdaptive != null) {
+                icon = Icon.createWithAdaptiveBitmapContentUri(uriAdaptive);
+            } else {
+                // Either this Icon has no data or is of an unknown type.
+                return null;
+            }
+
+            if (tintList != null) {
+                icon.setTintList(tintList);
+            }
+            if (blendMode != -1) {
+                icon.setTintBlendMode(BlendMode.fromValue(blendMode));
+            }
+            return icon;
+        };
+    }
+}
diff --git a/core/java/com/android/internal/os/WebViewZygoteInit.java b/core/java/com/android/internal/os/WebViewZygoteInit.java
index 9ed7384..8dee223 100644
--- a/core/java/com/android/internal/os/WebViewZygoteInit.java
+++ b/core/java/com/android/internal/os/WebViewZygoteInit.java
@@ -16,18 +16,15 @@
 
 package com.android.internal.os;
 
-import android.app.ApplicationLoaders;
 import android.app.LoadedApk;
 import android.content.pm.ApplicationInfo;
 import android.net.LocalSocket;
-import android.text.TextUtils;
 import android.util.Log;
 import android.webkit.WebViewFactory;
 import android.webkit.WebViewFactoryProvider;
 import android.webkit.WebViewLibraryLoader;
 
 import java.io.DataOutputStream;
-import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Method;
 
@@ -83,28 +80,6 @@
             Log.i(TAG, "Application preload done");
         }
 
-        @Override
-        protected void handlePreloadPackage(String packagePath, String libsPath, String libFileName,
-                String cacheKey) {
-            Log.i(TAG, "Beginning package preload");
-            // Ask ApplicationLoaders to create and cache a classloader for the WebView APK so that
-            // our children will reuse the same classloader instead of creating their own.
-            // This enables us to preload Java and native code in the webview zygote process and
-            // have the preloaded versions actually be used post-fork.
-            ClassLoader loader = ApplicationLoaders.getDefault().createAndCacheWebViewClassLoader(
-                    packagePath, libsPath, cacheKey);
-
-            // Add the APK to the Zygote's list of allowed files for children.
-            String[] packageList = TextUtils.split(packagePath, File.pathSeparator);
-            for (String packageEntry : packageList) {
-                Zygote.nativeAllowFileAcrossFork(packageEntry);
-            }
-
-            doPreload(loader, libFileName);
-
-            Log.i(TAG, "Package preload done");
-        }
-
         private void doPreload(ClassLoader loader, String libFileName) {
             // Load the native library using WebViewLibraryLoader to share the RELRO data with other
             // processes.
diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java
index cab84bb..fafa085 100644
--- a/core/java/com/android/internal/os/Zygote.java
+++ b/core/java/com/android/internal/os/Zygote.java
@@ -938,8 +938,6 @@
             throw new IllegalArgumentException(USAP_ERROR_PREFIX + "--get-pid");
         } else if (args.mPreloadDefault) {
             throw new IllegalArgumentException(USAP_ERROR_PREFIX + "--preload-default");
-        } else if (args.mPreloadPackage != null) {
-            throw new IllegalArgumentException(USAP_ERROR_PREFIX + "--preload-package");
         } else if (args.mPreloadApp != null) {
             throw new IllegalArgumentException(USAP_ERROR_PREFIX + "--preload-app");
         } else if (args.mStartChildZygote) {
diff --git a/core/java/com/android/internal/os/ZygoteArguments.java b/core/java/com/android/internal/os/ZygoteArguments.java
index 86b9a59..27ce64e 100644
--- a/core/java/com/android/internal/os/ZygoteArguments.java
+++ b/core/java/com/android/internal/os/ZygoteArguments.java
@@ -141,33 +141,12 @@
     String mAppDataDir;
 
     /**
-     * The APK path of the package to preload, when using --preload-package.
-     */
-    String mPreloadPackage;
-
-    /**
      * A Base64 string representing a serialize ApplicationInfo Parcel,
      when using --preload-app.
      */
     String mPreloadApp;
 
     /**
-     * The native library path of the package to preload, when using --preload-package.
-     */
-    String mPreloadPackageLibs;
-
-    /**
-     * The filename of the native library to preload, when using --preload-package.
-     */
-    String mPreloadPackageLibFileName;
-
-    /**
-     * The cache key under which to enter the preloaded package into the classloader cache, when
-     * using --preload-package.
-     */
-    String mPreloadPackageCacheKey;
-
-    /**
      * Whether this is a request to start preloading the default resources and classes. This
      * argument only makes sense when the zygote is in lazy preload mode (i.e, when it's started
      * with --enable-lazy-preload).
@@ -419,12 +398,6 @@
             } else if (arg.equals("--preload-app")) {
                 ++curArg;
                 mPreloadApp = args.nextArg();
-            } else if (arg.equals("--preload-package")) {
-                curArg += 4;
-                mPreloadPackage = args.nextArg();
-                mPreloadPackageLibs = args.nextArg();
-                mPreloadPackageLibFileName = args.nextArg();
-                mPreloadPackageCacheKey = args.nextArg();
             } else if (arg.equals("--preload-default")) {
                 mPreloadDefault = true;
                 expectRuntimeArgs = false;
@@ -504,11 +477,6 @@
             if (argCount > curArg) {
                 throw new IllegalArgumentException("Unexpected arguments after --query-abi-list.");
             }
-        } else if (mPreloadPackage != null) {
-            if (argCount > curArg) {
-                throw new IllegalArgumentException(
-                    "Unexpected arguments after --preload-package.");
-            }
         } else if (mPreloadApp != null) {
             if (argCount > curArg) {
                 throw new IllegalArgumentException(
diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java
index d4dcec9..2eab081 100644
--- a/core/java/com/android/internal/os/ZygoteConnection.java
+++ b/core/java/com/android/internal/os/ZygoteConnection.java
@@ -165,14 +165,6 @@
                     return null;
                 }
 
-                if (parsedArgs.mPreloadPackage != null) {
-                    handlePreloadPackage(parsedArgs.mPreloadPackage,
-                            parsedArgs.mPreloadPackageLibs,
-                            parsedArgs.mPreloadPackageLibFileName,
-                            parsedArgs.mPreloadPackageCacheKey);
-                    return null;
-                }
-
                 if (canPreloadApp() && parsedArgs.mPreloadApp != null) {
                     byte[] rawParcelData = Base64.getDecoder().decode(parsedArgs.mPreloadApp);
                     Parcel appInfoParcel = Parcel.obtain();
@@ -475,11 +467,6 @@
         return mSocketOutStream;
     }
 
-    protected void handlePreloadPackage(String packagePath, String libsPath, String libFileName,
-            String cacheKey) {
-        throw new RuntimeException("Zygote does not support package preloading");
-    }
-
     protected boolean canPreloadApp() {
         return false;
     }
diff --git a/core/proto/android/widget/remoteviews.proto b/core/proto/android/widget/remoteviews.proto
index f08ea1b..37d1c5b 100644
--- a/core/proto/android/widget/remoteviews.proto
+++ b/core/proto/android/widget/remoteviews.proto
@@ -21,6 +21,7 @@
 package android.widget;
 
 import "frameworks/base/core/proto/android/privacy.proto";
+import "frameworks/base/core/proto/android/content/res/color_state_list.proto";
 
 /**
  * An android.widget.RemoteViews object. This is used by RemoteViews.createPreviewFromProto
@@ -71,6 +72,23 @@
         optional int32 view_type_count = 4;
         optional bool attached = 5;
     }
+
+    /**
+     * An android.graphics.drawable Icon.
+     */
+    message Icon {
+        option (android.msg_privacy).dest = DEST_AUTOMATIC;
+        optional int32 blend_mode = 1;
+        optional android.content.res.ColorStateListProto tint_list = 2;
+        oneof icon {
+            bytes bitmap = 3;
+            string resource = 4;
+            bytes data = 5;
+            string uri = 6;
+            string uri_adaptive_bitmap = 7;
+            bytes adaptive_bitmap = 8;
+        };
+    }
 }
 
 
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 236e7c5..8cb7646 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -7069,6 +7069,9 @@
     <!-- Whether desktop mode is supported on the current device  -->
     <bool name="config_isDesktopModeSupported">false</bool>
 
+    <!-- Maximum number of active tasks on a given Desktop Windowing session. Set to 0 for unlimited. -->
+    <integer name="config_maxDesktopWindowingActiveTasks">0</integer>
+
     <!-- Frame rate compatibility value for Wallpaper
          FRAME_RATE_COMPATIBILITY_MIN (102) is used by default for lower power consumption -->
     <integer name="config_wallpaperFrameRateCompatibility">102</integer>
@@ -7093,4 +7096,8 @@
 
     <!-- Whether to enable usb state update via udc sysfs. -->
     <bool name="config_enableUdcSysfsUsbStateUpdate">false</bool>
+
+    <!-- Whether to enable the private space search illustration and search tile content in "Hide Private Space" settings page.
+         OEM/Partner can explicitly opt to hide the illustration and search tile content. -->
+    <bool name="config_enableSearchTileHideIllustrationInPrivateSpace">true</bool>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 09688f2..4a425cb 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -524,6 +524,7 @@
   <java-symbol type="bool" name="config_notificationCloseButtonSupported"/>
   <java-symbol type="bool" name="config_enableGaiaEducationInPrivateSpace"/>
   <java-symbol type="bool" name="config_enableUdcSysfsUsbStateUpdate"/>
+  <java-symbol type="bool" name="config_enableSearchTileHideIllustrationInPrivateSpace"/>
 
   <java-symbol type="color" name="tab_indicator_text_v4" />
 
@@ -5522,6 +5523,9 @@
   <!-- Whether desktop mode is supported on the current device  -->
   <java-symbol type="bool" name="config_isDesktopModeSupported" />
 
+  <!-- Maximum number of active tasks on a given Desktop Windowing session. Set to 0 for unlimited. -->
+  <java-symbol type="integer" name="config_maxDesktopWindowingActiveTasks"/>
+
   <!-- Frame rate compatibility value for Wallpaper -->
   <java-symbol type="integer" name="config_wallpaperFrameRateCompatibility" />
 
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index f5affd3..4f5107a 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -983,7 +983,7 @@
                 new MergedConfiguration(currentConfig, currentConfig),
                 false /* preserveWindow */, activityWindowInfo);
         final ResumeActivityItem resumeStateRequest =
-                ResumeActivityItem.obtain(activity.getActivityToken(), true /* isForward */,
+                new ResumeActivityItem(activity.getActivityToken(), true /* isForward */,
                         false /* shouldSendCompatFakeFocus*/);
 
         final ClientTransaction transaction = newTransaction(activity);
@@ -996,7 +996,7 @@
     @NonNull
     private static ClientTransaction newResumeTransaction(@NonNull Activity activity) {
         final ResumeActivityItem resumeStateRequest =
-                ResumeActivityItem.obtain(activity.getActivityToken(), true /* isForward */,
+                new ResumeActivityItem(activity.getActivityToken(), true /* isForward */,
                         false /* shouldSendCompatFakeFocus */);
 
         final ClientTransaction transaction = newTransaction(activity);
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
index ee7fa7c..911b759 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
@@ -21,13 +21,17 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
 import android.app.Activity;
+import android.app.ActivityManager;
 import android.app.ActivityThread;
 import android.app.ClientTransactionHandler;
 import android.content.res.Configuration;
@@ -102,8 +106,8 @@
 
     @Test
     public void testDestroyActivityItem_preExecute() {
-        final DestroyActivityItem item = DestroyActivityItem
-                .obtain(mActivityToken, false /* finished */);
+        final DestroyActivityItem item =
+                new DestroyActivityItem(mActivityToken, false /* finished */);
 
         item.preExecute(mHandler);
 
@@ -113,8 +117,8 @@
 
     @Test
     public void testDestroyActivityItem_postExecute() {
-        final DestroyActivityItem item = DestroyActivityItem
-                .obtain(mActivityToken, false /* finished */);
+        final DestroyActivityItem item =
+                new DestroyActivityItem(mActivityToken, false /* finished */);
         item.preExecute(mHandler);
 
         item.postExecute(mHandler, mPendingActions);
@@ -124,8 +128,8 @@
 
     @Test
     public void testDestroyActivityItem_execute() {
-        final DestroyActivityItem item = DestroyActivityItem
-                .obtain(mActivityToken, false /* finished */);
+        final DestroyActivityItem item =
+                new DestroyActivityItem(mActivityToken, false /* finished */);
 
         item.execute(mHandler, mActivityClientRecord, mPendingActions);
 
@@ -134,6 +138,41 @@
     }
 
     @Test
+    public void testResumeActivityItem_preExecute_withProcState_updatesProcessState() {
+        final ResumeActivityItem item = new ResumeActivityItem(mActivityToken,
+                ActivityManager.PROCESS_STATE_TOP /* procState */,
+                true /* isForward */,
+                false /* shouldSendCompatFakeFocus*/);
+
+        item.preExecute(mHandler);
+
+        verify(mHandler).updateProcessState(ActivityManager.PROCESS_STATE_TOP, false);
+    }
+
+    @Test
+    public void testResumeActivityItem_preExecute_withUnknownProcState_skipsProcessStateUpdate() {
+        final ResumeActivityItem item = new ResumeActivityItem(mActivityToken,
+                ActivityManager.PROCESS_STATE_UNKNOWN /* procState */,
+                true /* isForward */,
+                false /* shouldSendCompatFakeFocus*/);
+
+        item.preExecute(mHandler);
+
+        verify(mHandler, never()).updateProcessState(anyInt(), anyBoolean());
+    }
+
+    @Test
+    public void testResumeActivityItem_preExecute_withoutProcState_skipsProcessStateUpdate() {
+        final ResumeActivityItem item = new ResumeActivityItem(mActivityToken,
+                true /* isForward */,
+                false /* shouldSendCompatFakeFocus*/);
+
+        item.preExecute(mHandler);
+
+        verify(mHandler, never()).updateProcessState(anyInt(), anyBoolean());
+    }
+
+    @Test
     public void testWindowContextInfoChangeItem_execute() {
         final WindowContextInfoChangeItem item = WindowContextInfoChangeItem
                 .obtain(mWindowClientToken, mConfiguration, DEFAULT_DISPLAY);
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index da88478..1817b87 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -90,11 +90,6 @@
     }
 
     @Test
-    public void testRecycleDestroyActivityItem() {
-        testRecycle(() -> DestroyActivityItem.obtain(mActivityToken, true));
-    }
-
-    @Test
     public void testRecycleLaunchActivityItem() {
         final IBinder activityToken = new Binder();
         final Intent intent = new Intent("action");
@@ -166,11 +161,6 @@
     }
 
     @Test
-    public void testRecycleResumeActivityItem() {
-        testRecycle(() -> ResumeActivityItem.obtain(mActivityToken, 3, true, false));
-    }
-
-    @Test
     public void testRecycleStartActivityItem() {
         testRecycle(() -> StartActivityItem.obtain(mActivityToken,
                 new ActivityOptions.SceneTransitionInfo()));
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
index ad86e75..c2d56b6 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
@@ -275,7 +275,7 @@
         final IBinder token = mock(IBinder.class);
         final ClientTransaction destroyTransaction = new ClientTransaction();
         destroyTransaction.addTransactionItem(
-                DestroyActivityItem.obtain(token, false /* finished */));
+                new DestroyActivityItem(token, false /* finished */));
         destroyTransaction.preExecute(mTransactionHandler);
         // The activity should be added to to-be-destroyed container.
         assertEquals(1, activitiesToBeDestroyed.size());
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index e995035..dde6ab1 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -147,11 +147,12 @@
 
     @Test
     public void testDestroy() {
-        DestroyActivityItem item = DestroyActivityItem.obtain(mActivityToken, true /* finished */);
+        final DestroyActivityItem item =
+                new DestroyActivityItem(mActivityToken, true /* finished */);
         writeAndPrepareForReading(item);
 
         // Read from parcel and assert
-        DestroyActivityItem result = DestroyActivityItem.CREATOR.createFromParcel(mParcel);
+        final DestroyActivityItem result = DestroyActivityItem.CREATOR.createFromParcel(mParcel);
 
         assertEquals(item.hashCode(), result.hashCode());
         assertEquals(item, result);
@@ -248,12 +249,12 @@
     @Test
     public void testResume() {
         // Write to parcel
-        ResumeActivityItem item = ResumeActivityItem.obtain(mActivityToken, 27 /* procState */,
+        final ResumeActivityItem item = new ResumeActivityItem(mActivityToken, 27 /* procState */,
                 true /* isForward */, false /* shouldSendCompatFakeFocus */);
         writeAndPrepareForReading(item);
 
         // Read from parcel and assert
-        ResumeActivityItem result = ResumeActivityItem.CREATOR.createFromParcel(mParcel);
+        final ResumeActivityItem result = ResumeActivityItem.CREATOR.createFromParcel(mParcel);
 
         assertEquals(item.hashCode(), result.hashCode());
         assertEquals(item, result);
diff --git a/core/tests/coretests/src/android/view/ViewGroupTest.java b/core/tests/coretests/src/android/view/ViewGroupTest.java
index bce3f3e..ae3ad36 100644
--- a/core/tests/coretests/src/android/view/ViewGroupTest.java
+++ b/core/tests/coretests/src/android/view/ViewGroupTest.java
@@ -19,6 +19,7 @@
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
@@ -29,11 +30,16 @@
 import android.content.Context;
 import android.graphics.Region;
 import android.platform.test.annotations.Presubmit;
+import android.view.autofill.AutofillId;
 
 import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 
 /**
  * Test basic functions of ViewGroup.
@@ -182,6 +188,31 @@
         assertRegionContainPoint(3 /* x */, r, false /* contain */); // Outside of bounds
     }
 
+    @Test
+    public void testfindAutofillableViewsByTraversal() {
+        final Context context = getInstrumentation().getContext();
+        final TestView viewGroup = new TestView(context, 200 /* right */);
+
+        // viewA and viewC are autofillable. ViewB isn't.
+        final TestView viewA = spy(new AutofillableTestView(context, 100 /* right */));
+        final TestView viewB = spy(new NonAutofillableTestView(context, 200 /* right */));
+        final TestView viewC = spy(new AutofillableTestView(context, 300 /* right */));
+
+        viewGroup.addView(viewA);
+        viewGroup.addView(viewB);
+        viewGroup.addView(viewC);
+
+        List<View> autofillableViews = new ArrayList<>();
+        viewGroup.findAutofillableViewsByTraversal(autofillableViews);
+
+        verify(viewA).findAutofillableViewsByTraversal(autofillableViews);
+        verify(viewB).findAutofillableViewsByTraversal(autofillableViews);
+        verify(viewC).findAutofillableViewsByTraversal(autofillableViews);
+
+        assertEquals("Size of autofillable views", 2, autofillableViews.size());
+        assertTrue(autofillableViews.containsAll(Arrays.asList(viewA, viewC)));
+    }
+
     private static void getUnobscuredTouchableRegion(Region outRegion, View view) {
         outRegion.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
         final ViewParent parent = view.getParent();
@@ -210,4 +241,29 @@
             // We don't layout this view.
         }
     }
+
+    public static class AutofillableTestView extends TestView {
+        AutofillableTestView(Context context, int right) {
+            super(context, right);
+            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+            // Need to set autofill id in such a way that the view is considered part of activity.
+            setAutofillId(new AutofillId(LAST_APP_AUTOFILL_ID + 5));
+        }
+
+        @Override
+        public @AutofillType int getAutofillType() {
+            return AUTOFILL_TYPE_TEXT;
+        }
+    }
+
+    public static class NonAutofillableTestView extends TestView {
+        NonAutofillableTestView(Context context, int right) {
+            super(context, right);
+        }
+
+        @Override
+        public @AutofillType int getAutofillType() {
+            return AUTOFILL_TYPE_NONE;
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsSerializersTest.kt b/core/tests/coretests/src/android/widget/RemoteViewsSerializersTest.kt
new file mode 100644
index 0000000..44d10d3
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/RemoteViewsSerializersTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.widget
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Bitmap
+import android.graphics.BlendMode
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Icon
+import android.util.proto.ProtoInputStream
+import android.util.proto.ProtoOutputStream
+import android.widget.RemoteViewsSerializers.createIconFromProto
+import android.widget.RemoteViewsSerializers.writeIconToProto
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.frameworks.coretests.R
+import com.google.common.truth.Truth.assertThat
+import java.io.ByteArrayOutputStream
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class RemoteViewsSerializersTest {
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+
+    /**
+     * Based on android.graphics.drawable.IconTest#testParcel
+     */
+    @Test
+    fun testWriteIconToProto() {
+        val bitmap = (context.getDrawable(R.drawable.landscape) as BitmapDrawable).bitmap
+        val bitmapData = ByteArrayOutputStream().let {
+            bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
+            it.toByteArray()
+        }
+
+        for (icon in listOf(
+            Icon.createWithBitmap(bitmap),
+            Icon.createWithAdaptiveBitmap(bitmap),
+            Icon.createWithData(bitmapData, 0, bitmapData.size),
+            Icon.createWithResource(context, R.drawable.landscape),
+            Icon.createWithContentUri("content://com.example.myapp/my_icon"),
+            Icon.createWithAdaptiveBitmapContentUri("content://com.example.myapp/my_icon"),
+        )) {
+            icon.tintList = ColorStateList.valueOf(Color.RED)
+            icon.tintBlendMode = BlendMode.SRC_OVER
+            val bytes = ProtoOutputStream().let {
+                writeIconToProto(it, context.resources, icon)
+                it.bytes
+            }
+
+            val copy = ProtoInputStream(bytes).let {
+                createIconFromProto(it).apply(context.resources)
+            }
+            assertThat(copy.type).isEqualTo(icon.type)
+            assertThat(copy.tintBlendMode).isEqualTo(icon.tintBlendMode)
+            assertThat(equalColorStateLists(copy.tintList, icon.tintList)).isTrue()
+
+            when (icon.type) {
+                Icon.TYPE_DATA, Icon.TYPE_URI, Icon.TYPE_URI_ADAPTIVE_BITMAP,
+                Icon.TYPE_RESOURCE -> {
+                    assertThat(copy.sameAs(icon)).isTrue()
+                }
+
+                Icon.TYPE_BITMAP, Icon.TYPE_ADAPTIVE_BITMAP -> {
+                    assertThat(copy.bitmap.sameAs(icon.bitmap)).isTrue()
+                }
+            }
+        }
+    }
+}
+
+fun equalColorStateLists(a: ColorStateList?, b: ColorStateList?): Boolean {
+    if (a == null && b == null) return true
+    return a != null && b != null &&
+            a.colors.contentEquals(b.colors) &&
+            a.states.foldIndexed(true) { i, acc, it -> acc && it.contentEquals(b.states[i])}
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index a1ba24c..282385a 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -89,20 +89,15 @@
     private static final int DESKTOP_DENSITY_MAX = 1000;
 
     /**
-     * Default value for {@code MAX_TASK_LIMIT}.
-     */
-    @VisibleForTesting
-    public static final int DEFAULT_MAX_TASK_LIMIT = 4;
-
-    // TODO(b/335131008): add a config-overlay field for the max number of tasks in Desktop Mode
-    /**
-     * Flag declaring the maximum number of Tasks to show in Desktop Mode at any one time.
+     * Sysprop declaring the maximum number of Tasks to show in Desktop Mode at any one time.
      *
-     * <p> The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen
+     * <p>If it is not defined, then {@code R.integer.config_maxDesktopWindowingActiveTasks} is
+     * used.
+     *
+     * <p>The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen
      * recording window, or Bluetooth pairing window).
      */
-    private static final int MAX_TASK_LIMIT = SystemProperties.getInt(
-            "persist.wm.debug.desktop_max_task_limit", DEFAULT_MAX_TASK_LIMIT);
+    private static final String MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit";
 
     /**
      * Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
@@ -139,8 +134,9 @@
     /**
      * Return the maximum limit on the number of Tasks to show in Desktop Mode at any one time.
      */
-    public static int getMaxTaskLimit() {
-        return MAX_TASK_LIMIT;
+    public static int getMaxTaskLimit(@NonNull Context context) {
+        return SystemProperties.getInt(MAX_TASK_LIMIT_SYS_PROP,
+                context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks));
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index fca8a62..949a723 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -734,6 +734,9 @@
     public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
         if (canShowAsBubbleBar()) {
             mBubblePositioner.setBubbleBarLocation(bubbleBarLocation);
+            if (mLayerView != null && !mLayerView.isExpandedViewDragged()) {
+                mLayerView.updateExpandedView();
+            }
             BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
             bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation;
             mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
index fa1091c..d45ed0d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
@@ -38,6 +38,9 @@
     var isStuckToDismiss: Boolean = false
         private set
 
+    var isDragged: Boolean = false
+        private set
+
     private var expandedViewInitialTranslationX = 0f
     private var expandedViewInitialTranslationY = 0f
     private val magnetizedExpandedView: MagnetizedObject<BubbleBarExpandedView> =
@@ -94,6 +97,7 @@
             // While animating, don't allow new touch events
             if (expandedView.isAnimating) return false
             pinController.onDragStart(bubblePositioner.isBubbleBarOnLeft)
+            isDragged = true
             return true
         }
 
@@ -141,6 +145,7 @@
                 dismissView.hide()
             }
             isMoving = false
+            isDragged = false
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index badc409..9fa85cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -175,6 +175,11 @@
         return mIsExpanded;
     }
 
+    /** Return whether the expanded view is being dragged */
+    public boolean isExpandedViewDragged() {
+        return mDragController != null && mDragController.isDragged();
+    }
+
     /** Shows the expanded view of the provided bubble. */
     public void showExpandedView(BubbleViewProvider b) {
         BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView();
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 8f587d4..afe46f5 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
@@ -567,13 +567,15 @@
             Transitions transitions,
             @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
             ShellTaskOrganizer shellTaskOrganizer) {
+        int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context);
         if (!DesktopModeStatus.canEnterDesktopMode(context)
-                || DESKTOP_WINDOWING_MODE.isEnabled(context)) {
+                || DESKTOP_WINDOWING_MODE.isEnabled(context)
+                || maxTaskLimit <= 0) {
             return Optional.empty();
         }
         return Optional.of(
                 new DesktopTasksLimiter(
-                        transitions, desktopModeTaskRepository, shellTaskOrganizer));
+                        transitions, desktopModeTaskRepository, shellTaskOrganizer, maxTaskLimit));
     }
 
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index 534cc22..c5ed1be 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -26,7 +26,6 @@
 import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.protolog.ShellProtoLogGroup
-import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TransitionObserver
 
@@ -40,12 +39,17 @@
         transitions: Transitions,
         private val taskRepository: DesktopModeTaskRepository,
         private val shellTaskOrganizer: ShellTaskOrganizer,
+        private val maxTasksLimit: Int,
 ) {
     private val minimizeTransitionObserver = MinimizeTransitionObserver()
     @VisibleForTesting
     val leftoverMinimizedTasksRemover = LeftoverMinimizedTasksRemover()
 
     init {
+        require(maxTasksLimit > 0) {
+            "DesktopTasksLimiter should not be created with a maxTasksLimit at 0 or less. " +
+                    "Current value: $maxTasksLimit."
+        }
         transitions.registerObserver(minimizeTransitionObserver)
         taskRepository.addActiveTaskListener(leftoverMinimizedTasksRemover)
     }
@@ -194,12 +198,6 @@
     }
 
     /**
-     * Returns the maximum number of tasks that should ever be displayed at the same time in Desktop
-     * Mode.
-     */
-    fun getMaxTaskLimit(): Int = DesktopModeStatus.getMaxTaskLimit()
-
-    /**
      * Returns the Task to minimize given 1. a list of visible tasks ordered from front to back and
      * 2. a new task placed in front of all the others.
      */
@@ -216,7 +214,7 @@
     fun getTaskToMinimizeIfNeeded(
             visibleFreeformTaskIdsOrderedFrontToBack: List<Int>
     ): RunningTaskInfo? {
-        if (visibleFreeformTaskIdsOrderedFrontToBack.size <= getMaxTaskLimit()) {
+        if (visibleFreeformTaskIdsOrderedFrontToBack.size <= maxTasksLimit) {
             ProtoLog.v(
                     ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                     "DesktopTasksLimiter: no need to minimize; tasks below limit")
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 37510ef4..e66018f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -126,11 +126,11 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.atLeastOnce
 import org.mockito.kotlin.capture
 import org.mockito.quality.Strictness
-import org.mockito.Mockito.`when` as whenever
 
 /**
  * Test class for {@link DesktopTasksController}
@@ -204,7 +204,12 @@
     shellInit = spy(ShellInit(testExecutor))
     desktopModeTaskRepository = DesktopModeTaskRepository()
     desktopTasksLimiter =
-        DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer)
+        DesktopTasksLimiter(
+            transitions,
+            desktopModeTaskRepository,
+            shellTaskOrganizer,
+            MAX_TASK_LIMIT,
+        )
 
     whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
     whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
@@ -780,45 +785,41 @@
   @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun moveToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() {
-    val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
-    val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     val newTask = setUpFullscreenTask()
     val homeTask = setUpHomeTask()
 
     controller.moveToDesktop(newTask, transitionSource = UNKNOWN)
 
     val wct = getLatestEnterDesktopWct()
-    assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home
+    assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 1) // visible tasks + home
     wct.assertReorderAt(0, homeTask)
     wct.assertReorderSequenceInRange(
-      range = 1..<(taskLimit + 1),
-      *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
-      newTask
-    )
+        range = 1..<(MAX_TASK_LIMIT + 1),
+        *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
+        newTask)
   }
 
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun moveToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() {
-    val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
-    val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     val newTask = setUpFullscreenTask()
     val homeTask = setUpHomeTask()
 
     controller.moveToDesktop(newTask, transitionSource = UNKNOWN)
 
     val wct = getLatestEnterDesktopWct()
-    assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 2) // tasks + home + wallpaper
+    assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 2) // tasks + home + wallpaper
     // Move home to front
     wct.assertReorderAt(0, homeTask)
     // Add desktop wallpaper activity
     wct.assertPendingIntentAt(1, desktopWallpaperIntent)
     // Bring freeform tasks to front
     wct.assertReorderSequenceInRange(
-      range = 2..<(taskLimit + 2),
-      *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
-      newTask
-    )
+        range = 2..<(MAX_TASK_LIMIT + 2),
+        *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
+        newTask)
   }
 
   @Test
@@ -932,9 +933,8 @@
 
   @Test
   fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() {
-    val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
     setUpHomeTask()
-    val freeformTasks = (1..taskLimit + 1).map { _ -> setUpFreeformTask() }
+    val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() }
 
     controller.moveTaskToFront(freeformTasks[0])
 
@@ -1141,8 +1141,7 @@
   fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() {
     assumeTrue(ENABLE_SHELL_TRANSITIONS)
 
-    val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
-    val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     freeformTasks.forEach { markTaskVisible(it) }
     val fullscreenTask = createFullscreenTask()
 
@@ -1187,8 +1186,7 @@
   fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() {
     assumeTrue(ENABLE_SHELL_TRANSITIONS)
 
-    val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
-    val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     freeformTasks.forEach { markTaskVisible(it) }
     val newFreeformTask = createFreeformTask()
 
@@ -2599,9 +2597,10 @@
     return TransitionRequestInfo(type, task, null /* remoteTransition */)
   }
 
-  companion object {
+  private companion object {
     const val SECOND_DISPLAY = 2
-    private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
+    val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
+    const val MAX_TASK_LIMIT = 6
   }
 }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 8d9fd91..70f3bf8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -38,6 +38,7 @@
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.util.StubTransaction
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -77,8 +78,8 @@
 
         desktopTaskRepo = DesktopModeTaskRepository()
 
-        desktopTasksLimiter = DesktopTasksLimiter(
-                transitions, desktopTaskRepo, shellTaskOrganizer)
+        desktopTasksLimiter =
+            DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT)
     }
 
     @After
@@ -86,12 +87,18 @@
         mockitoSession.finishMocking()
     }
 
-    // Currently, the task limit can be overridden through an adb flag. This test ensures the limit
-    // hasn't been overridden.
     @Test
-    fun getMaxTaskLimit_isSameAsConstant() {
-        assertThat(desktopTasksLimiter.getMaxTaskLimit()).isEqualTo(
-            DesktopModeStatus.DEFAULT_MAX_TASK_LIMIT)
+    fun createDesktopTasksLimiter_withZeroLimit_shouldThrow() {
+        assertFailsWith<IllegalArgumentException> {
+            DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, 0)
+        }
+    }
+
+    @Test
+    fun createDesktopTasksLimiter_withNegativeLimit_shouldThrow() {
+        assertFailsWith<IllegalArgumentException> {
+            DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, -5)
+        }
     }
 
     @Test
@@ -247,8 +254,7 @@
 
     @Test
     fun addAndGetMinimizeTaskChangesIfNeeded_tasksWithinLimit_noTaskMinimized() {
-        val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
-        (1..<taskLimit).forEach { _ -> setUpFreeformTask() }
+        (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() }
 
         val wct = WindowContainerTransaction()
         val minimizedTaskId =
@@ -263,9 +269,8 @@
 
     @Test
     fun addAndGetMinimizeTaskChangesIfNeeded_tasksAboveLimit_backTaskMinimized() {
-        val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
         // The following list will be ordered bottom -> top, as the last task is moved to top last.
-        val tasks = (1..taskLimit).map { setUpFreeformTask() }
+        val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
 
         val wct = WindowContainerTransaction()
         val minimizedTaskId =
@@ -282,8 +287,7 @@
 
     @Test
     fun addAndGetMinimizeTaskChangesIfNeeded_nonMinimizedTasksWithinLimit_noTaskMinimized() {
-        val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
-        val tasks = (1..taskLimit).map { setUpFreeformTask() }
+        val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
         desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId)
 
         val wct = WindowContainerTransaction()
@@ -299,8 +303,7 @@
 
     @Test
     fun getTaskToMinimizeIfNeeded_tasksWithinLimit_returnsNull() {
-        val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
-        val tasks = (1..taskLimit).map { setUpFreeformTask() }
+        val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
 
         val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded(
                 visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId })
@@ -310,8 +313,7 @@
 
     @Test
     fun getTaskToMinimizeIfNeeded_tasksAboveLimit_returnsBackTask() {
-        val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
-        val tasks = (1..taskLimit + 1).map { setUpFreeformTask() }
+        val tasks = (1..MAX_TASK_LIMIT + 1).map { setUpFreeformTask() }
 
         val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded(
                 visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId })
@@ -321,9 +323,21 @@
     }
 
     @Test
+    fun getTaskToMinimizeIfNeeded_tasksAboveLimit_otherLimit_returnsBackTask() {
+        desktopTasksLimiter =
+            DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT2)
+        val tasks = (1..MAX_TASK_LIMIT2 + 1).map { setUpFreeformTask() }
+
+        val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded(
+            visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId })
+
+        // first == front, last == back
+        assertThat(minimizedTask).isEqualTo(tasks.last())
+    }
+
+    @Test
     fun getTaskToMinimizeIfNeeded_withNewTask_tasksAboveLimit_returnsBackTask() {
-        val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
-        val tasks = (1..taskLimit).map { setUpFreeformTask() }
+        val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
 
         val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded(
                 visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId },
@@ -358,4 +372,9 @@
                 visible = false
         )
     }
+
+    private companion object {
+        const val MAX_TASK_LIMIT = 6
+        const val MAX_TASK_LIMIT2 = 9
+    }
 }
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index 2fff4f5..a39f30b 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -44,6 +44,9 @@
         "-Werror",
         "-Wunreachable-code",
     ],
+    header_libs: [
+        "native_headers",
+    ],
     target: {
         windows: {
             // The Windows compiler warns incorrectly for value initialization with {}.
diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp
index 6744c6c..aebc4db 100644
--- a/libs/hwui/jni/ImageDecoder.cpp
+++ b/libs/hwui/jni/ImageDecoder.cpp
@@ -162,8 +162,8 @@
 
 static jobject ImageDecoder_nCreateFd(JNIEnv* env, jobject /*clazz*/,
         jobject fileDescriptor, jlong length, jboolean preferAnimation, jobject source) {
-#ifndef __ANDROID__ // LayoutLib for Windows does not support F_DUPFD_CLOEXEC
-    return throw_exception(env, kSourceException, "Only supported on Android", nullptr, source);
+#ifdef _WIN32  // LayoutLib for Windows does not support F_DUPFD_CLOEXEC
+    return throw_exception(env, kSourceException, "Not supported on Windows", nullptr, source);
 #else
     int descriptor = jniGetFDFromFileDescriptor(env, fileDescriptor);
 
diff --git a/media/jni/android_media_ImageWriter.cpp b/media/jni/android_media_ImageWriter.cpp
index f64233f..6776f61 100644
--- a/media/jni/android_media_ImageWriter.cpp
+++ b/media/jni/android_media_ImageWriter.cpp
@@ -24,7 +24,6 @@
 #include <utils/String8.h>
 #include <utils/Thread.h>
 
-#include <gui/IProducerListener.h>
 #include <gui/Surface.h>
 #include <ui/PublicFormat.h>
 #include <android_runtime/AndroidRuntime.h>
@@ -65,15 +64,18 @@
 
 // ----------------------------------------------------------------------------
 
-class JNIImageWriterContext : public BnProducerListener {
+class JNIImageWriterContext : public SurfaceListener {
 public:
     JNIImageWriterContext(JNIEnv* env, jobject weakThiz, jclass clazz);
 
     virtual ~JNIImageWriterContext();
 
-    // Implementation of IProducerListener, used to notify the ImageWriter that the consumer
+    // Implementation of SurfaceListener, used to notify the ImageWriter that the consumer
     // has returned a buffer and it is ready for ImageWriter to dequeue.
     virtual void onBufferReleased();
+    virtual bool needsReleaseNotify() override { return true; };
+    virtual void onBuffersDiscarded(const std::vector<sp<GraphicBuffer>>& /*buffers*/) override {};
+    virtual void onBufferDetached(int /*slot*/) override {};
 
     void setProducer(const sp<Surface>& producer) { mProducer = producer; }
     Surface* getProducer() { return mProducer.get(); }
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 1c3515a..2f90ccc 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -573,6 +573,7 @@
         "lottie_compose",
         "LowLightDreamLib",
         "TraceurCommon",
+        "Traceur-res",
         "//frameworks/libs/systemui:motion_tool_lib",
         "notification_flags_lib",
         "PlatformComposeCore",
@@ -749,6 +750,7 @@
         "androidx.compose.animation_animation-graphics",
         "androidx.lifecycle_lifecycle-viewmodel-compose",
         "TraceurCommon",
+        "Traceur-res",
     ],
 }
 
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 02e0423..eb9d0ab 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1243,4 +1243,13 @@
    metadata {
         purpose: PURPOSE_BUGFIX
    }
-}
\ No newline at end of file
+}
+flag {
+   name: "classic_flags_multi_user"
+   namespace: "systemui"
+   description: "Make the classic feature flag loading multi user aware."
+   bug: "345443431"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
new file mode 100644
index 0000000..01dbc6b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.education.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.education.data.repository.contextualEducationRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.education.GestureType
+import com.android.systemui.shared.education.GestureType.BACK_GESTURE
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val repository = kosmos.contextualEducationRepository
+    private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor
+
+    @Before
+    fun setup() {
+        underTest.start()
+    }
+
+    @Test
+    fun newEducationInfoOnMaxSignalCountReached() =
+        testScope.runTest {
+            tryTriggeringEducation(BACK_GESTURE)
+            val model by collectLastValue(underTest.educationTriggered)
+            assertThat(model?.gestureType).isEqualTo(BACK_GESTURE)
+        }
+
+    @Test
+    fun noEducationInfoBeforeMaxSignalCountReached() =
+        testScope.runTest {
+            repository.incrementSignalCount(BACK_GESTURE)
+            val model by collectLastValue(underTest.educationTriggered)
+            assertThat(model).isNull()
+        }
+
+    @Test
+    fun noEducationInfoWhenShortcutTriggeredPreviously() =
+        testScope.runTest {
+            val model by collectLastValue(underTest.educationTriggered)
+            repository.updateShortcutTriggerTime(BACK_GESTURE)
+            tryTriggeringEducation(BACK_GESTURE)
+            assertThat(model).isNull()
+        }
+
+    private suspend fun tryTriggeringEducation(gestureType: GestureType) {
+        // Increment max number of signal to try triggering education
+        for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
+            repository.incrementSignalCount(gestureType)
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt
index 6ddc074..7e9f437 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.scene.data.repository.Idle
 import com.android.systemui.scene.data.repository.setTransition
+import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -627,6 +628,7 @@
                 fakeSettings,
                 seenNotificationsInteractor,
                 statusBarStateController,
+                sceneInteractor = kosmos.sceneInteractor,
             )
         keyguardCoordinator.attach(notifPipeline)
         testScope.runTest {
diff --git a/packages/SystemUI/res/layout/custom_trace_settings_dialog.xml b/packages/SystemUI/res/layout/custom_trace_settings_dialog.xml
new file mode 100644
index 0000000..6180bf5
--- /dev/null
+++ b/packages/SystemUI/res/layout/custom_trace_settings_dialog.xml
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:orientation="vertical" >
+
+    <TextView
+        android:id="@+id/categories"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+    <TextView
+        android:id="@+id/cpu_buffer_size"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+    <!-- Attach to Bugreport Switch -->
+    <LinearLayout
+        android:id="@+id/attach_to_bugreport_switch_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/attach_to_bugreport_switch_label"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_weight="1"
+            android:layout_gravity="fill_vertical"
+            android:gravity="start"
+            android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+        <Switch
+            android:id="@+id/attach_to_bugreport_switch"
+            android:layout_width="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="end"
+            android:layout_gravity="fill_vertical"
+            android:layout_weight="0" />
+    </LinearLayout>
+
+    <!-- Winscope Switch -->
+    <LinearLayout
+        android:id="@+id/winscope_switch_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/winscope_switch_label"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_weight="1"
+            android:layout_gravity="fill_vertical"
+            android:gravity="start"
+            android:textAppearance="@style/TextAppearance.Dialog.Body.Message"/>
+
+        <Switch
+            android:id="@+id/winscope_switch"
+            android:layout_width="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="end"
+            android:layout_gravity="fill_vertical"
+            android:layout_weight="0" />
+    </LinearLayout>
+
+    <!-- Trace Debuggable Apps Switch -->
+    <LinearLayout
+        android:id="@+id/trace_debuggable_apps_switch_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/debuggable_apps_switch_label"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_weight="1"
+            android:layout_gravity="fill_vertical"
+            android:gravity="start"
+            android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+        <Switch
+            android:id="@+id/trace_debuggable_apps_switch"
+            android:layout_width="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="end"
+            android:layout_gravity="fill_vertical"
+            android:layout_weight="0" />
+    </LinearLayout>
+
+    <!-- Long Traces Switch -->
+    <LinearLayout
+        android:id="@+id/long_traces_switch_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/long_traces_switch_label"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_weight="1"
+            android:layout_gravity="fill_vertical"
+            android:gravity="start"
+            android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+        <Switch
+            android:id="@+id/long_traces_switch"
+            android:layout_width="wrap_content"
+            android:minHeight="@dimen/screenrecord_option_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="end"
+            android:layout_gravity="fill_vertical"
+            android:layout_weight="0" />
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/long_trace_size"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+
+    <TextView
+        android:id="@+id/long_trace_duration"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="@dimen/qqs_layout_margin_top"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message" />
+</LinearLayout>
+
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index acc12d7..8146cc5 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -948,6 +948,10 @@
     <string name="performance">Performance</string>
     <string name="user_interface">User Interface</string>
     <string name="thermal">Thermal</string>
+    <string name="custom">Custom</string>
+
+    <string name="custom_trace_settings_dialog_title">Custom Trace Settings</string>
+    <string name="restore_default">Restore Default</string>
 
     <!-- QuickSettings: Label for the toggle that controls whether One-handed mode is enabled. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_onehanded_label">One-handed mode</string>
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 56de5a3..8ae11ab 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -24,7 +24,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.Process;
@@ -81,7 +80,9 @@
 
     public SystemUIApplication() {
         super();
-        Trace.registerWithPerfetto();
+        if (!isSubprocess()) {
+            Trace.registerWithPerfetto();
+        }
         Log.v(TAG, "SystemUIApplication constructed.");
         // SysUI may be building without protolog preprocessing in some cases
         ProtoLog.REQUIRE_PROTOLOGTOOL = false;
@@ -182,9 +183,7 @@
         } else {
             // We don't need to startServices for sub-process that is doing some tasks.
             // (screenshots, sweetsweetdesserts or tuner ..)
-            String processName = ActivityThread.currentProcessName();
-            ApplicationInfo info = getApplicationInfo();
-            if (processName != null && processName.startsWith(info.processName + ":")) {
+            if (isSubprocess()) {
                 return;
             }
             // For a secondary user, boot-completed will never be called because it has already
@@ -195,6 +194,12 @@
         }
     }
 
+    /** Returns whether this is a subprocess (e.g. com.android.systemui:screenshot) */
+    private boolean isSubprocess() {
+        String processName = ActivityThread.currentProcessName();
+        return processName != null && processName.contains(":");
+    }
+
     /**
      * Makes sure that all the CoreStartables are running. If they are already running, this is a
      * no-op. This is needed to conditionally start all the services, as we only need to have it in
diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
index 0e2e2e6..b8019ab 100644
--- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.education.data.repository.ContextualEducationRepository
 import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl
 import com.android.systemui.education.domain.interactor.ContextualEducationInteractor
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
 import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor
 import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl
 import com.android.systemui.shared.education.GestureType
@@ -73,7 +74,7 @@
                 implLazy.get()
             } else {
                 // No-op implementation when the flag is disabled.
-                return NoOpCoreStartable
+                return NoOpContextualEducationInteractor
             }
         }
 
@@ -88,6 +89,18 @@
                 return NoOpKeyboardTouchpadEduStatsInteractor
             }
         }
+
+        @Provides
+        fun provideKeyboardTouchpadEduInteractor(
+            implLazy: Lazy<KeyboardTouchpadEduInteractor>
+        ): CoreStartable {
+            return if (Flags.keyboardTouchpadContextualEducation()) {
+                implLazy.get()
+            } else {
+                // No-op implementation when the flag is disabled.
+                return NoOpKeyboardTouchpadEduInteractor
+            }
+        }
     }
 
     private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor {
@@ -96,7 +109,11 @@
         override fun updateShortcutTriggerTime(gestureType: GestureType) {}
     }
 
-    private object NoOpCoreStartable : CoreStartable {
+    private object NoOpContextualEducationInteractor : CoreStartable {
+        override fun start() {}
+    }
+
+    private object NoOpKeyboardTouchpadEduInteractor : CoreStartable {
         override fun start() {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
index e2aa911..3036d97 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
@@ -19,12 +19,17 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.education.data.model.GestureEduModel
 import com.android.systemui.education.data.repository.ContextualEducationRepository
 import com.android.systemui.shared.education.GestureType
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.launch
 
 /**
@@ -36,16 +41,28 @@
 @Inject
 constructor(
     @Background private val backgroundScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val selectedUserInteractor: SelectedUserInteractor,
     private val repository: ContextualEducationRepository,
 ) : CoreStartable {
 
+    val backGestureModelFlow = readEduModelsOnSignalCountChanged(GestureType.BACK_GESTURE)
+
     override fun start() {
         backgroundScope.launch {
             selectedUserInteractor.selectedUser.collectLatest { repository.setUser(it) }
         }
     }
 
+    private fun readEduModelsOnSignalCountChanged(gestureType: GestureType): Flow<GestureEduModel> {
+        return repository
+            .readGestureEduModelFlow(gestureType)
+            .distinctUntilChanged(
+                areEquivalent = { old, new -> old.signalCount == new.signalCount }
+            )
+            .flowOn(backgroundDispatcher)
+    }
+
     suspend fun incrementSignalCount(gestureType: GestureType) =
         repository.incrementSignalCount(gestureType)
 
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
new file mode 100644
index 0000000..247abf1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.education.domain.interactor
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.education.data.model.GestureEduModel
+import com.android.systemui.education.shared.model.EducationInfo
+import com.android.systemui.education.shared.model.EducationUiType
+import com.android.systemui.shared.education.GestureType.BACK_GESTURE
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+
+/** Allow listening to new contextual education triggered */
+@SysUISingleton
+class KeyboardTouchpadEduInteractor
+@Inject
+constructor(
+    @Background private val backgroundScope: CoroutineScope,
+    private val contextualEducationInteractor: ContextualEducationInteractor
+) : CoreStartable {
+
+    companion object {
+        const val MAX_SIGNAL_COUNT: Int = 2
+    }
+
+    private val _educationTriggered = MutableStateFlow<EducationInfo?>(null)
+    val educationTriggered = _educationTriggered.asStateFlow()
+
+    override fun start() {
+        backgroundScope.launch {
+            contextualEducationInteractor.backGestureModelFlow
+                .mapNotNull { getEduType(it) }
+                .collect { _educationTriggered.value = EducationInfo(BACK_GESTURE, it) }
+        }
+    }
+
+    private fun getEduType(model: GestureEduModel): EducationUiType? {
+        if (isEducationNeeded(model)) {
+            return EducationUiType.Toast
+        } else {
+            return null
+        }
+    }
+
+    private fun isEducationNeeded(model: GestureEduModel): Boolean {
+        // Todo: b/354884305 - add complete education logic to show education in correct scenarios
+        val shortcutWasTriggered = model.lastShortcutTriggeredTime == null
+        val signalCountReached = model.signalCount >= MAX_SIGNAL_COUNT
+
+        return shortcutWasTriggered && signalCountReached
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt b/packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt
new file mode 100644
index 0000000..85f4012
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.education.shared.model
+
+import com.android.systemui.shared.education.GestureType
+
+/**
+ * Model for education triggered. [gestureType] indicates what gesture it is trying to educate about
+ * and [educationUiType] is how we educate user in the UI
+ */
+data class EducationInfo(val gestureType: GestureType, val educationUiType: EducationUiType)
+
+enum class EducationUiType {
+    Toast,
+    Notification,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index 69a157f..addb014 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -19,6 +19,7 @@
 import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
 
 import static com.android.settingslib.flags.Flags.legacyLeAudioSharing;
+import static com.android.systemui.Flags.communalHub;
 import static com.android.systemui.Flags.mediaLockscreenLaunchAnimation;
 import static com.android.systemui.media.controls.shared.model.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS;
 
@@ -90,6 +91,8 @@
 import com.android.systemui.animation.GhostedViewTransitionAnimatorController;
 import com.android.systemui.bluetooth.BroadcastDialogController;
 import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
+import com.android.systemui.communal.widgets.CommunalTransitionAnimatorController;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
@@ -202,10 +205,12 @@
     );
 
     // Time in millis for playing turbulence noise that is played after a touch ripple.
-    @VisibleForTesting static final long TURBULENCE_NOISE_PLAY_DURATION = 7500L;
+    @VisibleForTesting
+    static final long TURBULENCE_NOISE_PLAY_DURATION = 7500L;
 
     private final SeekBarViewModel mSeekBarViewModel;
     private final MediaFlags mMediaFlags;
+    private final CommunalSceneInteractor mCommunalSceneInteractor;
     private SeekBarObserver mSeekBarObserver;
     protected final Executor mBackgroundExecutor;
     private final DelayableExecutor mMainExecutor;
@@ -293,8 +298,9 @@
      * Initialize a new control panel
      *
      * @param backgroundExecutor background executor, used for processing artwork
-     * @param mainExecutor main thread executor, used if we receive callbacks on the background
-     *                     thread that then trigger UI changes.
+     * @param mainExecutor       main thread executor, used if we receive callbacks on the
+     *                           background
+     *                           thread that then trigger UI changes.
      * @param activityStarter    activity starter
      */
     @Inject
@@ -314,6 +320,7 @@
             MediaUiEventLogger logger,
             KeyguardStateController keyguardStateController,
             ActivityIntentHelper activityIntentHelper,
+            CommunalSceneInteractor communalSceneInteractor,
             NotificationLockscreenUserManager lockscreenUserManager,
             BroadcastDialogController broadcastDialogController,
             GlobalSettings globalSettings,
@@ -337,6 +344,7 @@
         mLockscreenUserManager = lockscreenUserManager;
         mBroadcastDialogController = broadcastDialogController;
         mMediaFlags = mediaFlags;
+        mCommunalSceneInteractor = communalSceneInteractor;
 
         mSeekBarViewModel.setLogSeek(() -> {
             if (mPackageName != null && mInstanceId != null) {
@@ -375,6 +383,7 @@
 
     /**
      * Get the recommendation view holder used to display Smartspace media recs.
+     *
      * @return the recommendation view holder
      */
     @Nullable
@@ -693,7 +702,7 @@
             // TODO(b/233698402): Use the package name instead of app label to avoid the
             // unexpected result.
             mIsCurrentBroadcastedApp = device != null
-                && TextUtils.equals(device.getName(),
+                    && TextUtils.equals(device.getName(),
                     mContext.getString(R.string.broadcasting_description_is_broadcasting));
             useDisabledAlpha = !mIsCurrentBroadcastedApp;
             // Always be enabled if the broadcast button is shown
@@ -764,7 +773,7 @@
                             PendingIntent deviceIntent = device.getIntent();
                             boolean showOverLockscreen = mKeyguardStateController.isShowing()
                                     && mActivityIntentHelper.wouldPendingShowOverLockscreen(
-                                        deviceIntent, mLockscreenUserManager.getCurrentUserId());
+                                    deviceIntent, mLockscreenUserManager.getCurrentUserId());
                             if (deviceIntent.isActivity()) {
                                 if (!showOverLockscreen) {
                                     mActivityStarter.postStartActivityDismissingKeyguard(
@@ -825,24 +834,26 @@
         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
         return mMetadataAnimationHandler.setNext(
-            new Triple(data.getSong(), data.getArtist(), data.isExplicit()),
-            () -> {
-                titleText.setText(data.getSong());
-                artistText.setText(data.getArtist());
-                setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator, data.isExplicit());
-                setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator, data.isExplicit());
+                new Triple(data.getSong(), data.getArtist(), data.isExplicit()),
+                () -> {
+                    titleText.setText(data.getSong());
+                    artistText.setText(data.getArtist());
+                    setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator,
+                            data.isExplicit());
+                    setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator,
+                            data.isExplicit());
 
-                // refreshState is required here to resize the text views (and prevent ellipsis)
-                mMediaViewController.refreshState();
-                return Unit.INSTANCE;
-            },
-            () -> {
-                // After finishing the enter animation, we refresh state. This could pop if
-                // something is incorrectly bound, but needs to be run if other elements were
-                // updated while the enter animation was running
-                mMediaViewController.refreshState();
-                return Unit.INSTANCE;
-            });
+                    // refreshState is required here to resize the text views (and prevent ellipsis)
+                    mMediaViewController.refreshState();
+                    return Unit.INSTANCE;
+                },
+                () -> {
+                    // After finishing the enter animation, we refresh state. This could pop if
+                    // something is incorrectly bound, but needs to be run if other elements were
+                    // updated while the enter animation was running
+                    mMediaViewController.refreshState();
+                    return Unit.INSTANCE;
+                });
     }
 
     // We may want to look into unifying this with bindRecommendationContentDescription if/when we
@@ -1105,7 +1116,7 @@
 
     private LayerDrawable setupGradientColorOnDrawable(Drawable albumArt, GradientDrawable gradient,
             ColorScheme mutableColorScheme, float startAlpha, float endAlpha) {
-        gradient.setColors(new int[] {
+        gradient.setColors(new int[]{
                 ColorUtilKt.getColorWithAlpha(
                         MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme),
                         startAlpha),
@@ -1113,7 +1124,7 @@
                         MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme),
                         endAlpha),
         });
-        return new LayerDrawable(new Drawable[] { albumArt, gradient });
+        return new LayerDrawable(new Drawable[]{albumArt, gradient});
     }
 
     private void scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer,
@@ -1143,7 +1154,7 @@
         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
         if (semanticActions != null) {
             // Hide all the generic buttons
-            for (ImageButton b: genericButtons) {
+            for (ImageButton b : genericButtons) {
                 setVisibleAndAlpha(collapsedSet, b.getId(), false);
                 setVisibleAndAlpha(expandedSet, b.getId(), false);
             }
@@ -1346,6 +1357,7 @@
                 /* shouldInverseNoiseLuminosity= */ false
         );
     }
+
     private void clearButton(final ImageButton button) {
         button.setImageDrawable(null);
         button.setContentDescription(null);
@@ -1421,19 +1433,33 @@
 
         // TODO(b/174236650): Make sure that the carousel indicator also fades out.
         // TODO(b/174236650): Instrument the animation to measure jank.
-        return new GhostedViewTransitionAnimatorController(player,
-                InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER) {
-            @Override
-            protected float getCurrentTopCornerRadius() {
-                return mContext.getResources().getDimension(R.dimen.notification_corner_radius);
-            }
+        final ActivityTransitionAnimator.Controller controller =
+                new GhostedViewTransitionAnimatorController(player,
+                        InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER) {
+                    @Override
+                    protected float getCurrentTopCornerRadius() {
+                        return mContext.getResources().getDimension(
+                                R.dimen.notification_corner_radius);
+                    }
 
-            @Override
-            protected float getCurrentBottomCornerRadius() {
-                // TODO(b/184121838): Make IlluminationDrawable support top and bottom radius.
-                return getCurrentTopCornerRadius();
-            }
-        };
+                    @Override
+                    protected float getCurrentBottomCornerRadius() {
+                        // TODO(b/184121838): Make IlluminationDrawable support top and bottom
+                        //  radius.
+                        return getCurrentTopCornerRadius();
+                    }
+                };
+
+        // When on the hub, wrap in the communal animation controller to ensure we exit the hub
+        // at the proper stage of the animation.
+        if (communalHub()
+                && mMediaViewController.getCurrentEndLocation()
+                == MediaHierarchyManager.LOCATION_COMMUNAL_HUB) {
+            mCommunalSceneInteractor.setIsLaunchingWidget(true);
+            return new CommunalTransitionAnimatorController(controller,
+                    mCommunalSceneInteractor);
+        }
+        return controller;
     }
 
     /** Bind this recommendation view based on the given data. */
@@ -1934,6 +1960,7 @@
 
     /**
      * Get the surface given the current end location for MediaViewController
+     *
      * @return surface used for Smartspace logging
      */
     protected int getSurfaceForSmartspaceLogging() {
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceSettingsDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceSettingsDialogDelegate.kt
new file mode 100644
index 0000000..56270ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceSettingsDialogDelegate.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.recordissue
+
+import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.widget.Switch
+import android.widget.TextView
+import com.android.systemui.recordissue.IssueRecordingState.Companion.TAG_TITLE_DELIMITER
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.traceur.PresetTraceConfigs
+import com.android.traceur.TraceConfig
+import com.android.traceur.res.R as T
+import java.util.function.Consumer
+
+class CustomTraceSettingsDialogDelegate(
+    private val factory: SystemUIDialog.Factory,
+    private val customTraceState: CustomTraceState,
+    private val tagTitles: Set<String>,
+    private val onSave: Runnable,
+) : SystemUIDialog.Delegate {
+
+    private val builder = TraceConfig.Builder(customTraceState.traceConfig)
+
+    override fun createDialog(): SystemUIDialog = factory.create(this)
+
+    override fun beforeCreate(dialog: SystemUIDialog?, savedInstanceState: Bundle?) {
+        super.beforeCreate(dialog, savedInstanceState)
+
+        dialog?.apply {
+            setTitle(R.string.custom_trace_settings_dialog_title)
+            setView(
+                LayoutInflater.from(context).inflate(R.layout.custom_trace_settings_dialog, null)
+            )
+            setPositiveButton(R.string.save) { _, _ ->
+                onSave.run()
+                customTraceState.traceConfig = builder.build()
+            }
+            setNegativeButton(R.string.cancel) { _, _ -> }
+        }
+    }
+
+    @SuppressLint("SetTextI18n")
+    override fun onCreate(dialog: SystemUIDialog?, savedInstanceState: Bundle?) {
+        super.onCreate(dialog, savedInstanceState)
+
+        dialog?.apply {
+            requireViewById<TextView>(R.id.categories).apply {
+                text =
+                    context.getString(T.string.categories) +
+                        "\n" +
+                        if (
+                            builder.tags == null ||
+                                builder.tags!! == PresetTraceConfigs.getDefaultConfig().tags
+                        ) {
+                            context.getString(R.string.notification_alert_title)
+                        } else {
+                            tagTitles
+                                .filter {
+                                    builder.tags!!.contains(it.substringBefore(TAG_TITLE_DELIMITER))
+                                }
+                                .joinToString()
+                        }
+                setOnClickListener { showCategorySelector(this) }
+            }
+            requireViewById<Switch>(R.id.attach_to_bugreport_switch).apply {
+                isChecked = builder.attachToBugreport
+                setOnCheckedChangeListener { _, isChecked -> builder.attachToBugreport = isChecked }
+            }
+            requireViewById<TextView>(R.id.cpu_buffer_size).setupSingleChoiceText(
+                T.array.buffer_size_values,
+                T.array.buffer_size_names,
+                builder.bufferSizeKb,
+                T.string.buffer_size,
+            ) {
+                builder.bufferSizeKb = it
+            }
+            val longTraceSizeText: TextView =
+                requireViewById<TextView>(R.id.long_trace_size).setupSingleChoiceText(
+                    T.array.long_trace_size_values,
+                    T.array.long_trace_size_names,
+                    builder.maxLongTraceSizeMb,
+                    T.string.max_long_trace_size,
+                ) {
+                    builder.maxLongTraceSizeMb = it
+                }
+            val longTraceDurationText: TextView =
+                requireViewById<TextView>(R.id.long_trace_duration).setupSingleChoiceText(
+                    T.array.long_trace_duration_values,
+                    T.array.long_trace_duration_names,
+                    builder.maxLongTraceDurationMinutes,
+                    T.string.max_long_trace_duration,
+                ) {
+                    builder.maxLongTraceDurationMinutes = it
+                }
+            requireViewById<Switch>(R.id.long_traces_switch).apply {
+                isChecked = builder.longTrace
+                val disabledAlpha by lazy { getDisabledAlpha(context) }
+                val alpha = if (isChecked) 1f else disabledAlpha
+                longTraceDurationText.alpha = alpha
+                longTraceSizeText.alpha = alpha
+
+                setOnCheckedChangeListener { _, isChecked ->
+                    builder.longTrace = isChecked
+                    longTraceDurationText.isEnabled = isChecked
+                    longTraceSizeText.isEnabled = isChecked
+
+                    val newAlpha = if (isChecked) 1f else disabledAlpha
+                    longTraceDurationText.alpha = newAlpha
+                    longTraceSizeText.alpha = newAlpha
+                }
+            }
+            requireViewById<Switch>(R.id.winscope_switch).apply {
+                isChecked = builder.winscope
+                setOnCheckedChangeListener { _, isChecked -> builder.winscope = isChecked }
+            }
+            requireViewById<Switch>(R.id.trace_debuggable_apps_switch).apply {
+                isChecked = builder.apps
+                setOnCheckedChangeListener { _, isChecked -> builder.apps = isChecked }
+            }
+            requireViewById<TextView>(R.id.long_traces_switch_label).text =
+                context.getString(T.string.long_traces)
+            requireViewById<TextView>(R.id.debuggable_apps_switch_label).text =
+                context.getString(T.string.trace_debuggable_applications)
+            requireViewById<TextView>(R.id.winscope_switch_label).text =
+                context.getString(T.string.winscope_tracing)
+            requireViewById<TextView>(R.id.attach_to_bugreport_switch_label).text =
+                context.getString(T.string.attach_to_bug_report)
+        }
+    }
+
+    @SuppressLint("SetTextI18n")
+    private fun showCategorySelector(root: TextView) {
+        showDialog(root.context) {
+            val tags = builder.tags ?: PresetTraceConfigs.getDefaultConfig().tags
+            val titlesToCheckmarks =
+                tagTitles.associateBy(
+                    { it },
+                    { tags.contains(it.substringBefore(TAG_TITLE_DELIMITER)) }
+                )
+            val titles = titlesToCheckmarks.keys.toTypedArray()
+            val checkmarks = titlesToCheckmarks.values.toBooleanArray()
+            val checkedTitleSuffixes =
+                titlesToCheckmarks.entries
+                    .filter { it.value }
+                    .map { it.key.substringAfter(TAG_TITLE_DELIMITER) }
+                    .toMutableSet()
+
+            val newTags = tags.toMutableSet()
+            setMultiChoiceItems(titles, checkmarks) { _, i, isChecked ->
+                val tag = titles[i].substringBefore(TAG_TITLE_DELIMITER)
+                val titleSuffix = titles[i].substringAfter(TAG_TITLE_DELIMITER)
+                if (isChecked) {
+                    newTags.add(tag)
+                    checkedTitleSuffixes.add(titleSuffix)
+                } else {
+                    newTags.remove(tag)
+                    checkedTitleSuffixes.remove(titleSuffix)
+                }
+            }
+            setPositiveButton(R.string.save) { _, _ ->
+                root.text =
+                    root.context.resources.getString(T.string.categories) +
+                        "\n" +
+                        checkedTitleSuffixes.joinToString()
+                builder.tags = newTags
+            }
+            setNeutralButton(R.string.restore_default) { _, _ ->
+                root.text =
+                    context.getString(T.string.categories) +
+                        "\n" +
+                        context.getString(R.string.notification_alert_title)
+                builder.tags = null
+            }
+            setNegativeButton(R.string.cancel) { _, _ -> }
+        }
+    }
+
+    @SuppressLint("SetTextI18n")
+    private fun TextView.setupSingleChoiceText(
+        resValues: Int,
+        resNames: Int,
+        startingValue: Int,
+        alertTitleRes: Int,
+        onChosen: Consumer<Int>,
+    ): TextView {
+        val values = resources.getStringArray(resValues).map { Integer.parseInt(it) }
+        val names = resources.getStringArray(resNames)
+        val startingIndex = values.indexOf(startingValue)
+        text = resources.getString(alertTitleRes) + "\n${names[startingIndex]}"
+
+        setOnClickListener {
+            showDialog(context) {
+                setTitle(alertTitleRes)
+                setSingleChoiceItems(names, startingIndex) { d, i ->
+                    text = resources.getString(alertTitleRes) + "\n${names[i]}"
+                    onChosen.accept(values[i])
+                    d.dismiss()
+                }
+            }
+        }
+        return this
+    }
+
+    private fun showDialog(context: Context, onBuilder: AlertDialog.Builder.() -> Unit) =
+        AlertDialog.Builder(context, R.style.Theme_SystemUI_Dialog_Alert)
+            .apply { onBuilder() }
+            .create()
+            .also { SystemUIDialog.applyFlags(it) }
+            .show()
+
+    private fun getDisabledAlpha(context: Context): Float {
+        val ta = context.obtainStyledAttributes(intArrayOf(android.R.attr.disabledAlpha))
+        val alpha = ta.getFloat(0, 0f)
+        ta.recycle()
+        return alpha
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceState.kt
new file mode 100644
index 0000000..14dfcc5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceState.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.recordissue
+
+import android.content.SharedPreferences
+import com.android.traceur.PresetTraceConfigs.TraceOptions
+import com.android.traceur.PresetTraceConfigs.getDefaultConfig
+import com.android.traceur.TraceConfig
+
+class CustomTraceState(private val prefs: SharedPreferences) {
+
+    private var enabledTags: Set<String>?
+        get() = prefs.getStringSet(KEY_TAGS, getDefaultConfig().tags) ?: getDefaultConfig().tags
+        set(value) = prefs.edit().putStringSet(KEY_TAGS, value).apply()
+
+    var traceConfig: TraceConfig
+        get() = TraceConfig(options, enabledTags)
+        set(value) {
+            enabledTags = value.tags
+            options = value.options
+        }
+
+    private var options: TraceOptions
+        get() =
+            TraceOptions(
+                prefs.getInt(KEY_CUSTOM_BUFFER_SIZE_KB, getDefaultConfig().bufferSizeKb),
+                prefs.getBoolean(KEY_WINSCOPE, getDefaultConfig().winscope),
+                prefs.getBoolean(KEY_APPS, getDefaultConfig().apps),
+                prefs.getBoolean(KEY_LONG_TRACE, getDefaultConfig().longTrace),
+                prefs.getBoolean(KEY_ATTACH_TO_BUGREPORT, getDefaultConfig().attachToBugreport),
+                prefs.getInt(KEY_LONG_TRACE_SIZE_MB, getDefaultConfig().maxLongTraceSizeMb),
+                prefs.getInt(
+                    KEY_LONG_TRACE_DURATION_MINUTES,
+                    getDefaultConfig().maxLongTraceDurationMinutes
+                ),
+            )
+        set(value) {
+            prefs
+                .edit()
+                .putInt(KEY_CUSTOM_BUFFER_SIZE_KB, value.bufferSizeKb)
+                .putBoolean(KEY_WINSCOPE, value.winscope)
+                .putBoolean(KEY_APPS, value.apps)
+                .putBoolean(KEY_LONG_TRACE, value.longTrace)
+                .putBoolean(KEY_ATTACH_TO_BUGREPORT, value.attachToBugreport)
+                .putInt(KEY_LONG_TRACE_SIZE_MB, value.maxLongTraceSizeMb)
+                .putInt(KEY_LONG_TRACE_DURATION_MINUTES, value.maxLongTraceDurationMinutes)
+                .apply()
+        }
+
+    companion object {
+        private const val KEY_CUSTOM_BUFFER_SIZE_KB = "key_bufferSizeKb"
+        private const val KEY_WINSCOPE = "key_winscope"
+        private const val KEY_APPS = "key_apps"
+        private const val KEY_LONG_TRACE = "key_longTrace"
+        private const val KEY_ATTACH_TO_BUGREPORT = "key_attachToBugReport"
+        private const val KEY_LONG_TRACE_SIZE_MB = "key_maxLongTraceSizeMb"
+        private const val KEY_LONG_TRACE_DURATION_MINUTES = "key_maxLongTraceDurationInMinutes"
+        private const val KEY_TAGS = "key_tags"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
index 7612900..16642ab 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
@@ -38,6 +38,8 @@
     private val prefs =
         userFileManager.getSharedPreferences(TILE_SPEC, Context.MODE_PRIVATE, userTracker.userId)
 
+    val customTraceState = CustomTraceState(prefs)
+
     var takeBugreport
         get() = prefs.getBoolean(KEY_TAKE_BUG_REPORT, false)
         set(value) = prefs.edit().putBoolean(KEY_TAKE_BUG_REPORT, value).apply()
@@ -55,7 +57,12 @@
         set(value) = prefs.edit().putInt(KEY_ISSUE_TYPE_RES, value).apply()
 
     val traceConfig: TraceConfig
-        get() = ALL_ISSUE_TYPES[issueTypeRes] ?: PresetTraceConfigs.getDefaultConfig()
+        get() = ALL_ISSUE_TYPES[issueTypeRes] ?: customTraceState.traceConfig
+
+    // The 1st part of the title before the ": " is the tag, and the 2nd part is the description
+    var tagTitles: Set<String>
+        get() = prefs.getStringSet(KEY_TAG_TITLES, emptySet()) ?: emptySet()
+        set(value) = prefs.edit().putStringSet(KEY_TAG_TITLES, value).apply()
 
     private val listeners = CopyOnWriteArrayList<Runnable>()
 
@@ -81,8 +88,10 @@
         private const val KEY_TAKE_BUG_REPORT = "key_takeBugReport"
         private const val HAS_APPROVED_SCREEN_RECORDING = "HasApprovedScreenRecord"
         private const val KEY_RECORD_SCREEN = "key_recordScreen"
+        private const val KEY_TAG_TITLES = "key_tagTitles"
         const val KEY_ISSUE_TYPE_RES = "key_issueTypeRes"
         const val ISSUE_TYPE_NOT_SET = -1
+        const val TAG_TITLE_DELIMITER = ": "
 
         val ALL_ISSUE_TYPES: Map<Int, TraceConfig?> =
             hashMapOf(
@@ -90,6 +99,7 @@
                 Pair(R.string.user_interface, PresetTraceConfigs.getUiConfig()),
                 Pair(R.string.battery, PresetTraceConfigs.getBatteryConfig()),
                 Pair(R.string.thermal, PresetTraceConfigs.getThermalConfig()),
+                Pair(R.string.custom, null),
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
index 8a51ad4..dd2dbf3 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
@@ -88,7 +88,7 @@
             setPositiveButton(R.string.qs_record_issue_start) { _, _ -> onStarted.run() }
         }
         bgExecutor.execute {
-            traceurMessageSender.onBoundToTraceur.add { traceurMessageSender.getTags() }
+            traceurMessageSender.onBoundToTraceur.add { traceurMessageSender.getTags(state) }
             traceurMessageSender.bindToTraceur(dialog.context)
         }
     }
@@ -170,18 +170,8 @@
     @MainThread
     private fun onIssueTypeClicked(context: Context, onIssueTypeSelected: Runnable) {
         val popupMenu = PopupMenu(context, issueTypeButton)
-
-        ALL_ISSUE_TYPES.keys.forEach {
-            popupMenu.menu.add(it).apply {
-                setIcon(R.drawable.arrow_pointing_down)
-                if (it != state.issueTypeRes) {
-                    iconTintList = ColorStateList.valueOf(Color.TRANSPARENT)
-                }
-                intent = Intent().putExtra(KEY_ISSUE_TYPE_RES, it)
-            }
-        }
-        popupMenu.apply {
-            setOnMenuItemClickListener {
+        val onMenuItemClickListener =
+            PopupMenu.OnMenuItemClickListener {
                 issueTypeButton.text = it.title
                 state.issueTypeRes =
                     it.intent?.getIntExtra(KEY_ISSUE_TYPE_RES, ISSUE_TYPE_NOT_SET)
@@ -189,6 +179,32 @@
                 onIssueTypeSelected.run()
                 true
             }
+        ALL_ISSUE_TYPES.keys.forEach {
+            popupMenu.menu.add(it).apply {
+                setIcon(R.drawable.arrow_pointing_down)
+                if (it != state.issueTypeRes) {
+                    iconTintList = ColorStateList.valueOf(Color.TRANSPARENT)
+                }
+                intent = Intent().putExtra(KEY_ISSUE_TYPE_RES, it)
+
+                if (it == R.string.custom) {
+                    setOnMenuItemClickListener {
+                        CustomTraceSettingsDialogDelegate(
+                                factory,
+                                state.customTraceState,
+                                state.tagTitles
+                            ) {
+                                onMenuItemClickListener.onMenuItemClick(it)
+                            }
+                            .createDialog()
+                            .show()
+                        true
+                    }
+                }
+            }
+        }
+        popupMenu.apply {
+            setOnMenuItemClickListener(onMenuItemClickListener)
             setForceShowIcon(true)
             show()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
index a31a9ef..8bfd14a 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
@@ -33,6 +33,7 @@
 import androidx.annotation.WorkerThread
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.recordissue.IssueRecordingState.Companion.TAG_TITLE_DELIMITER
 import com.android.traceur.FileSender
 import com.android.traceur.MessageConstants
 import com.android.traceur.TraceConfig
@@ -112,8 +113,8 @@
     }
 
     @WorkerThread
-    fun getTags() {
-        val replyHandler = Messenger(TagsHandler(backgroundLooper))
+    fun getTags(state: IssueRecordingState) {
+        val replyHandler = Messenger(TagsHandler(backgroundLooper, state))
         notifyTraceur(MessageConstants.TAGS_WHAT, replyTo = replyHandler)
     }
 
@@ -165,7 +166,8 @@
         }
     }
 
-    private class TagsHandler(looper: Looper) : Handler(looper) {
+    private class TagsHandler(looper: Looper, private val state: IssueRecordingState) :
+        Handler(looper) {
 
         override fun handleMessage(msg: Message) {
             if (MessageConstants.TAGS_WHAT == msg.what) {
@@ -174,16 +176,11 @@
                     msg.data.getStringArrayList(MessageConstants.BUNDLE_KEY_TAG_DESCRIPTIONS)
                 if (keys == null || values == null) {
                     throw IllegalArgumentException(
-                        "Neither keys: $keys, nor values: $values can " + "be null"
+                        "Neither keys: $keys, nor values: $values can be null"
                     )
                 }
-
-                val tags = keys.zip(values).map { "${it.first}: ${it.second}" }.toSet()
-                Log.e(
-                    TAG,
-                    "These tags: $tags will be saved and used for the Custom Trace" +
-                        " Config dialog in a future CL. This log will be removed."
-                )
+                state.tagTitles =
+                    keys.zip(values).map { it.first + TAG_TITLE_DELIMITER + it.second }.toSet()
             } else {
                 throw IllegalArgumentException("received unknown msg.what: " + msg.what)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index bd08685..67032f7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -789,6 +789,9 @@
 
     /** update Qs height state */
     void setExpansionHeight(float height) {
+        if (mExpansionHeight == height) {
+            return;
+        }
         int maxHeight = getMaxExpansionHeight();
         height = Math.min(Math.max(
                 height, getMinExpansionHeight()), maxHeight);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
index 5b25b11..0103fff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.expansionChanges
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -87,6 +89,7 @@
     private val secureSettings: SecureSettings,
     private val seenNotificationsInteractor: SeenNotificationsInteractor,
     private val statusBarStateController: StatusBarStateController,
+    private val sceneInteractor: SceneInteractor,
 ) : Coordinator, Dumpable {
 
     private val unseenNotifications = mutableSetOf<NotificationEntry>()
@@ -106,12 +109,15 @@
         // Whether or not keyguard is visible (or occluded).
         @Suppress("DEPRECATION")
         val isKeyguardPresentFlow: Flow<Boolean> =
-            keyguardTransitionInteractor
-                .transitionValue(
-                    scene = Scenes.Gone,
-                    stateWithoutSceneContainer = KeyguardState.GONE,
-                )
-                .map { it == 0f }
+            if (SceneContainerFlag.isEnabled) {
+                    sceneInteractor.transitionState.map {
+                        !it.isTransitioning(to = Scenes.Gone) && !it.isIdle(Scenes.Gone)
+                    }
+                } else {
+                    keyguardTransitionInteractor.transitions.map { step ->
+                        step.to != KeyguardState.GONE
+                    }
+                }
                 .distinctUntilChanged()
                 .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
index 99ed2d9..cd442cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
@@ -21,6 +21,7 @@
 import android.telephony.TelephonyManager.EXTRA_PLMN
 import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN
 import android.telephony.TelephonyManager.EXTRA_SHOW_SPN
+import android.telephony.TelephonyManager.EXTRA_SPN
 import com.android.systemui.log.table.Diffable
 import com.android.systemui.log.table.TableRowLogger
 
@@ -96,7 +97,8 @@
 
 fun Intent.toNetworkNameModel(separator: String): NetworkNameModel? {
     val showSpn = getBooleanExtra(EXTRA_SHOW_SPN, false)
-    val spn = getStringExtra(EXTRA_DATA_SPN)
+    val spn = getStringExtra(EXTRA_SPN)
+    val dataSpn = getStringExtra(EXTRA_DATA_SPN)
     val showPlmn = getBooleanExtra(EXTRA_SHOW_PLMN, false)
     val plmn = getStringExtra(EXTRA_PLMN)
 
@@ -112,6 +114,12 @@
         }
         str.append(spn)
     }
+    if (showSpn && dataSpn != null) {
+        if (str.isNotEmpty()) {
+            str.append(separator)
+        }
+        str.append(dataSpn)
+    }
 
     return if (str.isNotEmpty()) NetworkNameModel.IntentDerived(str.toString()) else null
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index fbfe41f..521aa5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -69,6 +69,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bluetooth.BroadcastDialogController
 import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.media.controls.MediaTestUtils
 import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
@@ -211,6 +212,8 @@
     @Mock private lateinit var activityIntentHelper: ActivityIntentHelper
     @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
 
+    @Mock private lateinit var communalSceneInteractor: CommunalSceneInteractor
+
     @Mock private lateinit var recommendationViewHolder: RecommendationViewHolder
     @Mock private lateinit var smartspaceAction: SmartspaceAction
     private lateinit var smartspaceData: SmartspaceMediaData
@@ -271,6 +274,7 @@
                     logger,
                     keyguardStateController,
                     activityIntentHelper,
+                    communalSceneInteractor,
                     lockscreenUserManager,
                     broadcastDialogController,
                     globalSettings,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
index 9a6423d..7ab3e29 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
@@ -61,7 +61,7 @@
     public void testCloseQsSideEffects() {
         enableSplitShade(true);
         mQsController.setExpandImmediate(true);
-        mQsController.setExpanded(true);
+        mQsController.setExpansionHeight(800);
         mQsController.closeQs();
 
         assertThat(mQsController.getExpanded()).isEqualTo(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 8fd0b31..9d83d5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -59,6 +59,7 @@
 import android.telephony.TelephonyManager.ERI_OFF
 import android.telephony.TelephonyManager.ERI_ON
 import android.telephony.TelephonyManager.EXTRA_CARRIER_ID
+import android.telephony.TelephonyManager.EXTRA_DATA_SPN
 import android.telephony.TelephonyManager.EXTRA_PLMN
 import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN
 import android.telephony.TelephonyManager.EXTRA_SHOW_SPN
@@ -85,7 +86,6 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.configWithOverride
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.createTestConfig
-import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.signalStrength
@@ -93,8 +93,6 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
@@ -112,6 +110,8 @@
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
 
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -815,9 +815,11 @@
             val intent = spnIntent()
             val captor = argumentCaptor<BroadcastReceiver>()
             verify(context).registerReceiver(captor.capture(), any())
-            captor.value!!.onReceive(context, intent)
+            captor.lastValue.onReceive(context, intent)
 
-            assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+            // spnIntent() sets all values to true and test strings
+            assertThat(latest)
+                .isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN$SEP$DATA_SPN"))
 
             job.cancel()
         }
@@ -831,17 +833,19 @@
             val intent = spnIntent()
             val captor = argumentCaptor<BroadcastReceiver>()
             verify(context).registerReceiver(captor.capture(), any())
-            captor.value!!.onReceive(context, intent)
+            captor.lastValue.onReceive(context, intent)
 
-            assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+            assertThat(latest)
+                .isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN$SEP$DATA_SPN"))
 
             // WHEN an intent with a different subId is sent
             val wrongSubIntent = spnIntent(subId = 101)
 
-            captor.value!!.onReceive(context, wrongSubIntent)
+            captor.lastValue.onReceive(context, wrongSubIntent)
 
             // THEN the previous intent's name is still used
-            assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+            assertThat(latest)
+                .isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN$SEP$DATA_SPN"))
 
             job.cancel()
         }
@@ -855,9 +859,10 @@
             val intent = spnIntent()
             val captor = argumentCaptor<BroadcastReceiver>()
             verify(context).registerReceiver(captor.capture(), any())
-            captor.value!!.onReceive(context, intent)
+            captor.lastValue.onReceive(context, intent)
 
-            assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+            assertThat(latest)
+                .isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN$SEP$DATA_SPN"))
 
             val intentWithoutInfo =
                 spnIntent(
@@ -865,7 +870,7 @@
                     showPlmn = false,
                 )
 
-            captor.value!!.onReceive(context, intentWithoutInfo)
+            captor.lastValue.onReceive(context, intentWithoutInfo)
 
             assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
 
@@ -884,10 +889,88 @@
             val intent = spnIntent()
             val captor = argumentCaptor<BroadcastReceiver>()
             verify(context).registerReceiver(captor.capture(), any())
-            captor.value!!.onReceive(context, intent)
+            captor.lastValue.onReceive(context, intent)
 
             // The value is still there despite no active subscribers
-            assertThat(underTest.networkName.value).isEqualTo(intent.toNetworkNameModel(SEP))
+            assertThat(underTest.networkName.value)
+                .isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN$SEP$DATA_SPN"))
+        }
+
+    @Test
+    fun networkName_allFieldsSet() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.networkName)
+            val captor = argumentCaptor<BroadcastReceiver>()
+            verify(context).registerReceiver(captor.capture(), any())
+
+            val intent =
+                spnIntent(
+                    subId = SUB_1_ID,
+                    showSpn = true,
+                    spn = SPN,
+                    dataSpn = null,
+                    showPlmn = true,
+                    plmn = PLMN,
+                )
+            captor.lastValue.onReceive(context, intent)
+            assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN"))
+        }
+
+    @Test
+    fun networkName_showPlmn_plmnNotNull_showSpn_spnNull_dataSpnNotNull() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.networkName)
+            val captor = argumentCaptor<BroadcastReceiver>()
+            verify(context).registerReceiver(captor.capture(), any())
+            val intent =
+                spnIntent(
+                    subId = SUB_1_ID,
+                    showSpn = true,
+                    spn = null,
+                    dataSpn = DATA_SPN,
+                    showPlmn = true,
+                    plmn = PLMN,
+                )
+            captor.lastValue.onReceive(context, intent)
+            assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+        }
+
+    @Test
+    fun networkName_showPlmn_noShowSPN() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.networkName)
+            val captor = argumentCaptor<BroadcastReceiver>()
+            verify(context).registerReceiver(captor.capture(), any())
+            val intent =
+                spnIntent(
+                    subId = SUB_1_ID,
+                    showSpn = false,
+                    spn = SPN,
+                    dataSpn = DATA_SPN,
+                    showPlmn = true,
+                    plmn = PLMN,
+                )
+            captor.lastValue.onReceive(context, intent)
+            assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN"))
+        }
+
+    @Test
+    fun networkName_showPlmn_plmnNull_showSpn() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.networkName)
+            val captor = argumentCaptor<BroadcastReceiver>()
+            verify(context).registerReceiver(captor.capture(), any())
+            val intent =
+                spnIntent(
+                    subId = SUB_1_ID,
+                    showSpn = true,
+                    spn = SPN,
+                    dataSpn = DATA_SPN,
+                    showPlmn = true,
+                    plmn = null,
+                )
+            captor.lastValue.onReceive(context, intent)
+            assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$SPN$SEP$DATA_SPN"))
         }
 
     @Test
@@ -1128,14 +1211,16 @@
     private fun spnIntent(
         subId: Int = SUB_1_ID,
         showSpn: Boolean = true,
-        spn: String = SPN,
+        spn: String? = SPN,
+        dataSpn: String? = DATA_SPN,
         showPlmn: Boolean = true,
-        plmn: String = PLMN,
+        plmn: String? = PLMN,
     ): Intent =
         Intent(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED).apply {
             putExtra(EXTRA_SUBSCRIPTION_INDEX, subId)
             putExtra(EXTRA_SHOW_SPN, showSpn)
             putExtra(EXTRA_SPN, spn)
+            putExtra(EXTRA_DATA_SPN, dataSpn)
             putExtra(EXTRA_SHOW_PLMN, showPlmn)
             putExtra(EXTRA_PLMN, plmn)
         }
@@ -1148,6 +1233,7 @@
         private const val SEP = "-"
 
         private const val SPN = "testSpn"
+        private const val DATA_SPN = "testDataSpn"
         private const val PLMN = "testPlmn"
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
index 5410882..bade91a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
@@ -28,11 +28,15 @@
     private val userGestureMap = mutableMapOf<Int, GestureEduModel>()
     private val _gestureEduModels = MutableStateFlow(GestureEduModel())
     private val gestureEduModelsFlow = _gestureEduModels.asStateFlow()
+    private var currentUser: Int = 0
 
     override fun setUser(userId: Int) {
         if (!userGestureMap.contains(userId)) {
             userGestureMap[userId] = GestureEduModel()
         }
+        // save data of current user to the map
+        userGestureMap[currentUser] = _gestureEduModels.value
+        // switch to data of new user
         _gestureEduModels.value = userGestureMap[userId]!!
     }
 
@@ -41,13 +45,15 @@
     }
 
     override suspend fun incrementSignalCount(gestureType: GestureType) {
+        val originalModel = _gestureEduModels.value
         _gestureEduModels.value =
-            GestureEduModel(
+            originalModel.copy(
                 signalCount = _gestureEduModels.value.signalCount + 1,
             )
     }
 
     override suspend fun updateShortcutTriggerTime(gestureType: GestureType) {
-        _gestureEduModels.value = GestureEduModel(lastShortcutTriggeredTime = clock.instant())
+        val originalModel = _gestureEduModels.value
+        _gestureEduModels.value = originalModel.copy(lastShortcutTriggeredTime = clock.instant())
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt
index 5b2dc2b..a7b322b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.education.data.repository.contextualEducationRepository
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.user.domain.interactor.selectedUserInteractor
 
@@ -25,6 +26,7 @@
     Kosmos.Fixture {
         ContextualEducationInteractor(
             backgroundScope = testScope.backgroundScope,
+            backgroundDispatcher = testDispatcher,
             repository = contextualEducationRepository,
             selectedUserInteractor = selectedUserInteractor
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
index 8f84e04..fb4e901 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
@@ -19,6 +19,14 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 
+var Kosmos.keyboardTouchpadEduInteractor by
+    Kosmos.Fixture {
+        KeyboardTouchpadEduInteractor(
+            backgroundScope = testScope.backgroundScope,
+            contextualEducationInteractor = contextualEducationInteractor
+        )
+    }
+
 var Kosmos.keyboardTouchpadEduStatsInteractor by
     Kosmos.Fixture {
         KeyboardTouchpadEduStatsInteractorImpl(
diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp
index 311addb..efa1397 100644
--- a/services/accessibility/Android.bp
+++ b/services/accessibility/Android.bp
@@ -26,6 +26,7 @@
     },
     srcs: [
         ":services.accessibility-sources",
+        ":statslog-accessibility-java-gen",
         "//frameworks/base/packages/SettingsLib/RestrictedLockUtils:SettingsLibRestrictedLockUtilsSrc",
     ],
     libs: [
@@ -37,7 +38,6 @@
         "a11ychecker-protos-java-proto-lite",
         "com_android_server_accessibility_flags_lib",
         "//frameworks/base/packages/SystemUI/aconfig:com_android_systemui_flags_lib",
-
     ],
 }
 
@@ -81,3 +81,12 @@
         "java/**/a11ychecker/proto/*.proto",
     ],
 }
+
+genrule {
+    name: "statslog-accessibility-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module accessibility" +
+        " --javaPackage com.android.server.accessibility.a11ychecker" +
+        " --javaClass AccessibilityCheckerStatsLog --minApiLevel 34",
+    out: ["java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsLog.java"],
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java
new file mode 100644
index 0000000..1b3ec5a
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerStatsdLogger.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.a11ychecker;
+
+import android.util.Slog;
+
+import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
+
+import java.util.Set;
+
+
+/**
+ * Wraps the StatsdLogger for AccessibilityCheckResultReported.
+ *
+ * @hide
+ */
+public class AccessibilityCheckerStatsdLogger {
+    private static final int ATOM_ID = 910;
+    private static final String LOG_TAG = "AccessibilityCheckerStatsdLogger";
+
+    /**
+     * Writes results to statsd.
+     */
+    public static void logResults(Set<AccessibilityCheckResultReported> results) {
+        Slog.i(LOG_TAG, String.format("Writing %d AccessibilityCheckResultReported events",
+                results.size()));
+
+        for (AccessibilityCheckResultReported result : results) {
+            AccessibilityCheckerStatsLog.write(ATOM_ID,
+                    result.getPackageName(),
+                    result.getAppVersionCode(),
+                    result.getUiElementPath(),
+                    result.getActivityName(),
+                    result.getWindowTitle(),
+                    result.getSourceComponentName(),
+                    result.getSourceVersionCode(),
+                    result.getResultCheckClass().getNumber(),
+                    result.getResultType().getNumber(),
+                    result.getResultId());
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index d42a3dc..0815384 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -155,6 +155,15 @@
     private static final int DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
             PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
 
+    // Comma separated list of all packages exempt from user impact level threshold. If a package
+    // in the list is crash looping, all the mitigations including factory reset will be performed.
+    private static final String PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD =
+            "persist.device_config.configuration.packages_exempt_from_impact_level_threshold";
+
+    // Comma separated list of default packages exempt from user impact level threshold.
+    private static final String DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD =
+            "com.android.systemui";
+
     private long mNumberOfNativeCrashPollsRemaining;
 
     private static final int DB_VERSION = 1;
@@ -201,6 +210,8 @@
     private final DeviceConfig.OnPropertiesChangedListener
             mOnPropertyChangedListener = this::onPropertyChanged;
 
+    private final Set<String> mPackagesExemptFromImpactLevelThreshold = new ArraySet<>();
+
     // The set of packages that have been synced with the ExplicitHealthCheckController
     @GuardedBy("mLock")
     private Set<String> mRequestedHealthCheckPackages = new ArraySet<>();
@@ -523,7 +534,7 @@
                               @FailureReasons int failureReason,
                               int currentObserverImpact,
                               int mitigationCount) {
-        if (currentObserverImpact < getUserImpactLevelLimit()) {
+        if (allowMitigations(currentObserverImpact, versionedPackage)) {
             synchronized (mLock) {
                 mLastMitigation = mSystemClock.uptimeMillis();
             }
@@ -531,6 +542,13 @@
         }
     }
 
+    private boolean allowMitigations(int currentObserverImpact,
+            VersionedPackage versionedPackage) {
+        return currentObserverImpact < getUserImpactLevelLimit()
+                || getPackagesExemptFromImpactLevelThreshold().contains(
+                versionedPackage.getPackageName());
+    }
+
     private long getMitigationWindowMs() {
         return SystemProperties.getLong(MITIGATION_WINDOW_MS, DEFAULT_MITIGATION_WINDOW_MS);
     }
@@ -662,6 +680,15 @@
                 DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD);
     }
 
+    private Set<String> getPackagesExemptFromImpactLevelThreshold() {
+        if (mPackagesExemptFromImpactLevelThreshold.isEmpty()) {
+            String packageNames = SystemProperties.get(PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD,
+                    DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD);
+            return Set.of(packageNames.split("\\s*,\\s*"));
+        }
+        return mPackagesExemptFromImpactLevelThreshold;
+    }
+
     /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */
     @Retention(SOURCE)
     @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 504c54a..ab63e24 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2864,6 +2864,11 @@
                         }
                     }
 
+                    if (newAdj == clientAdj && app.isolated) {
+                        // Make bound isolated processes have slightly worse score than their client
+                        newAdj = clientAdj + 1;
+                    }
+
                     if (adj >  newAdj) {
                         adj = newAdj;
                         if (state.setCurRawAdj(adj, dryRun)) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3d41f05..7a24e9df 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -496,19 +496,60 @@
     private AudioSystemThread mAudioSystemThread;
     /** @see AudioHandler */
     private AudioHandler mAudioHandler;
-    /** @see VolumeStreamState */
-    private VolumeStreamState[] mStreamStates;
+    /**
+     *  @see VolumeStreamState
+     *  Mapping which contains for each stream type its associated {@link VolumeStreamState}
+     **/
+    private SparseArray<VolumeStreamState> mStreamStates;
 
     /*package*/ int getVssVolumeForDevice(int stream, int device) {
-        return mStreamStates[stream].getIndex(device);
+        final VolumeStreamState streamState = mStreamStates.get(stream);
+        return streamState != null ? streamState.getIndex(device) : -1;
     }
 
-    /*package*/ VolumeStreamState getVssVolumeForStream(int stream) {
-        return mStreamStates[stream];
+    /**
+     * Returns the {@link VolumeStreamState} corresponding to the passed stream type. This can be
+     * {@code null} since not all possible stream types have a valid {@link VolumeStreamState} (e.g.
+     * {@link AudioSystem#STREAM_BLUETOOTH_SCO}) is deprecated and will return a {@code null} stream
+     * state).
+     *
+     * @param stream the stream type for querying the stream state
+     *
+     * @return the {@link VolumeStreamState} corresponding to the passed stream type or {@code null}
+     */
+    @Nullable
+    /*package*/ VolumeStreamState getVssForStream(int stream) {
+        return mStreamStates.get(stream);
+    }
+
+    /**
+     * Returns the {@link VolumeStreamState} corresponding to the passed stream type. In case
+     * there is no associated stream state for the given stream type we return the default stream
+     * state for {@link AudioSystem#STREAM_MUSIC} (or throw an {@link IllegalArgumentException} in
+     * the ramp up phase of the replaceStreamBtSco flag to ensure that this case will never happen).
+     *
+     * @param stream the stream type for querying the stream state
+     *
+     * @return the {@link VolumeStreamState} corresponding to the passed stream type
+     */
+    @NonNull
+    /*package*/ VolumeStreamState getVssForStreamOrDefault(int stream) {
+        VolumeStreamState streamState = mStreamStates.get(stream);
+        if (streamState == null) {
+            if (replaceStreamBtSco()) {
+                throw new IllegalArgumentException("No VolumeStreamState for stream " + stream);
+            } else {
+                Log.e(TAG, "No VolumeStreamState for stream " + stream
+                        + ". Returning default state for STREAM_MUSIC", new Exception());
+                streamState = mStreamStates.get(AudioSystem.STREAM_MUSIC);
+            }
+        }
+        return streamState;
     }
 
     /*package*/ int getMaxVssVolumeForStream(int stream) {
-        return mStreamStates[stream].getMaxIndex();
+        final VolumeStreamState streamState = mStreamStates.get(stream);
+        return streamState != null ? streamState.getMaxIndex() : -1;
     }
 
     private SettingsObserver mSettingsObserver;
@@ -550,13 +591,13 @@
         0   // STREAM_ASSISTANT
     };
 
-    /* mStreamVolumeAlias[] indicates for each stream if it uses the volume settings
+    /* sStreamVolumeAlias[] indicates for each stream if it uses the volume settings
      * of another stream: This avoids multiplying the volume settings for hidden
      * stream types that follow other stream behavior for volume settings
      * NOTE: do not create loops in aliases!
      * Some streams alias to different streams according to device category (phone or tablet) or
      * use case (in call vs off call...). See updateStreamVolumeAlias() for more details.
-     *  mStreamVolumeAlias contains STREAM_VOLUME_ALIAS_VOICE aliases for a voice capable device
+     *  sStreamVolumeAlias contains STREAM_VOLUME_ALIAS_VOICE aliases for a voice capable device
      *  (phone), STREAM_VOLUME_ALIAS_TELEVISION for a television or set-top box and
      *  STREAM_VOLUME_ALIAS_DEFAULT for other devices (e.g. tablets).*/
     private final int[] STREAM_VOLUME_ALIAS_VOICE = new int[] {
@@ -621,12 +662,12 @@
         AudioSystem.STREAM_MUSIC,           // STREAM_ACCESSIBILITY
         AudioSystem.STREAM_MUSIC            // STREAM_ASSISTANT
     };
-    protected static int[] mStreamVolumeAlias;
+    protected static SparseIntArray sStreamVolumeAlias;
     private static final int UNSET_INDEX = -1;
 
     /**
      * Map AudioSystem.STREAM_* constants to app ops.  This should be used
-     * after mapping through mStreamVolumeAlias.
+     * after mapping through sStreamVolumeAlias.
      */
     private static final int[] STREAM_VOLUME_OPS = new int[] {
         AppOpsManager.OP_AUDIO_VOICE_VOLUME,            // STREAM_VOICE_CALL
@@ -1416,7 +1457,7 @@
         mRecordMonitor = new RecordingActivityMonitor(mContext);
         mRecordMonitor.registerRecordingCallback(mVoiceRecordingActivityMonitor, true);
 
-        // must be called before readPersistedSettings() which needs a valid mStreamVolumeAlias[]
+        // must be called before readPersistedSettings() which needs a valid sStreamVolumeAlias[]
         // array initialized by updateStreamVolumeAlias()
         updateStreamVolumeAlias(false /*updateVolumes*/, TAG);
         readPersistedSettings();
@@ -1473,7 +1514,7 @@
         int numStreamTypes = AudioSystem.getNumStreamTypes();
         synchronized (VolumeStreamState.class) {
             for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
-                VolumeStreamState streamState = mStreamStates[streamType];
+                final VolumeStreamState streamState = getVssForStream(streamType);
                 if (streamState == null) {
                     continue;
                 }
@@ -2083,7 +2124,10 @@
         // keep track of any error during stream volume initialization
         int status = AudioSystem.AUDIO_STATUS_OK;
         for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
-            VolumeStreamState streamState = mStreamStates[streamType];
+            VolumeStreamState streamState = getVssForStream(streamType);
+            if (streamState == null) {
+                continue;
+            }
             final int res = AudioSystem.initStreamVolume(
                     streamType, MIN_STREAM_VOLUME[streamType], MAX_STREAM_VOLUME[streamType]);
             if (res != AudioSystem.AUDIO_STATUS_OK) {
@@ -2243,12 +2287,13 @@
             synchronized (VolumeStreamState.class) {
                 int numStreamTypes = AudioSystem.getNumStreamTypes();
                 for (int streamType = 0; streamType < numStreamTypes; streamType++) {
-                    if (mStreamVolumeAlias[streamType] >= 0) {
-                        mStreamStates[streamType]
-                                .setAllIndexes(mStreamStates[mStreamVolumeAlias[streamType]], TAG);
+                    int streamAlias = sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound=*/-1);
+                    final VolumeStreamState streamState = getVssForStream(streamType);
+                    if (streamAlias != -1 && streamState != null) {
+                        streamState.setAllIndexes(getVssForStream(streamAlias), TAG);
                         // apply stream volume
-                        if (!mStreamStates[streamType].mIsMuted) {
-                            mStreamStates[streamType].applyAllVolumes();
+                        if (!streamState.mIsMuted) {
+                            streamState.applyAllVolumes();
                         }
                     }
                 }
@@ -2348,11 +2393,17 @@
         if (device == AudioSystem.DEVICE_OUT_SPEAKER_SAFE) {
             device = AudioSystem.DEVICE_OUT_SPEAKER;
         }
-        if (!mStreamStates[streamType].hasIndexForDevice(device)) {
+
+        final VolumeStreamState streamState = getVssForStream(streamType);
+        if (streamState == null) {
+            // nothing to update
+            return;
+        }
+
+        if (!streamState.hasIndexForDevice(device)) {
             // set the default value, if device is affected by a full/fix/abs volume rule, it
             // will taken into account in checkFixedVolumeDevices()
-            mStreamStates[streamType].setIndex(
-                    mStreamStates[mStreamVolumeAlias[streamType]]
+            streamState.setIndex(getVssForStreamOrDefault(sStreamVolumeAlias.get(streamType))
                             .getIndex(AudioSystem.DEVICE_OUT_DEFAULT),
                     device, caller, true /*hasModifyAudioSettings*/);
         }
@@ -2365,11 +2416,11 @@
         for (AudioDeviceAttributes deviceAttributes : devicesForAttributes) {
             if (deviceAttributes.getType() == AudioDeviceInfo.convertInternalDeviceToDeviceType(
                     device)) {
-                mStreamStates[streamType].checkFixedVolumeDevices();
+                streamState.checkFixedVolumeDevices();
 
                 // Unmute streams if required and device is full volume
                 if (isStreamMute(streamType) && mFullVolumeDevices.contains(device)) {
-                    mStreamStates[streamType].mute(false, "updateVolumeStates(" + caller);
+                    streamState.mute(false, "updateVolumeStates(" + caller);
                 }
             }
         }
@@ -2379,22 +2430,27 @@
     {
         int numStreamTypes = AudioSystem.getNumStreamTypes();
         for (int streamType = 0; streamType < numStreamTypes; streamType++) {
-            if (mStreamStates[streamType] != null) {
-                mStreamStates[streamType].checkFixedVolumeDevices();
+            final VolumeStreamState vss = getVssForStream(streamType);
+            if (vss != null) {
+                vss.checkFixedVolumeDevices();
             }
         }
     }
 
     private void checkAllFixedVolumeDevices(int streamType) {
-        mStreamStates[streamType].checkFixedVolumeDevices();
+        final VolumeStreamState vss = getVssForStream(streamType);
+        if (vss == null) {
+            return;
+        }
+        vss.checkFixedVolumeDevices();
     }
 
     private void checkMuteAffectedStreams() {
         // any stream with a min level > 0 is not muteable by definition
         // STREAM_VOICE_CALL and STREAM_BLUETOOTH_SCO can be muted by applications
         // that has the the MODIFY_PHONE_STATE permission.
-        for (int i = 0; i < mStreamStates.length; i++) {
-            final VolumeStreamState vss = mStreamStates[i];
+        for (int i = 0; i < mStreamStates.size(); i++) {
+            final VolumeStreamState vss = mStreamStates.valueAt(i);
             if (vss != null && vss.mIndexMin > 0
                     && (vss.mStreamType != AudioSystem.STREAM_VOICE_CALL
                     && vss.mStreamType != AudioSystem.STREAM_BLUETOOTH_SCO)) {
@@ -2406,13 +2462,14 @@
 
     private void createStreamStates() {
         int numStreamTypes = AudioSystem.getNumStreamTypes();
-        VolumeStreamState[] streams = mStreamStates = new VolumeStreamState[numStreamTypes];
+        mStreamStates = new SparseArray<>(numStreamTypes);
 
         for (int i = 0; i < numStreamTypes; i++) {
-            // a negative mStreamVolumeAlias value means the stream state type is not supported
-            if (mStreamVolumeAlias[i] >= 0) {
-                streams[i] =
-                        new VolumeStreamState(System.VOLUME_SETTINGS_INT[mStreamVolumeAlias[i]], i);
+            final int streamAlias = sStreamVolumeAlias.get(i, /*valueIfKeyNotFound=*/-1);
+            // a negative sStreamVolumeAlias value means the stream state type is not supported
+            if (streamAlias >= 0) {
+                mStreamStates.set(i,
+                        new VolumeStreamState(System.VOLUME_SETTINGS_INT[streamAlias], i));
             }
         }
 
@@ -2431,24 +2488,25 @@
      * For other volume groups not linked to any streams, default music stream index is considered.
      */
     private void updateDefaultVolumes() {
-        for (int stream = 0; stream < mStreamStates.length; stream++) {
-            int streamVolumeAlias = mStreamVolumeAlias[stream];
+        for (int stream = 0; stream < mStreamStates.size(); stream++) {
+            int streamType = mStreamStates.keyAt(stream);
+            int streamVolumeAlias = sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound=*/-1);
             if (mUseVolumeGroupAliases) {
-                if (AudioSystem.DEFAULT_STREAM_VOLUME[stream] != UNSET_INDEX) {
+                if (AudioSystem.DEFAULT_STREAM_VOLUME[streamType] != UNSET_INDEX) {
                     // Already initialized through default property based mecanism.
                     continue;
                 }
                 streamVolumeAlias = AudioSystem.STREAM_MUSIC;
-                int defaultAliasVolume = getUiDefaultRescaledIndex(streamVolumeAlias, stream);
-                if ((defaultAliasVolume >= MIN_STREAM_VOLUME[stream])
-                        && (defaultAliasVolume <= MAX_STREAM_VOLUME[stream])) {
-                    AudioSystem.DEFAULT_STREAM_VOLUME[stream] = defaultAliasVolume;
+                int defaultAliasVolume = getUiDefaultRescaledIndex(streamVolumeAlias, streamType);
+                if ((defaultAliasVolume >= MIN_STREAM_VOLUME[streamType])
+                        && (defaultAliasVolume <= MAX_STREAM_VOLUME[streamType])) {
+                    AudioSystem.DEFAULT_STREAM_VOLUME[streamType] = defaultAliasVolume;
                     continue;
                 }
             }
-            if (streamVolumeAlias >= 0 && stream != streamVolumeAlias) {
-                AudioSystem.DEFAULT_STREAM_VOLUME[stream] =
-                        getUiDefaultRescaledIndex(streamVolumeAlias, stream);
+            if (streamVolumeAlias >= 0 && streamType != streamVolumeAlias) {
+                AudioSystem.DEFAULT_STREAM_VOLUME[streamType] =
+                        getUiDefaultRescaledIndex(streamVolumeAlias, streamType);
             }
         }
     }
@@ -2490,13 +2548,17 @@
                 continue;
             }
             StringBuilder alias = new StringBuilder();
-            if (mStreamVolumeAlias[i] != i) {
+            final int streamAlias = sStreamVolumeAlias.get(i, /*valueIfKeyNotFound*/-1);
+            if (streamAlias != i && streamAlias != -1) {
                 alias.append(" (aliased to: ")
-                        .append(AudioSystem.STREAM_NAMES[mStreamVolumeAlias[i]])
+                        .append(AudioSystem.STREAM_NAMES[streamAlias])
                         .append(")");
             }
             pw.println("- " + AudioSystem.STREAM_NAMES[i] + alias + ":");
-            mStreamStates[i].dump(pw);
+            final VolumeStreamState vss = getVssForStream(i);
+            if (vss != null) {
+                vss.dump(pw);
+            }
             pw.println("");
         }
         pw.print("\n- mute affected streams = 0x");
@@ -2505,6 +2567,13 @@
         pw.println(Integer.toHexString(mUserMutableStreams));
     }
 
+    private void initStreamVolumeAlias(int[] streamVolumeAlias) {
+        sStreamVolumeAlias = new SparseIntArray(streamVolumeAlias.length);
+        for (int i = 0; i < streamVolumeAlias.length; ++i) {
+            sStreamVolumeAlias.put(i, streamVolumeAlias[i]);
+        }
+    }
+
     private void updateStreamVolumeAlias(boolean updateVolumes, String caller) {
         int dtmfStreamAlias;
         final int a11yStreamAlias = sIndependentA11yVolume ?
@@ -2514,24 +2583,24 @@
                 AudioSystem.STREAM_ASSISTANT : AudioSystem.STREAM_MUSIC;
 
         if (mIsSingleVolume) {
-            mStreamVolumeAlias = STREAM_VOLUME_ALIAS_TELEVISION.clone();
+            initStreamVolumeAlias(STREAM_VOLUME_ALIAS_TELEVISION);
             dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
         } else if (mUseVolumeGroupAliases) {
-            mStreamVolumeAlias = STREAM_VOLUME_ALIAS_NONE.clone();
+            initStreamVolumeAlias(STREAM_VOLUME_ALIAS_NONE);
             dtmfStreamAlias = AudioSystem.STREAM_DTMF;
         } else {
             switch (mPlatformType) {
                 case AudioSystem.PLATFORM_VOICE:
-                    mStreamVolumeAlias = STREAM_VOLUME_ALIAS_VOICE.clone();
+                    initStreamVolumeAlias(STREAM_VOLUME_ALIAS_VOICE);
                     dtmfStreamAlias = AudioSystem.STREAM_RING;
                     break;
                 default:
-                    mStreamVolumeAlias = STREAM_VOLUME_ALIAS_DEFAULT.clone();
+                    initStreamVolumeAlias(STREAM_VOLUME_ALIAS_DEFAULT);
                     dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
             }
             if (!mNotifAliasRing) {
-                mStreamVolumeAlias[AudioSystem.STREAM_NOTIFICATION] =
-                        AudioSystem.STREAM_NOTIFICATION;
+                sStreamVolumeAlias.put(AudioSystem.STREAM_NOTIFICATION,
+                        AudioSystem.STREAM_NOTIFICATION);
             }
         }
 
@@ -2546,15 +2615,14 @@
             }
         }
 
-        mStreamVolumeAlias[AudioSystem.STREAM_DTMF] = dtmfStreamAlias;
-        mStreamVolumeAlias[AudioSystem.STREAM_ACCESSIBILITY] = a11yStreamAlias;
-        mStreamVolumeAlias[AudioSystem.STREAM_ASSISTANT] = assistantStreamAlias;
+        sStreamVolumeAlias.put(AudioSystem.STREAM_DTMF, dtmfStreamAlias);
+        sStreamVolumeAlias.put(AudioSystem.STREAM_ACCESSIBILITY, a11yStreamAlias);
+        sStreamVolumeAlias.put(AudioSystem.STREAM_ASSISTANT, assistantStreamAlias);
 
         if (replaceStreamBtSco()) {
             // we do not support STREAM_BLUETOOTH_SCO, this will lead to having
-            // mStreanStates[STREAM_BLUETOOTH_SCO] = null
-            // TODO: replace arrays with SparseIntArrays to avoid null checks
-            mStreamVolumeAlias[AudioSystem.STREAM_BLUETOOTH_SCO] = -1;
+            // mStreanStates.get(STREAM_BLUETOOTH_SCO) == null
+            sStreamVolumeAlias.delete(AudioSystem.STREAM_BLUETOOTH_SCO);
         }
 
         if (updateVolumes && mStreamStates != null) {
@@ -2562,17 +2630,17 @@
 
             synchronized (mSettingsLock) {
                 synchronized (VolumeStreamState.class) {
-                    mStreamStates[AudioSystem.STREAM_DTMF]
-                            .setAllIndexes(mStreamStates[dtmfStreamAlias], caller);
-                    mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].setSettingName(
+                    getVssForStreamOrDefault(AudioSystem.STREAM_DTMF)
+                            .setAllIndexes(getVssForStreamOrDefault(dtmfStreamAlias), caller);
+                    getVssForStreamOrDefault(AudioSystem.STREAM_ACCESSIBILITY).setSettingName(
                             System.VOLUME_SETTINGS_INT[a11yStreamAlias]);
-                    mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].setAllIndexes(
-                            mStreamStates[a11yStreamAlias], caller);
+                    getVssForStreamOrDefault(AudioSystem.STREAM_ACCESSIBILITY).setAllIndexes(
+                            getVssForStreamOrDefault(a11yStreamAlias), caller);
                 }
             }
             if (sIndependentA11yVolume) {
                 // restore the a11y values from the settings
-                mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].readSettings();
+                getVssForStreamOrDefault(AudioSystem.STREAM_ACCESSIBILITY).readSettings();
             }
 
             // apply stream mute states according to new value of mRingerModeAffectedStreams
@@ -2582,13 +2650,13 @@
                     SENDMSG_QUEUE,
                     0,
                     0,
-                    mStreamStates[AudioSystem.STREAM_DTMF], 0);
+                    getVssForStreamOrDefault(AudioSystem.STREAM_DTMF), 0);
             sendMsg(mAudioHandler,
                     MSG_SET_ALL_VOLUMES,
                     SENDMSG_QUEUE,
                     0,
                     0,
-                    mStreamStates[AudioSystem.STREAM_ACCESSIBILITY], 0);
+                    getVssForStreamOrDefault(AudioSystem.STREAM_ACCESSIBILITY), 0);
         }
         dispatchStreamAliasingUpdate();
     }
@@ -3065,7 +3133,8 @@
     }
 
     private int getIndexRange(int streamType) {
-        return (mStreamStates[streamType].getMaxIndex() - mStreamStates[streamType].getMinIndex());
+        return (getVssForStreamOrDefault(streamType).getMaxIndex() - getVssForStreamOrDefault(
+                streamType).getMinIndex());
     }
 
     private int rescaleIndex(VolumeInfo volumeInfo, int dstStream) {
@@ -3073,11 +3142,12 @@
                 || volumeInfo.getMinVolumeIndex() == VolumeInfo.INDEX_NOT_SET
                 || volumeInfo.getMaxVolumeIndex() == VolumeInfo.INDEX_NOT_SET) {
             Log.e(TAG, "rescaleIndex: volumeInfo has invalid index or range");
-            return mStreamStates[dstStream].getMinIndex();
+            return getVssForStreamOrDefault(dstStream).getMinIndex();
         }
         return rescaleIndex(volumeInfo.getVolumeIndex(),
                 volumeInfo.getMinVolumeIndex(), volumeInfo.getMaxVolumeIndex(),
-                mStreamStates[dstStream].getMinIndex(), mStreamStates[dstStream].getMaxIndex());
+                getVssForStreamOrDefault(dstStream).getMinIndex(),
+                getVssForStreamOrDefault(dstStream).getMaxIndex());
     }
 
     private int rescaleIndex(int index, int srcStream, VolumeInfo dstVolumeInfo) {
@@ -3088,14 +3158,17 @@
             return index;
         }
         return rescaleIndex(index,
-                mStreamStates[srcStream].getMinIndex(), mStreamStates[srcStream].getMaxIndex(),
+                getVssForStreamOrDefault(srcStream).getMinIndex(),
+                getVssForStreamOrDefault(srcStream).getMaxIndex(),
                 dstMin, dstMax);
     }
 
     private int rescaleIndex(int index, int srcStream, int dstStream) {
         return rescaleIndex(index,
-                mStreamStates[srcStream].getMinIndex(), mStreamStates[srcStream].getMaxIndex(),
-                mStreamStates[dstStream].getMinIndex(), mStreamStates[dstStream].getMaxIndex());
+                getVssForStreamOrDefault(srcStream).getMinIndex(),
+                getVssForStreamOrDefault(srcStream).getMaxIndex(),
+                getVssForStreamOrDefault(dstStream).getMinIndex(),
+                getVssForStreamOrDefault(dstStream).getMaxIndex());
     }
 
     private int rescaleIndex(int index, int srcMin, int srcMax, int dstMin, int dstMax) {
@@ -3618,7 +3691,7 @@
         streamType = replaceBtScoStreamWithVoiceCall(streamType, "adjustSuggestedStreamVolume");
 
         ensureValidStreamType(streamType);
-        final int resolvedStream = mStreamVolumeAlias[streamType];
+        final int resolvedStream = sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound=*/-1);
 
         // Play sounds on STREAM_RING only.
         if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 &&
@@ -3735,9 +3808,9 @@
         // use stream type alias here so that streams with same alias have the same behavior,
         // including with regard to silent mode control (e.g the use of STREAM_RING below and in
         // checkForRingerModeChange() in place of STREAM_RING or STREAM_NOTIFICATION)
-        int streamTypeAlias = mStreamVolumeAlias[streamType];
+        int streamTypeAlias = sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound=*/-1);
 
-        VolumeStreamState streamState = mStreamStates[streamTypeAlias];
+        VolumeStreamState streamState = getVssForStreamOrDefault(streamTypeAlias);
 
         final int device = getDeviceForStream(streamTypeAlias);
 
@@ -3823,7 +3896,7 @@
         if (!volumeAdjustmentAllowedByDnd(streamTypeAlias, flags)) {
             adjustVolume = false;
         }
-        int oldIndex = mStreamStates[streamType].getIndex(device);
+        int oldIndex = getVssForStreamOrDefault(streamType).getIndex(device);
 
         // Check if the volume adjustment should be handled by an absolute volume controller instead
         if (isAbsoluteVolumeDevice(device) && (flags & AudioManager.FLAG_ABSOLUTE_VOLUME) == 0) {
@@ -3879,7 +3952,7 @@
                         0);
             }
 
-            int newIndex = mStreamStates[streamType].getIndex(device);
+            int newIndex = getVssForStreamOrDefault(streamType).getIndex(device);
 
             int streamToDriveAbsVol = absVolumeIndexFix() ? getBluetoothContextualVolumeStream() :
                     AudioSystem.STREAM_MUSIC;
@@ -3906,7 +3979,7 @@
                             + newIndex + " stream=" + streamType);
                 }
                 mDeviceBroker.postSetLeAudioVolumeIndex(newIndex,
-                    mStreamStates[streamType].getMaxIndex(), streamType);
+                        getVssForStreamOrDefault(streamType).getMaxIndex(), streamType);
             }
 
             // Check if volume update should be send to Hearing Aid.
@@ -3922,7 +3995,7 @@
             }
         }
 
-        final int newIndex = mStreamStates[streamType].getIndex(device);
+        final int newIndex = getVssForStreamOrDefault(streamType).getIndex(device);
         if (adjustVolume) {
             synchronized (mHdmiClientLock) {
                 if (mHdmiManager != null) {
@@ -4004,22 +4077,22 @@
         synchronized (mSettingsLock) {
             synchronized (VolumeStreamState.class) {
                 List<Integer> streamsToMute = new ArrayList<>();
-                for (int stream = 0; stream < mStreamStates.length; stream++) {
-                    VolumeStreamState vss = mStreamStates[stream];
-                    if (vss != null && streamAlias == mStreamVolumeAlias[stream]
+                for (int stream = 0; stream < mStreamStates.size(); stream++) {
+                    final VolumeStreamState vss = mStreamStates.valueAt(stream);
+                    if (vss != null && streamAlias == sStreamVolumeAlias.get(vss.getStreamType())
                             && vss.isMutable()) {
                         if (!(mCameraSoundForced && (vss.getStreamType()
                                 == AudioSystem.STREAM_SYSTEM_ENFORCED))) {
                             boolean changed = vss.mute(state, /* apply= */ false,
                                     "muteAliasStreams");
                             if (changed) {
-                                streamsToMute.add(stream);
+                                streamsToMute.add(vss.getStreamType());
                             }
                         }
                     }
                 }
                 streamsToMute.forEach(streamToMute -> {
-                    mStreamStates[streamToMute].doMute();
+                    getVssForStreamOrDefault(streamToMute).doMute();
                     broadcastMuteSetting(streamToMute, state);
                 });
             }
@@ -4047,7 +4120,7 @@
         // vss.updateVolumeGroupIndex
         synchronized (mSettingsLock) {
             synchronized (VolumeStreamState.class) {
-                final VolumeStreamState streamState = mStreamStates[streamAlias];
+                final VolumeStreamState streamState = getVssForStreamOrDefault(streamAlias);
                 // if unmuting causes a change, it was muted
                 wasMuted = streamState.mute(false, "onUnmuteStreamOnSingleVolDevice");
                 if (wasMuted) {
@@ -4145,7 +4218,7 @@
      */
     /*package*/ void onSetStreamVolume(int streamType, int index, int flags, int device,
             String caller, boolean hasModifyAudioSettings, boolean canChangeMute) {
-        final int stream = mStreamVolumeAlias[streamType];
+        final int stream = sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound=*/-1);
         // setting volume on ui sounds stream type also controls silent mode
         if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
                 (stream == getUiSoundsStreamType())) {
@@ -4286,26 +4359,30 @@
         // that can interfere with the sending of the VOLUME_CHANGED_ACTION intent
         mAudioSystem.clearRoutingCache();
 
+        int streamType = replaceBtScoStreamWithVoiceCall(vi.getStreamType(), "setDeviceVolume");
+
+        final VolumeStreamState vss = getVssForStream(streamType);
+
         // log the current device that will be used when evaluating the sending of the
         // VOLUME_CHANGED_ACTION intent to see if the current device is the one being modified
-        final int currDev = getDeviceForStream(vi.getStreamType());
+        final int currDev = getDeviceForStream(streamType);
 
-        final boolean skipping = (currDev == ada.getInternalType());
+        final boolean skipping = (currDev == ada.getInternalType()) || (vss == null);
 
-        AudioService.sVolumeLogger.enqueue(new DeviceVolumeEvent(vi.getStreamType(), index, ada,
+        AudioService.sVolumeLogger.enqueue(new DeviceVolumeEvent(streamType, index, ada,
                 currDev, callingPackage, skipping));
 
         if (skipping) {
-            // setDeviceVolume was called on a device currently being used
+            // setDeviceVolume was called on a device currently being used or stream state is null
             return;
         }
 
         // TODO handle unmuting of current audio device
         // if a stream is not muted but the VolumeInfo is for muting, set the volume index
         // for the device to min volume
-        if (vi.hasMuteCommand() && vi.isMuted() && !isStreamMute(vi.getStreamType())) {
-            setStreamVolumeWithAttributionInt(vi.getStreamType(),
-                    mStreamStates[vi.getStreamType()].getMinIndex(),
+        if (vi.hasMuteCommand() && vi.isMuted() && !isStreamMute(streamType)) {
+            setStreamVolumeWithAttributionInt(streamType,
+                    vss.getMinIndex(),
                     /*flags*/ 0,
                     ada, callingPackage, null,
                     //TODO handle unmuting of current audio device
@@ -4319,22 +4396,22 @@
         if (vi.getMinVolumeIndex() == VolumeInfo.INDEX_NOT_SET
                 || vi.getMaxVolumeIndex() == VolumeInfo.INDEX_NOT_SET) {
             // assume index meant to be in stream type range, validate
-            if ((index * 10) < mStreamStates[vi.getStreamType()].getMinIndex()
-                    || (index * 10) > mStreamStates[vi.getStreamType()].getMaxIndex()) {
+            if ((index * 10) < vss.getMinIndex()
+                    || (index * 10) > vss.getMaxIndex()) {
                 throw new IllegalArgumentException("invalid volume index " + index
                         + " not between min/max for stream " + vi.getStreamType());
             }
         } else {
             // check if index needs to be rescaled
-            final int min = (mStreamStates[vi.getStreamType()].getMinIndex() + 5) / 10;
-            final int max = (mStreamStates[vi.getStreamType()].getMaxIndex() + 5) / 10;
+            final int min = (vss.getMinIndex() + 5) / 10;
+            final int max = (vss.getMaxIndex() + 5) / 10;
             if (vi.getMinVolumeIndex() != min || vi.getMaxVolumeIndex() != max) {
                 index = rescaleIndex(index,
                         /*srcMin*/ vi.getMinVolumeIndex(), /*srcMax*/ vi.getMaxVolumeIndex(),
                         /*dstMin*/ min, /*dstMax*/ max);
             }
         }
-        setStreamVolumeWithAttributionInt(vi.getStreamType(), index, /*flags*/ 0,
+        setStreamVolumeWithAttributionInt(streamType, index, /*flags*/ 0,
                 ada, callingPackage, null,
                 false /*canChangeMuteAndUpdateController*/);
     }
@@ -4762,7 +4839,7 @@
 
         if (AudioSystem.isLeAudioDeviceType(device)) {
             mDeviceBroker.postSetLeAudioVolumeIndex(index * 10,
-                    mStreamStates[streamType].getMaxIndex(), streamType);
+                    getVssForStreamOrDefault(streamType).getMaxIndex(), streamType);
         } else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) {
             mDeviceBroker.postSetHearingAidVolumeIndex(index * 10, streamType);
         } else {
@@ -4790,8 +4867,8 @@
         streamType = replaceBtScoStreamWithVoiceCall(streamType, "setStreamVolume");
 
         ensureValidStreamType(streamType);
-        int streamTypeAlias = mStreamVolumeAlias[streamType];
-        VolumeStreamState streamState = mStreamStates[streamTypeAlias];
+        int streamTypeAlias = sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound*/-1);
+        final VolumeStreamState streamState = getVssForStreamOrDefault(streamTypeAlias);
 
         if (!replaceStreamBtSco() && (streamType == AudioManager.STREAM_VOICE_CALL)
                 && isInCommunication() && mDeviceBroker.isBluetoothScoActive()) {
@@ -4857,7 +4934,7 @@
                         // ada is non-null when called from setDeviceVolume,
                         // which shouldn't update the mute state
                         canChangeMuteAndUpdateController /*canChangeMute*/);
-                index = mStreamStates[streamType].getIndex(device);
+                index = getVssForStreamOrDefault(streamType).getIndex(device);
             }
         }
 
@@ -4885,8 +4962,8 @@
                 Log.d(TAG, "setStreamVolume postSetLeAudioVolumeIndex index="
                         + index + " stream=" + streamType);
             }
-            mDeviceBroker.postSetLeAudioVolumeIndex(index, mStreamStates[streamType].getMaxIndex(),
-                    streamType);
+            mDeviceBroker.postSetLeAudioVolumeIndex(index,
+                    getVssForStreamOrDefault(streamType).getMaxIndex(), streamType);
         }
 
         if (device == AudioSystem.DEVICE_OUT_HEARING_AID
@@ -4916,7 +4993,7 @@
                         // ada is non-null when called from setDeviceVolume,
                         // which shouldn't update the mute state
                         canChangeMuteAndUpdateController /*canChangeMute*/);
-                index = mStreamStates[streamType].getIndex(device);
+                index = getVssForStreamOrDefault(streamType).getIndex(device);
             }
         }
 
@@ -5099,7 +5176,7 @@
     // UI update and Broadcast Intent
     protected void sendVolumeUpdate(int streamType, int oldIndex, int index, int flags, int device)
     {
-        streamType = mStreamVolumeAlias[streamType];
+        streamType = sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound=*/-1);
 
         if (streamType == AudioSystem.STREAM_MUSIC && isFullVolumeDevice(device)) {
             flags &= ~AudioManager.FLAG_SHOW_UI;
@@ -5147,7 +5224,7 @@
         if (isFullVolumeDevice(device)) {
             return;
         }
-        VolumeStreamState streamState = mStreamStates[streamType];
+        final VolumeStreamState streamState = getVssForStreamOrDefault(streamType);
 
         if (streamState.setIndex(index, device, caller, hasModifyAudioSettings) || force) {
             // Post message to set system volume (it in turn will post a message
@@ -5171,7 +5248,7 @@
 
         synchronized (VolumeStreamState.class) {
             ensureValidStreamType(streamType);
-            return mStreamStates[streamType].mIsMuted;
+            return getVssForStreamOrDefault(streamType).mIsMuted;
         }
     }
 
@@ -5270,7 +5347,7 @@
             if (applyRequired) {
                 // Assumes only STREAM_MUSIC going through DEVICE_OUT_REMOTE_SUBMIX
                 checkAllFixedVolumeDevices(AudioSystem.STREAM_MUSIC);
-                mStreamStates[AudioSystem.STREAM_MUSIC].applyAllVolumes();
+                getVssForStreamOrDefault(AudioSystem.STREAM_MUSIC).applyAllVolumes();
             }
         }
     }
@@ -5351,15 +5428,16 @@
 
     private int getStreamVolume(int streamType, int device) {
         synchronized (VolumeStreamState.class) {
-            int index = mStreamStates[streamType].getIndex(device);
+            final VolumeStreamState vss = getVssForStreamOrDefault(streamType);
+            int index = vss.getIndex(device);
 
             // by convention getStreamVolume() returns 0 when a stream is muted.
-            if (mStreamStates[streamType].mIsMuted) {
+            if (vss.mIsMuted) {
                 index = 0;
             }
-            if (index != 0 && (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) &&
-                    isFixedVolumeDevice(device)) {
-                index = mStreamStates[streamType].getMaxIndex();
+            if (index != 0 && (sStreamVolumeAlias.get(streamType) == AudioSystem.STREAM_MUSIC)
+                    && isFixedVolumeDevice(device)) {
+                index = vss.getMaxIndex();
             }
             return (index + 5) / 10;
         }
@@ -5382,20 +5460,27 @@
             return getDefaultVolumeInfo();
         }
 
-        int streamType = vi.getStreamType();
+        int streamType = replaceBtScoStreamWithVoiceCall(vi.getStreamType(), "getStreamMaxVolume");
         final VolumeInfo.Builder vib = new VolumeInfo.Builder(vi);
-        vib.setMinVolumeIndex((mStreamStates[streamType].mIndexMin + 5) / 10);
-        vib.setMaxVolumeIndex((mStreamStates[streamType].mIndexMax + 5) / 10);
+        final VolumeStreamState vss = getVssForStream(streamType);
+        if (vss == null) {
+            Log.w(TAG,
+                    "getDeviceVolume unsupported stream type " + streamType + ". Return default");
+            return getDefaultVolumeInfo();
+        }
+
+        vib.setMinVolumeIndex((vss.mIndexMin + 5) / 10);
+        vib.setMaxVolumeIndex((vss.mIndexMax + 5) / 10);
         synchronized (VolumeStreamState.class) {
             final int index;
             if (isFixedVolumeDevice(ada.getInternalType())) {
-                index = (mStreamStates[streamType].mIndexMax + 5) / 10;
+                index = (vss.mIndexMax + 5) / 10;
             } else {
-                index = (mStreamStates[streamType].getIndex(ada.getInternalType()) + 5) / 10;
+                index = (vss.getIndex(ada.getInternalType()) + 5) / 10;
             }
             vib.setVolumeIndex(index);
             // only set as a mute command if stream muted
-            if (mStreamStates[streamType].mIsMuted) {
+            if (vss.mIsMuted) {
                 vib.setMuted(true);
             }
             return vib.build();
@@ -5406,7 +5491,7 @@
     public int getStreamMaxVolume(int streamType) {
         streamType = replaceBtScoStreamWithVoiceCall(streamType, "getStreamMaxVolume");
         ensureValidStreamType(streamType);
-        return (mStreamStates[streamType].getMaxIndex() + 5) / 10;
+        return (getVssForStreamOrDefault(streamType).getMaxIndex() + 5) / 10;
     }
 
     /** @see AudioManager#getStreamMinVolumeInt(int)
@@ -5419,7 +5504,7 @@
                  || callingHasAudioSettingsPermission()
                  || (mContext.checkCallingPermission(MODIFY_AUDIO_ROUTING)
                         == PackageManager.PERMISSION_GRANTED);
-        return (mStreamStates[streamType].getMinIndex(isPrivileged) + 5) / 10;
+        return (getVssForStreamOrDefault(streamType).getMinIndex(isPrivileged) + 5) / 10;
     }
 
     @android.annotation.EnforcePermission(QUERY_AUDIO_STATE)
@@ -5432,7 +5517,7 @@
         ensureValidStreamType(streamType);
 
         int device = getDeviceForStream(streamType);
-        return (mStreamStates[streamType].getIndex(device) + 5) / 10;
+        return (getVssForStreamOrDefault(streamType).getIndex(device) + 5) / 10;
     }
 
     /**
@@ -5502,9 +5587,10 @@
                     .boxed().toList());
         }
         ArrayList<Integer> res = new ArrayList(1);
-        for (int stream : mStreamVolumeAlias) {
-            if (stream >= 0 && !res.contains(stream)) {
-                res.add(stream);
+        for (int stream = 0; stream < sStreamVolumeAlias.size(); ++stream) {
+            final int streamAlias = sStreamVolumeAlias.valueAt(stream);
+            if (!res.contains(streamAlias)) {
+                res.add(streamAlias);
             }
         }
         return res;
@@ -5525,7 +5611,7 @@
         // verify parameters
         ensureValidStreamType(sourceStreamType);
 
-        return mStreamVolumeAlias[sourceStreamType];
+        return sStreamVolumeAlias.get(sourceStreamType, /*valueIfKeyNotFound=*/-1);
     }
 
     /**
@@ -5547,7 +5633,7 @@
      */
     public int getUiSoundsStreamType() {
         return mUseVolumeGroupAliases ? STREAM_VOLUME_ALIAS_VOICE[AudioSystem.STREAM_SYSTEM]
-                : mStreamVolumeAlias[AudioSystem.STREAM_SYSTEM];
+                : sStreamVolumeAlias.get(AudioSystem.STREAM_SYSTEM);
     }
 
     /**
@@ -5559,7 +5645,7 @@
         return mUseVolumeGroupAliases
                 ? STREAM_VOLUME_ALIAS_VOICE[aliasStreamType]
                         == STREAM_VOLUME_ALIAS_VOICE[AudioSystem.STREAM_SYSTEM]
-                : aliasStreamType == mStreamVolumeAlias[AudioSystem.STREAM_SYSTEM];
+                : aliasStreamType == sStreamVolumeAlias.get(AudioSystem.STREAM_SYSTEM);
     }
 
     /** @see AudioManager#setMicrophoneMute(boolean) */
@@ -5853,6 +5939,10 @@
                 forceUse, eventSource, 0);
 
         for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
+            final VolumeStreamState vss = getVssForStream(streamType);
+            if (vss == null) {
+                continue;
+            }
             final boolean isMuted = isStreamMutedByRingerOrZenMode(streamType);
             final boolean muteAllowedBySco =
                     !((shouldRingSco || shouldRingBle) && streamType == AudioSystem.STREAM_RING);
@@ -5863,10 +5953,9 @@
             if (!shouldMute) {
                 // unmute
                 // ring and notifications volume should never be 0 when not silenced
-                if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING
-                        || mStreamVolumeAlias[streamType] == AudioSystem.STREAM_NOTIFICATION) {
+                if (sStreamVolumeAlias.get(streamType) == AudioSystem.STREAM_RING
+                        || sStreamVolumeAlias.get(streamType) == AudioSystem.STREAM_NOTIFICATION) {
                     synchronized (VolumeStreamState.class) {
-                        final VolumeStreamState vss = mStreamStates[streamType];
                         for (int i = 0; i < vss.mIndexMap.size(); i++) {
                             int device = vss.mIndexMap.keyAt(i);
                             int value = vss.mIndexMap.valueAt(i);
@@ -5881,20 +5970,20 @@
                               SENDMSG_QUEUE,
                               device,
                               0,
-                              mStreamStates[streamType],
+                              vss,
                               PERSIST_DELAY);
                     }
                 }
                 sRingerAndZenModeMutedStreams &= ~(1 << streamType);
                 sMuteLogger.enqueue(new AudioServiceEvents.RingerZenMutedStreamsEvent(
                         sRingerAndZenModeMutedStreams, "muteRingerModeStreams"));
-                mStreamStates[streamType].mute(false, "muteRingerModeStreams");
+                vss.mute(false, "muteRingerModeStreams");
             } else {
                 // mute
                 sRingerAndZenModeMutedStreams |= (1 << streamType);
                 sMuteLogger.enqueue(new AudioServiceEvents.RingerZenMutedStreamsEvent(
                         sRingerAndZenModeMutedStreams, "muteRingerModeStreams"));
-                mStreamStates[streamType].mute(true, "muteRingerModeStreams");
+                vss.mute(true, "muteRingerModeStreams");
             }
         }
     }
@@ -6317,15 +6406,15 @@
 
                 final int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE);
                 final int device = getDeviceForStream(streamType);
-                final int streamAlias = mStreamVolumeAlias[streamType];
+                final int streamAlias = sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound=*/
+                        -1);
 
                 if (DEBUG_MODE) {
                     Log.v(TAG, "onUpdateAudioMode: streamType=" + streamType
                             + ", streamAlias=" + streamAlias);
                 }
 
-                final int index = mStreamStates[streamAlias].getIndex(device);
-                final int maxIndex = mStreamStates[streamAlias].getMaxIndex();
+                final int index = getVssForStreamOrDefault(streamAlias).getIndex(device);
                 setStreamVolumeInt(streamAlias, index, device, true,
                         requesterPackage, true /*hasModifyAudioSettings*/);
 
@@ -6630,13 +6719,13 @@
         // restore volume settings
         int numStreamTypes = AudioSystem.getNumStreamTypes();
         for (int streamType = 0; streamType < numStreamTypes; streamType++) {
-            VolumeStreamState streamState = mStreamStates[streamType];
+            final VolumeStreamState streamState = getVssForStream(streamType);
 
             if (streamState == null) {
                 continue;
             }
 
-            if (userSwitch && mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) {
+            if (userSwitch && sStreamVolumeAlias.get(streamType) == AudioSystem.STREAM_MUSIC) {
                 continue;
             }
 
@@ -7204,7 +7293,7 @@
         } else {
             ringerModeAffectedStreams |= (1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
         }
-        if (mStreamVolumeAlias[AudioSystem.STREAM_DTMF] == AudioSystem.STREAM_RING) {
+        if (sStreamVolumeAlias.get(AudioSystem.STREAM_DTMF) == AudioSystem.STREAM_RING) {
             ringerModeAffectedStreams |= (1 << AudioSystem.STREAM_DTMF);
         } else {
             ringerModeAffectedStreams &= ~(1 << AudioSystem.STREAM_DTMF);
@@ -7260,7 +7349,7 @@
     }
 
     private void ensureValidStreamType(int streamType) {
-        if (streamType < 0 || streamType >= mStreamStates.length) {
+        if (streamType < 0 || streamType >= AudioSystem.getNumStreamTypes()) {
             throw new IllegalArgumentException("Bad stream type " + streamType);
         }
     }
@@ -7525,9 +7614,10 @@
                 ? MIN_STREAM_VOLUME[AudioSystem.STREAM_ALARM]
                 : Math.min(idx + 1, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM]);
         // update the VolumeStreamState for STREAM_ALARM and its aliases
-        for (int stream : mStreamVolumeAlias) {
-            if (stream >= 0 && mStreamVolumeAlias[stream] == AudioSystem.STREAM_ALARM) {
-                mStreamStates[stream].updateNoPermMinIndex(safeIndex);
+        for (int stream = 0; stream < sStreamVolumeAlias.size(); ++stream) {
+            final int streamAlias = sStreamVolumeAlias.valueAt(stream);
+            if (streamAlias == AudioSystem.STREAM_ALARM) {
+                getVssForStreamOrDefault(streamAlias).updateNoPermMinIndex(safeIndex);
             }
         }
     }
@@ -7642,21 +7732,21 @@
         stream = replaceBtScoStreamWithVoiceCall(stream, "getDeviceSetForStream");
         ensureValidStreamType(stream);
         synchronized (VolumeStreamState.class) {
-            return mStreamStates[stream].observeDevicesForStream_syncVSS(true);
+            return getVssForStreamOrDefault(stream).observeDevicesForStream_syncVSS(true);
         }
     }
 
     private void onObserveDevicesForAllStreams(int skipStream) {
         synchronized (mSettingsLock) {
             synchronized (VolumeStreamState.class) {
-                for (int stream = 0; stream < mStreamStates.length; stream++) {
-                    if (stream != skipStream && mStreamStates[stream] != null) {
+                for (int stream = 0; stream < mStreamStates.size(); stream++) {
+                    final VolumeStreamState vss = mStreamStates.valueAt(stream);
+                    if (vss != null && vss.getStreamType() != skipStream) {
                         Set<Integer> deviceSet =
-                                mStreamStates[stream].observeDevicesForStream_syncVSS(
-                                        false /*checkOthers*/);
+                                vss.observeDevicesForStream_syncVSS(false /*checkOthers*/);
                         for (Integer device : deviceSet) {
                             // Update volume states for devices routed for the stream
-                            updateVolumeStates(device, stream,
+                            updateVolumeStates(device, vss.getStreamType(),
                                     "AudioService#onObserveDevicesForAllStreams");
                         }
                     }
@@ -7689,7 +7779,7 @@
 
     private void onUpdateScoDeviceActive(boolean scoDeviceActive) {
         if (mScoDeviceActive.compareAndSet(!scoDeviceActive, scoDeviceActive)) {
-            getVssVolumeForStream(AudioSystem.STREAM_VOICE_CALL).updateIndexFactors();
+            getVssForStreamOrDefault(AudioSystem.STREAM_VOICE_CALL).updateIndexFactors();
         }
     }
 
@@ -8084,7 +8174,7 @@
     /** only public for mocking/spying, do not call outside of AudioService */
     @VisibleForTesting
     public void setMusicMute(boolean mute) {
-        mStreamStates[AudioSystem.STREAM_MUSIC].muteInternally(mute);
+        getVssForStreamOrDefault(AudioSystem.STREAM_MUSIC).muteInternally(mute);
     }
 
     private static final Set<Integer> DEVICE_MEDIA_UNMUTED_ON_PLUG_SET;
@@ -8115,8 +8205,8 @@
         if (mNm.getZenMode() != Settings.Global.ZEN_MODE_NO_INTERRUPTIONS
                 && !isStreamMutedByRingerOrZenMode(AudioSystem.STREAM_MUSIC)
                 && DEVICE_MEDIA_UNMUTED_ON_PLUG_SET.contains(newDevice)
-                && mStreamStates[AudioSystem.STREAM_MUSIC].mIsMuted
-                && mStreamStates[AudioSystem.STREAM_MUSIC].getIndex(newDevice) != 0
+                && getVssForStreamOrDefault(AudioSystem.STREAM_MUSIC).mIsMuted
+                && getVssForStreamOrDefault(AudioSystem.STREAM_MUSIC).getIndex(newDevice) != 0
                 && getDeviceSetForStreamDirect(AudioSystem.STREAM_MUSIC).contains(newDevice)) {
             if (DEBUG_VOL) {
                 Log.i(TAG, String.format("onAccessoryPlugMediaUnmute unmuting device=%d [%s]",
@@ -8125,7 +8215,8 @@
             // Locking mSettingsLock to avoid inversion when calling vss.mute -> vss.doMute ->
             // vss.updateVolumeGroupIndex
             synchronized (mSettingsLock) {
-                mStreamStates[AudioSystem.STREAM_MUSIC].mute(false, "onAccessoryPlugMediaUnmute");
+                getVssForStreamOrDefault(AudioSystem.STREAM_MUSIC).mute(false,
+                        "onAccessoryPlugMediaUnmute");
             }
         }
     }
@@ -8296,8 +8387,8 @@
                 }
 
                 if (replaceStreamBtSco()) {
-                    mIndexMin = mStreamStates[mPublicStreamType].getMinIndex() / 10;
-                    mIndexMax = mStreamStates[mPublicStreamType].getMaxIndex() / 10;
+                    mIndexMin = getVssForStreamOrDefault(mPublicStreamType).getMinIndex() / 10;
+                    mIndexMax = getVssForStreamOrDefault(mPublicStreamType).getMaxIndex() / 10;
                 } else {
                     mIndexMin = MIN_STREAM_VOLUME[mPublicStreamType];
                     mIndexMax = MAX_STREAM_VOLUME[mPublicStreamType];
@@ -8334,7 +8425,7 @@
          */
         private boolean isVssMuteBijective(int stream) {
             return isStreamAffectedByMute(stream)
-                    && (getMinIndex() == (mStreamStates[stream].getMinIndex() + 5) / 10)
+                    && (getMinIndex() == (getVssForStreamOrDefault(stream).getMinIndex() + 5) / 10)
                     && (getMinIndex() == 0 || isCallStream(stream));
         }
 
@@ -8380,7 +8471,8 @@
                         return;
                     }
 
-                    float stepFactor = mStreamStates[mPublicStreamType].getIndexStepFactor();
+                    float stepFactor = getVssForStreamOrDefault(
+                            mPublicStreamType).getIndexStepFactor();
                     switch (direction) {
                         case AudioManager.ADJUST_TOGGLE_MUTE: {
                             // Note: If muted by volume 0, unmute will restore volume 0.
@@ -8476,7 +8568,7 @@
             // This allows RX path muting by the audio HAL only when explicitly muted but not when
             // index is just set to 0 to repect BT requirements
             if (mHasValidStreamType && isVssMuteBijective(mPublicStreamType)
-                    && mStreamStates[mPublicStreamType].isFullyMuted()) {
+                    && getVssForStreamOrDefault(mPublicStreamType).isFullyMuted()) {
                 index = 0;
             } else if (isStreamBluetoothSco(mPublicStreamType) && index == 0) {
                 index = 1;
@@ -8484,7 +8576,7 @@
 
             if (replaceStreamBtSco()) {
                 index = (int) (mIndexMin + (index - mIndexMin)
-                        / mStreamStates[mPublicStreamType].getIndexStepFactor());
+                        / getVssForStreamOrDefault(mPublicStreamType).getIndexStepFactor());
             }
 
             if (DEBUG_VOL) {
@@ -8517,7 +8609,7 @@
         }
 
         private boolean isValidStream(int stream) {
-            return (stream != AudioSystem.STREAM_DEFAULT) && (stream < mStreamStates.length);
+            return (stream != AudioSystem.STREAM_DEFAULT) && getVssForStream(stream) != null;
         }
 
         public boolean isMusic() {
@@ -8535,10 +8627,10 @@
                     if (device != AudioSystem.DEVICE_OUT_DEFAULT) {
                         for (int stream : getLegacyStreamTypes()) {
                             if (isValidStream(stream)) {
-                                boolean streamMuted = mStreamStates[stream].mIsMuted;
+                                final VolumeStreamState vss = getVssForStreamOrDefault(stream);
+                                boolean streamMuted = vss.mIsMuted;
                                 int deviceForStream = getDeviceForStream(stream);
-                                int indexForStream =
-                                        (mStreamStates[stream].getIndex(deviceForStream) + 5) / 10;
+                                int indexForStream = (vss.getIndex(deviceForStream) + 5) / 10;
                                 if (device == deviceForStream) {
                                     if (indexForStream == index && (isMuted() == streamMuted)
                                             && isVssMuteBijective(stream)) {
@@ -8548,19 +8640,17 @@
                                     if (vgsVssSyncMuteOrder()) {
                                         if ((isMuted() != streamMuted) && isVssMuteBijective(
                                                 stream)) {
-                                            mStreamStates[stream].mute(isMuted(),
-                                                    "VGS.applyAllVolumes#1");
+                                            vss.mute(isMuted(), "VGS.applyAllVolumes#1");
                                         }
                                     }
                                     if (indexForStream != index) {
-                                        mStreamStates[stream].setIndex(index * 10, device, caller,
-                                                true /*hasModifyAudioSettings*/);
+                                        vss.setIndex(index * 10, device,
+                                                caller, true /*hasModifyAudioSettings*/);
                                     }
                                     if (!vgsVssSyncMuteOrder()) {
                                         if ((isMuted() != streamMuted) && isVssMuteBijective(
                                                 stream)) {
-                                            mStreamStates[stream].mute(isMuted(),
-                                                    "VGS.applyAllVolumes#1");
+                                            vss.mute(isMuted(), "VGS.applyAllVolumes#1");
                                         }
                                     }
                                 }
@@ -8584,11 +8674,12 @@
                 boolean forceDeviceSync = userSwitch && (mIndexMap.indexOfKey(deviceForVolume) < 0);
                 for (int stream : getLegacyStreamTypes()) {
                     if (isValidStream(stream)) {
-                        boolean streamMuted = mStreamStates[stream].mIsMuted;
-                        int defaultStreamIndex = (mStreamStates[stream].getIndex(
-                                        AudioSystem.DEVICE_OUT_DEFAULT) + 5) / 10;
+                        final VolumeStreamState vss = getVssForStreamOrDefault(stream);
+                        boolean streamMuted = vss.mIsMuted;
+                        int defaultStreamIndex = (vss.getIndex(AudioSystem.DEVICE_OUT_DEFAULT) + 5)
+                                / 10;
                         if (forceDeviceSync) {
-                            mStreamStates[stream].setIndex(index * 10, deviceForVolume, caller,
+                            vss.setIndex(index * 10, deviceForVolume, caller,
                                     true /*hasModifyAudioSettings*/);
                         }
                         if (defaultStreamIndex == index && (isMuted() == streamMuted)
@@ -8597,12 +8688,11 @@
                             continue;
                         }
                         if (defaultStreamIndex != index) {
-                            mStreamStates[stream].setIndex(
-                                    index * 10, AudioSystem.DEVICE_OUT_DEFAULT, caller,
+                            vss.setIndex(index * 10, AudioSystem.DEVICE_OUT_DEFAULT, caller,
                                     true /*hasModifyAudioSettings*/);
                         }
                         if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) {
-                            mStreamStates[stream].mute(isMuted(), "VGS.applyAllVolumes#2");
+                            vss.mute(isMuted(), "VGS.applyAllVolumes#2");
                         }
                     }
                 }
@@ -8950,7 +9040,7 @@
                 postObserveDevicesForAllStreams(mStreamType);
             }
             // log base stream changes to the event log
-            if (mStreamVolumeAlias[mStreamType] == mStreamType) {
+            if (sStreamVolumeAlias.get(mStreamType, /*valueIfKeyNotFound=*/-1) == mStreamType) {
                 EventLogTags.writeStreamDevicesChanged(mStreamType, prevDevices, devices);
             }
             // send STREAM_DEVICES_CHANGED_ACTION on the message handler so it is scheduled after
@@ -9202,10 +9292,11 @@
                     isCurrentDevice = (device == getDeviceForStream(mStreamType));
                     final int numStreamTypes = AudioSystem.getNumStreamTypes();
                     for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
-                        final VolumeStreamState aliasStreamState = mStreamStates[streamType];
-                        if (streamType != mStreamType &&
-                                mStreamVolumeAlias[streamType] == mStreamType &&
-                                (changed || !aliasStreamState.hasIndexForDevice(device))) {
+                        final VolumeStreamState aliasStreamState = getVssForStream(streamType);
+                        if (aliasStreamState != null && streamType != mStreamType
+                                && sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound*/-1)
+                                == mStreamType && (changed || !aliasStreamState.hasIndexForDevice(
+                                device))) {
                             final int scaledIndex =
                                     rescaleIndex(aliasIndex, mStreamType, streamType);
                             boolean changedAlias = aliasStreamState.setIndex(scaledIndex, device,
@@ -9240,7 +9331,7 @@
                 oldIndex = (oldIndex + 5) / 10;
                 index = (index + 5) / 10;
                 // log base stream changes to the event log
-                if (mStreamVolumeAlias[mStreamType] == mStreamType) {
+                if (sStreamVolumeAlias.get(mStreamType, /*valueIfKeyNotFound=*/-1) == mStreamType) {
                     if (caller == null) {
                         Log.w(TAG, "No caller for volume_changed event", new Throwable());
                     }
@@ -9252,7 +9343,9 @@
                 if ((index != oldIndex) && isCurrentDevice) {
                     // for single volume devices, only send the volume change broadcast
                     // on the alias stream
-                    if (!mIsSingleVolume || (mStreamVolumeAlias[mStreamType] == mStreamType)) {
+                    final int streamAlias = sStreamVolumeAlias.get(
+                            mStreamType, /*valueIfKeyNotFound=*/-1);
+                    if (!mIsSingleVolume || streamAlias == mStreamType) {
                         mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
                         mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE,
                                 oldIndex);
@@ -9267,9 +9360,9 @@
                                     mStreamType);
                         }
                         mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS,
-                                mStreamVolumeAlias[mStreamType]);
+                                streamAlias);
 
-                        if (mStreamType == mStreamVolumeAlias[mStreamType]) {
+                        if (mStreamType == streamAlias) {
                             String aliasStreamIndexesString = "";
                             if (!aliasStreamIndexes.isEmpty()) {
                                 aliasStreamIndexesString =
@@ -9527,7 +9620,7 @@
         public void checkFixedVolumeDevices() {
             synchronized (VolumeStreamState.class) {
                 // ignore settings for fixed volume devices: volume should always be at max or 0
-                if (mStreamVolumeAlias[mStreamType] == AudioSystem.STREAM_MUSIC) {
+                if (sStreamVolumeAlias.get(mStreamType) == AudioSystem.STREAM_MUSIC) {
                     for (int i = 0; i < mIndexMap.size(); i++) {
                         int device = mIndexMap.keyAt(i);
                         int index = mIndexMap.valueAt(i);
@@ -9673,7 +9766,11 @@
     }
 
     private void onSetVolumeIndexOnDevice(@NonNull DeviceVolumeUpdate update) {
-        final VolumeStreamState streamState = mStreamStates[update.mStreamType];
+        final VolumeStreamState streamState = getVssForStream(update.mStreamType);
+        if (streamState == null) {
+            Log.w(TAG, "Invalid onSetVolumeIndexOnDevice for stream type " + update.mStreamType);
+            return;
+        }
         if (update.hasVolumeIndex()) {
             int index = update.getVolumeIndex();
             if (mSoundDoseHelper.checkSafeMediaVolume(update.mStreamType, index, update.mDevice)) {
@@ -9704,8 +9801,10 @@
             // Apply change to all streams using this one as alias
             int numStreamTypes = AudioSystem.getNumStreamTypes();
             for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
-                if (streamType != streamState.mStreamType &&
-                        mStreamVolumeAlias[streamType] == streamState.mStreamType) {
+                final VolumeStreamState vss = getVssForStream(streamType);
+                if (vss != null && streamType != streamState.mStreamType
+                        && sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound=*/-1)
+                                == streamState.mStreamType) {
                     // Make sure volume is also maxed out on A2DP device for aliased stream
                     // that may have a different device selected
                     int streamDevice = getDeviceForStream(streamType);
@@ -9713,9 +9812,9 @@
                             && (isAbsoluteVolumeDevice(device)
                                 || isA2dpAbsoluteVolumeDevice(device)
                                 || AudioSystem.isLeAudioDeviceType(device))) {
-                        mStreamStates[streamType].applyDeviceVolume_syncVSS(device);
+                        vss.applyDeviceVolume_syncVSS(device);
                     }
-                    mStreamStates[streamType].applyDeviceVolume_syncVSS(streamDevice);
+                    vss.applyDeviceVolume_syncVSS(streamDevice);
                 }
             }
         }
@@ -9749,9 +9848,11 @@
             // Apply change to all streams using this one as alias
             int numStreamTypes = AudioSystem.getNumStreamTypes();
             for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
-                if (streamType != streamState.mStreamType &&
-                        mStreamVolumeAlias[streamType] == streamState.mStreamType) {
-                    mStreamStates[streamType].applyAllVolumes();
+                final VolumeStreamState vss = getVssForStream(streamType);
+                if (vss != null && streamType != streamState.mStreamType
+                        && sStreamVolumeAlias.get(streamType, /*valueIfKeyNotFound=*/-1)
+                                == streamState.mStreamType) {
+                    vss.applyAllVolumes();
                 }
             }
         }
@@ -10201,7 +10302,7 @@
         }
         sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE,
                     AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0,
-                    mStreamStates[AudioSystem.STREAM_MUSIC], 0);
+                    getVssForStreamOrDefault(AudioSystem.STREAM_MUSIC), 0);
     }
 
     /**
@@ -10321,7 +10422,7 @@
                         SENDMSG_QUEUE,
                         0,
                         0,
-                        mStreamStates[AudioSystem.STREAM_MUSIC], 0);
+                        getVssForStreamOrDefault(AudioSystem.STREAM_MUSIC), 0);
             } else if (action.equals(Intent.ACTION_USER_BACKGROUND)) {
                 // Disable audio recording for the background user/profile
                 int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
@@ -11488,13 +11589,15 @@
                 if (cameraSoundForcedChanged) {
                     if (!mIsSingleVolume) {
                         synchronized (VolumeStreamState.class) {
-                            VolumeStreamState s = mStreamStates[AudioSystem.STREAM_SYSTEM_ENFORCED];
+                            final VolumeStreamState s = getVssForStreamOrDefault(
+                                    AudioSystem.STREAM_SYSTEM_ENFORCED);
                             if (cameraSoundForced) {
                                 s.setAllIndexesToMax();
                                 mRingerModeAffectedStreams &=
                                         ~(1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
                             } else {
-                                s.setAllIndexes(mStreamStates[AudioSystem.STREAM_SYSTEM], TAG);
+                                s.setAllIndexes(getVssForStreamOrDefault(AudioSystem.STREAM_SYSTEM),
+                                        TAG);
                                 mRingerModeAffectedStreams |=
                                         (1 << AudioSystem.STREAM_SYSTEM_ENFORCED);
                             }
@@ -11511,7 +11614,7 @@
                             SENDMSG_QUEUE,
                             0,
                             0,
-                            mStreamStates[AudioSystem.STREAM_SYSTEM_ENFORCED], 0);
+                            getVssForStreamOrDefault(AudioSystem.STREAM_SYSTEM_ENFORCED), 0);
 
                 }
             }
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index ded93e6..dc79ab2 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -633,7 +633,7 @@
     }
 
     /*package*/ void enforceSafeMediaVolume(String caller) {
-        AudioService.VolumeStreamState streamState = mAudioService.getVssVolumeForStream(
+        AudioService.VolumeStreamState streamState = mAudioService.getVssForStreamOrDefault(
                 AudioSystem.STREAM_MUSIC);
 
         for (int i = 0; i < mSafeMediaVolumeDevices.size(); ++i)  {
@@ -665,7 +665,7 @@
     @GuardedBy("mSafeMediaVolumeStateLock")
     private boolean checkSafeMediaVolume_l(int streamType, int index, int device) {
         return (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)
-                    && (AudioService.mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC)
+                    && (AudioService.sStreamVolumeAlias.get(streamType) == AudioSystem.STREAM_MUSIC)
                     && safeDevicesContains(device)
                     && (index > safeMediaVolumeIndex(device));
     }
@@ -908,7 +908,7 @@
                 return;
             }
 
-            if (AudioService.mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC
+            if (AudioService.sStreamVolumeAlias.get(streamType) == AudioSystem.STREAM_MUSIC
                     && safeDevicesContains(device)) {
                 float attenuationDb = -AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC,
                         (newIndex + 5) / 10, device);
diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java
index 222c5a8..dc611fc 100644
--- a/services/core/java/com/android/server/display/DisplayBrightnessState.java
+++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java
@@ -332,6 +332,16 @@
         }
 
         /**
+         * Sets the {@link BrightnessReason} using the int-based reason enum. This is a convenience
+         * function so we don't have to type out the constructor syntax everywhere.
+         *
+         * @param brightnessReason The int-based brightness enum.
+         */
+        public Builder setBrightnessReason(int brightnessReason) {
+            return setBrightnessReason(new BrightnessReason(brightnessReason));
+        }
+
+        /**
          * Gets the {@link com.android.server.display.brightness.strategy.DisplayBrightnessStrategy}
          * name
          */
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessReason.java b/services/core/java/com/android/server/display/brightness/BrightnessReason.java
index 9bf10a7..9a0ee03 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessReason.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessReason.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display.brightness;
 
+import android.annotation.Nullable;
 import android.util.Slog;
 
 import java.util.Objects;
@@ -66,6 +67,16 @@
     // Any number of MODIFIER_*
     private int mModifier;
 
+    // Tag used to identify the source of the brightness (usually a specific activity/window).
+    private CharSequence mTag;
+
+    public BrightnessReason() {
+    }
+
+    public BrightnessReason(int reason) {
+        setReason(reason);
+    }
+
     /**
      * A utility to clone a BrightnessReason from another BrightnessReason event
      *
@@ -74,6 +85,7 @@
     public void set(BrightnessReason other) {
         setReason(other == null ? REASON_UNKNOWN : other.mReason);
         setModifier(other == null ? 0 : other.mModifier);
+        setTag(other == null ? null : other.mTag);
     }
 
     /**
@@ -85,19 +97,20 @@
         setModifier(modifier | this.mModifier);
     }
 
-
     @Override
     public boolean equals(Object obj) {
         if (!(obj instanceof BrightnessReason)) {
             return false;
         }
         BrightnessReason other = (BrightnessReason) obj;
-        return other.mReason == mReason && other.mModifier == mModifier;
+        return other.mReason == mReason
+                && other.mModifier == mModifier
+                && Objects.equals(other.mTag != null ? other.mTag.toString() : null, mTag);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mReason, mModifier);
+        return Objects.hash(mReason, mModifier, mTag);
     }
 
     @Override
@@ -115,6 +128,11 @@
     public String toString(int adjustments) {
         final StringBuilder sb = new StringBuilder();
         sb.append(reasonToString(mReason));
+
+        if (mTag != null) {
+            sb.append("(").append(mTag).append(")");
+        }
+
         sb.append(" [");
         if ((adjustments & ADJUSTMENT_AUTO_TEMP) != 0) {
             sb.append(" temp_adj");
@@ -149,8 +167,23 @@
         return sb.toString();
     }
 
+    public void setTag(@Nullable CharSequence tag) {
+        mTag = tag;
+    }
+
     /**
-     * A utility to set the reason of the BrightnessReason object
+     * Gets the tag to identify who requested the brightness.
+     */
+    @Nullable public CharSequence getTag() {
+        return mTag;
+    }
+
+    public int getReason() {
+        return mReason;
+    }
+
+    /**
+     * Sets the reason of the BrightnessReason object
      *
      * @param reason The value to which the reason is to be updated.
      */
@@ -162,16 +195,12 @@
         }
     }
 
-    public int getReason() {
-        return mReason;
-    }
-
     public int getModifier() {
         return mModifier;
     }
 
     /**
-     * A utility to set the modified of the current BrightnessReason object
+     * Sets the modifier bitflags of the current BrightnessReason object
      *
      * @param modifier The value to which the modifier is to be updated
      */
diff --git a/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java
index 40a495c..3fc15d1 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java
@@ -16,9 +16,10 @@
 
 package com.android.server.display.brightness.strategy;
 
+import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
+
 import com.android.server.display.DisplayBrightnessState;
 import com.android.server.display.brightness.BrightnessReason;
-import com.android.server.display.brightness.BrightnessUtils;
 import com.android.server.display.brightness.StrategyExecutionRequest;
 import com.android.server.display.brightness.StrategySelectionNotifyRequest;
 
@@ -33,9 +34,14 @@
             StrategyExecutionRequest strategyExecutionRequest) {
         // Todo(b/241308599): Introduce a validator class and add validations before setting
         // the brightness
-        return BrightnessUtils.constructDisplayBrightnessState(BrightnessReason.REASON_OVERRIDE,
-                strategyExecutionRequest.getDisplayPowerRequest().screenBrightnessOverride,
-                getName());
+        DisplayPowerRequest dpr = strategyExecutionRequest.getDisplayPowerRequest();
+        BrightnessReason reason = new BrightnessReason(BrightnessReason.REASON_OVERRIDE);
+        reason.setTag(dpr.screenBrightnessOverrideTag);
+        return new DisplayBrightnessState.Builder()
+                .setBrightness(dpr.screenBrightnessOverride)
+                .setBrightnessReason(reason)
+                .setDisplayBrightnessStrategyName(getName())
+                .build();
     }
 
     @Override
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 0940544..ed71765 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -170,6 +170,7 @@
 import com.android.internal.inputmethod.UnbindReason;
 import com.android.internal.os.TransferPipe;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.AccessibilityManagerInternal;
@@ -5700,24 +5701,12 @@
     @GuardedBy("ImfLock.class")
     private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeId,
             @UserIdInt int userId) {
-        final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
-        if (mConcurrentMultiUserModeEnabled || userId == mCurrentUserId) {
-            if (!settings.getMethodMap().containsKey(imeId)
-                    || !settings.getEnabledInputMethodList()
-                    .contains(settings.getMethodMap().get(imeId))) {
-                return false; // IME is not found or not enabled.
-            }
-            setInputMethodLocked(imeId, subtypeId, userId);
-            return true;
-        }
-        if (!settings.getMethodMap().containsKey(imeId)
-                || !settings.getEnabledInputMethodList().contains(
-                settings.getMethodMap().get(imeId))) {
+        final var settings = InputMethodSettingsRepository.get(userId);
+        final var enabledImes = settings.getEnabledInputMethodList();
+        if (!CollectionUtils.any(enabledImes, imi -> imi.getId().equals(imeId))) {
             return false; // IME is not found or not enabled.
         }
-        settings.putSelectedInputMethod(imeId);
-        // For non-current user, only reset subtypeId (instead of setting the given one).
-        settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
+        setInputMethodLocked(imeId, subtypeId, userId);
         return true;
     }
 
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index d9f3622..9acf030 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -292,7 +292,7 @@
                         == PackageManager.PERMISSION_GRANTED;
         final boolean hasModifyAudioRoutingPermission =
                 checkCallerHasModifyAudioRoutingPermission(pid, uid);
-
+        boolean hasMediaContentControlPermission = checkMediaContentControlPermission(uid, pid);
         boolean hasMediaRoutingControlPermission =
                 checkMediaRoutingControlPermission(uid, pid, packageName);
 
@@ -307,6 +307,7 @@
                         userId,
                         hasConfigureWifiDisplayPermission,
                         hasModifyAudioRoutingPermission,
+                        hasMediaContentControlPermission,
                         hasMediaRoutingControlPermission);
             }
         } finally {
@@ -1133,6 +1134,7 @@
             int userId,
             boolean hasConfigureWifiDisplayPermission,
             boolean hasModifyAudioRoutingPermission,
+            boolean hasMediaContentControlPermission,
             boolean hasMediaRoutingControlPermission) {
         final IBinder binder = router.asBinder();
         if (mAllRouterRecords.get(binder) != null) {
@@ -1151,6 +1153,7 @@
                         packageName,
                         hasConfigureWifiDisplayPermission,
                         hasModifyAudioRoutingPermission,
+                        hasMediaContentControlPermission,
                         hasMediaRoutingControlPermission);
         try {
             binder.linkToDeath(routerRecord, 0);
@@ -2067,9 +2070,10 @@
         public final int mPid;
         public final boolean mHasConfigureWifiDisplayPermission;
         public final boolean mHasModifyAudioRoutingPermission;
+        public final boolean mHasMediaContentControlPermission;
+        public final boolean mHasMediaRoutingControl;
         public final AtomicBoolean mHasBluetoothRoutingPermission;
         public final int mRouterId;
-        public final boolean mHasMediaRoutingControl;
         public @ScanningState int mScanningState = SCANNING_STATE_NOT_SCANNING;
 
         public RouteDiscoveryPreference mDiscoveryPreference;
@@ -2083,6 +2087,7 @@
                 String packageName,
                 boolean hasConfigureWifiDisplayPermission,
                 boolean hasModifyAudioRoutingPermission,
+                boolean hasMediaContentControlPermission,
                 boolean hasMediaRoutingControl) {
             mUserRecord = userRecord;
             mPackageName = packageName;
@@ -2093,9 +2098,10 @@
             mPid = pid;
             mHasConfigureWifiDisplayPermission = hasConfigureWifiDisplayPermission;
             mHasModifyAudioRoutingPermission = hasModifyAudioRoutingPermission;
+            mHasMediaContentControlPermission = hasMediaContentControlPermission;
+            mHasMediaRoutingControl = hasMediaRoutingControl;
             mHasBluetoothRoutingPermission =
                     new AtomicBoolean(checkCallerHasBluetoothPermissions(mPid, mUid));
-            mHasMediaRoutingControl = hasMediaRoutingControl;
             mRouterId = mNextRouterOrManagerId.getAndIncrement();
         }
 
diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java
index 77bdc45..72594b3 100644
--- a/services/core/java/com/android/server/power/PowerGroup.java
+++ b/services/core/java/com/android/server/power/PowerGroup.java
@@ -430,8 +430,8 @@
         return mDisplayPowerRequest.policy;
     }
 
-    boolean updateLocked(float screenBrightnessOverride, boolean useProximitySensor,
-            boolean boostScreenBrightness, int dozeScreenState,
+    boolean updateLocked(float screenBrightnessOverride, CharSequence overrideTag,
+            boolean useProximitySensor, boolean boostScreenBrightness, int dozeScreenState,
             @Display.StateReason int dozeScreenStateReason,
             float dozeScreenBrightness, boolean overrideDrawWakeLock,
             PowerSaveState powerSaverState, boolean quiescent,
@@ -441,6 +441,7 @@
         mDisplayPowerRequest.policy = getDesiredScreenPolicyLocked(quiescent, dozeAfterScreenOff,
                 bootCompleted, screenBrightnessBoostInProgress, brightWhenDozing);
         mDisplayPowerRequest.screenBrightnessOverride = screenBrightnessOverride;
+        mDisplayPowerRequest.screenBrightnessOverrideTag = overrideTag;
         mDisplayPowerRequest.useProximitySensor = useProximitySensor;
         mDisplayPowerRequest.boostScreenBrightness = boostScreenBrightness;
 
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 6fe1ccd..10faf14 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -629,6 +629,9 @@
     private float mScreenBrightnessOverrideFromWindowManager =
             PowerManager.BRIGHTNESS_INVALID_FLOAT;
 
+    // Tag identifying the window/activity that requested the brightness override.
+    private CharSequence mScreenBrightnessOverrideFromWmTag = null;
+
     // The window manager has determined the user to be inactive via other means.
     // Set this to false to disable.
     private boolean mUserInactiveOverrideFromWindowManager;
@@ -3623,16 +3626,18 @@
 
                 // Determine appropriate screen brightness.
                 final float screenBrightnessOverride;
+                CharSequence overrideTag = null;
                 if (!mBootCompleted) {
                     // Keep the brightness steady during boot. This requires the
                     // bootloader brightness and the default brightness to be identical.
                     screenBrightnessOverride = mScreenBrightnessDefault;
                 } else if (isValidBrightness(mScreenBrightnessOverrideFromWindowManager)) {
                     screenBrightnessOverride = mScreenBrightnessOverrideFromWindowManager;
+                    overrideTag = mScreenBrightnessOverrideFromWmTag;
                 } else {
                     screenBrightnessOverride = PowerManager.BRIGHTNESS_INVALID_FLOAT;
                 }
-                boolean ready = powerGroup.updateLocked(screenBrightnessOverride,
+                boolean ready = powerGroup.updateLocked(screenBrightnessOverride, overrideTag,
                         shouldUseProximitySensorLocked(), shouldBoostScreenBrightness(),
                         mDozeScreenStateOverrideFromDreamManager,
                         mDozeScreenStateOverrideReasonFromDreamManager,
@@ -4417,11 +4422,13 @@
         }
     }
 
-    private void setScreenBrightnessOverrideFromWindowManagerInternal(float brightness) {
+    private void setScreenBrightnessOverrideFromWindowManagerInternal(
+            float brightness, CharSequence tag) {
         synchronized (mLock) {
             if (!BrightnessSynchronizer.floatEquals(mScreenBrightnessOverrideFromWindowManager,
                     brightness)) {
                 mScreenBrightnessOverrideFromWindowManager = brightness;
+                mScreenBrightnessOverrideFromWmTag = tag;
                 mDirty |= DIRTY_SETTINGS;
                 updatePowerStateLocked();
             }
@@ -4760,6 +4767,8 @@
             pw.println("  mStayOnWhilePluggedInSetting=" + mStayOnWhilePluggedInSetting);
             pw.println("  mScreenBrightnessOverrideFromWindowManager="
                     + mScreenBrightnessOverrideFromWindowManager);
+            pw.println("  mScreenBrightnessOverrideFromWmTag="
+                    + mScreenBrightnessOverrideFromWmTag);
             pw.println("  mUserActivityTimeoutOverrideFromWindowManager="
                     + mUserActivityTimeoutOverrideFromWindowManager);
             pw.println("  mUserInactiveOverrideFromWindowManager="
@@ -7074,12 +7083,14 @@
     @VisibleForTesting
     final class LocalService extends PowerManagerInternal {
         @Override
-        public void setScreenBrightnessOverrideFromWindowManager(float screenBrightness) {
+        public void setScreenBrightnessOverrideFromWindowManager(
+                float screenBrightness, CharSequence tag) {
             if (screenBrightness < PowerManager.BRIGHTNESS_MIN
                     || screenBrightness > PowerManager.BRIGHTNESS_MAX) {
                 screenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+                tag = null;
             }
-            setScreenBrightnessOverrideFromWindowManagerInternal(screenBrightness);
+            setScreenBrightnessOverrideFromWindowManagerInternal(screenBrightness, tag);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/power/stats/GnssPowerCalculator.java b/services/core/java/com/android/server/power/stats/GnssPowerCalculator.java
index ab22e3e..1003a81 100644
--- a/services/core/java/com/android/server/power/stats/GnssPowerCalculator.java
+++ b/services/core/java/com/android/server/power/stats/GnssPowerCalculator.java
@@ -126,7 +126,7 @@
         long totalTime = 0;
         double totalPower = 0;
         for (int i = 0; i < GnssSignalQuality.NUM_GNSS_SIGNAL_QUALITY_LEVELS; i++) {
-            long timePerLevel = stats.getGpsSignalQualityTime(i, rawRealtimeUs, statsType);
+            long timePerLevel = stats.getGpsSignalQualityTime(i, rawRealtimeUs, statsType) / 1000;
             totalTime += timePerLevel;
             totalPower += mAveragePowerPerSignalQuality[i] * timePerLevel;
         }
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
index 0f13492..39954b8 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
@@ -307,7 +307,10 @@
                 }
             }
             if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) {
-                builder.addConsumedPowerForCustomComponent(powerComponentId, powerAllProcStates);
+                if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent(powerComponentId)) {
+                    builder.addConsumedPowerForCustomComponent(powerComponentId,
+                            powerAllProcStates);
+                }
             } else {
                 builder.addConsumedPower(powerComponentId, powerAllProcStates,
                         BatteryConsumer.POWER_MODEL_UNDEFINED);
@@ -319,7 +322,9 @@
                 batteryUsageStatsBuilder.getAggregateBatteryConsumerBuilder(
                         BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS);
         if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) {
-            allAppsScope.addConsumedPowerForCustomComponent(powerComponentId, powerAllApps);
+            if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent(powerComponentId)) {
+                allAppsScope.addConsumedPowerForCustomComponent(powerComponentId, powerAllApps);
+            }
         } else {
             BatteryConsumer.Key key = allAppsScope.getKey(powerComponentId,
                     BatteryConsumer.PROCESS_STATE_ANY, screenState, powerState);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 516fc65..f31280e 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4078,8 +4078,8 @@
 
             try {
                 if (DEBUG_SWITCH) Slog.i(TAG_SWITCH, "Destroying: " + this);
-                mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
-                        DestroyActivityItem.obtain(token, finishing));
+                final DestroyActivityItem item = new DestroyActivityItem(token, finishing);
+                mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item);
             } catch (Exception e) {
                 // We can just ignore exceptions here...  if the process has crashed, our death
                 // notification will clean things up.
@@ -9997,7 +9997,7 @@
                     preserveWindow, getActivityWindowInfo());
             final ActivityLifecycleItem lifecycleItem;
             if (andResume) {
-                lifecycleItem = ResumeActivityItem.obtain(token, isTransitionForward(),
+                lifecycleItem = new ResumeActivityItem(token, isTransitionForward(),
                         shouldSendCompatFakeFocus());
             } else {
                 lifecycleItem = PauseActivityItem.obtain(token);
diff --git a/services/core/java/com/android/server/wm/ActivityRefresher.java b/services/core/java/com/android/server/wm/ActivityRefresher.java
index bc82271..c02501f 100644
--- a/services/core/java/com/android/server/wm/ActivityRefresher.java
+++ b/services/core/java/com/android/server/wm/ActivityRefresher.java
@@ -86,7 +86,7 @@
                         + "activityRecord=%s", activity);
         final RefreshCallbackItem refreshCallbackItem = RefreshCallbackItem.obtain(
                 activity.token, cycleThroughStop ? ON_STOP : ON_PAUSE);
-        final ResumeActivityItem resumeActivityItem = ResumeActivityItem.obtain(
+        final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(
                 activity.token, /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
         try {
             activity.mAtmService.getLifecycleManager().scheduleTransactionAndLifecycleItems(
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index e81b440..021caaf 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -942,7 +942,7 @@
                 // Set desired final state.
                 final ActivityLifecycleItem lifecycleItem;
                 if (andResume) {
-                    lifecycleItem = ResumeActivityItem.obtain(r.token, isTransitionForward,
+                    lifecycleItem = new ResumeActivityItem(r.token, isTransitionForward,
                             r.shouldSendCompatFakeFocus());
                 } else if (r.isVisibleRequested()) {
                     lifecycleItem = PauseActivityItem.obtain(r.token);
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index f2ccbc4..bded98c 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -183,6 +183,7 @@
 
     private Object mLastWindowFreezeSource = null;
     private float mScreenBrightnessOverride = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+    private CharSequence mScreenBrightnessOverrideTag;
     private long mUserActivityTimeout = -1;
     private boolean mUpdateRotation = false;
     // Only set while traversing the default display based on its content.
@@ -770,6 +771,7 @@
         }
 
         mScreenBrightnessOverride = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+        mScreenBrightnessOverrideTag = null;
         mUserActivityTimeout = -1;
         mObscureApplicationContentOnSecondaryDisplays = false;
         mSustainedPerformanceModeCurrent = false;
@@ -881,11 +883,15 @@
             final float brightnessOverride = mScreenBrightnessOverride < PowerManager.BRIGHTNESS_MIN
                     || mScreenBrightnessOverride > PowerManager.BRIGHTNESS_MAX
                     ? PowerManager.BRIGHTNESS_INVALID_FLOAT : mScreenBrightnessOverride;
+            CharSequence overrideTag = null;
+            if (brightnessOverride != PowerManager.BRIGHTNESS_INVALID_FLOAT) {
+                overrideTag = mScreenBrightnessOverrideTag;
+            }
             int brightnessFloatAsIntBits = Float.floatToIntBits(brightnessOverride);
             // Post these on a handler such that we don't call into power manager service while
             // holding the window manager lock to avoid lock contention with power manager lock.
             mHandler.obtainMessage(SET_SCREEN_BRIGHTNESS_OVERRIDE, brightnessFloatAsIntBits,
-                    0).sendToTarget();
+                    0, overrideTag).sendToTarget();
             mHandler.obtainMessage(SET_USER_ACTIVITY_TIMEOUT, mUserActivityTimeout).sendToTarget();
         }
 
@@ -1040,6 +1046,7 @@
             if (!syswin && w.mAttrs.screenBrightness >= 0
                     && Float.isNaN(mScreenBrightnessOverride)) {
                 mScreenBrightnessOverride = w.mAttrs.screenBrightness;
+                mScreenBrightnessOverrideTag = w.getWindowTag();
             }
 
             // This function assumes that the contents of the default display are processed first
@@ -1112,7 +1119,7 @@
             switch (msg.what) {
                 case SET_SCREEN_BRIGHTNESS_OVERRIDE:
                     mWmService.mPowerManagerInternal.setScreenBrightnessOverrideFromWindowManager(
-                            Float.intBitsToFloat(msg.arg1));
+                            Float.intBitsToFloat(msg.arg1), (CharSequence) msg.obj);
                     break;
                 case SET_USER_ACTIVITY_TIMEOUT:
                     mWmService.mPowerManagerInternal.
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 29e82f7..0b8b51b 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1635,7 +1635,7 @@
                 final int topProcessState = mAtmService.mTopProcessState;
                 next.app.setPendingUiCleanAndForceProcessStateUpTo(topProcessState);
                 next.abortAndClearOptionsAnimation();
-                final ResumeActivityItem resumeActivityItem = ResumeActivityItem.obtain(
+                final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(
                         next.token, topProcessState, dc.isNextTransitionForward(),
                         next.shouldSendCompatFakeFocus());
                 mAtmService.getLifecycleManager().scheduleTransactionItem(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
index 990c383..04b79b4 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java
@@ -18,7 +18,6 @@
 
 import static org.junit.Assert.assertEquals;
 
-
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -43,17 +42,34 @@
         assertEquals(mBrightnessReason.getReason(), BrightnessReason.REASON_UNKNOWN);
         assertEquals(mBrightnessReason.getModifier(), 0);
 
+        CharSequence tag = "my tag";
         mBrightnessReason.set(
                 getReason(BrightnessReason.REASON_BOOST, BrightnessReason.MODIFIER_THROTTLED));
+        mBrightnessReason.setTag(tag);
+
         assertEquals(mBrightnessReason.getReason(), BrightnessReason.REASON_BOOST);
         assertEquals(mBrightnessReason.getModifier(), BrightnessReason.MODIFIER_THROTTLED);
+        assertEquals(mBrightnessReason.getTag().toString(), tag);
     }
 
     @Test
-    public void toStringGeneratesExpectedString() {
-        String actualString = mBrightnessReason.toString();
-        String expectedString = "doze [ low_pwr ]";
-        assertEquals(actualString, expectedString);
+    public void toStringGeneratedExpectedString() {
+        assertEquals("doze [ low_pwr ]", mBrightnessReason.toString());
+    }
+
+    @Test
+    public void overrideTagString() {
+        // Should not print out the tag for "doze"
+        mBrightnessReason.setTag("my/tag");
+        assertEquals("doze(my/tag) [ low_pwr ]", mBrightnessReason.toString());
+
+        // Should print out tag for "override"
+        mBrightnessReason.setReason(BrightnessReason.REASON_OVERRIDE);
+        assertEquals("override(my/tag) [ low_pwr ]", mBrightnessReason.toString());
+
+        // Should not print anything if no tag.
+        mBrightnessReason.setTag(null);
+        assertEquals("override [ low_pwr ]", mBrightnessReason.toString());
     }
 
     @Test
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java
index 94b8d68..39def75 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java
@@ -256,7 +256,9 @@
                 .setBrightnessFactor(brightnessFactor)
                 .build();
 
+        CharSequence tag = "my/tag";
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
+                /* overrideTag= */ tag,
                 /* useProximitySensor= */ false,
                 /* boostScreenBrightness= */ false,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -274,6 +276,7 @@
                 mPowerGroup.mDisplayPowerRequest;
         assertThat(displayPowerRequest.policy).isEqualTo(POLICY_DIM);
         assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS);
+        assertThat(displayPowerRequest.screenBrightnessOverrideTag.toString()).isEqualTo(tag);
         assertThat(displayPowerRequest.useProximitySensor).isEqualTo(false);
         assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(false);
         assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN);
@@ -297,6 +300,7 @@
         mPowerGroup.setWakeLockSummaryLocked(WAKE_LOCK_DOZE);
 
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
+                /* overrideTag= */ null,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -336,6 +340,7 @@
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING);
 
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
+                /* overrideTag= */ null,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -374,6 +379,7 @@
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
 
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
+                /* overrideTag= */ null,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -412,6 +418,7 @@
         mPowerGroup.sleepLocked(TIMESTAMP1, UID, GO_TO_SLEEP_REASON_TIMEOUT);
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
+                /* overrideTag= */ null,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -451,6 +458,7 @@
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
         mPowerGroup.setWakeLockSummaryLocked(WAKE_LOCK_SCREEN_BRIGHT);
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
+                /* overrideTag= */ null,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -488,6 +496,7 @@
                 .build();
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
+                /* overrideTag= */ null,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -526,6 +535,7 @@
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         mPowerGroup.setUserActivitySummaryLocked(USER_ACTIVITY_SCREEN_BRIGHT);
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
+                /* overrideTag= */ null,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -563,6 +573,7 @@
                 .build();
         assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
+                /* overrideTag= */ null,
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
index 7aa208b..5de323b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
@@ -207,4 +207,28 @@
         assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
                 .isEqualTo("Mon,Wed,Fri-Sat,10:00 AM-4:00 PM");
     }
+
+    @Test
+    public void getShortDaysSummary_onlyDays() {
+        ScheduleInfo scheduleInfo = new ScheduleInfo();
+        scheduleInfo.startHour = 10;
+        scheduleInfo.endHour = 16;
+        scheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
+                Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
+
+        assertThat(SystemZenRules.getShortDaysSummary(mContext, scheduleInfo))
+                .isEqualTo("Mon-Fri");
+    }
+
+    @Test
+    public void getTimeSummary_onlyTime() {
+        ScheduleInfo scheduleInfo = new ScheduleInfo();
+        scheduleInfo.startHour = 11;
+        scheduleInfo.endHour = 15;
+        scheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
+                Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
+
+        assertThat(SystemZenRules.getTimeSummary(mContext, scheduleInfo))
+                .isEqualTo("11:00 AM-3:00 PM");
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java
index 6ad1044..68ccb31 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java
@@ -193,7 +193,7 @@
 
         final RefreshCallbackItem refreshCallbackItem = RefreshCallbackItem.obtain(mActivity.token,
                 cycleThroughStop ? ON_STOP : ON_PAUSE);
-        final ResumeActivityItem resumeActivityItem = ResumeActivityItem.obtain(mActivity.token,
+        final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(mActivity.token,
                 /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
 
         verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index eaa1641..c43f414 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -309,7 +309,7 @@
 
         final RefreshCallbackItem refreshCallbackItem = RefreshCallbackItem.obtain(mActivity.token,
                 cycleThroughStop ? ON_STOP : ON_PAUSE);
-        final ResumeActivityItem resumeActivityItem = ResumeActivityItem.obtain(mActivity.token,
+        final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(mActivity.token,
                 /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
 
         verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
index e9fcc40..12bc3ed 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -615,7 +615,7 @@
 
         final RefreshCallbackItem refreshCallbackItem = RefreshCallbackItem.obtain(mActivity.token,
                 cycleThroughStop ? ON_STOP : ON_PAUSE);
-        final ResumeActivityItem resumeActivityItem = ResumeActivityItem.obtain(mActivity.token,
+        final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(mActivity.token,
                 /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
 
         verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
diff --git a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
index 3722fef..c0e90f9 100644
--- a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
@@ -35,6 +35,7 @@
 
 import android.Manifest;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
@@ -77,6 +78,7 @@
 import java.io.File;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -413,6 +415,311 @@
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(2);
     }
 
+    @Test
+    @DisableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
+    public void testCrashLoopWithRescuePartyAndRollbackObserver() throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
+        RollbackPackageHealthObserver rollbackObserver =
+                setUpRollbackPackageHealthObserver(watchdog);
+        VersionedPackage versionedPackageA = new VersionedPackage(APP_A, VERSION_CODE);
+
+        when(mMockPackageManager.getApplicationInfo(anyString(), anyInt())).then(inv -> {
+            ApplicationInfo info = new ApplicationInfo();
+            info.flags |= ApplicationInfo.FLAG_PERSISTENT
+                    | ApplicationInfo.FLAG_SYSTEM;
+            return info;
+        });
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: SCOPED_DEVICE_CONFIG_RESET
+        verify(rescuePartyObserver).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        verify(rescuePartyObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+        verify(rollbackObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: ALL_DEVICE_CONFIG_RESET
+        verify(rescuePartyObserver).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+        verify(rescuePartyObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 3);
+        verify(rollbackObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: WARM_REBOOT
+        verify(rescuePartyObserver).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 3);
+        verify(rescuePartyObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
+        verify(rollbackObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: Low impact rollback
+        verify(rollbackObserver).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        verify(rescuePartyObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
+
+        // update available rollbacks to mock rollbacks being applied after the call to
+        // rollbackObserver.execute
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL));
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD reached. No more mitigations applied
+        verify(rescuePartyObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
+        verify(rollbackObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
+    public void testCrashLoopWithRescuePartyAndRollbackObserverEnableDeprecateFlagReset()
+            throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
+        RollbackPackageHealthObserver rollbackObserver =
+                setUpRollbackPackageHealthObserver(watchdog);
+        VersionedPackage versionedPackageA = new VersionedPackage(APP_A, VERSION_CODE);
+
+        when(mMockPackageManager.getApplicationInfo(anyString(), anyInt())).then(inv -> {
+            ApplicationInfo info = new ApplicationInfo();
+            info.flags |= ApplicationInfo.FLAG_PERSISTENT
+                    | ApplicationInfo.FLAG_SYSTEM;
+            return info;
+        });
+
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: WARM_REBOOT
+        verify(rescuePartyObserver).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        verify(rescuePartyObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+        verify(rollbackObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: Low impact rollback
+        verify(rollbackObserver).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        verify(rescuePartyObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+
+        // update available rollbacks to mock rollbacks being applied after the call to
+        // rollbackObserver.execute
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL));
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD reached. No more mitigations applied
+        verify(rescuePartyObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+        verify(rollbackObserver, never()).execute(versionedPackageA,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
+    public void testCrashLoopSystemUIWithRescuePartyAndRollbackObserver() throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
+        RollbackPackageHealthObserver rollbackObserver =
+                setUpRollbackPackageHealthObserver(watchdog);
+        String systemUi = "com.android.systemui";
+        VersionedPackage versionedPackageUi = new VersionedPackage(
+                systemUi, VERSION_CODE);
+        RollbackInfo rollbackInfoUi = getRollbackInfo(systemUi, VERSION_CODE, 1,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_LOW,
+                ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL, rollbackInfoUi));
+
+        when(mMockPackageManager.getApplicationInfo(anyString(), anyInt())).then(inv -> {
+            ApplicationInfo info = new ApplicationInfo();
+            info.flags |= ApplicationInfo.FLAG_PERSISTENT
+                    | ApplicationInfo.FLAG_SYSTEM;
+            return info;
+        });
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: SCOPED_DEVICE_CONFIG_RESET
+        verify(rescuePartyObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: ALL_DEVICE_CONFIG_RESET
+        verify(rescuePartyObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 3);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: WARM_REBOOT
+        verify(rescuePartyObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 3);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: Low impact rollback
+        verify(rollbackObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+
+        // update available rollbacks to mock rollbacks being applied after the call to
+        // rollbackObserver.execute
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL));
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: RESET_SETTINGS_UNTRUSTED_DEFAULTS
+        verify(rescuePartyObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 4);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 5);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: RESET_SETTINGS_UNTRUSTED_CHANGES
+        verify(rescuePartyObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 5);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 6);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: RESET_SETTINGS_TRUSTED_DEFAULTS
+        verify(rescuePartyObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 6);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 7);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: Factory reset. High impact rollbacks are performed only for boot loops.
+        verify(rescuePartyObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 7);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 8);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS)
+    public void testCrashLoopSystemUIWithRescuePartyAndRollbackObserverEnableDeprecateFlagReset()
+            throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
+        RollbackPackageHealthObserver rollbackObserver =
+                setUpRollbackPackageHealthObserver(watchdog);
+        String systemUi = "com.android.systemui";
+        VersionedPackage versionedPackageUi = new VersionedPackage(
+                systemUi, VERSION_CODE);
+        RollbackInfo rollbackInfoUi = getRollbackInfo(systemUi, VERSION_CODE, 1,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_LOW,
+                ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL, rollbackInfoUi));
+
+        when(mMockPackageManager.getApplicationInfo(anyString(), anyInt())).then(inv -> {
+            ApplicationInfo info = new ApplicationInfo();
+            info.flags |= ApplicationInfo.FLAG_PERSISTENT
+                    | ApplicationInfo.FLAG_SYSTEM;
+            return info;
+        });
+
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: WARM_REBOOT
+        verify(rescuePartyObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: Low impact rollback
+        verify(rollbackObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 1);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+
+        // update available rollbacks to mock rollbacks being applied after the call to
+        // rollbackObserver.execute
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(
+                List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL));
+
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH);
+
+        // Mitigation: Factory reset. High impact rollbacks are performed only for boot loops.
+        verify(rescuePartyObserver).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+        verify(rescuePartyObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 3);
+        verify(rollbackObserver, never()).execute(versionedPackageUi,
+                PackageWatchdog.FAILURE_REASON_APP_CRASH, 2);
+    }
+
     RollbackPackageHealthObserver setUpRollbackPackageHealthObserver(PackageWatchdog watchdog) {
         RollbackPackageHealthObserver rollbackObserver =
                 spy(new RollbackPackageHealthObserver(mSpyContext, mApexManager));
@@ -424,7 +731,6 @@
         watchdog.registerHealthObserver(rollbackObserver);
         return rollbackObserver;
     }
-
     RescuePartyObserver setUpRescuePartyObserver(PackageWatchdog watchdog) {
         setCrashRecoveryPropRescueBootCount(0);
         RescuePartyObserver rescuePartyObserver = spy(RescuePartyObserver.getInstance(mSpyContext));
@@ -686,4 +992,20 @@
         mTestLooper.moveTimeForward(milliSeconds);
         mTestLooper.dispatchAll();
     }
+
+    private void raiseFatalFailureAndDispatch(PackageWatchdog watchdog,
+            List<VersionedPackage> packages, int failureReason) {
+        long triggerFailureCount = watchdog.getTriggerFailureCount();
+        if (failureReason == PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK
+                || failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
+            triggerFailureCount = 1;
+        }
+        for (int i = 0; i < triggerFailureCount; i++) {
+            watchdog.onPackageFailure(packages, failureReason);
+        }
+        mTestLooper.dispatchAll();
+        if (Flags.recoverabilityDetection()) {
+            moveTimeForwardAndDispatch(watchdog.DEFAULT_MITIGATION_WINDOW_MS);
+        }
+    }
 }
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 4c81939..3f9016b 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -55,7 +55,10 @@
             cflags: ["-D_DARWIN_UNLIMITED_STREAMS"],
         },
     },
-    header_libs: ["jni_headers"],
+    header_libs: [
+        "jni_headers",
+        "native_headers",
+    ],
     static_libs: [
         "libandroidfw",
         "libutils",