Merge "Add KILL_UID permission to shell for CTS test." into main
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index e043a5d..a458b4e 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -1980,6 +1980,7 @@
     public void registerAllResourcesReference(@NonNull Resources resources) {
         if (android.content.res.Flags.registerResourcePaths()) {
             synchronized (mLock) {
+                cleanupReferences(mAllResourceReferences, mAllResourceReferencesQueue);
                 mAllResourceReferences.add(
                         new WeakReference<>(resources, mAllResourceReferencesQueue));
             }
diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
index b9eba9c..ce8661e 100644
--- a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
@@ -1028,6 +1028,9 @@
                     // Camera is already closed, so nothing left to do
                     if (DEBUG) Log.v(TAG, mIdString +
                             "Camera was already closed or busy, skipping unconfigure");
+                } catch (SecurityException e) {
+                    // UID state change revoked camera permission
+                    Log.e(TAG, mIdString + "Exception while unconfiguring outputs: ", e);
                 }
             }
         }
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 6c1aa90..36e816a 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -92,10 +92,9 @@
             boolean waitForNegativeProximity);
 
     /**
-     * Returns {@code true} if the proximity sensor screen-off function is available for the given
-     * display.
+     * Returns {@code true} if the proximity sensor screen-off function is available.
      */
-    public abstract boolean isProximitySensorAvailable(int displayId);
+    public abstract boolean isProximitySensorAvailable();
 
     /**
      * Registers a display group listener which will be informed of the addition, removal, or change
diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl
index e85e580..5f62b8b 100644
--- a/core/java/android/os/IPowerManager.aidl
+++ b/core/java/android/os/IPowerManager.aidl
@@ -42,9 +42,7 @@
 
     void updateWakeLockWorkSource(IBinder lock, in WorkSource ws, String historyTag);
     void updateWakeLockCallback(IBinder lock, IWakeLockCallback callback);
-    @UnsupportedAppUsage
     boolean isWakeLockLevelSupported(int level);
-    boolean isWakeLockLevelSupportedWithDisplayId(int level, int displayId);
 
     void userActivity(int displayId, long time, int event, int flags);
     void wakeUp(long time, int reason, String details, String opPackageName);
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 32db3be..b9bae5b 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -1344,9 +1344,6 @@
      * @see #ON_AFTER_RELEASE
      */
     public WakeLock newWakeLock(int levelAndFlags, String tag) {
-        if (android.companion.virtualdevice.flags.Flags.displayPowerManagerApis()) {
-            return newWakeLock(levelAndFlags, tag, mContext.getDisplayId());
-        }
         validateWakeLockParameters(levelAndFlags, tag);
         return new WakeLock(levelAndFlags, tag, mContext.getOpPackageName(),
                 Display.INVALID_DISPLAY);
@@ -1737,10 +1734,6 @@
      */
     public boolean isWakeLockLevelSupported(int level) {
         try {
-            if (android.companion.virtualdevice.flags.Flags.displayPowerManagerApis()) {
-                return mService.isWakeLockLevelSupportedWithDisplayId(
-                        level, mContext.getDisplayId());
-            }
             return mService.isWakeLockLevelSupported(level);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -1804,9 +1797,6 @@
      * @see android.content.Intent#ACTION_SCREEN_OFF
      */
     public boolean isInteractive() {
-        if (android.companion.virtualdevice.flags.Flags.displayPowerManagerApis()) {
-            return isInteractive(mContext.getDisplayId());
-        }
         return mInteractiveCache.query(null);
     }
 
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index a86c961..aedf8e0 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -106,3 +106,10 @@
     description: "Clear StrongAuth on add credential"
     bug: "320817991"
 }
+
+flag {
+    name: "afl_api"
+    namespace: "platform_security"
+    description: "AFL feature"
+    bug: "365994454"
+}
diff --git a/core/java/android/service/carrier/CarrierMessagingServiceWrapper.java b/core/java/android/service/carrier/CarrierMessagingServiceWrapper.java
index de2c6f77..afff8fe 100644
--- a/core/java/android/service/carrier/CarrierMessagingServiceWrapper.java
+++ b/core/java/android/service/carrier/CarrierMessagingServiceWrapper.java
@@ -20,6 +20,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -28,8 +29,10 @@
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.telephony.SmsMessage;
 
+import com.android.internal.telephony.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.util.List;
@@ -91,8 +94,12 @@
         mOnServiceReadyCallback = onServiceReadyCallback;
         mServiceReadyCallbackExecutor = executor;
         mContext = context;
-        return context.bindService(intent, mCarrierMessagingServiceConnection,
-                Context.BIND_AUTO_CREATE);
+        return Flags.supportCarrierServicesForHsum()
+                ? context.bindServiceAsUser(intent, mCarrierMessagingServiceConnection,
+                Context.BIND_AUTO_CREATE,
+                UserHandle.of(ActivityManager.getCurrentUser()))
+                : context.bindService(intent, mCarrierMessagingServiceConnection,
+                        Context.BIND_AUTO_CREATE);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 0dca97c..b7d9677 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -65,6 +65,7 @@
 import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
 import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
@@ -384,9 +385,11 @@
             Context context,
             ShellInit shellInit,
             Transitions transitions,
+            Optional<DesktopFullImmersiveTransitionHandler> desktopImmersiveTransitionHandler,
             WindowDecorViewModel windowDecorViewModel) {
         return new FreeformTaskTransitionObserver(
-                context, shellInit, transitions, windowDecorViewModel);
+                context, shellInit, transitions, desktopImmersiveTransitionHandler,
+                windowDecorViewModel);
     }
 
     @WMSingleton
@@ -621,6 +624,7 @@
             ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
             DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
             @DynamicOverride DesktopRepository desktopRepository,
+            Optional<DesktopFullImmersiveTransitionHandler> desktopFullImmersiveTransitionHandler,
             DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
             LaunchAdjacentController launchAdjacentController,
             RecentsTransitionHandler recentsTransitionHandler,
@@ -636,7 +640,8 @@
                 returnToDragStartAnimator, enterDesktopTransitionHandler,
                 exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler,
                 toggleResizeDesktopTaskTransitionHandler,
-                dragToDesktopTransitionHandler, desktopRepository,
+                dragToDesktopTransitionHandler, desktopFullImmersiveTransitionHandler.get(),
+                desktopRepository,
                 desktopModeLoggerTransitionObserver, launchAdjacentController,
                 recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
                 recentTasksController.orElse(null), interactionJankMonitor, mainHandler);
@@ -671,6 +676,19 @@
 
     @WMSingleton
     @Provides
