Merge "Add a new aconfig file for ADPF flags" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 1c6e40e..963307b 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -2085,8 +2085,12 @@
         if (DEBUG) {
             Slog.v(TAG, debugPrefix + " ready=" + jobReady);
         }
-        if (!jobReady) {
-            return job.getPendingJobReasons();
+        final JobRestriction restriction = checkIfRestricted(job);
+        if (DEBUG) {
+            Slog.v(TAG, debugPrefix + " restriction=" + restriction);
+        }
+        if (!jobReady || restriction != null) {
+            return job.getPendingJobReasons(restriction);
         }
 
         final boolean userStarted = areUsersStartedLocked(job);
@@ -2106,18 +2110,6 @@
             return new int[] { JobScheduler.PENDING_JOB_REASON_APP };
         }
 
-        final JobRestriction restriction = checkIfRestricted(job);
-        if (DEBUG) {
-            Slog.v(TAG, debugPrefix + " restriction=" + restriction);
-        }
-        if (restriction != null) {
-            // Currently this will return _DEVICE_STATE because of thermal reasons.
-            // TODO (b/372031023): does it make sense to move this along with the
-            //  pendingJobReasons() call above and also get the pending reasons from
-            //  all of the restriction controllers?
-            return new int[] { restriction.getPendingReason() };
-        }
-
         // The following can be a little more expensive, so we are doing it later,
         // but still before checking with the package manager!
         final boolean jobPending = mPendingJobQueue.contains(job);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index b0784f1..a3eaefd 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -66,6 +66,7 @@
 import com.android.server.job.JobServerProtoEnums;
 import com.android.server.job.JobStatusDumpProto;
 import com.android.server.job.JobStatusShortInfoProto;
+import com.android.server.job.restrictions.JobRestriction;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -2179,11 +2180,20 @@
      * This will return all potential reasons why the job is pending.
      */
     @NonNull
-    public int[] getPendingJobReasons() {
+    public int[] getPendingJobReasons(@Nullable JobRestriction restriction) {
         final int unsatisfiedConstraints = ~satisfiedConstraints
                 & (requiredConstraints | mDynamicConstraints | IMPLICIT_CONSTRAINTS);
         final ArrayList<Integer> reasons = constraintsToPendingJobReasons(unsatisfiedConstraints);
 
+        if (restriction != null) {
+            // Currently only ThermalStatusRestriction extends the JobRestriction class and
+            // returns PENDING_JOB_REASON_DEVICE_STATE if the job is restricted because of thermal.
+            @JobScheduler.PendingJobReason final int reason = restriction.getPendingReason();
+            if (!reasons.contains(reason)) {
+                reasons.addLast(reason);
+            }
+        }
+
         if (reasons.isEmpty()) {
             if (getEffectiveStandbyBucket() == NEVER_INDEX) {
                 Slog.wtf(TAG, "App in NEVER bucket querying pending job reason");
diff --git a/core/api/current.txt b/core/api/current.txt
index 9bb71ef..96968f5 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -21098,6 +21098,7 @@
     method public abstract android.inputmethodservice.AbstractInputMethodService.AbstractInputMethodImpl onCreateInputMethodInterface();
     method public abstract android.inputmethodservice.AbstractInputMethodService.AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface();
     method public boolean onGenericMotionEvent(android.view.MotionEvent);
+    method @FlaggedApi("android.view.inputmethod.verify_key_event") public boolean onShouldVerifyKeyEvent(@NonNull android.view.KeyEvent);
     method public boolean onTrackballEvent(android.view.MotionEvent);
   }
 
@@ -21115,6 +21116,7 @@
     method public void dispatchTrackballEvent(int, android.view.MotionEvent, android.view.inputmethod.InputMethodSession.EventCallback);
     method public boolean isEnabled();
     method public boolean isRevoked();
+    method @FlaggedApi("android.view.inputmethod.verify_key_event") public boolean onShouldVerifyKeyEvent(@NonNull android.view.KeyEvent);
     method public void revokeSelf();
     method public void setEnabled(boolean);
   }
diff --git a/core/java/Android.bp b/core/java/Android.bp
index bc38294..bdd7a37 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -236,6 +236,7 @@
         "android/os/GpuHeadroomParamsInternal.aidl",
         "android/os/IHintManager.aidl",
         "android/os/IHintSession.aidl",
+        "android/os/SessionCreationConfig.aidl",
     ],
     unstable: true,
     backend: {
diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java
index 4bc5bd2..26308f6 100644
--- a/core/java/android/inputmethodservice/AbstractInputMethodService.java
+++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java
@@ -16,6 +16,9 @@
 
 package android.inputmethodservice;
 
+import static android.view.inputmethod.Flags.FLAG_VERIFY_KEY_EVENT;
+
+import android.annotation.FlaggedApi;
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -193,6 +196,12 @@
             }
         }
 
+        @FlaggedApi(FLAG_VERIFY_KEY_EVENT)
+        @Override
+        public boolean onShouldVerifyKeyEvent(@NonNull KeyEvent event) {
+            return AbstractInputMethodService.this.onShouldVerifyKeyEvent(event);
+        }
+
         /**
          * Take care of dispatching incoming trackball events to the appropriate
          * callbacks on the service, and tell the client when this is done.
@@ -308,6 +317,14 @@
         return false;
     }
 
+    /**
+     * @see InputMethodService#onShouldVerifyKeyEvent(KeyEvent)
+     */
+    @FlaggedApi(FLAG_VERIFY_KEY_EVENT)
+    public boolean onShouldVerifyKeyEvent(@NonNull KeyEvent event) {
+        return false;
+    }
+
     /** @hide */
     @Override
     public final int getWindowType() {
diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java
index 62b131a..9b37533 100644
--- a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java
@@ -16,12 +16,16 @@
 
 package android.inputmethodservice;
 
+import static android.view.inputmethod.Flags.verifyKeyEvent;
+
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.graphics.Rect;
+import android.hardware.input.InputManager;
 import android.os.Bundle;
 import android.os.Looper;
 import android.os.Message;
+import android.os.SystemClock;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.InputChannel;
@@ -41,6 +45,8 @@
 import com.android.internal.os.HandlerCaller;
 import com.android.internal.os.SomeArgs;
 
+import java.util.Objects;
+
 class IInputMethodSessionWrapper extends IInputMethodSession.Stub
         implements HandlerCaller.Callback {
     private static final String TAG = "InputMethodWrapper";
@@ -56,6 +62,7 @@
     private static final int DO_REMOVE_IME_SURFACE = 130;
     private static final int DO_FINISH_INPUT = 140;
     private static final int DO_INVALIDATE_INPUT = 150;
+    private final Context mContext;
 
 
     @UnsupportedAppUsage
@@ -66,6 +73,7 @@
 
     public IInputMethodSessionWrapper(Context context,
             InputMethodSession inputMethodSession, InputChannel channel) {
+        mContext = context;
         mCaller = new HandlerCaller(context, null,
                 this, true /*asyncHandler*/);
         mInputMethodSession = inputMethodSession;
@@ -233,6 +241,8 @@
     }
     private final class ImeInputEventReceiver extends InputEventReceiver
             implements InputMethodSession.EventCallback {
+        // Time after which a KeyEvent is invalid
+        private static final long KEY_EVENT_ALLOW_PERIOD_MS = 100L;
         private final SparseArray<InputEvent> mPendingEvents = new SparseArray<InputEvent>();
 
         public ImeInputEventReceiver(InputChannel inputChannel, Looper looper) {
@@ -247,10 +257,23 @@
                 return;
             }
 
+            if (event instanceof KeyEvent keyEvent && needsVerification(keyEvent)) {
+                // any KeyEvent with modifiers (e.g. Ctrl/Alt/Fn) must be verified that
+                // they originated from system.
+                InputManager im = mContext.getSystemService(InputManager.class);
+                Objects.requireNonNull(im);
+                final long age = SystemClock.uptimeMillis() - keyEvent.getEventTime();
+                if (age >= KEY_EVENT_ALLOW_PERIOD_MS && im.verifyInputEvent(keyEvent) == null) {
+                    Log.w(TAG, "Unverified or Invalid KeyEvent injected into IME. Dropping "
+                            + keyEvent);
+                    finishInputEvent(event, false /* handled */);
+                    return;
+                }
+            }
+
             final int seq = event.getSequenceNumber();
             mPendingEvents.put(seq, event);
-            if (event instanceof KeyEvent) {
-                KeyEvent keyEvent = (KeyEvent)event;
+            if (event instanceof KeyEvent keyEvent) {
                 mInputMethodSession.dispatchKeyEvent(seq, keyEvent, this);
             } else {
                 MotionEvent motionEvent = (MotionEvent)event;
@@ -271,5 +294,21 @@
                 finishInputEvent(event, handled);
             }
         }
+
+        private boolean hasKeyModifiers(KeyEvent event) {
+            if (event.hasNoModifiers()) {
+                return false;
+            }
+            return event.hasModifiers(KeyEvent.META_CTRL_ON)
+                    || event.hasModifiers(KeyEvent.META_ALT_ON)
+                    || event.hasModifiers(KeyEvent.KEYCODE_FUNCTION);
+        }
+
+        private boolean needsVerification(KeyEvent event) {
+            //TODO(b/331730488): Handle a11y events as well.
+            return verifyKeyEvent()
+                    && (hasKeyModifiers(event)
+                            || mInputMethodSession.onShouldVerifyKeyEvent(event));
+        }
     }
 }
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 977c5bd..4bde8e2 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -56,6 +56,7 @@
 import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED;
 import static android.view.inputmethod.Flags.FLAG_CONNECTIONLESS_HANDWRITING;
 import static android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API;
+import static android.view.inputmethod.Flags.FLAG_VERIFY_KEY_EVENT;
 import static android.view.inputmethod.Flags.ctrlShiftShortcut;
 import static android.view.inputmethod.Flags.predictiveBackIme;
 
@@ -3777,6 +3778,23 @@
     }
 
     /**
+     * Received by the IME before dispatch to {@link #onKeyDown(int, KeyEvent)} to let the system
+     * know if the {@link KeyEvent} needs to be verified that it originated from the system.
+     * {@link KeyEvent}s may originate from outside of the system and any sensitive keys should be
+     * marked for verification. One example of this could be using key shortcuts for switching to
+     * another IME.
+     *
+     * @param keyEvent the event that may need verification.
+     * @return {@code true} if {@link KeyEvent} should have its HMAC verified before dispatch,
+     * {@code false} otherwise.
+     */
+    @FlaggedApi(FLAG_VERIFY_KEY_EVENT)
+    @Override
+    public boolean onShouldVerifyKeyEvent(@NonNull KeyEvent keyEvent) {
+        return false;
+    }
+
+    /**
      * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent)
      * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle
      * the event).
diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl
index 3312055..a043da9 100644
--- a/core/java/android/os/IHintManager.aidl
+++ b/core/java/android/os/IHintManager.aidl
@@ -20,6 +20,7 @@
 import android.os.CpuHeadroomParamsInternal;
 import android.os.GpuHeadroomParamsInternal;
 import android.os.IHintSession;
+import android.os.SessionCreationConfig;
 import android.hardware.power.ChannelConfig;
 import android.hardware.power.SessionConfig;
 import android.hardware.power.SessionTag;
@@ -34,8 +35,8 @@
      * Throws UnsupportedOperationException if ADPF is not supported, and IllegalStateException
      * if creation is supported but fails.
      */
-    IHintSession createHintSessionWithConfig(in IBinder token, in int[] threadIds,
-            in long durationNanos, in SessionTag tag, out SessionConfig config);
+    IHintSession createHintSessionWithConfig(in IBinder token, in SessionTag tag,
+            in SessionCreationConfig creationConfig, out SessionConfig config);
 
     /**
      * Get preferred rate limit in nanoseconds.
@@ -56,4 +57,9 @@
     long getCpuHeadroomMinIntervalMillis();
     float getGpuHeadroom(in GpuHeadroomParamsInternal params);
     long getGpuHeadroomMinIntervalMillis();
+
+    /**
+     * Get Maximum number of graphics pipeline threads allowed per-app.
+     */
+    int getMaxGraphicsPipelineThreadsCount();
 }
diff --git a/core/java/android/os/SessionCreationConfig.aidl b/core/java/android/os/SessionCreationConfig.aidl
new file mode 100644
index 0000000..cdc0ef4
--- /dev/null
+++ b/core/java/android/os/SessionCreationConfig.aidl
@@ -0,0 +1,39 @@
+/*
+ *
+ * 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 android.os;
+
+import android.hardware.power.SessionTag;
+import android.hardware.power.SessionMode;
+
+/** {@hide} */
+parcelable SessionCreationConfig {
+    /**
+     * List of tids to be included in the hint session.
+     */
+    int[] tids;
+
+    /**
+     * The initial target work duration of this hint session in nanoseconds.
+     */
+    long targetWorkDurationNanos;
+
+    /**
+     * List of the modes to be enabled upon session creation.
+     */
+    SessionMode[] modesToEnable;
+}
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 2ef8764..5ac53f1 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -12,6 +12,15 @@
 }
 
 flag {
+    name: "adpf_graphics_pipeline"
+    is_exported: true
+    namespace: "game"
+    description: "Guards use of SessionCreationConfig and Graphics Pipeline mode"
+    is_fixed_read_only: true
+    bug: "367803904"
+}
+
+flag {
     name: "adpf_hwui_gpu"
     namespace: "game"
     description: "Guards use of the FMQ channel for ADPF"
diff --git a/core/java/android/text/style/TtsSpan.java b/core/java/android/text/style/TtsSpan.java
index b7b8f0b1..e257f34 100644
--- a/core/java/android/text/style/TtsSpan.java
+++ b/core/java/android/text/style/TtsSpan.java
@@ -107,11 +107,13 @@
 
     /**
      * The text associated with this span is a time, consisting of a number of
-     * hours and minutes, specified with {@link #ARG_HOURS} and
-     * {@link #ARG_MINUTES}.
+     * hours, minutes, and seconds specified with {@link #ARG_HOURS}, {@link #ARG_MINUTES}, and
+     * {@link #ARG_SECONDS}.
      * Also accepts the arguments {@link #ARG_GENDER},
      * {@link #ARG_ANIMACY}, {@link #ARG_MULTIPLICITY} and
-     * {@link #ARG_CASE}.
+     * {@link #ARG_CASE}. This is different from {@link #TYPE_DURATION}. This should be used to
+     * convey a particular moment in time, such as a clock time, while {@link #TYPE_DURATION} should
+     * be used to convey an interval of time.
      */
     public static final String TYPE_TIME = "android.type.time";
 
@@ -309,16 +311,18 @@
     public static final String ARG_UNIT = "android.arg.unit";
 
     /**
-     * Argument used to specify the hours of a time. The hours should be
-     * provided as an integer in the range from 0 up to and including 24.
-     * Can be used with {@link #TYPE_TIME}.
+     * Argument used to specify the hours of a time or duration. The hours should be
+     * provided as an integer in the range from 0 up to and including 24 for
+     * {@link #TYPE_TIME}.
+     * Can be used with {@link #TYPE_TIME} or {@link #TYPE_DURATION}.
      */
     public static final String ARG_HOURS = "android.arg.hours";
 
     /**
-     * Argument used to specify the minutes of a time. The minutes should be
-     * provided as an integer in the range from 0 up to and including 59.
-     * Can be used with {@link #TYPE_TIME}.
+     * Argument used to specify the minutes of a time or duration. The minutes should be
+     * provided as an integer in the range from 0 up to and including 59 for
+     * {@link #TYPE_TIME}.
+     * Can be used with {@link #TYPE_TIME} or {@link #TYPE_DURATION}.
      */
     public static final String ARG_MINUTES = "android.arg.minutes";
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 19d3dc4..5b4b5300 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -110,7 +110,6 @@
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED;
-import static android.view.accessibility.Flags.fixMergedContentChangeEventV2;
 import static android.view.accessibility.Flags.forceInvertColor;
 import static android.view.accessibility.Flags.reduceWindowContentChangedEventThrottle;
 import static android.view.flags.Flags.addSchandleToVriSurface;
@@ -12290,31 +12289,20 @@
             }
 
             if (mSource != null) {
-                if (fixMergedContentChangeEventV2()) {
-                    View newSource = getCommonPredecessor(mSource, source);
-                    if (newSource != null) {
-                        newSource = newSource.getSelfOrParentImportantForA11y();
-                    }
-                    if (newSource == null) {
-                        // If there is no common predecessor, then mSource points to
-                        // a removed view, hence in this case always prefer the source.
-                        newSource = source;
-                    }
-
-                    mChangeTypes |= changeType;
-                    if (mSource != newSource) {
-                        mChangeTypes |= AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
-                        mSource = newSource;
-                    }
-                } else {
+                View newSource = getCommonPredecessor(mSource, source);
+                if (newSource != null) {
+                    newSource = newSource.getSelfOrParentImportantForA11y();
+                }
+                if (newSource == null) {
                     // If there is no common predecessor, then mSource points to
                     // a removed view, hence in this case always prefer the source.
-                    View predecessor = getCommonPredecessor(mSource, source);
-                    if (predecessor != null) {
-                        predecessor = predecessor.getSelfOrParentImportantForA11y();
-                    }
-                    mSource = (predecessor != null) ? predecessor : source;
-                    mChangeTypes |= changeType;
+                    newSource = source;
+                }
+
+                mChangeTypes |= changeType;
+                if (mSource != newSource) {
+                    mChangeTypes |= AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
+                    mSource = newSource;
                 }
 
                 final int performingAction = mAccessibilityManager.getPerformingAction();
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 8a006fa..e60fc3a 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -99,16 +99,6 @@
 
 flag {
     namespace: "accessibility"
-    name: "fix_merged_content_change_event_v2"
-    description: "Fixes event type and source of content change event merged in ViewRootImpl"
-    bug: "277305460"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
-    namespace: "accessibility"
     name: "flash_notification_system_api"
     is_exported: true
     description: "Makes flash notification APIs as system APIs for calling from mainline module"
diff --git a/core/java/android/view/inputmethod/InputMethodSession.java b/core/java/android/view/inputmethod/InputMethodSession.java
index 4f48cb6..1806a83 100644
--- a/core/java/android/view/inputmethod/InputMethodSession.java
+++ b/core/java/android/view/inputmethod/InputMethodSession.java
@@ -16,6 +16,7 @@
 
 package android.view.inputmethod;
 
+import android.annotation.NonNull;
 import android.graphics.Rect;
 import android.inputmethodservice.InputMethodService;
 import android.os.Bundle;
@@ -125,6 +126,23 @@
     public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback);
 
     /**
+     * Received by the IME before dispatch to {@link InputMethodService#onKeyDown(int, KeyEvent)}
+     * to let the system know if the {@link KeyEvent} needs to be verified that it originated from
+     * the system. {@link KeyEvent}s may originate from outside of the system and any sensitive keys
+     * should be marked for verification. One example of this could be using key shortcuts for
+     * switching to another IME.
+     *
+     * @param event the event that may need verification.
+     * @return {@code true} if {@link KeyEvent} should have its HMAC verified before dispatch,
+     * {@code false} otherwise.
+     *
+     * @hide
+     */
+    default boolean onShouldVerifyKeyEvent(@NonNull KeyEvent event) {
+        return false;
+    }
+
+    /**
      * This method is called when there is a track ball event.
      *
      * <p>
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index deaf957..73abc47 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -184,3 +184,11 @@
   bug: "350047836"
   is_fixed_read_only: true
 }
+
+flag {
+  name: "verify_key_event"
+  namespace: "input_method"
+  description: "Verify KeyEvents in IME"
+  bug: "331730488"
+  is_fixed_read_only: true
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
index 1acde73..4723eb2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
@@ -119,7 +119,8 @@
         )
     }
 
-    fun moveTaskToNonImmersive(taskInfo: RunningTaskInfo) {
+    /** Starts a transition to move an immersive task out of immersive. */
+    fun moveTaskToNonImmersive(taskInfo: RunningTaskInfo, reason: ExitReason) {
         if (inProgress) {
             logV(
                 "Cannot start exit because transition(s) already in progress: %s",
@@ -131,7 +132,7 @@
         val wct = WindowContainerTransaction().apply {
             setBounds(taskInfo.token, getExitDestinationBounds(taskInfo))
         }
-        logV("Moving task ${taskInfo.taskId} out of immersive mode")
+        logV("Moving task %d out of immersive mode, reason: %s", taskInfo.taskId, reason)
         val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
         state = TransitionState(
             transition = transition,
@@ -151,10 +152,11 @@
     fun exitImmersiveIfApplicable(
         transition: IBinder,
         wct: WindowContainerTransaction,
-        displayId: Int
+        displayId: Int,
+        reason: ExitReason,
     ) {
         if (!Flags.enableFullyImmersiveInDesktop()) return
-        val result = exitImmersiveIfApplicable(wct, displayId)
+        val result = exitImmersiveIfApplicable(wct, displayId, excludeTaskId = null, reason)
         result.asExit()?.runOnTransitionStart?.invoke(transition)
     }
 
@@ -170,6 +172,7 @@
         wct: WindowContainerTransaction,
         displayId: Int,
         excludeTaskId: Int? = null,
+        reason: ExitReason,
     ): ExitResult {
         if (!Flags.enableFullyImmersiveInDesktop()) return ExitResult.NoExit
         val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId)
@@ -179,7 +182,10 @@
         }
         val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask)
             ?: return ExitResult.NoExit
-        logV("Appending immersive exit for task: $immersiveTask in display: $displayId")
+        logV(
+            "Appending immersive exit for task: %d in display: %d for reason: %s",
+            immersiveTask, displayId, reason
+        )
         wct.setBounds(taskInfo.token, getExitDestinationBounds(taskInfo))
         return ExitResult.Exit(
             exitingTask = immersiveTask,
@@ -198,14 +204,15 @@
      */
     fun exitImmersiveIfApplicable(
         wct: WindowContainerTransaction,
-        taskInfo: RunningTaskInfo
+        taskInfo: RunningTaskInfo,
+        reason: ExitReason,
     ): ExitResult {
         if (!Flags.enableFullyImmersiveInDesktop()) return ExitResult.NoExit
         if (desktopRepository.isTaskInFullImmersiveState(taskInfo.taskId)) {
             // A full immersive task is being minimized, make sure the immersive state is broken
             // (i.e. resize back to max bounds).
             wct.setBounds(taskInfo.token, getExitDestinationBounds(taskInfo))
-            logV("Appending immersive exit for task: ${taskInfo.taskId}")
+            logV("Appending immersive exit for task: %d for reason: %s", taskInfo.taskId, reason)
             return ExitResult.Exit(
                 exitingTask = taskInfo.taskId,
                 runOnTransitionStart = { transition ->
@@ -550,6 +557,15 @@
         ENTER, EXIT
     }
 
+    /** The reason for moving the task out of desktop immersive mode. */
+    enum class ExitReason {
+        APP_NOT_IMMERSIVE, // The app stopped requesting immersive treatment.
+        USER_INTERACTION, // Explicit user intent request, e.g. a button click.
+        TASK_LAUNCH, // A task launched/moved on top of the immersive task.
+        MINIMIZED, // The immersive task was minimized.
+        CLOSED, // The immersive task was closed.
+    }
+
     private fun logV(msg: String, vararg arguments: Any?) {
         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index a0bdd9f..eb930b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -70,7 +70,6 @@
 import com.android.internal.policy.ScreenDecorationsUtils
 import com.android.internal.protolog.ProtoLog
 import com.android.window.flags.Flags
-import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut
 import com.android.wm.shell.Flags.enableFlexibleSplit
 import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
@@ -133,6 +132,8 @@
 import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion.DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS
 import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION
 import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCREEN_ANIMATION_DURATION
+import com.android.wm.shell.recents.RecentsTransitionStateListener.RecentsTransitionState
+import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING
 
 /** Handles moving tasks in and out of desktop */
 class DesktopTasksController(
@@ -209,7 +210,9 @@
     val draggingTaskId
         get() = dragToDesktopTransitionHandler.draggingTaskId
 
-    private var recentsAnimationRunning = false
+    @RecentsTransitionState
+    private var recentsTransitionState = TRANSITION_STATE_NOT_RUNNING
+
     private lateinit var splitScreenController: SplitScreenController
     lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
     // Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
@@ -238,10 +241,15 @@
         dragToDesktopTransitionHandler.dragToDesktopStateListener = dragToDesktopStateListener
         recentsTransitionHandler.addTransitionStateListener(
             object : RecentsTransitionStateListener {
-                override fun onAnimationStateChanged(running: Boolean) {
-                    logV("Recents animation state changed running=%b", running)
-                    recentsAnimationRunning = running
-                    desktopTilingDecorViewModel.onOverviewAnimationStateChange(running)
+                override fun onTransitionStateChanged(@RecentsTransitionState state: Int) {
+                    logV(
+                        "Recents transition state changed: %s",
+                        RecentsTransitionStateListener.stateToString(state)
+                    )
+                    recentsTransitionState = state
+                    desktopTilingDecorViewModel.onOverviewAnimationStateChange(
+                        RecentsTransitionStateListener.isAnimating(state)
+                    )
                 }
             }
         )
@@ -381,6 +389,7 @@
             wct = wct,
             displayId = DEFAULT_DISPLAY,
             excludeTaskId = taskId,
+            reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
         )
         wct.startTask(
             taskId,
@@ -413,6 +422,7 @@
             wct = wct,
             displayId = task.displayId,
             excludeTaskId = task.taskId,
+            reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
         )
         // Bring other apps to front first
         val taskIdToMinimize =
@@ -460,7 +470,11 @@
             bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
         addMoveToDesktopChanges(wct, taskInfo)
         val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(
-            wct, taskInfo.displayId)
+            wct = wct,
+            displayId = taskInfo.displayId,
+            excludeTaskId = null,
+            reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH
+        )
         val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
         desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted(
             DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS.toInt()
@@ -508,8 +522,11 @@
                 taskId
             )
         )
-        return desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo).asExit()
-            ?.runOnTransitionStart
+        return desktopImmersiveController.exitImmersiveIfApplicable(
+            wct = wct,
+            taskInfo = taskInfo,
+            reason = DesktopImmersiveController.ExitReason.CLOSED
+        ).asExit()?.runOnTransitionStart
     }
 
     fun minimizeTask(taskInfo: RunningTaskInfo) {
@@ -518,7 +535,11 @@
         val wct = WindowContainerTransaction()
         performDesktopExitCleanupIfNeeded(taskId, wct)
         // Notify immersive handler as it might need to exit immersive state.
-        val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo)
+        val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(
+            wct = wct,
+            taskInfo = taskInfo,
+            reason = DesktopImmersiveController.ExitReason.MINIMIZED
+        )
 
         wct.reorder(taskInfo.token, false)
         val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct)
@@ -675,6 +696,7 @@
             wct = wct,
             displayId = displayId,
             excludeTaskId = launchingTaskId,
+            reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
         )
         if (remoteTransition == null) {
             val t = desktopMixedTransitionHandler.startLaunchTransition(
@@ -766,7 +788,10 @@
     /** Moves a task in/out of full immersive state within the desktop. */
     fun toggleDesktopTaskFullImmersiveState(taskInfo: RunningTaskInfo) {
         if (taskRepository.isTaskInFullImmersiveState(taskInfo.taskId)) {
-            exitDesktopTaskFromFullImmersive(taskInfo)
+            exitDesktopTaskFromFullImmersive(
+                taskInfo,
+                DesktopImmersiveController.ExitReason.USER_INTERACTION,
+            )
         } else {
             moveDesktopTaskToFullImmersive(taskInfo)
         }
@@ -777,9 +802,12 @@
         desktopImmersiveController.moveTaskToImmersive(taskInfo)
     }
 
-    private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) {
+    private fun exitDesktopTaskFromFullImmersive(
+        taskInfo: RunningTaskInfo,
+        reason: DesktopImmersiveController.ExitReason,
+    ) {
         check(taskInfo.isFreeform) { "Task must already be in freeform" }
-        desktopImmersiveController.moveTaskToNonImmersive(taskInfo)
+        desktopImmersiveController.moveTaskToNonImmersive(taskInfo, reason)
     }
 
     /**
@@ -1292,6 +1320,8 @@
         // Check if we should skip handling this transition
         var reason = ""
         val triggerTask = request.triggerTask
+        val recentsAnimationRunning =
+            RecentsTransitionStateListener.isAnimating(recentsTransitionState)
         var shouldHandleMidRecentsFreeformLaunch =
             recentsAnimationRunning && isFreeformRelaunch(triggerTask, request)
         val isDragAndDropFullscreenTransition = taskContainsDragAndDropCookie(triggerTask)
@@ -1479,6 +1509,7 @@
                     wct = wct,
                     displayId = callingTask.displayId,
                     excludeTaskId = requestedTaskId,
+                    reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
                 )
             val transition = transitions.startTransition(TRANSIT_OPEN, wct, null)
             taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) }
@@ -1628,7 +1659,12 @@
         }
         // Desktop Mode is showing and we're launching a new Task:
         // 1) Exit immersive if needed.
-        desktopImmersiveController.exitImmersiveIfApplicable(transition, wct, task.displayId)
+        desktopImmersiveController.exitImmersiveIfApplicable(
+            transition = transition,
+            wct = wct,
+            displayId = task.displayId,
+            reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
+        )
         // 2) minimize a Task if needed.
         val taskIdToMinimize = addAndGetMinimizeChanges(task.displayId, wct, task.taskId)
         addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize)
@@ -1665,7 +1701,10 @@
                 taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) }
                 addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize)
                 desktopImmersiveController.exitImmersiveIfApplicable(
-                    transition, wct, task.displayId
+                    transition,
+                    wct,
+                    task.displayId,
+                    reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH
                 )
             }
         } else if (taskRepository.isActiveTask(task.taskId)) {
@@ -2282,9 +2321,13 @@
         if (!Flags.enableFullyImmersiveInDesktop()) return
         val inImmersive = taskRepository.isTaskInFullImmersiveState(taskInfo.taskId)
         val requestingImmersive = taskInfo.requestingImmersive
-        if (inImmersive && !requestingImmersive) {
+        if (inImmersive && !requestingImmersive
+            && !RecentsTransitionStateListener.isRunning(recentsTransitionState)) {
             // Exit immersive if the app is no longer requesting it.
-            exitDesktopTaskFromFullImmersive(taskInfo)
+            exitDesktopTaskFromFullImmersive(
+                taskInfo,
+                DesktopImmersiveController.ExitReason.APP_NOT_IMMERSIVE
+            )
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 6da4f51..d917f93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -665,8 +665,10 @@
                 }
                 mTransitionHandler.addTransitionStateListener(new RecentsTransitionStateListener() {
                     @Override
-                    public void onAnimationStateChanged(boolean running) {
-                        executor.execute(() -> listener.accept(running));
+                    public void onTransitionStateChanged(@RecentsTransitionState int state) {
+                        executor.execute(() -> {
+                            listener.accept(RecentsTransitionStateListener.isAnimating(state));
+                        });
                     }
                 });
             });
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 1c58dbb..032dac9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -32,6 +32,9 @@
 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
+import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING;
+import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING;
+import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED;
 import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
 import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_END_RECENTS_TRANSITION;
@@ -166,13 +169,19 @@
         // only care about latest one.
         mAnimApp = appThread;
 
-        // TODO(b/366021931): Formalize this later
-        final boolean isSyntheticRequest = options.containsKey("is_synthetic_recents_transition");
-        if (isSyntheticRequest) {
-            return startSyntheticRecentsTransition(listener);
-        } else {
-            return startRealRecentsTransition(intent, fillIn, options, listener);
+        for (int i = 0; i < mStateListeners.size(); i++) {
+            mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_REQUESTED);
         }
+        // TODO(b/366021931): Formalize this later
+        final boolean isSyntheticRequest = options.getBoolean(
+                "is_synthetic_recents_transition", /* defaultValue= */ false);
+        final IBinder transition;
+        if (isSyntheticRequest) {
+            transition = startSyntheticRecentsTransition(listener);
+        } else {
+            transition = startRealRecentsTransition(intent, fillIn, options, listener);
+        }
+        return transition;
     }
 
     /**
@@ -542,7 +551,7 @@
             mPendingFinishTransition = null;
             mControllers.remove(this);
             for (int i = 0; i < mStateListeners.size(); i++) {
-                mStateListeners.get(i).onAnimationStateChanged(false);
+                mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_NOT_RUNNING);
             }
         }
 
@@ -578,7 +587,7 @@
                         new RemoteAnimationTarget[0],
                         new Rect(0, 0, 0, 0), new Rect(), new Bundle());
                 for (int i = 0; i < mStateListeners.size(); i++) {
-                    mStateListeners.get(i).onAnimationStateChanged(true);
+                    mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING);
                 }
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error starting recents animation", e);
@@ -809,7 +818,7 @@
                         wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]),
                         new Rect(0, 0, 0, 0), new Rect(), b);
                 for (int i = 0; i < mStateListeners.size(); i++) {
-                    mStateListeners.get(i).onAnimationStateChanged(true);
+                    mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING);
                 }
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error starting recents animation", e);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
index 95874c8..ea7cfd3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
@@ -16,12 +16,47 @@
 
 package com.android.wm.shell.recents;
 
-import android.os.IBinder;
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /** The listener for the events from {@link RecentsTransitionHandler}. */
 public interface RecentsTransitionStateListener {
 
-    /** Notifies whether the recents animation is running. */
-    default void onAnimationStateChanged(boolean running) {
+    @IntDef(prefix = { "TRANSITION_STATE_" }, value = {
+            TRANSITION_STATE_NOT_RUNNING,
+            TRANSITION_STATE_REQUESTED,
+            TRANSITION_STATE_ANIMATING,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface RecentsTransitionState {}
+
+    int TRANSITION_STATE_NOT_RUNNING = 1;
+    int TRANSITION_STATE_REQUESTED = 2;
+    int TRANSITION_STATE_ANIMATING = 3;
+
+    /** Notifies whether the recents transition state changes. */
+    default void onTransitionStateChanged(@RecentsTransitionState int state) {
+    }
+
+    /** Returns whether the recents transition is running. */
+    static boolean isRunning(@RecentsTransitionState int state) {
+        return state >= TRANSITION_STATE_REQUESTED;
+    }
+
+    /** Returns whether the recents transition is animating. */
+    static boolean isAnimating(@RecentsTransitionState int state) {
+        return state >= TRANSITION_STATE_ANIMATING;
+    }
+
+    /** Returns a string representation of the given state. */
+    static String stateToString(@RecentsTransitionState int state) {
+        return switch (state) {
+            case TRANSITION_STATE_NOT_RUNNING -> "TRANSITION_STATE_NOT_RUNNING";
+            case TRANSITION_STATE_REQUESTED -> "TRANSITION_STATE_REQUESTED";
+            case TRANSITION_STATE_ANIMATING -> "TRANSITION_STATE_ANIMATING";
+            default -> "UNKNOWN";
+        };
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
index 7b71e41..b8c9151 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
@@ -23,6 +23,7 @@
 import android.view.WindowManager
 import android.window.TaskSnapshot
 import androidx.compose.ui.graphics.toArgb
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer
 import com.android.wm.shell.shared.split.SplitScreenConstants
 import com.android.wm.shell.splitscreen.SplitScreenController
@@ -101,6 +102,7 @@
             flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                     WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
             view = menuView.rootView,
+            ignoreCutouts = DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context),
         )
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 456f2c0..8a2b394 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -45,6 +45,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.compose.ui.graphics.toArgb
 import androidx.core.view.isGone
+import com.android.window.flags.Flags
 import com.android.wm.shell.R
 import com.android.wm.shell.apptoweb.isBrowserApp
 import com.android.wm.shell.shared.split.SplitScreenConstants
@@ -218,7 +219,8 @@
                             WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
                             WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
                     view = handleMenuView.rootView,
-                    forciblyShownTypes = if (forceShowSystemBars) { systemBars() } else { 0 }
+                    forciblyShownTypes = if (forceShowSystemBars) { systemBars() } else { 0 },
+                    ignoreCutouts = Flags.showAppHandleLargeScreens()
                 )
             } else {
                 parentDecor.addWindow(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 8b6aaaf..4a01e8e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -25,6 +25,7 @@
 import android.view.View
 import android.view.WindowInsets
 import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
 import com.android.wm.shell.windowdecor.WindowManagerWrapper
 
 /**
@@ -40,6 +41,7 @@
     height: Int,
     flags: Int,
     @WindowInsets.Type.InsetsType forciblyShownTypes: Int = 0,
+    ignoreCutouts: Boolean = false,
     override val view: View
 ) : AdditionalViewContainer() {
     val lp: WindowManager.LayoutParams = WindowManager.LayoutParams(
@@ -52,6 +54,10 @@
         gravity = Gravity.LEFT or Gravity.TOP
         setTrustedOverlay()
         this.forciblyShownTypes = forciblyShownTypes
+        if (ignoreCutouts) {
+            fitInsetsTypes = 0
+            layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+        }
     }
 
     constructor(
@@ -63,7 +69,8 @@
         width: Int,
         height: Int,
         flags: Int,
-        @LayoutRes layoutId: Int
+        @LayoutRes layoutId: Int,
+        ignoreCutouts: Boolean = false
     ) : this(
         windowManagerWrapper = windowManagerWrapper,
         taskId = taskId,
@@ -72,7 +79,8 @@
         width = width,
         height = height,
         flags = flags,
-        view = LayoutInflater.from(context).inflate(layoutId, null /* parent */)
+        view = LayoutInflater.from(context).inflate(layoutId, null /* parent */),
+        ignoreCutouts = ignoreCutouts
     )
 
     constructor(
@@ -83,7 +91,8 @@
         y: Int,
         width: Int,
         height: Int,
-        flags: Int
+        flags: Int,
+        ignoreCutouts: Boolean = false
     ) : this(
         windowManagerWrapper = windowManagerWrapper,
         taskId = taskId,
@@ -92,7 +101,8 @@
         width = width,
         height = height,
         flags = flags,
-        view = View(context)
+        view = View(context),
+        ignoreCutouts = ignoreCutouts
     )
 
     init {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index 5f25f42..3f65d93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -38,6 +38,7 @@
 import androidx.core.view.ViewCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
 import com.android.internal.policy.SystemBarUtils
+import com.android.window.flags.Flags
 import com.android.wm.shell.R
 import com.android.wm.shell.shared.animation.Interpolators
 import com.android.wm.shell.windowdecor.WindowManagerWrapper
@@ -143,7 +144,8 @@
         if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return
         statusBarInputLayer = AdditionalSystemViewContainer(context, windowManagerWrapper,
             taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight,
-            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+            ignoreCutouts = Flags.showAppHandleLargeScreens()
         )
         val view = statusBarInputLayer?.view ?: error("Unable to find statusBarInputLayer View")
         val lp = statusBarInputLayer?.lp ?: error("Unable to find statusBarInputLayer " +
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
index f935ac7..9c31b46 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
@@ -45,6 +45,7 @@
     private Intent mBaseIntent = new Intent();
     private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD;
     private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED;
+    private @WindowConfiguration.ActivityType int mTopActivityType = ACTIVITY_TYPE_STANDARD;
     private int mDisplayId = Display.DEFAULT_DISPLAY;
     private ActivityManager.TaskDescription.Builder mTaskDescriptionBuilder = null;
     private final Point mPositionInParent = new Point();
@@ -102,6 +103,12 @@
         return this;
     }
 
+    public TestRunningTaskInfoBuilder setTopActivityType(
+            @WindowConfiguration.ActivityType int activityType) {
+        mTopActivityType = activityType;
+        return this;
+    }
+
     public TestRunningTaskInfoBuilder setWindowingMode(
             @WindowConfiguration.WindowingMode int windowingMode) {
         mWindowingMode = windowingMode;
@@ -154,6 +161,7 @@
         info.configuration.windowConfiguration.setBounds(mBounds);
         info.configuration.windowConfiguration.setActivityType(mActivityType);
         info.configuration.windowConfiguration.setWindowingMode(mWindowingMode);
+        info.topActivityType = mTopActivityType;
         info.token = mToken;
         info.isResizeable = true;
         info.supportsMultiWindow = true;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
index a4f4d05..4666276 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
@@ -42,6 +42,7 @@
 import com.android.wm.shell.TestShellExecutor
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitReason.USER_INTERACTION
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
@@ -168,7 +169,7 @@
             immersive = true
         )
 
-        controller.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task, USER_INTERACTION)
         controller.onTransitionReady(
             transition = mockBinder,
             info = createTransitionInfo(
@@ -195,7 +196,7 @@
         )
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600))
 
-        controller.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task, USER_INTERACTION)
         controller.onTransitionReady(
             transition = mockBinder,
             info = createTransitionInfo(
@@ -252,8 +253,8 @@
         whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
 
-        controller.moveTaskToNonImmersive(task)
-        controller.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task, USER_INTERACTION)
+        controller.moveTaskToNonImmersive(task, USER_INTERACTION)
 
         verify(mockTransitions, times(1))
             .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))
@@ -272,7 +273,7 @@
             immersive = true
         )
 