+    static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler(
+            Context context,
+            Transitions transitions,
+            @DynamicOverride DesktopRepository desktopRepository) {
+        if (DesktopModeStatus.canEnterDesktopMode(context)) {
+            return Optional.of(
+                    new DesktopFullImmersiveTransitionHandler(transitions, desktopRepository));
+        }
+        return Optional.empty();
+    }
+
+    @WMSingleton
+    @Provides
     static ReturnToDragStartAnimator provideReturnToDragStartAnimator(
             Context context, InteractionJankMonitor interactionJankMonitor) {
         return new ReturnToDragStartAnimator(context, interactionJankMonitor);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 3464fef..702552e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -77,10 +77,12 @@
             PipTaskListener pipTaskListener,
             @NonNull PipScheduler pipScheduler,
             @NonNull PipTransitionState pipStackListenerController,
+            @NonNull PipDisplayLayoutState pipDisplayLayoutState,
             @NonNull PipUiStateChangeController pipUiStateChangeController) {
         return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
                 pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener,
-                pipScheduler, pipStackListenerController, pipUiStateChangeController);
+                pipScheduler, pipStackListenerController, pipDisplayLayoutState,
+                pipUiStateChangeController);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
new file mode 100644
index 0000000..f749aa1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode
+
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.animation.DecelerateInterpolator
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import androidx.core.animation.addListener
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionHandler
+import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
+
+/**
+ * A [TransitionHandler] to move a task in/out of desktop's full immersive state where the task
+ * remains freeform while being able to take fullscreen bounds and have its App Header visibility
+ * be transient below the status bar like in fullscreen immersive mode.
+ */
+class DesktopFullImmersiveTransitionHandler(
+    private val transitions: Transitions,
+    private val desktopRepository: DesktopRepository,
+    private val transactionSupplier: () -> SurfaceControl.Transaction,
+) : TransitionHandler {
+
+    constructor(
+        transitions: Transitions,
+        desktopRepository: DesktopRepository,
+    ) : this(transitions, desktopRepository, { SurfaceControl.Transaction() })
+
+    private var state: TransitionState? = null
+
+    /** Whether there is an immersive transition that hasn't completed yet. */
+    private val inProgress: Boolean
+        get() = state != null
+
+    private val rectEvaluator = RectEvaluator()
+
+    /** A listener to invoke on animation changes during entry/exit. */
+    var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null
+
+    /** Starts a transition to enter full immersive state inside the desktop. */
+    fun enterImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+        if (inProgress) {
+            ProtoLog.v(
+                ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+                "FullImmersive: cannot start entry because transition already in progress."
+            )
+            return
+        }
+
+        val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
+        state = TransitionState(
+            transition = transition,
+            displayId = taskInfo.displayId,
+            taskId = taskInfo.taskId,
+            direction = Direction.ENTER
+        )
+    }
+
+    fun exitImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+        if (inProgress) {
+            ProtoLog.v(
+                ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+                "$TAG: cannot start exit because transition already in progress."
+            )
+            return
+        }
+
+        val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
+        state = TransitionState(
+            transition = transition,
+            displayId = taskInfo.displayId,
+            taskId = taskInfo.taskId,
+            direction = Direction.EXIT
+        )
+    }
+
+    override fun startAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: Transitions.TransitionFinishCallback
+    ): Boolean {
+        val state = requireState()
+        if (transition != state.transition) return false
+        animateResize(
+            transitionState = state,
+            info = info,
+            startTransaction = startTransaction,
+            finishTransaction = finishTransaction,
+            finishCallback = finishCallback
+        )
+        return true
+    }
+
+    private fun animateResize(
+        transitionState: TransitionState,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: Transitions.TransitionFinishCallback
+    ) {
+        val change = info.changes.first { c ->
+            val taskInfo = c.taskInfo
+            return@first taskInfo != null && taskInfo.taskId == transitionState.taskId
+        }
+        val leash = change.leash
+        val startBounds = change.startAbsBounds
+        val endBounds = change.endAbsBounds
+
+        val updateTransaction = transactionSupplier()
+        ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds).apply {
+            duration = FULL_IMMERSIVE_ANIM_DURATION_MS
+            interpolator = DecelerateInterpolator()
+            addListener(
+                onStart = {
+                    startTransaction
+                        .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat())
+                        .setWindowCrop(leash, startBounds.width(), startBounds.height())
+                        .show(leash)
+                    onTaskResizeAnimationListener
+                        ?.onAnimationStart(transitionState.taskId, startTransaction, startBounds)
+                        ?: startTransaction.apply()
+                },
+                onEnd = {
+                    finishTransaction
+                        .setPosition(leash, endBounds.left.toFloat(), endBounds.top.toFloat())
+                        .setWindowCrop(leash, endBounds.width(), endBounds.height())
+                        .apply()
+                    onTaskResizeAnimationListener?.onAnimationEnd(transitionState.taskId)
+                    finishCallback.onTransitionFinished(null /* wct */)
+                    clearState()
+                }
+            )
+            addUpdateListener { animation ->
+                val rect = animation.animatedValue as Rect
+                updateTransaction
+                    .setPosition(leash, rect.left.toFloat(), rect.top.toFloat())
+                    .setWindowCrop(leash, rect.width(), rect.height())
+                    .apply()
+                onTaskResizeAnimationListener
+                    ?.onBoundsChange(transitionState.taskId, updateTransaction, rect)
+                    ?: updateTransaction.apply()
+            }
+            start()
+        }
+    }
+
+    override fun handleRequest(
+        transition: IBinder,
+        request: TransitionRequestInfo
+    ): WindowContainerTransaction? = null
+
+    override fun onTransitionConsumed(
+        transition: IBinder,
+        aborted: Boolean,
+        finishTransaction: SurfaceControl.Transaction?
+    ) {
+        val state = this.state ?: return
+        if (transition == state.transition && aborted) {
+            clearState()
+        }
+        super.onTransitionConsumed(transition, aborted, finishTransaction)
+    }
+
+    /**
+     * Called when any transition in the system is ready to play. This is needed to update the
+     * repository state before window decorations are drawn (which happens immediately after
+     * |onTransitionReady|, before this transition actually animates) because drawing decorations
+     * depends in whether the task is in full immersive state or not.
+     */
+    fun onTransitionReady(transition: IBinder) {
+        val state = this.state ?: return
+        // TODO: b/369443668 - this assumes invoking the exit transition is the only way to exit
+        //  immersive, which isn't realistic. The app could crash, the user could dismiss it from
+        //  overview, etc. This (or its caller) should search all transitions to look for any
+        //  immersive task exiting that state to keep the repository properly updated.
+        if (transition == state.transition) {
+            when (state.direction) {
+                Direction.ENTER -> {
+                    desktopRepository.setTaskInFullImmersiveState(
+                        displayId = state.displayId,
+                        taskId = state.taskId,
+                        immersive = true
+                    )
+                }
+                Direction.EXIT -> {
+                    desktopRepository.setTaskInFullImmersiveState(
+                        displayId = state.displayId,
+                        taskId = state.taskId,
+                        immersive = false
+                    )
+                }
+            }
+        }
+    }
+
+    private fun clearState() {
+        state = null
+    }
+
+    private fun requireState(): TransitionState =
+        state ?: error("Expected non-null transition state")
+
+    /** The state of the currently running transition. */
+    private data class TransitionState(
+        val transition: IBinder,
+        val displayId: Int,
+        val taskId: Int,
+        val direction: Direction
+    )
+
+    private enum class Direction {
+        ENTER, EXIT
+    }
+
+    private companion object {
+        private const val TAG = "FullImmersiveHandler"
+
+        private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L
+    }
+}
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 2303e71..4e548a6 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
@@ -134,6 +134,7 @@
     private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler,
     private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
     private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
+    private val immersiveTransitionHandler: DesktopFullImmersiveTransitionHandler,
     private val taskRepository: DesktopRepository,
     private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
     private val launchAdjacentController: LaunchAdjacentController,
@@ -231,6 +232,7 @@
         toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
         enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
         dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener
+        immersiveTransitionHandler.onTaskResizeAnimationListener = listener
     }
 
     fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) {
@@ -649,6 +651,35 @@
         }
     }
 