-        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
@@ -293,7 +294,7 @@
             immersive = false
         )
 
-        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
@@ -314,7 +315,7 @@
             immersive = true
         )
 
-        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         assertThat(wct.hasBoundsChange(task.token)).isTrue()
     }
@@ -332,7 +333,7 @@
             immersive = false
         )
 
-        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         assertThat(wct.hasBoundsChange(task.token)).isFalse()
     }
@@ -353,7 +354,8 @@
         controller.exitImmersiveIfApplicable(
             wct = wct,
             displayId = DEFAULT_DISPLAY,
-            excludeTaskId = task.taskId
+            excludeTaskId = task.taskId,
+            reason = USER_INTERACTION,
         ).asExit()?.runOnTransitionStart?.invoke(transition)
 
         assertThat(controller.pendingExternalExitTransitions.any { exit ->
@@ -374,7 +376,7 @@
             immersive = true
         )
 
-        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION)
 
         assertThat(wct.hasBoundsChange(task.token)).isTrue()
     }
@@ -391,7 +393,7 @@
             immersive = false
         )
 
-        controller.exitImmersiveIfApplicable(wct, task)
+        controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION)
 
         assertThat(wct.hasBoundsChange(task.token)).isFalse()
     }
@@ -409,7 +411,7 @@
             immersive = true
         )
 
-        controller.exitImmersiveIfApplicable(wct, task)
+        controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION)
             .asExit()?.runOnTransitionStart?.invoke(transition)
 
         assertThat(controller.pendingExternalExitTransitions.any { exit ->
@@ -430,7 +432,7 @@
             immersive = false
         )
 
-        val result = controller.exitImmersiveIfApplicable(wct, task)
+        val result = controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION)
 
         assertThat(result).isEqualTo(DesktopImmersiveController.ExitResult.NoExit)
     }
@@ -447,7 +449,8 @@
             immersive = false
         )
 
-        val result = controller.exitImmersiveIfApplicable(wct, task.displayId)
+        val result = controller.exitImmersiveIfApplicable(
+            wct, task.displayId, excludeTaskId = null, USER_INTERACTION)
 
         assertThat(result).isEqualTo(DesktopImmersiveController.ExitResult.NoExit)
     }
@@ -464,7 +467,7 @@
             taskId = task.taskId,
             immersive = true
         )
-        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         controller.onTransitionReady(
             transition = transition,
@@ -495,7 +498,7 @@
             taskId = task.taskId,
             immersive = true
         )
-        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         controller.onTransitionReady(
             transition = transition,
@@ -530,7 +533,7 @@
             taskId = task.taskId,
             immersive = true
         )
-        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         controller.onTransitionReady(
             transition = transition,
@@ -560,7 +563,7 @@
             immersive = true
         )
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600))
-        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         controller.onTransitionReady(
             transition = transition,
@@ -587,7 +590,7 @@
             immersive = true
         )
 
-        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION)
 
         assertThat(
             wct.hasBoundsChange(task.token, calculateMaximizeBounds(mockDisplayLayout, task))
@@ -611,7 +614,7 @@
         val preImmersiveBounds = Rect(100, 100, 500, 500)
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, preImmersiveBounds)
 
-        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION)
 
         assertThat(
             wct.hasBoundsChange(task.token, preImmersiveBounds)
@@ -634,7 +637,7 @@
             immersive = true
         )
 
-        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION)
 
         assertThat(
             wct.hasBoundsChange(task.token, calculateInitialBounds(mockDisplayLayout, task))
@@ -652,10 +655,10 @@
             taskId = task.taskId,
             immersive = true
         )
-        controller.exitImmersiveIfApplicable(wct, task)
+        controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION)
             .asExit()?.runOnTransitionStart?.invoke(Binder())
 
-        controller.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task, USER_INTERACTION)
 
         verify(mockTransitions, never()).startTransition(any(), any(), any())
     }
@@ -674,7 +677,7 @@
             immersive = true
         )
 
-        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         assertThat(controller.isImmersiveChange(transition, change)).isTrue()
     }
@@ -692,7 +695,7 @@
             immersive = true
         )
 
-        controller.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task, USER_INTERACTION)
 
         controller.animateResizeChange(
             change = TransitionInfo.Change(task.token, SurfaceControl()).apply {
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 5df3957..490e6b9 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
@@ -113,6 +113,8 @@
 import com.android.wm.shell.recents.RecentTasksController
 import com.android.wm.shell.recents.RecentsTransitionHandler
 import com.android.wm.shell.recents.RecentsTransitionStateListener
+import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING
+import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN
 import com.android.wm.shell.shared.split.SplitScreenConstants
@@ -291,10 +293,10 @@
     tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
     whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), any<RunningTaskInfo>()))
+      .exitImmersiveIfApplicable(any(), any<RunningTaskInfo>(), any()))
       .thenReturn(ExitResult.NoExit)
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull()))
+      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull(), any()))
       .thenReturn(ExitResult.NoExit)
 
     controller = createController()
@@ -1793,7 +1795,7 @@
 
     controller.minimizeTask(task)
 
-    verify(mMockDesktopImmersiveController).exitImmersiveIfApplicable(any(), eq(task))
+    verify(mMockDesktopImmersiveController).exitImmersiveIfApplicable(any(), eq(task), any())
   }
 
   @Test
@@ -1803,7 +1805,7 @@
     val runOnTransit = RunOnStartTransitionCallback()
     whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
       .thenReturn(transition)
-    whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task)))
+    whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any()))
       .thenReturn(
         ExitResult.Exit(
         exitingTask = task.taskId,
@@ -2192,7 +2194,7 @@
     markTaskVisible(freeformTask)
 
     // Mark recents animation running
-    recentsTransitionStateListener.onAnimationStateChanged(true)
+    recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_ANIMATING)
 
     // Open a fullscreen task, check that it does not result in a WCT with changes to it
     val fullscreenTask = createFullscreenTask()
@@ -2206,7 +2208,7 @@
     markTaskVisible(freeformTask)
 
     // Mark recents animation running
-    recentsTransitionStateListener.onAnimationStateChanged(true)
+    recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_ANIMATING)
 
     // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA.
     val result = controller.handleRequest(Binder(), createTransition(freeformTask))
@@ -3189,7 +3191,7 @@
     val wctCaptor = argumentCaptor<WindowContainerTransaction>()
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull()))
+      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull(), any()))
       .thenReturn(ExitResult.NoExit)
     whenever(desktopMixedTransitionHandler
       .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull()))
@@ -3212,7 +3214,7 @@
     val runOnStart = RunOnStartTransitionCallback()
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull()))
+      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull(), any()))
       .thenReturn(ExitResult.Exit(immersiveTask.taskId, runOnStart))
     whenever(desktopMixedTransitionHandler
       .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull()))
@@ -3315,7 +3317,8 @@
     whenever(transitions.startTransition(eq(TRANSIT_OPEN), any(), anyOrNull()))
       .thenReturn(transition)
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId)))
+      .exitImmersiveIfApplicable(
+        any(), eq(immersiveTask.displayId), eq(freeformTask.taskId), any()))
       .thenReturn(
         ExitResult.Exit(
         exitingTask = immersiveTask.taskId,
@@ -3325,7 +3328,7 @@
     runOpenInstance(immersiveTask, freeformTask.taskId)
 
     verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId))
+      .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId), any())
     runOnStartTransit.assertOnlyInvocation(transition)
   }
 
@@ -3740,7 +3743,7 @@
 
     controller.toggleDesktopTaskFullImmersiveState(task)
 
-    verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(task)
+    verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(eq(task), any())
   }
 
   @Test
@@ -3752,7 +3755,7 @@
     task.requestedVisibleTypes = WindowInsets.Type.statusBars()
     controller.onTaskInfoChanged(task)
 
-    verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(task)
+    verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(eq(task), any())
   }
 
   @Test
@@ -3764,7 +3767,20 @@
     task.requestedVisibleTypes = WindowInsets.Type.statusBars()
     controller.onTaskInfoChanged(task)
 
-    verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(task)
+    verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(eq(task), any())
+  }
+
+  @Test
+  @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun onTaskInfoChanged_inImmersiveUnrequestsImmersive_inRecentsTransition_noExit() {
+    val task = setUpFreeformTask(DEFAULT_DISPLAY)
+    taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = true)
+    recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_REQUESTED)
+
+    task.requestedVisibleTypes = WindowInsets.Type.statusBars()
+    controller.onTaskInfoChanged(task)
+
+    verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(eq(task), any())
   }
 
   @Test
@@ -3774,7 +3790,7 @@
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId))
+      .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()))
       .thenReturn(
         ExitResult.Exit(
         exitingTask = 5,
@@ -3785,7 +3801,7 @@
     controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
 
     verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)
+      .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any())
     runOnStartTransit.assertOnlyInvocation(transition)
   }
 
@@ -3796,7 +3812,7 @@
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId))
+      .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()))
       .thenReturn(
         ExitResult.Exit(
         exitingTask = 5,
@@ -3807,7 +3823,7 @@
     controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
 
     verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)
+      .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any())
     runOnStartTransit.assertOnlyInvocation(transition)
   }
 
@@ -3817,7 +3833,7 @@
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
+      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any()))
       .thenReturn(
         ExitResult.Exit(
         exitingTask = 5,
@@ -3830,7 +3846,7 @@
     controller.moveTaskToFront(task.taskId, remoteTransition = null)
 
     verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))
+      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any())
     runOnStartTransit.assertOnlyInvocation(transition)
   }
 
@@ -3840,7 +3856,7 @@
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
+      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any()))
       .thenReturn(
         ExitResult.Exit(
         exitingTask = 5,
@@ -3853,7 +3869,7 @@
     controller.moveTaskToFront(task.taskId, remoteTransition = null)
 
     verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))
+      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any())
     runOnStartTransit.assertOnlyInvocation(transition)
   }
 
@@ -3867,7 +3883,7 @@
     controller.handleRequest(binder, createTransition(task))
 
     verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
+      .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId), any())
   }
 
   @Test
@@ -3879,7 +3895,7 @@
     controller.handleRequest(binder, createTransition(task))
 
     verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
+      .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId), any())
   }
 
   @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
index 6087763..f0f5fe1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
@@ -16,7 +16,16 @@
 
 package com.android.wm.shell.recents;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING;
+import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING;
+import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_START_RECENTS_TRANSITION;
+
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
@@ -27,6 +36,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.IApplicationThread;
 import android.app.KeyguardManager;
@@ -38,7 +48,10 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -47,6 +60,7 @@
 import com.android.internal.os.IResultReceiver;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.TaskStackListenerImpl;
@@ -56,7 +70,9 @@
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.HomeTransitionObserver;
+import com.android.wm.shell.transition.TransitionInfoBuilder;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.StubTransaction;
 
 import org.junit.After;
 import org.junit.Before;
@@ -93,6 +109,8 @@
     private IRecentTasksListener mRecentTasksListener;
     @Mock
     private TaskStackTransitionObserver mTaskStackTransitionObserver;
+    @Mock
+    private Transitions mTransitions;
 
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -129,10 +147,9 @@
                 null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController),
                 mMainExecutor);
 
-        final Transitions transitions = mock(Transitions.class);
-        doReturn(mMainExecutor).when(transitions).getMainExecutor();
+        doReturn(mMainExecutor).when(mTransitions).getMainExecutor();
         mRecentsTransitionHandler = new RecentsTransitionHandler(mShellInit, mShellTaskOrganizer,
-                transitions, mRecentTasksController, mock(HomeTransitionObserver.class));
+                mTransitions, mRecentTasksController, mock(HomeTransitionObserver.class));
 
         mShellInit.init();
     }
@@ -146,12 +163,8 @@
     public void testStartSyntheticRecentsTransition_callsOnAnimationStartAndFinishCallback() throws Exception {
         final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class);
         final IResultReceiver finishCallback = mock(IResultReceiver.class);
-        doReturn(new Binder()).when(runner).asBinder();
-        Bundle options = new Bundle();
-        options.putBoolean("is_synthetic_recents_transition", true);
-        IBinder transition = mRecentsTransitionHandler.startRecentsTransition(
-                mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class),
-                runner);
+
+        final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner);
         verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
 
         // Finish and verify no transition remains and that the provided finish callback is called
@@ -165,12 +178,8 @@
     @Test
     public void testStartSyntheticRecentsTransition_callsOnAnimationCancel() throws Exception {
         final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class);
-        doReturn(new Binder()).when(runner).asBinder();
-        Bundle options = new Bundle();
-        options.putBoolean("is_synthetic_recents_transition", true);
-        IBinder transition = mRecentsTransitionHandler.startRecentsTransition(
-                mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class),
-                runner);
+
+        final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner);
         verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
 
         mRecentsTransitionHandler.findController(transition).cancel("test");
@@ -178,4 +187,137 @@
         verify(runner).onAnimationCanceled(any(), any());
         assertNull(mRecentsTransitionHandler.findController(transition));
     }
+
+    @Test
+    public void testStartTransition_updatesStateListeners() {
+        final TestTransitionStateListener listener = new TestTransitionStateListener();
+        mRecentsTransitionHandler.addTransitionStateListener(listener);
+
+        startRecentsTransition(/* synthetic= */ false);
+        mMainExecutor.flushAll();
+
+        assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_REQUESTED);
+    }
+
+    @Test
+    public void testStartAnimation_updatesStateListeners() {
+        final TestTransitionStateListener listener = new TestTransitionStateListener();
+        mRecentsTransitionHandler.addTransitionStateListener(listener);
+
+        final IBinder transition = startRecentsTransition(/* synthetic= */ false);
+        mRecentsTransitionHandler.startAnimation(
+                transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(),
+                mock(Transitions.TransitionFinishCallback.class));
+        mMainExecutor.flushAll();
+
+        assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_ANIMATING);
+    }
+
+    @Test
+    public void testFinishTransition_updatesStateListeners() {
+        final TestTransitionStateListener listener = new TestTransitionStateListener();
+        mRecentsTransitionHandler.addTransitionStateListener(listener);
+
+        final IBinder transition = startRecentsTransition(/* synthetic= */ false);
+        mRecentsTransitionHandler.startAnimation(
+                transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(),
+                mock(Transitions.TransitionFinishCallback.class));
+        mRecentsTransitionHandler.findController(transition).finish(true /* toHome */,
+                false /* sendUserLeaveHint */, mock(IResultReceiver.class));
+        mMainExecutor.flushAll();
+
+        assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_NOT_RUNNING);
+    }
+
+    @Test
+    public void testCancelTransition_updatesStateListeners() {
+        final TestTransitionStateListener listener = new TestTransitionStateListener();
+        mRecentsTransitionHandler.addTransitionStateListener(listener);
+
+        final IBinder transition = startRecentsTransition(/* synthetic= */ false);
+        mRecentsTransitionHandler.findController(transition).cancel("test");
+        mMainExecutor.flushAll();
+
+        assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_NOT_RUNNING);
+    }
+
+    @Test
+    public void testStartAnimation_synthetic_updatesStateListeners() {
+        final TestTransitionStateListener listener = new TestTransitionStateListener();
+        mRecentsTransitionHandler.addTransitionStateListener(listener);
+
+        startRecentsTransition(/* synthetic= */ true);
+        mMainExecutor.flushAll();
+
+        assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_ANIMATING);
+    }
+
+    @Test
+    public void testFinishTransition_synthetic_updatesStateListeners() {
+        final TestTransitionStateListener listener = new TestTransitionStateListener();
+        mRecentsTransitionHandler.addTransitionStateListener(listener);
+
+        final IBinder transition = startRecentsTransition(/* synthetic= */ true);
+        mRecentsTransitionHandler.findController(transition).finish(true /* toHome */,
+                false /* sendUserLeaveHint */, mock(IResultReceiver.class));
+        mMainExecutor.flushAll();
+
+        assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_NOT_RUNNING);
+    }
+
+    @Test
+    public void testCancelTransition_synthetic_updatesStateListeners() {
+        final TestTransitionStateListener listener = new TestTransitionStateListener();
+        mRecentsTransitionHandler.addTransitionStateListener(listener);
+
+        final IBinder transition = startRecentsTransition(/* synthetic= */ true);
+        mRecentsTransitionHandler.findController(transition).cancel("test");
+        mMainExecutor.flushAll();
+
+        assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_NOT_RUNNING);
+    }
+
+    private IBinder startRecentsTransition(boolean synthetic) {
+        return startRecentsTransition(synthetic, mock(IRecentsAnimationRunner.class));
+    }
+
+    private IBinder startRecentsTransition(boolean synthetic,
+            @NonNull IRecentsAnimationRunner runner) {
+        doReturn(new Binder()).when(runner).asBinder();
+        final Bundle options = new Bundle();
+        options.putBoolean("is_synthetic_recents_transition", synthetic);
+        final IBinder transition = new Binder();
+        when(mTransitions.startTransition(anyInt(), any(), any())).thenReturn(transition);
+        return mRecentsTransitionHandler.startRecentsTransition(
+                mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class),
+                runner);
+    }
+
+    private TransitionInfo createTransitionInfo() {
+        final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder()
+                .setTopActivityType(ACTIVITY_TYPE_HOME)
+                .build();
+        final TransitionInfo.Change homeChange = new TransitionInfo.Change(
+                task.token, new SurfaceControl());
+        homeChange.setMode(TRANSIT_TO_FRONT);
+        homeChange.setTaskInfo(task);
+        return new TransitionInfoBuilder(TRANSIT_START_RECENTS_TRANSITION)
+                .addChange(homeChange)
+                .build();
+    }
+
+    private static class TestTransitionStateListener implements RecentsTransitionStateListener {
+        @RecentsTransitionState
+        private int mState = TRANSITION_STATE_NOT_RUNNING;
+
+        @Override
+        public void onTransitionStateChanged(int state) {
+            mState = state;
+        }
+
+        @RecentsTransitionState
+        int getState() {
+            return mState;
+        }
+    }
 }
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 6b41ddd..9da7bec 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -355,12 +355,14 @@
     APerformanceHint_getManager; # introduced=Tiramisu
     APerformanceHint_createSession; # introduced=Tiramisu
     APerformanceHint_getPreferredUpdateRateNanos; # introduced=Tiramisu
+    APerformanceHint_getMaxGraphicsPipelineThreadsCount; # introduced=36
     APerformanceHint_updateTargetWorkDuration; # introduced=Tiramisu
     APerformanceHint_reportActualWorkDuration; # introduced=Tiramisu
     APerformanceHint_closeSession; # introduced=Tiramisu
     APerformanceHint_setThreads; # introduced=UpsideDownCake
     APerformanceHint_setPreferPowerEfficiency; # introduced=VanillaIceCream
     APerformanceHint_reportActualWorkDuration2; # introduced=VanillaIceCream
+    APerformanceHint_createSessionUsingConfig; # introduced=36
     APerformanceHint_notifyWorkloadIncrease; # introduced=36
     APerformanceHint_notifyWorkloadReset; # introduced=36
     APerformanceHint_borrowSessionFromJava; # introduced=36
@@ -370,6 +372,12 @@
     AWorkDuration_setActualTotalDurationNanos; # introduced=VanillaIceCream
     AWorkDuration_setActualCpuDurationNanos; # introduced=VanillaIceCream
     AWorkDuration_setActualGpuDurationNanos; # introduced=VanillaIceCream
+    ASessionCreationConfig_create; # introduced=36
+    ASessionCreationConfig_release; # introduced=36
+    ASessionCreationConfig_setTids; # introduced=36
+    ASessionCreationConfig_setTargetWorkDurationNanos; # introduced=36
+    ASessionCreationConfig_setPreferPowerEfficiency; # introduced=36
+    ASessionCreationConfig_setGraphicsPipeline; # introduced=36
   local:
     *;
 };
@@ -379,8 +387,10 @@
     AThermal_setIThermalServiceForTesting;
     APerformanceHint_setIHintManagerForTesting;
     APerformanceHint_sendHint;
+    APerformanceHint_setUseGraphicsPipelineForTesting;
     APerformanceHint_getThreadIds;
     APerformanceHint_createSessionInternal;
+    APerformanceHint_createSessionUsingConfigInternal;
     APerformanceHint_setUseFMQForTesting;
     APerformanceHint_getRateLimiterPropertiesForTesting;
     APerformanceHint_setUseNewLoadHintBehaviorForTesting;
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index bc1945e..c67e93c 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -26,6 +26,7 @@
 #include <aidl/android/hardware/power/WorkDurationFixedV1.h>
 #include <aidl/android/os/IHintManager.h>
 #include <aidl/android/os/IHintSession.h>
+#include <aidl/android/os/SessionCreationConfig.h>
 #include <android-base/stringprintf.h>
 #include <android-base/thread_annotations.h>
 #include <android/binder_manager.h>