+    /** Moves a task in/out of full immersive state within the desktop. */
+    fun toggleDesktopTaskFullImmersiveState(taskInfo: RunningTaskInfo) {
+        if (taskRepository.isTaskInFullImmersiveState(taskInfo.taskId)) {
+            exitDesktopTaskFromFullImmersive(taskInfo)
+        } else {
+            moveDesktopTaskToFullImmersive(taskInfo)
+        }
+    }
+
+    private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) {
+        check(taskInfo.isFreeform) { "Task must already be in freeform" }
+        val wct = WindowContainerTransaction().apply {
+            setBounds(taskInfo.token, Rect())
+        }
+        immersiveTransitionHandler.enterImmersive(taskInfo, wct)
+    }
+
+    private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) {
+        check(taskInfo.isFreeform) { "Task must already be in freeform" }
+        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+        val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
+        val destinationBounds = getMaximizeBounds(taskInfo, stableBounds)
+
+        val wct = WindowContainerTransaction().apply {
+            setBounds(taskInfo.token, destinationBounds)
+        }
+        immersiveTransitionHandler.exitImmersive(taskInfo, wct)
+    }
+
     /**
      * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the stable
      * bounds) and a free floating state (either the last saved bounds if available or the default
@@ -685,18 +716,7 @@
             // and toggle to the stable bounds.
             taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
 
-            if (taskInfo.isResizeable) {
-                // if resizable then expand to entire stable bounds (full display minus insets)
-                destinationBounds.set(stableBounds)
-            } else {
-                // if non-resizable then calculate max bounds according to aspect ratio
-                val activityAspectRatio = calculateAspectRatio(taskInfo)
-                val newSize = maximizeSizeGivenAspectRatio(taskInfo,
-                    Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
-                val newBounds = centerInArea(
-                    newSize, stableBounds, stableBounds.left, stableBounds.top)
-                destinationBounds.set(newBounds)
-            }
+            destinationBounds.set(getMaximizeBounds(taskInfo, stableBounds))
         }
 
 
@@ -719,6 +739,20 @@
         }
     }
 
+    private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect {
+        if (taskInfo.isResizeable) {
+            // if resizable then expand to entire stable bounds (full display minus insets)
+            return Rect(stableBounds)
+        } else {
+            // if non-resizable then calculate max bounds according to aspect ratio
+            val activityAspectRatio = calculateAspectRatio(taskInfo)
+            val newSize = maximizeSizeGivenAspectRatio(taskInfo,
+                Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
+            return centerInArea(
+                newSize, stableBounds, stableBounds.left, stableBounds.top)
+        }
+    }
+
     private fun isTaskMaximized(
         taskInfo: RunningTaskInfo,
         stableBounds: Rect
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index ffcc526..cb0354ec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -27,6 +27,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.window.flags.Flags;
+import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -36,6 +38,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * The {@link Transitions.TransitionHandler} that handles freeform task launches, closes,
@@ -44,6 +47,7 @@
  */
 public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver {
     private final Transitions mTransitions;
+    private final Optional<DesktopFullImmersiveTransitionHandler> mImmersiveTransitionHandler;
     private final WindowDecorViewModel mWindowDecorViewModel;
 
     private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo =
@@ -53,8 +57,10 @@
             Context context,
             ShellInit shellInit,
             Transitions transitions,
+            Optional<DesktopFullImmersiveTransitionHandler> immersiveTransitionHandler,
             WindowDecorViewModel windowDecorViewModel) {
         mTransitions = transitions;
+        mImmersiveTransitionHandler = immersiveTransitionHandler;
         mWindowDecorViewModel = windowDecorViewModel;
         if (Transitions.ENABLE_SHELL_TRANSITIONS && FreeformComponents.isFreeformEnabled(context)) {
             shellInit.addInitCallback(this::onInit, this);
@@ -72,6 +78,13 @@
             @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startT,
             @NonNull SurfaceControl.Transaction finishT) {
+        if (Flags.enableFullyImmersiveInDesktop()) {
+            // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository
+            //  is updated from there **before** the |mWindowDecorViewModel| methods are invoked.
+            //  Otherwise window decoration relayout won't run with the immersive state up to date.
+            mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition));
+        }
+
         final ArrayList<ActivityManager.RunningTaskInfo> taskInfoList = new ArrayList<>();
         final ArrayList<WindowContainerToken> taskParents = new ArrayList<>();
         for (TransitionInfo.Change change : info.getChanges()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index e9c4c14..73be8db 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -324,10 +324,16 @@
             int launcherRotation, Rect hotseatKeepClearArea) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "getSwipePipToHomeBounds: %s", componentName);
-        // preemptively add the keep clear area for Hotseat, so that it is taken into account
-        // when calculating the entry destination bounds of PiP window
+        // Preemptively add the keep clear area for Hotseat, so that it is taken into account
+        // when calculating the entry destination bounds of PiP window.
         mPipBoundsState.setNamedUnrestrictedKeepClearArea(
                 PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, hotseatKeepClearArea);
+
+        // Set the display layout rotation early to calculate final orientation bounds that
+        // the animator expects, this will also be used to detect the fixed rotation when
+        // Shell resolves the type of the animation we are undergoing.
+        mPipDisplayLayoutState.rotateTo(launcherRotation);
+
         mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, pictureInPictureParams,
                 mPipBoundsAlgorithm);
         return mPipBoundsAlgorithm.getEntryDestinationBounds();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index dc0bc78..62a60fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.pip2.phone;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.Surface.ROTATION_270;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_PIP;
@@ -33,6 +34,7 @@
 import android.app.ActivityManager;
 import android.app.PictureInPictureParams;
 import android.content.Context;
+import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -49,6 +51,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
 import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip.PipTransitionController;
@@ -82,7 +85,7 @@
      * The fixed start delay in ms when fading out the content overlay from bounds animation.
      * The fadeout animation is guaranteed to start after the client has drawn under the new config.
      */
-    private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 400;
+    private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 500;
 
     //
     // Dependencies
@@ -92,6 +95,7 @@
     private final PipTaskListener mPipTaskListener;
     private final PipScheduler mPipScheduler;
     private final PipTransitionState mPipTransitionState;
+    private final PipDisplayLayoutState mPipDisplayLayoutState;
 
     //
     // Transition caches
@@ -124,6 +128,7 @@
             PipTaskListener pipTaskListener,
             PipScheduler pipScheduler,
             PipTransitionState pipTransitionState,
+            PipDisplayLayoutState pipDisplayLayoutState,
             PipUiStateChangeController pipUiStateChangeController) {
         super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
                 pipBoundsAlgorithm);
@@ -134,6 +139,7 @@
         mPipScheduler.setPipTransitionController(this);
         mPipTransitionState = pipTransitionState;
         mPipTransitionState.addPipTransitionStateChangedListener(this);
+        mPipDisplayLayoutState = pipDisplayLayoutState;
     }
 
     @Override
@@ -321,11 +327,30 @@
                             (destinationBounds.width() - overlaySize) / 2f,
                             (destinationBounds.height() - overlaySize) / 2f);
         }
-
         startTransaction.merge(finishTransaction);
+
+        final int startRotation = pipChange.getStartRotation();
+        final int endRotation = mPipDisplayLayoutState.getRotation();
+        if (endRotation != startRotation) {
+            boolean isClockwise = (endRotation - startRotation) == -ROTATION_270;
+
+            // Display bounds were already updated to represent the final orientation,
+            // so we just need to readjust the origin, and perform rotation about (0, 0).
+            Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds();
+            int originTranslateX = isClockwise ? 0 : -displayBounds.width();
+            int originTranslateY = isClockwise ? -displayBounds.height() : 0;
+
+            Matrix transformTensor = new Matrix();
+            final float[] matrixTmp = new float[9];
+            transformTensor.setTranslate(originTranslateX + destinationBounds.left,
+                    originTranslateY + destinationBounds.top);
+            final float degrees = (endRotation - startRotation) * 90f;
+            transformTensor.postRotate(degrees);
+            startTransaction.setMatrix(pipLeash, transformTensor, matrixTmp);
+        }
         startTransaction.apply();
         finishCallback.onTransitionFinished(null /* finishWct */);
-        onClientDrawAtTransitionEnd();
+        finishInner();
         return true;
     }
 
@@ -397,7 +422,7 @@
                 sourceRectHint, PipEnterExitAnimator.BOUNDS_ENTER, Surface.ROTATION_0);
 
         tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