@@ -65,6 +66,13 @@
 
 constexpr int64_t SEND_HINT_TIMEOUT = std::chrono::nanoseconds(100ms).count();
 struct AWorkDuration : public hal::WorkDuration {};
+struct ASessionCreationConfig : public SessionCreationConfig {};
+
+bool kForceGraphicsPipeline = false;
+
+bool useGraphicsPipeline() {
+    return android::os::adpf_graphics_pipeline() || kForceGraphicsPipeline;
+}
 
 // A pair of values that determine the behavior of the
 // load hint rate limiter, to only allow "X hints every Y seconds"
@@ -142,7 +150,11 @@
                                            bool isJava = false);
     APerformanceHintSession* getSessionFromJava(JNIEnv* _Nonnull env, jobject _Nonnull sessionObj);
 
+    APerformanceHintSession* createSessionUsingConfig(ASessionCreationConfig* sessionCreationConfig,
+                                                      hal::SessionTag tag = hal::SessionTag::APP,
+                                                      bool isJava = false);
     int64_t getPreferredRateNanos() const;
+    int32_t getMaxGraphicsPipelineThreadsCount();
     FMQWrapper& getFMQWrapper();
     bool canSendLoadHints(std::vector<hal::SessionHint>& hints, int64_t now) REQUIRES(sHintMutex);
     void initJava(JNIEnv* _Nonnull env);
@@ -163,6 +175,7 @@
     std::shared_ptr<IHintManager> mHintManager;
     ndk::SpAIBinder mToken;
     const int64_t mPreferredRateNanos;
+    std::optional<int32_t> mMaxGraphicsPipelineThreadsCount;
     FMQWrapper mFMQWrapper;
     double mHintBudget = kMaxLoadHintsPerInterval;
     int64_t mLastBudgetReplenish = 0;
@@ -220,8 +233,10 @@
     std::vector<int32_t> mLastThreadIDs GUARDED_BY(sHintMutex);
     std::optional<hal::SessionConfig> mSessionConfig GUARDED_BY(sHintMutex);
     // Tracing helpers
-    void traceThreads(std::vector<int32_t>& tids) REQUIRES(sHintMutex);
+    void traceThreads(const std::vector<int32_t>& tids) REQUIRES(sHintMutex);
     void tracePowerEfficient(bool powerEfficient);
+    void traceGraphicsPipeline(bool graphicsPipeline);
+    void traceModes(const std::vector<hal::SessionMode>& modesToEnable);
     void traceActualDuration(int64_t actualDuration);
     void traceBatchSize(size_t batchSize);
     void traceTargetDuration(int64_t targetDuration);
@@ -311,27 +326,45 @@
 APerformanceHintSession* APerformanceHintManager::createSession(
         const int32_t* threadIds, size_t size, int64_t initialTargetWorkDurationNanos,
         hal::SessionTag tag, bool isJava) {
-    std::vector<int32_t> tids(threadIds, threadIds + size);
-    std::shared_ptr<IHintSession> session;
     ndk::ScopedAStatus ret;
     hal::SessionConfig sessionConfig{.id = -1};
-    ret = mHintManager->createHintSessionWithConfig(mToken, tids, initialTargetWorkDurationNanos,
-                                                    tag, &sessionConfig, &session);
+
+    SessionCreationConfig creationConfig{
+            .tids = std::vector<int32_t>(threadIds, threadIds + size),
+            .targetWorkDurationNanos = initialTargetWorkDurationNanos,
+    };
+
+    return APerformanceHintManager::createSessionUsingConfig(static_cast<ASessionCreationConfig*>(
+                                                                     &creationConfig),
+                                                             tag, isJava);
+}
+
+APerformanceHintSession* APerformanceHintManager::createSessionUsingConfig(
+        ASessionCreationConfig* sessionCreationConfig, hal::SessionTag tag, bool isJava) {
+    std::shared_ptr<IHintSession> session;
+    hal::SessionConfig sessionConfig{.id = -1};
+    ndk::ScopedAStatus ret;
+
+    ret = mHintManager->createHintSessionWithConfig(mToken, tag,
+                                                    *static_cast<SessionCreationConfig*>(
+                                                            sessionCreationConfig),
+                                                    &sessionConfig, &session);
 
     if (!ret.isOk() || !session) {
         ALOGE("%s: PerformanceHint cannot create session. %s", __FUNCTION__, ret.getMessage());
         return nullptr;
     }
     auto out = new APerformanceHintSession(mHintManager, std::move(session), mPreferredRateNanos,
-                                           initialTargetWorkDurationNanos, isJava,
+                                           sessionCreationConfig->targetWorkDurationNanos, isJava,
                                            sessionConfig.id == -1
                                                    ? std::nullopt
                                                    : std::make_optional<hal::SessionConfig>(
                                                              std::move(sessionConfig)));
     std::scoped_lock lock(sHintMutex);
-    out->traceThreads(tids);
-    out->traceTargetDuration(initialTargetWorkDurationNanos);
-    out->tracePowerEfficient(false);
+    out->traceThreads(sessionCreationConfig->tids);
+    out->traceTargetDuration(sessionCreationConfig->targetWorkDurationNanos);
+    out->traceModes(sessionCreationConfig->modesToEnable);
+
     return out;
 }
 
@@ -351,6 +384,23 @@
     return mPreferredRateNanos;
 }
 
+int32_t APerformanceHintManager::getMaxGraphicsPipelineThreadsCount() {
+    if (!mMaxGraphicsPipelineThreadsCount.has_value()) {
+        int32_t threadsCount = -1;
+        ndk::ScopedAStatus ret = mHintManager->getMaxGraphicsPipelineThreadsCount(&threadsCount);
+        if (!ret.isOk()) {
+            ALOGE("%s: PerformanceHint cannot get max graphics pipeline threads count. %s",
+                  __FUNCTION__, ret.getMessage());
+            return -1;
+        }
+        if (threadsCount <= 0) {
+            threadsCount = -1;
+        }
+        mMaxGraphicsPipelineThreadsCount.emplace(threadsCount);
+    }
+    return mMaxGraphicsPipelineThreadsCount.value();
+}
+
 FMQWrapper& APerformanceHintManager::getFMQWrapper() {
     return mFMQWrapper;
 }
@@ -787,7 +837,7 @@
 
 // ===================================== Tracing helpers
 
-void APerformanceHintSession::traceThreads(std::vector<int32_t>& tids) {
+void APerformanceHintSession::traceThreads(const std::vector<int32_t>& tids) {
     std::set<int32_t> tidSet{tids.begin(), tids.end()};
 
     // Disable old TID tracing
@@ -813,6 +863,28 @@
     ATrace_setCounter((mSessionName + " power efficiency mode").c_str(), powerEfficient);
 }
 
+void APerformanceHintSession::traceGraphicsPipeline(bool graphicsPipeline) {
+    ATrace_setCounter((mSessionName + " graphics pipeline mode").c_str(), graphicsPipeline);
+}
+
+void APerformanceHintSession::traceModes(const std::vector<hal::SessionMode>& modesToEnable) {
+    // Iterate through all modes to trace, set to enable for all modes in modesToEnable,
+    // and set to disable for those are not.
+    for (hal::SessionMode mode :
+         {hal::SessionMode::POWER_EFFICIENCY, hal::SessionMode::GRAPHICS_PIPELINE}) {
+        bool isEnabled =
+                find(modesToEnable.begin(), modesToEnable.end(), mode) != modesToEnable.end();
+        switch (mode) {
+            case hal::SessionMode::POWER_EFFICIENCY:
+                tracePowerEfficient(isEnabled);
+                break;
+            case hal::SessionMode::GRAPHICS_PIPELINE:
+                traceGraphicsPipeline(isEnabled);
+                break;
+        }
+    }
+}
+
 void APerformanceHintSession::traceActualDuration(int64_t actualDuration) {
     ATrace_setCounter((mSessionName + " actual duration").c_str(), actualDuration);
 }
@@ -855,6 +927,22 @@
     return manager->createSession(threadIds, size, initialTargetWorkDurationNanos);
 }
 
+APerformanceHintSession* APerformanceHint_createSessionUsingConfig(
+        APerformanceHintManager* manager, ASessionCreationConfig* sessionCreationConfig) {
+    VALIDATE_PTR(manager);
+    VALIDATE_PTR(sessionCreationConfig);
+    return manager->createSessionUsingConfig(sessionCreationConfig);
+}
+
+APerformanceHintSession* APerformanceHint_createSessionUsingConfigInternal(
+        APerformanceHintManager* manager, ASessionCreationConfig* sessionCreationConfig,
+        SessionTag tag) {
+    VALIDATE_PTR(manager);
+    VALIDATE_PTR(sessionCreationConfig);
+    return manager->createSessionUsingConfig(sessionCreationConfig,
+                                             static_cast<hal::SessionTag>(tag));
+}
+
 APerformanceHintSession* APerformanceHint_createSessionInternal(
         APerformanceHintManager* manager, const int32_t* threadIds, size_t size,
         int64_t initialTargetWorkDurationNanos, SessionTag tag) {
@@ -885,6 +973,11 @@
     return manager->getPreferredRateNanos();
 }
 
+int APerformanceHint_getMaxGraphicsPipelineThreadsCount(APerformanceHintManager* manager) {
+    VALIDATE_PTR(manager);
+    return manager->getMaxGraphicsPipelineThreadsCount();
+}
+
 int APerformanceHint_updateTargetWorkDuration(APerformanceHintSession* session,
                                               int64_t targetDurationNanos) {
     VALIDATE_PTR(session)
@@ -1018,6 +1111,81 @@
     gForceFMQEnabled = enabled;
 }
 
+ASessionCreationConfig* ASessionCreationConfig_create() {
+    return new ASessionCreationConfig();
+}
+
+void ASessionCreationConfig_release(ASessionCreationConfig* config) {
+    VALIDATE_PTR(config)
+
+    delete config;
+}
+
+int ASessionCreationConfig_setTids(ASessionCreationConfig* config, const pid_t* tids, size_t size) {
+    VALIDATE_PTR(config)
+    VALIDATE_PTR(tids)
+
+    if (!useGraphicsPipeline()) {
+        return ENOTSUP;
+    }
+
+    if (size <= 0) {
+        LOG_ALWAYS_FATAL_IF(size <= 0,
+                            "%s: Invalid value. Thread id list size should be greater than zero.",
+                            __FUNCTION__);
+        return EINVAL;
+    }
+    config->tids = std::vector<int32_t>(tids, tids + size);
+    return 0;
+}
+
+int ASessionCreationConfig_setTargetWorkDurationNanos(ASessionCreationConfig* config,
+                                                      int64_t targetWorkDurationNanos) {
+    VALIDATE_PTR(config)
+    VALIDATE_INT(targetWorkDurationNanos, >= 0)
+
+    if (!useGraphicsPipeline()) {
+        return ENOTSUP;
+    }
+
+    config->targetWorkDurationNanos = targetWorkDurationNanos;
+    return 0;
+}
+
+int ASessionCreationConfig_setPreferPowerEfficiency(ASessionCreationConfig* config, bool enabled) {
+    VALIDATE_PTR(config)
+
+    if (!useGraphicsPipeline()) {
+        return ENOTSUP;
+    }
+
+    if (enabled) {
+        config->modesToEnable.push_back(hal::SessionMode::POWER_EFFICIENCY);
+    } else {
+        std::erase(config->modesToEnable, hal::SessionMode::POWER_EFFICIENCY);
+    }
+    return 0;
+}
+
+int ASessionCreationConfig_setGraphicsPipeline(ASessionCreationConfig* config, bool enabled) {
+    VALIDATE_PTR(config)
+
+    if (!useGraphicsPipeline()) {
+        return ENOTSUP;
+    }
+
+    if (enabled) {
+        config->modesToEnable.push_back(hal::SessionMode::GRAPHICS_PIPELINE);
+    } else {
+        std::erase(config->modesToEnable, hal::SessionMode::GRAPHICS_PIPELINE);
+    }
+    return 0;
+}
+
+void APerformanceHint_setUseGraphicsPipelineForTesting(bool enabled) {
+    kForceGraphicsPipeline = enabled;
+}
+
 void APerformanceHint_getRateLimiterPropertiesForTesting(int32_t* maxLoadHintsPerInterval,
                                                          int64_t* loadHintInterval) {
     *maxLoadHintsPerInterval = kMaxLoadHintsPerInterval;
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index f707a0e..c5fb808 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -18,9 +18,11 @@
 
 #include <aidl/android/hardware/power/ChannelConfig.h>
 #include <aidl/android/hardware/power/SessionConfig.h>
+#include <aidl/android/hardware/power/SessionMode.h>
 #include <aidl/android/hardware/power/SessionTag.h>
 #include <aidl/android/hardware/power/WorkDuration.h>
 #include <aidl/android/os/IHintManager.h>
+#include <aidl/android/os/SessionCreationConfig.h>
 #include <android/binder_manager.h>
 #include <android/binder_status.h>
 #include <android/performance_hint.h>
@@ -36,6 +38,7 @@
 namespace hal = aidl::android::hardware::power;
 using aidl::android::os::IHintManager;
 using aidl::android::os::IHintSession;
+using aidl::android::os::SessionCreationConfig;
 using ndk::ScopedAStatus;
 using ndk::SpAIBinder;
 using HalChannelMessageContents = hal::ChannelMessage::ChannelMessageContents;
@@ -49,11 +52,13 @@
 class MockIHintManager : public IHintManager {
 public:
     MOCK_METHOD(ScopedAStatus, createHintSessionWithConfig,
-                (const SpAIBinder& token, const ::std::vector<int32_t>& tids, int64_t durationNanos,
-                 hal::SessionTag tag, hal::SessionConfig* config,
+                (const SpAIBinder& token, hal::SessionTag tag,
+                 const SessionCreationConfig& creationConfig, hal::SessionConfig* config,
                  std::shared_ptr<IHintSession>* _aidl_return),
                 (override));
     MOCK_METHOD(ScopedAStatus, getHintSessionPreferredRate, (int64_t * _aidl_return), (override));
+    MOCK_METHOD(ScopedAStatus, getMaxGraphicsPipelineThreadsCount, (int32_t* _aidl_return),
+                (override));
     MOCK_METHOD(ScopedAStatus, setHintSessionThreads,
                 (const std::shared_ptr<IHintSession>& hintSession,
                  const ::std::vector<int32_t>& tids),
@@ -105,6 +110,7 @@
         APerformanceHint_getRateLimiterPropertiesForTesting(&mMaxLoadHintsPerInterval,
                                                             &mLoadHintInterval);
         APerformanceHint_setIHintManagerForTesting(&mMockIHintManager);
+        APerformanceHint_setUseGraphicsPipelineForTesting(true);
         APerformanceHint_setUseNewLoadHintBehaviorForTesting(true);
     }
 
@@ -118,21 +124,22 @@
         APerformanceHint_setUseFMQForTesting(mUsingFMQ);
         ON_CALL(*mMockIHintManager, getHintSessionPreferredRate(_))
                 .WillByDefault(DoAll(SetArgPointee<0>(123L), [] { return ScopedAStatus::ok(); }));
+        ON_CALL(*mMockIHintManager, getMaxGraphicsPipelineThreadsCount(_))
+                .WillByDefault(DoAll(SetArgPointee<0>(5), [] { return ScopedAStatus::ok(); }));
         return APerformanceHint_getManager();
     }
 
     APerformanceHintSession* createSession(APerformanceHintManager* manager,
                                            int64_t targetDuration = 56789L, bool isHwui = false) {
         mMockSession = ndk::SharedRefBase::make<NiceMock<MockIHintSession>>();
-        int64_t sessionId = 123;
+        const int64_t sessionId = 123;
         std::vector<int32_t> tids;
         tids.push_back(1);
         tids.push_back(2);
 
-        ON_CALL(*mMockIHintManager,
-                createHintSessionWithConfig(_, Eq(tids), Eq(targetDuration), _, _, _))
-                .WillByDefault(DoAll(SetArgPointee<4>(hal::SessionConfig({.id = sessionId})),
-                                     SetArgPointee<5>(std::shared_ptr<IHintSession>(mMockSession)),
+        ON_CALL(*mMockIHintManager, createHintSessionWithConfig(_, _, _, _, _))
+                .WillByDefault(DoAll(SetArgPointee<3>(hal::SessionConfig({.id = sessionId})),
+                                     SetArgPointee<4>(std::shared_ptr<IHintSession>(mMockSession)),
                                      [] { return ScopedAStatus::ok(); }));
 
         ON_CALL(*mMockIHintManager, setHintSessionThreads(_, _)).WillByDefault([] {
@@ -157,6 +164,43 @@
         return APerformanceHint_createSession(manager, tids.data(), tids.size(), targetDuration);
     }
 
+    APerformanceHintSession* createSessionUsingConfig(APerformanceHintManager* manager,
+                                                      SessionCreationConfig config,
+                                                      bool isHwui = false) {
+        mMockSession = ndk::SharedRefBase::make<NiceMock<MockIHintSession>>();
+        const int64_t sessionId = 123;
+
+        ON_CALL(*mMockIHintManager, createHintSessionWithConfig(_, _, _, _, _))
+                .WillByDefault(DoAll(SetArgPointee<3>(hal::SessionConfig({.id = sessionId})),
+                                     SetArgPointee<4>(std::shared_ptr<IHintSession>(mMockSession)),
+                                     [] { return ScopedAStatus::ok(); }));
+
+        ON_CALL(*mMockIHintManager, setHintSessionThreads(_, _)).WillByDefault([] {
+            return ScopedAStatus::ok();
+        });
+        ON_CALL(*mMockSession, sendHint(_)).WillByDefault([] { return ScopedAStatus::ok(); });
+        ON_CALL(*mMockSession, setMode(_, true)).WillByDefault([] { return ScopedAStatus::ok(); });
+        ON_CALL(*mMockSession, close()).WillByDefault([] { return ScopedAStatus::ok(); });
+        ON_CALL(*mMockSession, updateTargetWorkDuration(_)).WillByDefault([] {
+            return ScopedAStatus::ok();
+        });
+        ON_CALL(*mMockSession, reportActualWorkDuration(_, _)).WillByDefault([] {
+            return ScopedAStatus::ok();
+        });
+        ON_CALL(*mMockSession, reportActualWorkDuration2(_)).WillByDefault([] {
+            return ScopedAStatus::ok();
+        });
+
+        if (isHwui) {
+            return APerformanceHint_createSessionUsingConfigInternal(
+                    manager, reinterpret_cast<ASessionCreationConfig*>(&config), SessionTag::HWUI);
+        }
+
+        return APerformanceHint_createSessionUsingConfig(manager,
+                                                         reinterpret_cast<ASessionCreationConfig*>(
+                                                                 &config));
+    }
+
     void setFMQEnabled(bool enabled) {
         mUsingFMQ = enabled;
         if (enabled) {
@@ -272,16 +316,26 @@
 }
 
 TEST_F(PerformanceHintTest, TestUpdatedSessionCreation) {
-    EXPECT_CALL(*mMockIHintManager, createHintSessionWithConfig(_, _, _, _, _, _)).Times(1);
+    EXPECT_CALL(*mMockIHintManager, createHintSessionWithConfig(_, _, _, _, _)).Times(1);
     APerformanceHintManager* manager = createManager();
     APerformanceHintSession* session = createSession(manager);
     ASSERT_TRUE(session);
     APerformanceHint_closeSession(session);
 }
 
+TEST_F(PerformanceHintTest, TestSessionCreationUsingConfig) {
+    EXPECT_CALL(*mMockIHintManager, createHintSessionWithConfig(_, _, _, _, _)).Times(1);
+    SessionCreationConfig config{.tids = std::vector<int32_t>(1, 2),
+                                 .targetWorkDurationNanos = 5678,
+                                 .modesToEnable = std::vector<hal::SessionMode>(0)};
+    APerformanceHintManager* manager = createManager();
+    APerformanceHintSession* session = createSessionUsingConfig(manager, config);
+    ASSERT_TRUE(session);
+    APerformanceHint_closeSession(session);
+}
+
 TEST_F(PerformanceHintTest, TestHwuiSessionCreation) {
-    EXPECT_CALL(*mMockIHintManager,
-                createHintSessionWithConfig(_, _, _, hal::SessionTag::HWUI, _, _))
+    EXPECT_CALL(*mMockIHintManager, createHintSessionWithConfig(_, hal::SessionTag::HWUI, _, _, _))
             .Times(1);
     APerformanceHintManager* manager = createManager();
     APerformanceHintSession* session = createSession(manager, 56789L, true);
@@ -447,3 +501,16 @@
                                                reinterpret_cast<AWorkDuration*>(&duration));
     expectToReadFromFmq<HalChannelMessageContents::Tag::workDuration>(durationExpected);
 }
+
+TEST_F(PerformanceHintTest, TestASessionCreationConfig) {
+    ASessionCreationConfig* config = ASessionCreationConfig_create();
+    ASSERT_NE(config, nullptr);
+
+    const int32_t testTids[2] = {1, 2};
+    const size_t size = 2;
+    EXPECT_EQ(ASessionCreationConfig_setTids(config, testTids, size), 0);
+    EXPECT_EQ(ASessionCreationConfig_setTargetWorkDurationNanos(config, 20), 0);
+    EXPECT_EQ(ASessionCreationConfig_setPreferPowerEfficiency(config, true), 0);
+    EXPECT_EQ(ASessionCreationConfig_setGraphicsPipeline(config, true), 0);
+    ASessionCreationConfig_release(config);
+}
diff --git a/services/core/java/com/android/server/input/AppLaunchShortcutManager.java b/services/core/java/com/android/server/input/AppLaunchShortcutManager.java
index f3820e5..8c028bc 100644
--- a/services/core/java/com/android/server/input/AppLaunchShortcutManager.java
+++ b/services/core/java/com/android/server/input/AppLaunchShortcutManager.java
@@ -210,16 +210,16 @@
 
     /**
      * Handle the shortcut to {@link IShortcutService}
-     * @param keyCode The key code of the event.
-     * @param metaState The meta key modifier state.
-     * @return True if invoked the shortcut, otherwise false.
+     * @return true if invoked the shortcut, otherwise false.
      */
-    private boolean handleShortcutService(int keyCode, int metaState) {
-        final long shortcutCodeMeta = metaState & SHORTCUT_CODE_META_MASK;
+    public boolean handleShortcutService(KeyEvent event) {
+        // TODO(b/358569822): Ideally shortcut service custom shortcuts should be either
+        //  migrated to bookmarks or customizable shortcut APIs.
+        final long shortcutCodeMeta = event.getMetaState() & SHORTCUT_CODE_META_MASK;
         if (shortcutCodeMeta == 0) {
             return false;
         }
-        long shortcutCode = keyCode | (shortcutCodeMeta << Integer.SIZE);
+        long shortcutCode = event.getKeyCode() | (shortcutCodeMeta << Integer.SIZE);
         IShortcutService shortcutService = mShortcutKeyServices.get(shortcutCode);
         if (shortcutService != null) {
             try {
@@ -292,7 +292,6 @@
             return InterceptKeyResult.DO_NOTHING;
         }
 
-        final int metaState = event.getModifiers();
         final int keyCode = event.getKeyCode();
         if (keyCode == KeyEvent.KEYCODE_SEARCH) {
             if (event.getAction() == KeyEvent.ACTION_DOWN) {
@@ -313,15 +312,7 @@
         }
 
         // Intercept shortcuts defined in bookmarks or through application launch keycodes
-        AppLaunchData appLaunchData = interceptShortcut(event);
-
-        // TODO(b/358569822): Ideally shortcut service custom shortcuts should be either
-        //  migrated to bookmarks or customizable shortcut APIs.
-        if (appLaunchData == null && handleShortcutService(keyCode, metaState)) {
-            return InterceptKeyResult.CONSUME_KEY;
-        }
-
-        return new InterceptKeyResult(/* consumed =*/ false, appLaunchData);
+        return new InterceptKeyResult(/* consumed =*/ false, interceptShortcut(event));
     }
 
     /**
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index bb0b190..55d2de2 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -793,6 +793,11 @@
                 return true;
         }
 
+        // Handle shortcuts through shortcut services
+        if (mAppLaunchShortcutManager.handleShortcutService(event)) {
+            return true;
+        }
+
         // Handle custom shortcuts
         if (firstDown) {
             InputGestureData customGesture;
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 7c9d9c5..76049ca 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -325,7 +325,7 @@
             return;
         }
 
-        if (Flags.offloadApi()) {
+        if (Flags.offloadApi() && Flags.offloadImplementation()) {
             HubInfoRegistry registry;
             try {
                 registry = new HubInfoRegistry(mContextHubWrapper);
@@ -527,8 +527,8 @@
         try {
             mContextHubWrapper.registerEndpointCallback(
                     new ContextHubHalEndpointCallback(mHubInfoRegistry));
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException while registering IEndpointCallback", e);
+        } catch (RemoteException | UnsupportedOperationException e) {
+            Log.e(TAG, "Exception while registering IEndpointCallback", e);
         }
     }
 
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index e6f784c..749952e 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -184,6 +184,7 @@
     private static final boolean DEFAULT_SHOW_BADGE = true;
 
     private static final boolean DEFAULT_APP_LOCKED_IMPORTANCE  = false;
+    private static final boolean DEFAULT_CAN_HAVE_PROMOTED_NOTIFS = true;
 
     static final boolean DEFAULT_BUBBLES_ENABLED = true;
     @VisibleForTesting
@@ -369,8 +370,8 @@
                     null, ATT_USER_DEMOTED_INVALID_MSG_APP, false);
             r.hasSentValidBubble = parser.getAttributeBoolean(null, ATT_SENT_VALID_BUBBLE, false);
             if (android.app.Flags.uiRichOngoing()) {
-                r.canHavePromotedNotifs =
-                        parser.getAttributeBoolean(null, ATT_PROMOTE_NOTIFS, false);
+                r.canHavePromotedNotifs = parser.getAttributeBoolean(null, ATT_PROMOTE_NOTIFS,
+                        DEFAULT_CAN_HAVE_PROMOTED_NOTIFS);
             }
 
             final int innerDepth = parser.getDepth();
@@ -748,7 +749,7 @@
                 r.userDemotedMsgApp);
         out.attributeBoolean(null, ATT_SENT_VALID_BUBBLE, r.hasSentValidBubble);
         if (android.app.Flags.uiRichOngoing()) {
-            if (r.canHavePromotedNotifs) {
+            if (r.canHavePromotedNotifs != DEFAULT_CAN_HAVE_PROMOTED_NOTIFS) {
                 out.attributeBoolean(null, ATT_PROMOTE_NOTIFS, r.canHavePromotedNotifs);
             }
         }
@@ -2331,7 +2332,8 @@
                     pw.print(" fixedImportance=");
                     pw.print(r.fixedImportance);
                 }
-                if (android.app.Flags.uiRichOngoing() && r.canHavePromotedNotifs) {
+                if (android.app.Flags.uiRichOngoing()
+                        && r.canHavePromotedNotifs != DEFAULT_CAN_HAVE_PROMOTED_NOTIFS) {
                     pw.print(" promoted=");
                     pw.print(r.canHavePromotedNotifs);
                 }
@@ -3184,7 +3186,8 @@
         long creationTime;
 
         @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
-        boolean canHavePromotedNotifs = false;
+        // Until we enable the UI, we should return false.
+        boolean canHavePromotedNotifs = android.app.Flags.uiRichOngoing();
 
         @UserIdInt int userId;
 
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 17459df..71f67d8 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -52,6 +52,7 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SessionCreationConfig;
 import android.os.SystemProperties;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -70,6 +71,7 @@
 import com.android.server.LocalServices;
 import com.android.server.ServiceThread;
 import com.android.server.SystemService;
+import com.android.server.power.hint.HintManagerService.AppHintSession.SessionModes;
 import com.android.server.utils.Slogf;
 
 import java.io.FileDescriptor;
@@ -102,6 +104,8 @@
 
     @VisibleForTesting final long mHintSessionPreferredRate;
 
+    @VisibleForTesting static final int MAX_GRAPHICS_PIPELINE_THREADS_COUNT = 5;
+
     // Multi-level map storing all active AppHintSessions.
     // First level is keyed by the UID of the client process creating the session.
     // Second level is keyed by an IBinder passed from client process. This is used to observe
@@ -129,6 +133,14 @@
     @GuardedBy("mSessionSnapshotMapLock")
     private ArrayMap<Integer, ArrayMap<Integer, AppHintSessionSnapshot>> mSessionSnapshotMap;
 
+    /*
+     * App UID to Thread mapping.
+     * Thread is a sub class bookkeeping TID, thread mode (especially graphics pipeline mode)
+     * This is to bookkeep and track the thread usage.
+     */
+    @GuardedBy("mThreadsUsageObject")
+    private ArrayMap<Integer, ArraySet<ThreadUsageTracker>> mThreadsUsageMap;
+
     /** Lock to protect mActiveSessions and the UidObserver. */
     private final Object mLock = new Object();
 
@@ -144,6 +156,9 @@
      */
     private final Object mSessionSnapshotMapLock = new Object();
 
+    /** Lock to protect mThreadsUsageMap. */
+    private final Object mThreadsUsageObject = new Object();
+
     @GuardedBy("mNonIsolatedTidsLock")
     private final Map<Integer, Set<Long>> mNonIsolatedTids;
 
@@ -262,6 +277,7 @@
         mActiveSessions = new ArrayMap<>();
         mChannelMap = new ArrayMap<>();
         mSessionSnapshotMap = new ArrayMap<>();
+        mThreadsUsageMap = new ArrayMap<>();
         mNativeWrapper = injector.createNativeWrapper();
         mNativeWrapper.halInit();
         mHintSessionPreferredRate = mNativeWrapper.halGetHintSessionPreferredRate();
@@ -357,6 +373,36 @@
         }
     }
 
+    private static class ThreadUsageTracker {
+        /*
+         * Thread object for tracking thread usage per UID
+         */
+        int mTid;
+        boolean mIsGraphicsPipeline;
+
+        ThreadUsageTracker(int tid) {
+            mTid = tid;
+            mIsGraphicsPipeline = false;
+        }
+
+        ThreadUsageTracker(int tid, boolean isGraphicsPipeline) {
+            mTid = tid;
+            mIsGraphicsPipeline = isGraphicsPipeline;
+        }
+
+        public int getTid() {
+            return mTid;
+        }
+
+        public boolean isGraphicsPipeline() {
+            return mIsGraphicsPipeline;
+        }
+
+        public void setGraphicsPipeline(boolean isGraphicsPipeline) {
+            mIsGraphicsPipeline = isGraphicsPipeline;
+        }
+    }
+
     private class AppHintSessionSnapshot {
         /*
          * Per-Uid and Per-SessionTag snapshot that tracks metrics including
@@ -368,6 +414,7 @@
         int mMaxConcurrentSession;
         int mMaxThreadCount;
         int mPowerEfficientSessionCount;
+        int mGraphicsPipelineSessionCount;
 
         final int mTargetDurationNsCountPQSize = 100;
         PriorityQueue<TargetDurationRecord> mTargetDurationNsCountPQ;
@@ -425,6 +472,7 @@
             mMaxConcurrentSession = 0;
             mMaxThreadCount = 0;
             mPowerEfficientSessionCount = 0;
+            mGraphicsPipelineSessionCount = 0;
             mTargetDurationNsCountPQ = new PriorityQueue<>(1);
         }
 
@@ -443,6 +491,10 @@
             mPowerEfficientSessionCount += 1;
         }
 
+        void logGraphicsPipelineSession() {
+            mGraphicsPipelineSessionCount += 1;
+        }
+
         void updateThreadCount(int threadCount) {
             mMaxThreadCount = Math.max(mMaxThreadCount, threadCount);
         }
@@ -473,6 +525,10 @@
             return mPowerEfficientSessionCount;
         }
 
+        int getGraphicsPipelineSessionCount() {
+            return mGraphicsPipelineSessionCount;
+        }
+
         long[] targetDurationNsList() {
             final int listSize = 5;
             long[] targetDurations = new long[listSize];
@@ -1163,24 +1219,40 @@
                 + " doesn't belong to the calling application " + callingUid;
     }
 
+    private boolean contains(final int[] array, final int target) {
+        for (int element : array) {
+            if (element == target) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @VisibleForTesting
     final class BinderService extends IHintManager.Stub {
         @Override
         public IHintSession createHintSessionWithConfig(@NonNull IBinder token,
-                @NonNull int[] tids, long durationNanos, @SessionTag int tag,
-                SessionConfig config) {
+                    @SessionTag int tag, SessionCreationConfig creationConfig,
+                    SessionConfig config) {
             if (!isHalSupported()) {
                 throw new UnsupportedOperationException("PowerHAL is not supported!");
             }
 
             java.util.Objects.requireNonNull(token);
-            java.util.Objects.requireNonNull(tids);
+            java.util.Objects.requireNonNull(creationConfig.tids);
+
+            final int[] tids = creationConfig.tids;
             Preconditions.checkArgument(tids.length != 0, "tids should"
                     + " not be empty.");
 
+
             final int callingUid = Binder.getCallingUid();
             final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
             final long identity = Binder.clearCallingIdentity();
+            final long durationNanos = creationConfig.targetWorkDurationNanos;
+
+            Preconditions.checkArgument(checkGraphicsPipelineValid(creationConfig, callingUid),
+                    "not enough of available graphics pipeline thread.");
             try {
                 final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray(tids.length)
                         : null;
@@ -1274,8 +1346,9 @@
                             .computeIfAbsent(tag, k -> new AppHintSessionSnapshot())
                             .updateUponSessionCreation(tids.length, durationNanos);
                 }
+                AppHintSession hs = null;
                 synchronized (mLock) {
-                    AppHintSession hs = new AppHintSession(callingUid, callingTgid, tag, tids,
+                    hs = new AppHintSession(callingUid, callingTgid, tag, tids,
                             token, halSessionPtr, durationNanos);
                     ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap =
                             mActiveSessions.get(callingUid);
@@ -1290,9 +1363,29 @@
                     }
                     sessionSet.add(hs);
                     mUsesFmq = mUsesFmq || hasChannel(callingTgid, callingUid);
-
-                    return hs;
                 }
+
+                if (hs != null) {
+                    boolean isGraphicsPipeline = false;
+                    if (creationConfig.modesToEnable != null) {
+                        for (int sessionMode : creationConfig.modesToEnable) {
+                            if (sessionMode == SessionModes.GRAPHICS_PIPELINE.ordinal()) {
+                                isGraphicsPipeline = true;
+                            }
+                            hs.setMode(sessionMode, true);
+                        }
+                    }
+
+                    synchronized (mThreadsUsageObject) {
+                        mThreadsUsageMap.computeIfAbsent(callingUid, k -> new ArraySet<>());
+                        ArraySet<ThreadUsageTracker> threadsSet = mThreadsUsageMap.get(callingUid);
+                        for (int i = 0; i < tids.length; ++i) {
+                            threadsSet.add(new ThreadUsageTracker(tids[i], isGraphicsPipeline));
+                        }
+                    }
+                }
+
+                return hs;
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
@@ -1335,6 +1428,11 @@
         }
 
         @Override
+        public int getMaxGraphicsPipelineThreadsCount() {
+            return MAX_GRAPHICS_PIPELINE_THREADS_COUNT;
+        }
+
+        @Override
         public void setHintSessionThreads(@NonNull IHintSession hintSession, @NonNull int[] tids) {
             AppHintSession appHintSession = (AppHintSession) hintSession;
             appHintSession.setThreads(tids);
@@ -1470,6 +1568,7 @@
                 return;
             }
             pw.println("HintSessionPreferredRate: " + mHintSessionPreferredRate);
+            pw.println("MaxGraphicsPipelineThreadsCount: " + MAX_GRAPHICS_PIPELINE_THREADS_COUNT);
             pw.println("HAL Support: " + isHalSupported());
             pw.println("Active Sessions:");
             synchronized (mLock) {
@@ -1507,6 +1606,45 @@
             }
         }
 
+        private boolean checkGraphicsPipelineValid(SessionCreationConfig creationConfig, int uid) {
+            if (creationConfig.modesToEnable == null) {
+                return true;
+            }
+            boolean setGraphicsPipeline = false;
+            for (int modeToEnable : creationConfig.modesToEnable) {
+                if (modeToEnable == SessionModes.GRAPHICS_PIPELINE.ordinal()) {
+                    setGraphicsPipeline = true;
+                }
+            }
+            if (!setGraphicsPipeline) {
+                return true;
+            }
+
+            synchronized (mThreadsUsageObject) {
+                // count used graphics pipeline threads for the calling UID
+                // consider the case that new tids are overlapping with in session tids
+                ArraySet<ThreadUsageTracker> threadsSet = mThreadsUsageMap.get(uid);
+                if (threadsSet == null) {
+                    return true;
+                }
+
+                final int newThreadCount = creationConfig.tids.length;
+                int graphicsPipelineThreadCount = 0;
+                for (ThreadUsageTracker t : threadsSet) {
+                    // count graphics pipeline threads in use
+                    // and exclude overlapping ones
+                    if (t.isGraphicsPipeline()) {
+                        graphicsPipelineThreadCount++;
+                        if (contains(creationConfig.tids, t.getTid())) {
+                            graphicsPipelineThreadCount--;
+                        }
+                    }
+                }
+                return graphicsPipelineThreadCount + newThreadCount
+                        <= MAX_GRAPHICS_PIPELINE_THREADS_COUNT;
+            }
+        }
+
         private void logPerformanceHintSessionAtom(int uid, long sessionId,
                 long targetDuration, int[] tids, @SessionTag int sessionTag) {
             FrameworkStatsLog.write(FrameworkStatsLog.PERFORMANCE_HINT_SESSION_REPORTED, uid,
@@ -1537,11 +1675,14 @@
         protected boolean mUpdateAllowedByProcState;
         protected int[] mNewThreadIds;
         protected boolean mPowerEfficient;
+        protected boolean mGraphicsPipeline;
         protected boolean mHasBeenPowerEfficient;
+        protected boolean mHasBeenGraphicsPipeline;
         protected boolean mShouldForcePause;
 
-        private enum SessionModes {
+        enum SessionModes {
             POWER_EFFICIENCY,
+            GRAPHICS_PIPELINE,
         };
 
         protected AppHintSession(
@@ -1556,7 +1697,9 @@
             mTargetDurationNanos = durationNanos;
             mUpdateAllowedByProcState = true;
             mPowerEfficient = false;
+            mGraphicsPipeline = false;
             mHasBeenPowerEfficient = false;
+            mHasBeenGraphicsPipeline = false;
             mShouldForcePause = false;
             final boolean allowed = mUidObserver.isUidForeground(mUid);
             updateHintAllowedByProcState(allowed);
@@ -1677,6 +1820,25 @@
                 }
                 sessionSnapshot.updateUponSessionClose();
             }
+
+            if (mGraphicsPipeline) {
+                synchronized (mThreadsUsageObject) {
+                    ArraySet<ThreadUsageTracker> threadsSet = mThreadsUsageMap.get(mUid);
+                    if (threadsSet == null) {
+                        Slogf.w(TAG, "Threads Set is null for uid " + mUid);
+                        return;
+                    }
+                    // remove all tids associated with this session
+                    for (int i = 0; i < threadsSet.size(); ++i) {
+                        if (contains(mThreadIds, threadsSet.valueAt(i).getTid())) {
+                            threadsSet.removeAt(i);
+                        }
+                    }
+                    if (threadsSet.isEmpty()) {
+                        mThreadsUsageMap.remove(mUid);
+                    }
+                }
+            }
             if (powerhintThreadCleanup()) {
                 synchronized (mNonIsolatedTidsLock) {
                     final int[] tids = getTidsInternal();
@@ -1713,6 +1875,33 @@
                 throw new IllegalArgumentException("Thread id list can't be empty.");
             }
 
+
+            final int callingUid = Binder.getCallingUid();
+            if (mGraphicsPipeline) {
+                synchronized (mThreadsUsageObject) {
+                    // replace original tids with new tids
+                    ArraySet<ThreadUsageTracker> threadsSet = mThreadsUsageMap.get(callingUid);
+                    int graphicsPipelineThreadCount = 0;
+                    if (threadsSet != null) {
+                        // We count the graphics pipeline threads that are
+                        // *not* in this session, since those in this session
+                        // will be replaced. Then if the count plus the new tids
+                        // is over max available graphics pipeline threads we raise
+                        // an exception.
+                        for (ThreadUsageTracker t : threadsSet) {
+                            if (t.isGraphicsPipeline() && !contains(mThreadIds, t.getTid())) {
+                                graphicsPipelineThreadCount++;
+                            }
+                        }
+                        if (graphicsPipelineThreadCount + tids.length
+                                > MAX_GRAPHICS_PIPELINE_THREADS_COUNT) {
+                            throw new IllegalArgumentException(
+                                    "Not enough available graphics pipeline threads.");
+                        }
+                    }
+                }
+            }
+
             synchronized (this) {
                 if (mHalSessionPtr == 0) {
                     return;
@@ -1724,7 +1913,6 @@
                     return;
                 }
                 if (checkTid) {
-                    final int callingUid = Binder.getCallingUid();
                     final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
                     final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray() : null;
                     final long identity = Binder.clearCallingIdentity();
@@ -1770,6 +1958,23 @@
                     }
                 }
                 mNativeWrapper.halSetThreads(mHalSessionPtr, tids);
+
+                synchronized (mThreadsUsageObject) {
+                    // replace old tids with new ones
+                    ArraySet<ThreadUsageTracker> threadsSet = mThreadsUsageMap.get(callingUid);
+                    if (threadsSet == null) {
+                        mThreadsUsageMap.put(callingUid, new ArraySet<ThreadUsageTracker>());
+                        threadsSet = mThreadsUsageMap.get(callingUid);
+                    }
+                    for (int i = 0; i < threadsSet.size(); ++i) {
+                        if (contains(mThreadIds, threadsSet.valueAt(i).getTid())) {
+                            threadsSet.removeAt(i);
+                        }
+                    }
+                    for (int tid : tids) {
+                        threadsSet.add(new ThreadUsageTracker(tid, mGraphicsPipeline));
+                    }
+                }
                 mThreadIds = tids;
                 mNewThreadIds = null;
                 // if the update is allowed but the session is force paused by tid clean up, then
@@ -1831,26 +2036,49 @@
                         + " greater than zero.");
                 if (mode == SessionModes.POWER_EFFICIENCY.ordinal()) {
                     mPowerEfficient = enabled;
+                } else if (mode == SessionModes.GRAPHICS_PIPELINE.ordinal()) {
+                    mGraphicsPipeline = enabled;
                 }
                 mNativeWrapper.halSetMode(mHalSessionPtr, mode, enabled);
             }
-            if (enabled && (mode == SessionModes.POWER_EFFICIENCY.ordinal())) {
-                if (!mHasBeenPowerEfficient) {
-                    mHasBeenPowerEfficient = true;
-                    synchronized (mSessionSnapshotMapLock) {
-                        ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots =
-                                mSessionSnapshotMap.get(mUid);
-                        if (sessionSnapshots == null) {
-                            Slogf.w(TAG, "Session snapshot map is null for uid " + mUid);
-                            return;
+            if (enabled) {
+                if (mode == SessionModes.POWER_EFFICIENCY.ordinal()) {
+                    if (!mHasBeenPowerEfficient) {
+                        mHasBeenPowerEfficient = true;
+                        synchronized (mSessionSnapshotMapLock) {
+                            ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots =
+                                    mSessionSnapshotMap.get(mUid);
+                            if (sessionSnapshots == null) {
+                                Slogf.w(TAG, "Session snapshot map is null for uid " + mUid);
+                                return;
+                            }
+                            AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag);
+                            if (sessionSnapshot == null) {
+                                Slogf.w(TAG, "Session snapshot is null for uid " + mUid
+                                        + " and tag " + mTag);
+                                return;
+                            }
+                            sessionSnapshot.logPowerEfficientSession();
                         }
-                        AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag);
-                        if (sessionSnapshot == null) {
-                            Slogf.w(TAG, "Session snapshot is null for uid " + mUid
-                                    + " and tag " + mTag);
-                            return;
+                    }
+                } else if (mode == SessionModes.GRAPHICS_PIPELINE.ordinal()) {
+                    if (!mHasBeenGraphicsPipeline) {
+                        mHasBeenGraphicsPipeline = true;
+                        synchronized (mSessionSnapshotMapLock) {
+                            ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots =
+                                    mSessionSnapshotMap.get(mUid);
+                            if (sessionSnapshots == null) {
+                                Slogf.w(TAG, "Session snapshot map is null for uid " + mUid);
+                                return;
+                            }
+                            AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag);
+                            if (sessionSnapshot == null) {
+                                Slogf.w(TAG, "Session snapshot is null for uid " + mUid
+                                        + " and tag " + mTag);
+                                return;
+                            }
+                            sessionSnapshot.logGraphicsPipelineSession();
                         }
-                        sessionSnapshot.logPowerEfficientSession();
                     }
                 }
             }
@@ -1877,6 +2105,12 @@
             }
         }
 
+        public boolean isGraphicsPipeline() {
+            synchronized (this) {
+                return mGraphicsPipeline;
+            }
+        }
+
         public int getUid() {
             return mUid;
         }
@@ -1964,6 +2198,7 @@
                 pw.println(prefix + "SessionAllowedByProcState: " + mUpdateAllowedByProcState);
                 pw.println(prefix + "SessionForcePaused: " + mShouldForcePause);
                 pw.println(prefix + "PowerEfficient: " + (mPowerEfficient ? "true" : "false"));
+                pw.println(prefix + "GraphicsPipeline: " + (mGraphicsPipeline ? "true" : "false"));
             }
         }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 6292cbf..dde213d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -19245,21 +19245,23 @@
         }
     }
 
-    private boolean isAnyResetPasswordTokenActiveForUser(int userId) {
+    private boolean isAnyResetPasswordTokenActiveForUserLocked(int userId) {
         return mDevicePolicyEngine
                 .getLocalPoliciesSetByAdmins(PolicyDefinition.RESET_PASSWORD_TOKEN, userId)
-                .values()
+                .entrySet()
                 .stream()
-                .anyMatch((p) -> isResetPasswordTokenActiveForUserLocked(p.getValue(), userId));
+                .anyMatch((e) -> {
+                    EnforcingAdmin admin = e.getKey();
+                    PolicyValue<Long> policyValue = e.getValue();
+                    return isResetPasswordTokenActiveForUserLocked(policyValue.getValue(), userId)
+                              && isEncryptionAware(admin.getPackageName(), userId);
+                });
     }
 
     private boolean isResetPasswordTokenActiveForUserLocked(
             long passwordTokenHandle, int userHandle) {
-        if (passwordTokenHandle != 0) {
-            return mInjector.binderWithCleanCallingIdentity(() ->
+        return passwordTokenHandle != 0 && mInjector.binderWithCleanCallingIdentity(() ->
                     mLockPatternUtils.isEscrowTokenActive(passwordTokenHandle, userHandle));
-        }
-        return false;
     }
 
     @Override
@@ -21108,10 +21110,10 @@
         Preconditions.checkCallAuthorization(isSystemUid(getCallerIdentity()),
                 String.format(NOT_SYSTEM_CALLER_MSG,
                         "call canProfileOwnerResetPasswordWhenLocked"));
-        if (Flags.resetPasswordWithTokenCoexistence()) {
-            return isAnyResetPasswordTokenActiveForUser(userId);
-        }
         synchronized (getLockObject()) {
+            if (Flags.resetPasswordWithTokenCoexistence()) {
+                return isAnyResetPasswordTokenActiveForUserLocked(userId);
+            }
             final ActiveAdmin poAdmin = getProfileOwnerAdminLocked(userId);
             DevicePolicyData policy = getUserData(userId);
             if (poAdmin == null
@@ -21120,26 +21122,29 @@
                             policy.mPasswordTokenHandle, userId)) {
                 return false;
             }
-            final ApplicationInfo poAppInfo;
-            try {
-                poAppInfo = mIPackageManager.getApplicationInfo(
-                        poAdmin.info.getPackageName(), 0 /* flags */, userId);
-            } catch (RemoteException e) {
-                Slogf.e(LOG_TAG, "Failed to query PO app info", e);
-                return false;
-            }
-            if (poAppInfo == null) {
-                Slogf.wtf(LOG_TAG, "Cannot find AppInfo for profile owner");
-                return false;
-            }
-            if (!poAppInfo.isEncryptionAware()) {
-                return false;
-            }
-            Slogf.d(LOG_TAG, "PO should be able to reset password from direct boot");
-            return true;
+            return isEncryptionAware(poAdmin.info.getPackageName(), userId);
         }
     }
 
+    private boolean isEncryptionAware(String packageName, int userId) {
+        final ApplicationInfo poAppInfo;
+        try {
+            poAppInfo = mIPackageManager.getApplicationInfo(packageName, 0 /* flags */, userId);
+        } catch (RemoteException e) {
+            Slogf.e(LOG_TAG, "Failed to query PO / role holder's app info", e);
+            return false;
+        }
+        if (poAppInfo == null) {
+            Slogf.wtf(LOG_TAG, "Cannot find AppInfo for PO / role holder");
+            return false;
+        }
+        if (!poAppInfo.isEncryptionAware()) {
+            return false;
+        }
+        Slogf.d(LOG_TAG, "PO / role holder should be able to reset password from direct boot");
+        return true;
+    }
+
     @Override
     public String getEnrollmentSpecificId(String callerPackage) {
         if (!mHasFeature) {
diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 0881b4c..e631cb6 100644
--- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -66,6 +66,7 @@
 import android.os.PerformanceHintManager;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.SessionCreationConfig;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -200,7 +201,7 @@
         when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID),
                 eq(SESSION_TIDS_C), eq(0L), anyInt(),
                 any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[2],
-                SESSION_IDS[2]));
+                    SESSION_IDS[2]));
 
         when(mIPowerMock.getInterfaceVersion()).thenReturn(6);
         when(mIPowerMock.getSessionChannel(anyInt(), anyInt())).thenReturn(mConfig);
@@ -305,6 +306,14 @@
         return mService;
     }
 