-                this::onClientDrawAtTransitionEnd);
+                this::finishInner);
         finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
 
         animator.setAnimationEndCallback(() ->
@@ -430,7 +455,7 @@
         animator.setAnimationEndCallback(() -> {
             finishCallback.onTransitionFinished(null);
             // This should update the pip transition state accordingly after we stop playing.
-            onClientDrawAtTransitionEnd();
+            finishInner();
         });
 
         animator.start();
@@ -605,7 +630,7 @@
     // Miscellaneous callbacks and listeners
     //
 
-    private void onClientDrawAtTransitionEnd() {
+    private void finishInner() {
         if (mPipTransitionState.getSwipePipToHomeOverlay() != null) {
             startOverlayFadeoutAnimation();
         } else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index bf175b7..bcf48d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -538,6 +538,14 @@
         decoration.closeMaximizeMenu();
     }
 
+    private void onEnterOrExitImmersive(int taskId) {
+        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+        if (decoration == null) {
+            return;
+        }
+        mDesktopTasksController.toggleDesktopTaskFullImmersiveState(decoration.mTaskInfo);
+    }
+
     private void onSnapResize(int taskId, boolean left) {
         final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
         if (decoration == null) {
@@ -755,7 +763,16 @@
                 //  back to the decoration using
                 //  {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which
                 //  should shared with the maximize menu's maximize/restore actions.
-                onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
+                if (Flags.enableFullyImmersiveInDesktop()
+                        && TaskInfoKt.getRequestingImmersive(decoration.mTaskInfo)) {
+                    // Task is requesting immersive, so it should either enter or exit immersive,
+                    // depending on immersive state.
+                    onEnterOrExitImmersive(decoration.mTaskInfo.taskId);
+                } else {
+                    // Full immersive is disabled or task doesn't request/support it, so just
+                    // toggle between maximize/restore states.
+                    onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
+                }
             } else if (id == R.id.minimize_window) {
                 final WindowContainerTransaction wct = new WindowContainerTransaction();
                 mDesktopTasksController.onDesktopWindowMinimize(wct, mTaskId);
@@ -935,14 +952,18 @@
             }
             final boolean touchingButton = (id == R.id.close_window || id == R.id.maximize_window
                     || id == R.id.open_menu_button || id == R.id.minimize_window);
+            final boolean dragAllowed =
+                    !mDesktopRepository.isTaskInFullImmersiveState(taskInfo.taskId);
             switch (e.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN: {
-                    mDragPointerId = e.getPointerId(0);
-                    final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart(
-                            0 /* ctrlType */, e.getRawX(0),
-                            e.getRawY(0));
-                    updateDragStatus(e.getActionMasked());
-                    mOnDragStartInitialBounds.set(initialBounds);
+                    if (dragAllowed) {
+                        mDragPointerId = e.getPointerId(0);
+                        final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart(
+                                0 /* ctrlType */, e.getRawX(0),
+                                e.getRawY(0));
+                        updateDragStatus(e.getActionMasked());
+                        mOnDragStartInitialBounds.set(initialBounds);
+                    }
                     mHasLongClicked = false;
                     // Do not consume input event if a button is touched, otherwise it would
                     // prevent the button's ripple effect from showing.
@@ -951,6 +972,9 @@
                 case ACTION_MOVE: {
                     // If a decor's resize drag zone is active, don't also try to reposition it.
                     if (decoration.isHandlingDragResize()) break;
+                    // Dragging the header isn't allowed, so skip the positioning work.
+                    if (!dragAllowed) break;
+
                     decoration.closeMaximizeMenu();
                     if (e.findPointerIndex(mDragPointerId) == -1) {
                         mDragPointerId = e.getPointerId(0);
@@ -1036,6 +1060,10 @@
                     && action != MotionEvent.ACTION_CANCEL)) {
                 return false;
             }
+            if (mDesktopRepository.isTaskInFullImmersiveState(mTaskId)) {
+                // Disallow double-tap to resize when in full immersive.
+                return false;
+            }
             onMaximizeOrRestore(mTaskId, "double_tap");
             return true;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index c7e8422..25d37fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -517,8 +517,8 @@
             closeManageWindowsMenu();
             closeMaximizeMenu();
         }
-        updateDragResizeListener(oldDecorationSurface);
-        updateMaximizeMenu(startT);
+        updateDragResizeListener(oldDecorationSurface, inFullImmersive);
+        updateMaximizeMenu(startT, inFullImmersive);
         Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
     }
 
@@ -571,11 +571,12 @@
         return mUserContext.getUser();
     }
 
-    private void updateDragResizeListener(SurfaceControl oldDecorationSurface) {
-        if (!isDragResizable(mTaskInfo)) {
+    private void updateDragResizeListener(SurfaceControl oldDecorationSurface,
+            boolean inFullImmersive) {
+        if (!isDragResizable(mTaskInfo, inFullImmersive)) {
             if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
                 // We still want to track caption bar's exclusion region on a non-resizeable task.
-                updateExclusionRegion();
+                updateExclusionRegion(inFullImmersive);
             }
             closeDragResizeListener();
             return;
@@ -609,11 +610,16 @@
                         getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res),
                         getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop)
                 || !mTaskInfo.positionInParent.equals(mPositionInParent)) {
-            updateExclusionRegion();
+            updateExclusionRegion(inFullImmersive);
         }
     }
 
-    private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
+    private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo,
+            boolean inFullImmersive) {
+        if (inFullImmersive) {
+            // Task cannot be resized in full immersive.
+            return false;
+        }
         if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue()) {
             return taskInfo.isFreeform();
         }
@@ -677,8 +683,8 @@
         mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
     }
 
-    private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
-        if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) {
+    private void updateMaximizeMenu(SurfaceControl.Transaction startT, boolean inFullImmersive) {
+        if (!isDragResizable(mTaskInfo, inFullImmersive) || !isMaximizeMenuActive()) {
             return;
         }
         if (!mTaskInfo.isVisible()) {
@@ -1546,24 +1552,29 @@
         mPositionInParent.set(mTaskInfo.positionInParent);
     }
 
-    private void updateExclusionRegion() {
+    private void updateExclusionRegion(boolean inFullImmersive) {
         // An outdated position in parent is one reason for this to be called; update it here.
         updatePositionInParent();
         mExclusionRegionListener
-                .onExclusionRegionChanged(mTaskInfo.taskId, getGlobalExclusionRegion());
+                .onExclusionRegionChanged(mTaskInfo.taskId,
+                        getGlobalExclusionRegion(inFullImmersive));
     }
 
     /**
      * Create a new exclusion region from the corner rects (if resizeable) and caption bounds
      * of this task.
      */
-    private Region getGlobalExclusionRegion() {
+    private Region getGlobalExclusionRegion(boolean inFullImmersive) {
         Region exclusionRegion;
-        if (mDragResizeListener != null && isDragResizable(mTaskInfo)) {
+        if (mDragResizeListener != null && isDragResizable(mTaskInfo, inFullImmersive)) {
             exclusionRegion = mDragResizeListener.getCornersRegion();
         } else {
             exclusionRegion = new Region();
         }
+        if (inFullImmersive) {
+            // Task can't be moved in full immersive, so skip excluding the caption region.
+            return exclusionRegion;
+        }
         exclusionRegion.union(new Rect(0, 0, mResult.mWidth,
                 getCaptionHeight(mTaskInfo.getWindowingMode())));
         exclusionRegion.translate(mPositionInParent.x, mPositionInParent.y);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
new file mode 100644
index 0000000..cae6095
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode
+
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for [DesktopFullImmersiveTransitionHandler].
+ *
+ * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandler
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
+
+    @Mock private lateinit var mockTransitions: Transitions
+    private lateinit var desktopRepository: DesktopRepository
+    private val transactionSupplier = { SurfaceControl.Transaction() }
+
+    private lateinit var immersiveHandler: DesktopFullImmersiveTransitionHandler
+
+    @Before
+    fun setUp() {
+        desktopRepository = DesktopRepository(
+            context, ShellInit(TestShellExecutor()), mock(), mock()
+        )
+        immersiveHandler = DesktopFullImmersiveTransitionHandler(
+            transitions = mockTransitions,
+            desktopRepository = desktopRepository,
+            transactionSupplier = transactionSupplier
+        )
+    }
+
+    @Test
+    fun enterImmersive_transitionReady_updatesRepository() {
+        val task = createFreeformTask()
+        val wct = WindowContainerTransaction()
+        val mockBinder = mock(IBinder::class.java)
+        whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+            .thenReturn(mockBinder)
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = task.displayId,
+            taskId = task.taskId,
+            immersive = false
+        )
+
+        immersiveHandler.enterImmersive(task, wct)
+        immersiveHandler.onTransitionReady(mockBinder)
+
+        assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isTrue()
+    }
+
+    @Test
+    fun exitImmersive_transitionReady_updatesRepository() {
+        val task = createFreeformTask()
+        val wct = WindowContainerTransaction()
+        val mockBinder = mock(IBinder::class.java)
+        whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+            .thenReturn(mockBinder)
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = task.displayId,
+            taskId = task.taskId,
+            immersive = true
+        )
+
+        immersiveHandler.exitImmersive(task, wct)
+        immersiveHandler.onTransitionReady(mockBinder)
+
+        assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
+    }
+
+    @Test
+    fun enterImmersive_inProgress_ignores() {
+        val task = createFreeformTask()
+        val wct = WindowContainerTransaction()
+        val mockBinder = mock(IBinder::class.java)
+        whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+            .thenReturn(mockBinder)
+
+        immersiveHandler.enterImmersive(task, wct)
+        immersiveHandler.enterImmersive(task, wct)
+
+        verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+    }
+
+    @Test
+    fun exitImmersive_inProgress_ignores() {
+        val task = createFreeformTask()
+        val wct = WindowContainerTransaction()
+        val mockBinder = mock(IBinder::class.java)
+        whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+            .thenReturn(mockBinder)
+
+        immersiveHandler.exitImmersive(task, wct)
+        immersiveHandler.exitImmersive(task, wct)
+
+        verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+    }
+}
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 0ccd160..9e5c1a6 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
@@ -147,6 +147,7 @@
 import org.mockito.Mockito.times
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argThat
 import org.mockito.kotlin.atLeastOnce
 import org.mockito.kotlin.capture
 import org.mockito.kotlin.eq
@@ -183,6 +184,8 @@
   @Mock
   lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler
   @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
+  @Mock
+  lateinit var mockDesktopFullImmersiveTransitionHandler: DesktopFullImmersiveTransitionHandler
   @Mock lateinit var launchAdjacentController: LaunchAdjacentController
   @Mock lateinit var splitScreenController: SplitScreenController
   @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
@@ -289,6 +292,7 @@
         dragAndDropTransitionHandler,
         toggleResizeDesktopTaskTransitionHandler,
         dragToDesktopTransitionHandler,
+        mockDesktopFullImmersiveTransitionHandler,
         taskRepository,
         desktopModeLoggerTransitionObserver,
         launchAdjacentController,
@@ -3123,6 +3127,30 @@
       verify(shellController, times(1)).addUserChangeListener(any())
   }
 
+  @Test
+  fun toggleImmersive_enter_resizesToDisplayBounds() {
+    val task = setUpFreeformTask(DEFAULT_DISPLAY)
+    taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, false /* immersive */)
+
+    controller.toggleDesktopTaskFullImmersiveState(task)
+
+    verify(mockDesktopFullImmersiveTransitionHandler).enterImmersive(eq(task), argThat { wct ->
+      wct.hasBoundsChange(task.token, Rect())
+    })
+  }
+
+  @Test
+  fun toggleImmersive_exit_resizesToStableBounds() {
+    val task = setUpFreeformTask(DEFAULT_DISPLAY)
+    taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, true /* immersive */)
+
+    controller.toggleDesktopTaskFullImmersiveState(task)
+
+    verify(mockDesktopFullImmersiveTransitionHandler).exitImmersive(eq(task), argThat { wct ->
+      wct.hasBoundsChange(task.token, STABLE_BOUNDS)
+    })
+  }
+
   /**
    * Assert that an unhandled drag event launches a PendingIntent with the
    * windowing mode and bounds we are expecting.
@@ -3488,6 +3516,13 @@
       .isEqualTo(windowingMode)
 }
 
+private fun WindowContainerTransaction.hasBoundsChange(
+  token: WindowContainerToken,
+  bounds: Rect
+): Boolean = this.changes.any { change ->
+  change.key == token.asBinder() && change.value.configuration.windowConfiguration.bounds == bounds
+}
+
 private fun WindowContainerTransaction?.anyDensityConfigChange(
     token: WindowContainerToken
 ): Boolean {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 499e339..77b2b0d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.freeform;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 
@@ -30,6 +31,8 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.IBinder;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.view.SurfaceControl;
 import android.window.IWindowContainerToken;
 import android.window.TransitionInfo;
@@ -37,28 +40,38 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.window.flags.Flags;
+import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.TransitionInfoBuilder;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 /**
  * Tests of {@link FreeformTaskTransitionObserver}
  */
 @SmallTest
 public class FreeformTaskTransitionObserverTest {
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Mock
     private ShellInit mShellInit;
     @Mock
     private Transitions mTransitions;
     @Mock
+    private DesktopFullImmersiveTransitionHandler mDesktopFullImmersiveTransitionHandler;
+    @Mock
     private WindowDecorViewModel mWindowDecorViewModel;
 
     private FreeformTaskTransitionObserver mTransitionObserver;
@@ -74,7 +87,9 @@
         doReturn(pm).when(context).getPackageManager();
 
         mTransitionObserver = new FreeformTaskTransitionObserver(
-                context, mShellInit, mTransitions, mWindowDecorViewModel);
+                context, mShellInit, mTransitions,
+                Optional.of(mDesktopFullImmersiveTransitionHandler),
+                mWindowDecorViewModel);
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(
                     Runnable.class);
@@ -223,6 +238,19 @@
         verify(mWindowDecorViewModel).destroyWindowDecoration(change2.getTaskInfo());
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    public void onTransitionReady_forwardsToDesktopImmersiveHandler() {
+        final IBinder transition = mock(IBinder.class);
+        final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, 0).build();
+        final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+        final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+
+        mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+
+        verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition);
+    }
+
     private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
         final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
         taskInfo.taskId = taskId;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 83bd15b..4aa7e18 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -1221,9 +1221,48 @@
         assertEquals(decor.mTaskInfo.token.asBinder(), wct.getHierarchyOps().get(0).getContainer())
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun testMaximizeButtonClick_requestingImmersive_togglesDesktopImmersiveState() {
+        val onClickListenerCaptor = forClass(View.OnClickListener::class.java)
+                as ArgumentCaptor<View.OnClickListener>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            onCaptionButtonClickListener = onClickListenerCaptor,
+            requestingImmersive = true,
+        )
+        val view = mock(View::class.java)
+        whenever(view.id).thenReturn(R.id.maximize_window)
+
+        onClickListenerCaptor.value.onClick(view)
+
+        verify(mockDesktopTasksController)
+            .toggleDesktopTaskFullImmersiveState(decor.mTaskInfo)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun testMaximizeButtonClick_notRequestingImmersive_togglesDesktopTaskSize() {
+        val onClickListenerCaptor = forClass(View.OnClickListener::class.java)
+                as ArgumentCaptor<View.OnClickListener>
+        val decor = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            onCaptionButtonClickListener = onClickListenerCaptor,
+            requestingImmersive = false,
+        )
+        val view = mock(View::class.java)
+        whenever(view.id).thenReturn(R.id.maximize_window)
+
+        onClickListenerCaptor.value.onClick(view)
+
+        verify(mockDesktopTasksController)
+            .toggleDesktopTaskSize(decor.mTaskInfo)
+    }
+
     private fun createOpenTaskDecoration(
         @WindowingMode windowingMode: Int,
         taskSurface: SurfaceControl = SurfaceControl(),
+        requestingImmersive: Boolean = false,
         onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> =
             forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
         onLeftSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> =
@@ -1243,7 +1282,10 @@
         onCaptionButtonTouchListener: ArgumentCaptor<View.OnTouchListener> =
             forClass(View.OnTouchListener::class.java) as ArgumentCaptor<View.OnTouchListener>
     ): DesktopModeWindowDecoration {
-        val decor = setUpMockDecorationForTask(createTask(windowingMode = windowingMode))
+        val decor = setUpMockDecorationForTask(createTask(
+            windowingMode = windowingMode,
+            requestingImmersive = requestingImmersive
+        ))
         onTaskOpening(decor.mTaskInfo, taskSurface)
         verify(decor).setOnMaximizeOrRestoreClickListener(onMaxOrRestoreListenerCaptor.capture())
         verify(decor).setOnLeftSnapClickListener(onLeftSnapClickListenerCaptor.capture())
@@ -1282,6 +1324,7 @@
             activityType: Int = ACTIVITY_TYPE_STANDARD,
             focused: Boolean = true,
             activityInfo: ActivityInfo = ActivityInfo(),
+            requestingImmersive: Boolean = false
     ): RunningTaskInfo {
         return TestRunningTaskInfoBuilder()
                 .setDisplayId(displayId)
@@ -1292,6 +1335,11 @@
                     topActivityInfo = activityInfo
                     isFocused = focused
                     isResizeable = true
+                    requestedVisibleTypes = if (requestingImmersive) {
+                        statusBars().inv()
+                    } else {
+                        statusBars()
+                    }
                 }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 1e9541e..6d1d9cb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -189,6 +189,7 @@
                     internalTransitionInteractor.currentTransitionInfoInternal,
                     keyguardInteractor.statusBarState,
                     keyguardInteractor.isKeyguardDismissible,