+    private SessionCreationConfig makeSessionCreationConfig(int[] tids,
+            long targetWorkDurationNanos) {
+        SessionCreationConfig config = new SessionCreationConfig();
+        config.tids = tids;
+        config.targetWorkDurationNanos = targetWorkDurationNanos;
+        return config;
+    }
+
     @Test
     public void testInitializeService() {
         HintManagerService service = createService();
@@ -316,12 +325,14 @@
     public void testCreateHintSessionInvalidPid() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(new int[]{TID, 1}, DEFAULT_TARGET_DURATION);
         // Make sure we throw exception when adding a TID doesn't belong to the processes
         // In this case, we add `init` PID into the list.
         SessionConfig config = new SessionConfig();
         assertThrows(SecurityException.class,
                 () -> service.getBinderServiceInstance().createHintSessionWithConfig(token,
-                        new int[]{TID, 1}, DEFAULT_TARGET_DURATION, SessionTag.OTHER, config));
+                        SessionTag.OTHER, creationConfig, config));
     }
 
     @Test
@@ -329,17 +340,23 @@
         HintManagerService service = createService();
         IBinder token = new Binder();
         makeConfigCreationUnsupported();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         IHintSession a = service.getBinderServiceInstance().createHintSessionWithConfig(token,
-                SESSION_TIDS_A, DEFAULT_TARGET_DURATION, SessionTag.OTHER, new SessionConfig());
+                SessionTag.OTHER, creationConfig, new SessionConfig());
         assertNotNull(a);
 