+                    keyguardInteractor.isKeyguardOccluded,
                 )
                 .collect {
                     (
@@ -196,7 +197,8 @@
                         startedStep,
                         currentTransitionInfo,
                         statusBarState,
-                        isKeyguardUnlocked) ->
+                        isKeyguardUnlocked,
+                        isKeyguardOccluded) ->
                     val id = transitionId
                     if (id != null) {
                         if (startedStep.to == KeyguardState.PRIMARY_BOUNCER) {
@@ -236,9 +238,13 @@
                             if (nextState == TransitionState.CANCELED) {
                                 transitionRepository.startTransition(
                                     TransitionInfo(
-                                        ownerName = name,
+                                        ownerName =
+                                            "$name " +
+                                                "(on behalf of FromPrimaryBouncerInteractor)",
                                         from = KeyguardState.PRIMARY_BOUNCER,
-                                        to = KeyguardState.LOCKSCREEN,
+                                        to =
+                                            if (isKeyguardOccluded) KeyguardState.OCCLUDED
+                                            else KeyguardState.LOCKSCREEN,
                                         modeOnCanceled = TransitionModeOnCanceled.REVERSE,
                                         animator =
                                             getDefaultAnimatorForTransitionsToState(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 5896659..2bff7c86 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -30,6 +30,7 @@
 import static com.android.systemui.classifier.Classifier.GENERIC;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 import static com.android.systemui.classifier.Classifier.UNLOCK;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.AOD;
 import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING;
 import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING_LOCKSCREEN_HOSTED;
 import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE;
@@ -1213,6 +1214,16 @@
                     }, mMainDispatcher);
         }
 
+        if (MigrateClocksToBlueprint.isEnabled()) {
+            collectFlow(mView, mKeyguardTransitionInteractor.transition(
+                    Edge.Companion.create(AOD, LOCKSCREEN)),
+                    (TransitionStep step) -> {
+                    if (step.getTransitionState() == TransitionState.FINISHED) {
+                        updateExpandedHeightToMaxHeight();
+                    }
+                }, mMainDispatcher);
+        }
+
         // Ensures that flags are updated when an activity launches
         collectFlow(mView,
                 mShadeAnimationInteractor.isLaunchingActivity(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index f5a90196..0e9ef06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -556,11 +556,13 @@
             return null;
         }).when(mView).setOnTouchListener(any(NotificationPanelViewController.TouchHandler.class));
 
-        // Dreaming->Lockscreen
+        // Any edge transition
         when(mKeyguardTransitionInteractor.transition(any()))
                 .thenReturn(emptyFlow());
         when(mKeyguardTransitionInteractor.transition(any(), any()))
                 .thenReturn(emptyFlow());
+
+        // Dreaming->Lockscreen
         when(mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha())
                 .thenReturn(emptyFlow());
         when(mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(anyInt()))
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index 86246e2..72f62c5 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -5,7 +5,8 @@
     { "name": "hoststubgen-test-tiny-test" },
     { "name": "hoststubgen-invoke-test" },
     { "name": "RavenwoodMockitoTest_device" },
-    { "name": "RavenwoodBivalentTest_device" },
+    // TODO(b/371215487): Re-enable when the test is fixed.
+    // { "name": "RavenwoodBivalentTest_device" },
 
     { "name": "RavenwoodBivalentInstTest_nonself_inst" },
     { "name": "RavenwoodBivalentInstTest_self_inst_device" },
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 83bc75e..9219cc12 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -18398,25 +18398,34 @@
                     "Cannot kill the dependents of a package without its name.");
         }
 
+        userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
+                userId, true, ALLOW_FULL_ONLY, "killPackageDependents", null);
+        final int[] userIds = mUserController.expandUserId(userId);
+
         final long callingId = Binder.clearCallingIdentity();
         IPackageManager pm = AppGlobals.getPackageManager();
-        int pkgUid = -1;
         try {
-            pkgUid = pm.getPackageUid(packageName, MATCH_DEBUG_TRIAGED_MISSING, userId);
-        } catch (RemoteException e) {
-        }
-        if (userId != UserHandle.USER_ALL && pkgUid == -1) {
-            throw new IllegalArgumentException(
-                    "Cannot kill dependents of non-existing package " + packageName);
-        }
-        try {
-            synchronized(this) {
-                synchronized (mProcLock) {
-                    mProcessList.killPackageProcessesLSP(packageName, UserHandle.getAppId(pkgUid),
-                            userId, ProcessList.FOREGROUND_APP_ADJ,
-                            ApplicationExitInfo.REASON_DEPENDENCY_DIED,
-                            ApplicationExitInfo.SUBREASON_UNKNOWN,
-                            "dep: " + packageName);
+            for (int targetUserId : userIds) {
+                int pkgUid = -1;
+                try {
+                    pkgUid = pm.getPackageUid(packageName, MATCH_DEBUG_TRIAGED_MISSING,
+                            targetUserId);
+                } catch (RemoteException e) {
+                }
+                if (userId != UserHandle.USER_ALL && pkgUid == -1) {
+                    throw new IllegalArgumentException(
+                            "Cannot kill dependents of non-existing package " + packageName);
+                }
+                synchronized (this) {
+                    synchronized (mProcLock) {
+                        mProcessList.killPackageProcessesLSP(packageName,
+                                UserHandle.getAppId(pkgUid),
+                                targetUserId,
+                                ProcessList.FOREGROUND_APP_ADJ,
+                                ApplicationExitInfo.REASON_DEPENDENCY_DIED,
+                                ApplicationExitInfo.SUBREASON_UNKNOWN,
+                                "dep: " + packageName);
+                    }
                 }
             }
         } finally {
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index e0cf96f..596e375 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -72,6 +72,9 @@
 import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
 import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled;
 
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_PROXY_OPERATION;
 import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
 
 import android.Manifest;
@@ -160,6 +163,7 @@
 import com.android.internal.pm.pkg.component.ParsedAttribution;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
@@ -2829,12 +2833,26 @@
 
     @Override
     public int checkOperation(int code, int uid, String packageName) {
+        if (Flags.appopAccessTrackingLoggingEnabled()) {
+            FrameworkStatsLog.write(
+                    FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
+                    uid, code,
+                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
+                    false);
+        }
         return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, null,
                 Context.DEVICE_ID_DEFAULT, false /*raw*/);
     }
 
     @Override
     public int checkOperationForDevice(int code, int uid, String packageName, int virtualDeviceId) {
+        if (Flags.appopAccessTrackingLoggingEnabled()) {
+            FrameworkStatsLog.write(
+                    FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
+                    uid, code,
+                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
+                    false);
+        }
         return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, null,
                 virtualDeviceId, false /*raw*/);
     }
@@ -3015,6 +3033,14 @@
     public SyncNotedAppOp noteProxyOperationWithState(int code,
             AttributionSourceState attributionSourceState, boolean shouldCollectAsyncNotedOp,
             String message, boolean shouldCollectMessage, boolean skipProxyOperation) {
+        if (Flags.appopAccessTrackingLoggingEnabled()) {
+            FrameworkStatsLog.write(
+                    FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
+                    attributionSourceState.uid, code,
+                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_PROXY_OPERATION,
+                    attributionSourceState.attributionTag != null);
+        }
+
         AttributionSource attributionSource = new AttributionSource(attributionSourceState);
         return mCheckOpsDelegateDispatcher.noteProxyOperation(code, attributionSource,
                 shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation);
@@ -3096,6 +3122,14 @@
     public SyncNotedAppOp noteOperation(int code, int uid, String packageName,
             String attributionTag, boolean shouldCollectAsyncNotedOp, String message,
             boolean shouldCollectMessage) {
+        if (Flags.appopAccessTrackingLoggingEnabled()) {
+            FrameworkStatsLog.write(
+                    FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
+                    uid, code,
+                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION,
+                    attributionTag != null);
+        }
+
         return mCheckOpsDelegateDispatcher.noteOperation(code, uid, packageName,
                 attributionTag, Context.DEVICE_ID_DEFAULT, shouldCollectAsyncNotedOp, message,
                 shouldCollectMessage);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3dc531e..bb503aa 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -5233,9 +5233,10 @@
         }
 
         @Override
-        public boolean isProximitySensorAvailable(int displayId) {
+        public boolean isProximitySensorAvailable() {
             synchronized (mSyncRoot) {
-                return mDisplayPowerControllers.get(displayId).isProximitySensorAvailable();
+                return mDisplayPowerControllers.get(Display.DEFAULT_DISPLAY)
+                        .isProximitySensorAvailable();
             }
         }
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 99e66a2..6c2d4f7 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -12018,6 +12018,10 @@
         if (record != null && (record.getSbn().getNotification().flags
                 & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
                 && !record.isCanceledAfterLifetimeExtension()) {
+            // Mark that the notification is being updated due to cancelation, so it won't
+            // be updated again if the app cancels multiple times.
+            record.setCanceledAfterLifetimeExtension(true);
+
             boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND;
 
             // Save the original Record's post silently value, so we can restore it after we send
@@ -12033,9 +12037,6 @@
             PostNotificationTracker tracker = mPostNotificationTrackerFactory.newTracker(null);
             tracker.addCleanupRunnable(() -> {
                 synchronized (mNotificationLock) {
-                    // Mark that the notification has been updated due to cancelation, so it won't
-                    // be updated again if the app cancels multiple times.
-                    record.setCanceledAfterLifetimeExtension(true);
                     // Set the post silently status to the record's previous value.
                     record.setPostSilently(savedPostSilentlyState);
                     // Remove FLAG_ONLY_ALERT_ONCE if the notification did not previously have it.
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 65f2241..21ab781 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -743,7 +743,6 @@
                 int reason, int uid, int opUid, String opPackageName, String details) {
             mWakefulnessChanging = true;
             mDirty |= DIRTY_WAKEFULNESS;
-            mInjector.invalidateIsInteractiveCaches();
             if (wakefulness == WAKEFULNESS_AWAKE) {
                 // Kick user activity to prevent newly awake group from timing out instantly.
                 // The dream may end without user activity if the dream app crashes / is updated,
@@ -2036,7 +2035,7 @@
     }
 
     @SuppressWarnings("deprecation")
-    private boolean isWakeLockLevelSupportedInternal(int level, int displayId) {
+    private boolean isWakeLockLevelSupportedInternal(int level) {
         synchronized (mLock) {
             switch (level) {
                 case PowerManager.PARTIAL_WAKE_LOCK:
@@ -2048,8 +2047,7 @@
                     return true;
 
                 case PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK:
-                    return mSystemReady
-                            && mDisplayManagerInternal.isProximitySensorAvailable(displayId);
+                    return mSystemReady && mDisplayManagerInternal.isProximitySensorAvailable();
                 case PowerManager.SCREEN_TIMEOUT_OVERRIDE_WAKE_LOCK:
                     return mSystemReady && mFeatureFlags.isEarlyScreenTimeoutDetectorEnabled()
                             && mScreenTimeoutOverridePolicy != null;
@@ -2266,6 +2264,7 @@
             int opUid, String opPackageName, String details) {
         mPowerGroups.get(groupId).setWakefulnessLocked(wakefulness, eventTime, uid, reason, opUid,
                 opPackageName, details);
+        mInjector.invalidateIsInteractiveCaches();
     }
 
     @SuppressWarnings("deprecation")
@@ -2330,6 +2329,8 @@
         Trace.traceBegin(Trace.TRACE_TAG_POWER, traceMethodName);
         try {
             // Phase 2: Handle wakefulness change and bookkeeping.
+            // Under lock, invalidate before set ensures caches won't return stale values.
+            mInjector.invalidateIsInteractiveCaches();
             mWakefulnessRaw = newWakefulness;
             mWakefulnessChanging = true;
             mDirty |= DIRTY_WAKEFULNESS;
@@ -2427,7 +2428,6 @@
     void onPowerGroupEventLocked(int event, PowerGroup powerGroup) {
         mWakefulnessChanging = true;
         mDirty |= DIRTY_WAKEFULNESS;
-        mInjector.invalidateIsInteractiveCaches();
         final int groupId = powerGroup.getGroupId();
         if (event == DisplayGroupPowerChangeListener.DISPLAY_GROUP_REMOVED) {
             mPowerGroups.delete(groupId);
@@ -3975,9 +3975,6 @@
 
     private boolean isInteractiveInternal(int displayId, int uid) {
         synchronized (mLock) {
-            if (!mSystemReady) {
-                return isGloballyInteractiveInternal();
-            }
             DisplayInfo displayInfo = mDisplayManagerInternal.getDisplayInfo(displayId);
             if (displayInfo == null) {
                 Slog.w(TAG, "Did not find DisplayInfo for displayId " + displayId);
@@ -5978,17 +5975,7 @@
         public boolean isWakeLockLevelSupported(int level) {
             final long ident = Binder.clearCallingIdentity();
             try {
-                return isWakeLockLevelSupportedInternal(level, Display.DEFAULT_DISPLAY);
-            } finally {
-                Binder.restoreCallingIdentity(ident);
-            }
-        }
-
-        @Override // Binder call
-        public boolean isWakeLockLevelSupportedWithDisplayId(int level, int displayId) {
-            final long ident = Binder.clearCallingIdentity();
-            try {
-                return isWakeLockLevelSupportedInternal(level, displayId);
+                return isWakeLockLevelSupportedInternal(level);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
diff --git a/services/tests/powerservicetests/src/com/android/server/power/LowPowerStandbyControllerTest.java b/services/tests/powerservicetests/src/com/android/server/power/LowPowerStandbyControllerTest.java
index 3e731a3..0e815d0 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/LowPowerStandbyControllerTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/LowPowerStandbyControllerTest.java
@@ -163,7 +163,6 @@
         addLocalServiceMock(ActivityManagerInternal.class, mActivityManagerInternalMock);
 
         when(mIPowerManagerMock.isInteractive()).thenReturn(true);
-        when(mIPowerManagerMock.isDisplayInteractive(anyInt())).thenReturn(true);
 
         when(mDeviceConfigWrapperMock.enableCustomPolicy()).thenReturn(true);
         when(mDeviceConfigWrapperMock.enableStandbyPorts()).thenReturn(true);
@@ -900,13 +899,11 @@
 
     private void setInteractive() throws Exception {
         when(mIPowerManagerMock.isInteractive()).thenReturn(true);
-        when(mIPowerManagerMock.isDisplayInteractive(anyInt())).thenReturn(true);
         mContextSpy.sendBroadcast(new Intent(Intent.ACTION_SCREEN_ON));
     }
 
     private void setNonInteractive() throws Exception {
         when(mIPowerManagerMock.isInteractive()).thenReturn(false);
-        when(mIPowerManagerMock.isDisplayInteractive(anyInt())).thenReturn(false);
         mContextSpy.sendBroadcast(new Intent(Intent.ACTION_SCREEN_OFF));
     }
 
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index e9e21de..54a02cf 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -1659,64 +1659,6 @@
     }
 
     @Test
-    public void testIsWakeLockLevelSupported_returnsCorrectValue() {
-        createService();
-        startSystem();
-        PowerManagerService.BinderService service = mService.getBinderServiceInstance();
-        assertThat(service.isWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK)).isTrue();
-        assertThat(service.isWakeLockLevelSupported(PowerManager.SCREEN_BRIGHT_WAKE_LOCK)).isTrue();
-        assertThat(service.isWakeLockLevelSupported(PowerManager.FULL_WAKE_LOCK)).isTrue();
-        assertThat(service.isWakeLockLevelSupported(PowerManager.DOZE_WAKE_LOCK)).isTrue();
-        assertThat(service.isWakeLockLevelSupported(PowerManager.DRAW_WAKE_LOCK)).isTrue();
-
-        when(mDisplayManagerInternalMock.isProximitySensorAvailable(eq(Display.DEFAULT_DISPLAY)))
-                .thenReturn(true);
-        assertThat(service.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK))
-                .isTrue();
-
-        when(mDisplayManagerInternalMock.isProximitySensorAvailable(eq(Display.DEFAULT_DISPLAY)))
-                .thenReturn(false);
-        assertThat(service.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK))
-                .isFalse();
-    }
-
-    @Test
-    public void testIsWakeLockLevelSupportedWithDisplayId_nonDefaultDisplay_returnsCorrectValue() {
-        createService();
-        startSystem();
-        int displayId = Display.DEFAULT_DISPLAY + 1;
-        PowerManagerService.BinderService service = mService.getBinderServiceInstance();
-        assertThat(service.isWakeLockLevelSupportedWithDisplayId(
-                PowerManager.PARTIAL_WAKE_LOCK, displayId))
-                .isTrue();
-        assertThat(service.isWakeLockLevelSupportedWithDisplayId(
-                PowerManager.SCREEN_BRIGHT_WAKE_LOCK, displayId))
-                .isTrue();
-        assertThat(service.isWakeLockLevelSupportedWithDisplayId(
-                PowerManager.FULL_WAKE_LOCK, displayId))
-                .isTrue();
-        assertThat(service.isWakeLockLevelSupportedWithDisplayId(
-                PowerManager.DOZE_WAKE_LOCK, displayId))
-                .isTrue();
-        assertThat(service.isWakeLockLevelSupportedWithDisplayId(
-                PowerManager.DRAW_WAKE_LOCK, displayId))
-                .isTrue();
-
-        when(mDisplayManagerInternalMock.isProximitySensorAvailable(eq(displayId)))
-                .thenReturn(true);
-        assertThat(service.isWakeLockLevelSupportedWithDisplayId(
-                PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, displayId))
-                .isTrue();
-
-        when(mDisplayManagerInternalMock.isProximitySensorAvailable(eq(displayId)))
-                .thenReturn(false);
-        assertThat(service.isWakeLockLevelSupportedWithDisplayId(
-                PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, displayId))
-                .isFalse();
-    }
-
-
-    @Test
     public void testWakeLock_affectsProperDisplayGroup() {
         final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
         final AtomicReference<DisplayManagerInternal.DisplayGroupListener> listener =
@@ -1757,47 +1699,6 @@
     }
 
     @Test
-    public void testWakeLock_nonDefaultDisplay_affectsProperDisplayGroup() {
-        final int nonDefaultDisplayId = Display.DEFAULT_DISPLAY + 1;
-        final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
-        final AtomicReference<DisplayManagerInternal.DisplayGroupListener> listener =
-                new AtomicReference<>();
-        doAnswer((Answer<Void>) invocation -> {
-            listener.set(invocation.getArgument(0));
-            return null;
-        }).when(mDisplayManagerInternalMock).registerDisplayGroupListener(any());
-        final DisplayInfo info = new DisplayInfo();
-        info.displayGroupId = nonDefaultDisplayGroupId;
-        when(mDisplayManagerInternalMock.getDisplayInfo(nonDefaultDisplayId)).thenReturn(info);
-
-        final String pkg = mContextSpy.getOpPackageName();
-        final Binder token = new Binder();
-        final String tag = "testWakeLock_nonDefaultDisplay_affectsProperDisplayGroup";
-
-        setMinimumScreenOffTimeoutConfig(5);
-        createService();
-        startSystem();
-        listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId);
-
-        mService.getBinderServiceInstance().acquireWakeLock(token,
-                PowerManager.SCREEN_BRIGHT_WAKE_LOCK, tag, pkg,
-                null /* workSource */, null /* historyTag */, nonDefaultDisplayId, null);
-
-        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
-        assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo(
-                WAKEFULNESS_AWAKE);
-        assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo(
-                WAKEFULNESS_AWAKE);
-
-        advanceTime(15000);
-        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
-        assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo(
-                WAKEFULNESS_ASLEEP);
-        assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo(
-                WAKEFULNESS_AWAKE);
-    }
-
-    @Test
     public void testInvalidDisplayGroupWakeLock_affectsAllDisplayGroups() {
         final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
         final AtomicReference<DisplayManagerInternal.DisplayGroupListener> listener =
@@ -2689,15 +2590,14 @@
         when(mDisplayManagerInternalMock.getDisplayInfo(nonDefaultDisplay)).thenReturn(info);
         createService();
         startSystem();
-        verify(mInvalidateInteractiveCachesMock).call();
-
         listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId);
-        verify(mInvalidateInteractiveCachesMock, times(2)).call();
+
+        verify(mInvalidateInteractiveCachesMock).call();
 
         mService.setWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP, WAKEFULNESS_ASLEEP,
                 mClock.now(), 0, PowerManager.GO_TO_SLEEP_REASON_APPLICATION, 0, null, null);
 
-        verify(mInvalidateInteractiveCachesMock, times(3)).call();
+        verify(mInvalidateInteractiveCachesMock, times(2)).call();
     }
 
     @Test
@@ -2716,15 +2616,14 @@
         when(mDisplayManagerInternalMock.getDisplayInfo(nonDefaultDisplay)).thenReturn(info);
         createService();
         startSystem();
-        verify(mInvalidateInteractiveCachesMock).call();
-
         listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId);
-        verify(mInvalidateInteractiveCachesMock, times(2)).call();
+
+        verify(mInvalidateInteractiveCachesMock).call();
 
         mService.setWakefulnessLocked(nonDefaultDisplayGroupId, WAKEFULNESS_ASLEEP, mClock.now(),
                 0, PowerManager.GO_TO_SLEEP_REASON_APPLICATION, 0, null, null);
 
-        verify(mInvalidateInteractiveCachesMock, times(3)).call();
+        verify(mInvalidateInteractiveCachesMock, times(2)).call();
     }
 
     @Test
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 3c120e1..1349ee0 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -3206,7 +3206,6 @@
         // Send two cancelations.
         mBinderService.cancelNotificationWithTag(mPkg, mPkg, sbn.getTag(), sbn.getId(),
                 sbn.getUserId());
-        waitForIdle();
         mBinderService.cancelNotificationWithTag(mPkg, mPkg, sbn.getTag(), sbn.getId(),
                 sbn.getUserId());
         waitForIdle();