+        creationConfig.tids = SESSION_TIDS_B;
+        creationConfig.targetWorkDurationNanos = DOUBLED_TARGET_DURATION;
         IHintSession b = service.getBinderServiceInstance().createHintSessionWithConfig(token,
-                SESSION_TIDS_B, DOUBLED_TARGET_DURATION, SessionTag.OTHER, new SessionConfig());
+                SessionTag.OTHER, creationConfig, new SessionConfig());
         assertNotEquals(a, b);
 
+        creationConfig.tids = SESSION_TIDS_C;
+        creationConfig.targetWorkDurationNanos = 0L;
         IHintSession c = service.getBinderServiceInstance().createHintSessionWithConfig(token,
-                SESSION_TIDS_C, 0L, SessionTag.OTHER, new SessionConfig());
+                SessionTag.OTHER, creationConfig, new SessionConfig());
         assertNotNull(c);
         verify(mNativeWrapperMock, times(3)).halCreateHintSession(anyInt(), anyInt(),
                 any(int[].class), anyLong());
@@ -349,22 +366,28 @@
     public void testCreateHintSessionWithConfig() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         SessionConfig config = new SessionConfig();
         IHintSession a = service.getBinderServiceInstance().createHintSessionWithConfig(token,
-                SESSION_TIDS_A, DEFAULT_TARGET_DURATION, SessionTag.OTHER, config);
+                SessionTag.OTHER, creationConfig, config);
         assertNotNull(a);
         assertEquals(SESSION_IDS[0], config.id);
 
         SessionConfig config2 = new SessionConfig();
+        creationConfig.tids = SESSION_TIDS_B;
+        creationConfig.targetWorkDurationNanos = DOUBLED_TARGET_DURATION;
         IHintSession b = service.getBinderServiceInstance().createHintSessionWithConfig(token,
-                SESSION_TIDS_B, DOUBLED_TARGET_DURATION, SessionTag.APP, config2);
+                SessionTag.APP, creationConfig, config2);
         assertNotEquals(a, b);
         assertEquals(SESSION_IDS[1], config2.id);
 
         SessionConfig config3 = new SessionConfig();
+        creationConfig.tids = SESSION_TIDS_C;
+        creationConfig.targetWorkDurationNanos = 0L;
         IHintSession c = service.getBinderServiceInstance().createHintSessionWithConfig(token,
-                SESSION_TIDS_C, 0L, SessionTag.GAME, config3);
+                SessionTag.GAME, creationConfig, config3);
         assertNotNull(c);
         assertEquals(SESSION_IDS[2], config3.id);
         verify(mNativeWrapperMock, times(3)).halCreateHintSessionWithConfig(anyInt(), anyInt(),
@@ -372,13 +395,48 @@
     }
 
     @Test
-    public void testPauseResumeHintSession() throws Exception {
+    public void testCreateGraphicsPipelineSessions() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
 
+        final int threadCount =
+                service.getBinderServiceInstance().getMaxGraphicsPipelineThreadsCount();
+        long sessionPtr1 = 1111L;
+        long sessionId1 = 11111L;
+        CountDownLatch stopLatch1 = new CountDownLatch(1);
+        int[] tids1 = createThreads(threadCount, stopLatch1);
+        when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID), eq(tids1),
+                eq(DEFAULT_TARGET_DURATION), anyInt(), any(SessionConfig.class)))
+                .thenAnswer(fakeCreateWithConfig(sessionPtr1, sessionId1));
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(tids1, DEFAULT_TARGET_DURATION);
+
+        creationConfig.modesToEnable = new int[] {1}; // GRAPHICS_PIPELINE
+
+        SessionConfig config = new SessionConfig();
+        IHintSession a = service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                SessionTag.OTHER, creationConfig, config);
+        assertNotNull(a);
+        assertEquals(sessionId1, config.id);
+
+        creationConfig.tids = createThreads(1, stopLatch1);
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                    SessionTag.OTHER, creationConfig, config);
+        });
+    }
+
+    @Test
+    public void testPauseResumeHintSession() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
 
         // Set session to background and calling updateHintAllowedByProcState() would invoke
         // pause();
@@ -414,9 +472,11 @@
     public void testCloseHintSession() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         IHintSession a = service.getBinderServiceInstance().createHintSessionWithConfig(token,
-                SESSION_TIDS_A, DEFAULT_TARGET_DURATION, SessionTag.OTHER, new SessionConfig());
+                SessionTag.OTHER, creationConfig, new SessionConfig());
 
         a.close();
         verify(mNativeWrapperMock, times(1)).halCloseHintSession(anyLong());
@@ -426,9 +486,11 @@
     public void testUpdateTargetWorkDuration() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         IHintSession a = service.getBinderServiceInstance().createHintSessionWithConfig(token,
-                SESSION_TIDS_A, DEFAULT_TARGET_DURATION, SessionTag.OTHER, new SessionConfig());
+                SessionTag.OTHER, creationConfig, new SessionConfig());
 
         assertThrows(IllegalArgumentException.class, () -> {
             a.updateTargetWorkDuration(-1L);
@@ -446,10 +508,12 @@
     public void testReportActualWorkDuration() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
 
         a.updateTargetWorkDuration(100L);
         a.reportActualWorkDuration(DURATIONS_THREE, TIMESTAMPS_THREE);
@@ -489,10 +553,12 @@
     public void testSendHint() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
 
         a.sendHint(PerformanceHintManager.Session.CPU_LOAD_RESET);
         verify(mNativeWrapperMock, times(1)).halSendHint(anyLong(),
@@ -516,10 +582,12 @@
     public void testDoHintInBackground() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
 
         service.mUidObserver.onUidStateChanged(
                 a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
@@ -538,10 +606,12 @@
     public void testDoHintInForeground() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
 
         service.mUidObserver.onUidStateChanged(
                 a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
@@ -552,10 +622,12 @@
     public void testSetThreads() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
 
         a.updateTargetWorkDuration(100L);
 
@@ -591,9 +663,11 @@
         when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID), eq(tids1),
                 eq(DEFAULT_TARGET_DURATION), anyInt(), any(SessionConfig.class)))
                 .thenReturn(sessionPtr1);
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(tids1, DEFAULT_TARGET_DURATION);
         AppHintSession session1 = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, tids1, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
         assertNotNull(session1);
 
         // trigger UID state change by making the process foreground->background, but because the
@@ -626,9 +700,11 @@
         when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID), eq(tids1),
                 eq(DEFAULT_TARGET_DURATION), anyInt(), any(SessionConfig.class)))
                 .thenReturn(sessionPtr1);
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(tids1, DEFAULT_TARGET_DURATION);
         AppHintSession session1 = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, tids1, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
         assertNotNull(session1);
 
         // let all session 1 threads to exit and the cleanup should force pause the session 1
@@ -734,10 +810,12 @@
     public void testSetMode() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
 
         a.setMode(0, true);
         verify(mNativeWrapperMock, times(1)).halSetMode(anyLong(),
@@ -746,12 +824,19 @@
         a.setMode(0, false);
         verify(mNativeWrapperMock, times(1)).halSetMode(anyLong(),
                 eq(0), eq(false));
+    }
 
-        assertThrows(IllegalArgumentException.class, () -> {
-            a.setMode(-1, true);
-        });
+    @Test
+    public void testSetModeSessionInBackGround() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
-        reset(mNativeWrapperMock);
+        AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
+
         // Set session to background, then the duration would not be updated.
         service.mUidObserver.onUidStateChanged(
                 a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
@@ -763,6 +848,40 @@
     }
 
     @Test
+    public void testSetModeInvalid() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
+        AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            a.setMode(-1, true);
+        });
+    }
+
+    @Test
+    public void testSetModeUponSessionCreation() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+        creationConfig.modesToEnable = new int[] {0, 1};
+
+        AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
+        assertNotNull(a);
+        verify(mNativeWrapperMock, times(1)).halSetMode(anyLong(),
+                eq(0), eq(true));
+        verify(mNativeWrapperMock, times(1)).halSetMode(anyLong(),
+                eq(1), eq(true));
+    }
+
+    @Test
     public void testGetChannel() throws Exception {
         HintManagerService service = createService();
         Binder token = new Binder();
@@ -950,9 +1069,12 @@
     private void runAppHintSession(HintManagerService service, int logId,
             AtomicReference<Boolean> shouldRun) throws Exception {
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
         // we will start some threads and get their valid TIDs to update
         int threadCount = 3;
         // the list of TIDs
@@ -1017,10 +1139,12 @@
     public void testReportActualWorkDuration2() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
+        SessionCreationConfig creationConfig =
+                makeSessionCreationConfig(SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
-                        SessionTag.OTHER, new SessionConfig());
+                .createHintSessionWithConfig(token, SessionTag.OTHER,
+                        creationConfig, new SessionConfig());
 
         a.updateTargetWorkDuration(100L);
         a.reportActualWorkDuration2(WORK_DURATIONS_FIVE);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index e6b4bc9..30af4ea 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -17405,6 +17405,9 @@
     @Test
     @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testPostPromotableNotification_noPermission() throws Exception {
+        mBinderService.setCanBePromoted(mPkg, mUid, false, true);
+        assertThat(mBinderService.appCanBePromoted(mPkg, mUid)).isFalse();
+
         Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
                 .setSmallIcon(android.R.drawable.sym_def_app_icon)
                 .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG"))
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index dda060d..80e86a1 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -256,6 +256,7 @@
     @Parameters(name = "{0}")
     public static List<FlagsParameterization> getParams() {
         return FlagsParameterization.allCombinationsOf(
+                android.app.Flags.FLAG_API_RICH_ONGOING,
                 FLAG_NOTIFICATION_CLASSIFICATION, FLAG_MODES_UI);
     }
 
@@ -6511,12 +6512,21 @@
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
+    @DisableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
     public void testNoAppHasPermissionToPromoteByDefault() {
         mHelper.setShowBadge(PKG_P, UID_P, true);
         assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse();
     }
 
     @Test
+    @EnableFlags({android.app.Flags.FLAG_API_RICH_ONGOING,
+            android.app.Flags.FLAG_UI_RICH_ONGOING})
+    public void testAllAppsHavePermissionToPromoteByDefault() {
+        mHelper.setShowBadge(PKG_P, UID_P, true);
+        assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
+    }
+
+    @Test
     @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
     public void testSetCanBePromoted() {
         mHelper.setCanBePromoted(PKG_P, UID_P, true, true);