Merge "Fix null object reference" into udc-qpr-dev
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index 2b5175c..634089b 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -248,6 +248,13 @@
     public boolean topActivityEligibleForUserAspectRatioButton;
 
     /**
+     * Whether the user has forced the activity to be fullscreen through the user aspect ratio
+     * settings.
+     * @hide
+     */
+    public boolean isUserFullscreenOverrideEnabled;
+
+    /**
      * Hint about the letterbox state of the top activity.
      * @hide
      */
@@ -543,7 +550,8 @@
                 && isSleeping == that.isSleeping
                 && Objects.equals(mTopActivityLocusId, that.mTopActivityLocusId)
                 && parentTaskId == that.parentTaskId
-                && Objects.equals(topActivity, that.topActivity);
+                && Objects.equals(topActivity, that.topActivity)
+                && isUserFullscreenOverrideEnabled == that.isUserFullscreenOverrideEnabled;
     }
 
     /**
@@ -574,7 +582,8 @@
                 && (!hasCompatUI() || configuration.getLayoutDirection()
                     == that.configuration.getLayoutDirection())
                 && (!hasCompatUI() || configuration.uiMode == that.configuration.uiMode)
-                && (!hasCompatUI() || isVisible == that.isVisible);
+                && (!hasCompatUI() || isVisible == that.isVisible)
+                && isUserFullscreenOverrideEnabled == that.isUserFullscreenOverrideEnabled;
     }
 
     /**
@@ -630,6 +639,7 @@
         topActivityLetterboxHorizontalPosition = source.readInt();
         topActivityLetterboxWidth = source.readInt();
         topActivityLetterboxHeight = source.readInt();
+        isUserFullscreenOverrideEnabled = source.readBoolean();
     }
 
     /**
@@ -686,6 +696,7 @@
         dest.writeInt(topActivityLetterboxHorizontalPosition);
         dest.writeInt(topActivityLetterboxWidth);
         dest.writeInt(topActivityLetterboxHeight);
+        dest.writeBoolean(isUserFullscreenOverrideEnabled);
     }
 
     @Override
@@ -732,6 +743,7 @@
                         + topActivityLetterboxHorizontalPosition
                 + " topActivityLetterboxWidth=" + topActivityLetterboxWidth
                 + " topActivityLetterboxHeight=" + topActivityLetterboxHeight
+                + " isUserFullscreenOverrideEnabled=" + isUserFullscreenOverrideEnabled
                 + " locusId=" + mTopActivityLocusId
                 + " displayAreaFeatureId=" + displayAreaFeatureId
                 + " cameraCompatControlState="
diff --git a/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button.xml b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button.xml
new file mode 100644
index 0000000..6e4752c
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="48"
+        android:viewportHeight="48">
+    <path
+        android:fillColor="@color/compat_controls_background"
+        android:strokeAlpha="0.8"
+        android:fillAlpha="0.8"
+        android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0"/>
+    <group
+        android:translateX="12"
+        android:translateY="12">
+        <path
+            android:fillColor="@color/compat_controls_text"
+            android:pathData="M19,12h-2v3h-3v2h5v-5zM7,9h3L10,7L5,7v5h2L7,9zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z"/>
+    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button_ripple.xml b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button_ripple.xml
new file mode 100644
index 0000000..141a1ce
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/user_aspect_ratio_settings_button_ripple.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+        android:color="@color/compat_background_ripple">
+    <item android:drawable="@drawable/user_aspect_ratio_settings_button"/>
+</ripple>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
index dfaeeeb..257fe15 100644
--- a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
@@ -55,7 +55,7 @@
 
     <include android:id="@+id/size_compat_hint"
         android:visibility="gone"
-        android:layout_width="@dimen/size_compat_hint_width"
+        android:layout_width="@dimen/compat_hint_width"
         android:layout_height="wrap_content"
         layout="@layout/compat_mode_hint"/>
 
diff --git a/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml
new file mode 100644
index 0000000..433d854
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+<com.android.wm.shell.compatui.UserAspectRatioSettingsLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:gravity="bottom|end">
+
+    <include android:id="@+id/user_aspect_ratio_settings_hint"
+        android:visibility="gone"
+        android:layout_width="@dimen/compat_hint_width"
+        android:layout_height="wrap_content"
+        layout="@layout/compat_mode_hint"/>
+
+    <ImageButton
+        android:id="@+id/user_aspect_ratio_settings_button"
+        android:visibility="gone"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/compat_button_margin"
+        android:layout_marginBottom="@dimen/compat_button_margin"
+        android:src="@drawable/user_aspect_ratio_settings_button_ripple"
+        android:background="@android:color/transparent"
+        android:contentDescription="@string/user_aspect_ratio_settings_button_description"/>
+
+</com.android.wm.shell.compatui.UserAspectRatioSettingsLayout>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 64fed1c..0502a99 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -260,8 +260,8 @@
         + compat_button_margin - compat_hint_corner_radius - compat_hint_point_width / 2). -->
     <dimen name="compat_hint_padding_end">7dp</dimen>
 
-    <!-- The width of the size compat hint. -->
-    <dimen name="size_compat_hint_width">188dp</dimen>
+    <!-- The width of the compat hint. -->
+    <dimen name="compat_hint_width">188dp</dimen>
 
     <!-- The width of the camera compat hint. -->
     <dimen name="camera_compat_hint_width">143dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index b192fdf..8cbc3d0 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -173,7 +173,13 @@
     <string name="accessibility_bubble_dismissed">Bubble dismissed.</string>
 
     <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] -->
-    <string name="restart_button_description">Tap to restart this app for a better view.</string>
+    <string name="restart_button_description">Tap to restart this app for a better view</string>
+
+    <!-- Tooltip text of the button for the user aspect ratio settings. [CHAR LIMIT=NONE] -->
+    <string name="user_aspect_ratio_settings_button_hint">Change this app\'s aspect ratio in Settings</string>
+
+    <!-- Content description of the button for the user aspect ratio settings. [CHAR LIMIT=NONE] -->
+    <string name="user_aspect_ratio_settings_button_description">Change aspect ratio</string>
 
     <!-- Description of the camera compat button for applying stretched issues treatment in the hint for
          compatibility control. [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 0998e71..54f8984 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -16,12 +16,17 @@
 
 package com.android.wm.shell.compatui;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.TaskInfo;
 import android.app.TaskInfo.CameraCompatControlState;
 import android.content.Context;
+import android.content.Intent;
 import android.content.res.Configuration;
 import android.hardware.display.DisplayManager;
+import android.provider.Settings;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
@@ -41,7 +46,6 @@
 import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
@@ -104,6 +108,13 @@
     private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>();
 
     /**
+     * The active user aspect ratio settings button layout if there is one (there can be at most
+     * one active).
+     */
+    @Nullable
+    private UserAspectRatioSettingsWindowManager mUserAspectRatioSettingsLayout;
+
+    /**
      * The active Letterbox Education layout if there is one (there can be at most one active).
      *
      * <p>An active layout is a layout that is eligible to be shown for the associated task but
@@ -121,38 +132,51 @@
     /** Avoid creating display context frequently for non-default display. */
     private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0);
 
+    @NonNull
     private final Context mContext;
+    @NonNull
     private final ShellController mShellController;
+    @NonNull
     private final DisplayController mDisplayController;
+    @NonNull
     private final DisplayInsetsController mDisplayInsetsController;
+    @NonNull
     private final DisplayImeController mImeController;
+    @NonNull
     private final SyncTransactionQueue mSyncQueue;
+    @NonNull
     private final ShellExecutor mMainExecutor;
+    @NonNull
     private final Lazy<Transitions> mTransitionsLazy;
+    @NonNull
     private final DockStateReader mDockStateReader;
+    @NonNull
     private final CompatUIConfiguration mCompatUIConfiguration;
     // Only show each hint once automatically in the process life.
+    @NonNull
     private final CompatUIHintsState mCompatUIHintsState;
+    @NonNull
     private final CompatUIShellCommandHandler mCompatUIShellCommandHandler;
 
-    private CompatUICallback mCallback;
+    @Nullable
+    private CompatUICallback mCompatUICallback;
 
     // Indicates if the keyguard is currently showing, in which case compat UIs shouldn't
     // be shown.
     private boolean mKeyguardShowing;
 
-    public CompatUIController(Context context,
-            ShellInit shellInit,
-            ShellController shellController,
-            DisplayController displayController,
-            DisplayInsetsController displayInsetsController,
-            DisplayImeController imeController,
-            SyncTransactionQueue syncQueue,
-            ShellExecutor mainExecutor,
-            Lazy<Transitions> transitionsLazy,
-            DockStateReader dockStateReader,
-            CompatUIConfiguration compatUIConfiguration,
-            CompatUIShellCommandHandler compatUIShellCommandHandler) {
+    public CompatUIController(@NonNull Context context,
+            @NonNull ShellInit shellInit,
+            @NonNull ShellController shellController,
+            @NonNull DisplayController displayController,
+            @NonNull DisplayInsetsController displayInsetsController,
+            @NonNull DisplayImeController imeController,
+            @NonNull SyncTransactionQueue syncQueue,
+            @NonNull ShellExecutor mainExecutor,
+            @NonNull Lazy<Transitions> transitionsLazy,
+            @NonNull DockStateReader dockStateReader,
+            @NonNull CompatUIConfiguration compatUIConfiguration,
+            @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler) {
         mContext = context;
         mShellController = shellController;
         mDisplayController = displayController;
@@ -175,9 +199,9 @@
         mCompatUIShellCommandHandler.onInit();
     }
 
-    /** Sets the callback for UI interactions. */
-    public void setCompatUICallback(CompatUICallback callback) {
-        mCallback = callback;
+    /** Sets the callback for Compat UI interactions. */
+    public void setCompatUICallback(@NonNull CompatUICallback compatUiCallback) {
+        mCompatUICallback = compatUiCallback;
     }
 
     /**
@@ -187,7 +211,7 @@
      * @param taskInfo {@link TaskInfo} task the activity is in.
      * @param taskListener listener to handle the Task Surface placement.
      */
-    public void onCompatInfoChanged(TaskInfo taskInfo,
+    public void onCompatInfoChanged(@NonNull TaskInfo taskInfo,
             @Nullable ShellTaskOrganizer.TaskListener taskListener) {
         if (taskInfo != null && !taskInfo.topActivityInSizeCompat) {
             mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
@@ -203,6 +227,16 @@
         createOrUpdateRestartDialogLayout(taskInfo, taskListener);
         if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) {
             createOrUpdateReachabilityEduLayout(taskInfo, taskListener);
+            // The user aspect ratio button should not be handled when a new TaskInfo is
+            // sent because of a double tap or when in multi-window mode.
+            if (taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+                mUserAspectRatioSettingsLayout.release();
+                mUserAspectRatioSettingsLayout = null;
+                return;
+            }
+            if (!taskInfo.isFromLetterboxDoubleTap) {
+                createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener);
+            }
         }
     }
 
@@ -280,8 +314,8 @@
         return mDisplaysWithIme.contains(displayId);
     }
 
-    private void createOrUpdateCompatLayout(TaskInfo taskInfo,
-            ShellTaskOrganizer.TaskListener taskListener) {
+    private void createOrUpdateCompatLayout(@NonNull TaskInfo taskInfo,
+            @Nullable ShellTaskOrganizer.TaskListener taskListener) {
         CompatUIWindowManager layout = mActiveCompatLayouts.get(taskInfo.taskId);
         if (layout != null) {
             if (layout.needsToBeRecreated(taskInfo, taskListener)) {
@@ -314,7 +348,7 @@
     CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
             ShellTaskOrganizer.TaskListener taskListener) {
         return new CompatUIWindowManager(context,
-                taskInfo, mSyncQueue, mCallback, taskListener,
+                taskInfo, mSyncQueue, mCompatUICallback, taskListener,
                 mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState,
                 mCompatUIConfiguration, this::onRestartButtonClicked);
     }
@@ -328,12 +362,12 @@
             mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId);
             onCompatInfoChanged(taskInfoState.first, taskInfoState.second);
         } else {
-            mCallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId);
+            mCompatUICallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId);
         }
     }
 
-    private void createOrUpdateLetterboxEduLayout(TaskInfo taskInfo,
-            ShellTaskOrganizer.TaskListener taskListener) {
+    private void createOrUpdateLetterboxEduLayout(@NonNull TaskInfo taskInfo,
+            @Nullable ShellTaskOrganizer.TaskListener taskListener) {
         if (mActiveLetterboxEduLayout != null) {
             if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener)) {
                 mActiveLetterboxEduLayout.release();
@@ -377,8 +411,8 @@
                 mDockStateReader, mCompatUIConfiguration);
     }
 
-    private void createOrUpdateRestartDialogLayout(TaskInfo taskInfo,
-            ShellTaskOrganizer.TaskListener taskListener) {
+    private void createOrUpdateRestartDialogLayout(@NonNull TaskInfo taskInfo,
+            @Nullable ShellTaskOrganizer.TaskListener taskListener) {
         RestartDialogWindowManager layout =
                 mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId);
         if (layout != null) {
@@ -423,7 +457,7 @@
     private void onRestartDialogCallback(
             Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) {
         mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId);
-        mCallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId);
+        mCompatUICallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId);
     }
 
     private void onRestartDialogDismissCallback(
@@ -432,8 +466,8 @@
         onCompatInfoChanged(stateInfo.first, stateInfo.second);
     }
 
-    private void createOrUpdateReachabilityEduLayout(TaskInfo taskInfo,
-            ShellTaskOrganizer.TaskListener taskListener) {
+    private void createOrUpdateReachabilityEduLayout(@NonNull TaskInfo taskInfo,
+            @Nullable ShellTaskOrganizer.TaskListener taskListener) {
         if (mActiveReachabilityEduLayout != null) {
             if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener)) {
                 mActiveReachabilityEduLayout.release();
@@ -474,14 +508,67 @@
             ShellTaskOrganizer.TaskListener taskListener) {
         return new ReachabilityEduWindowManager(context, taskInfo, mSyncQueue,
                 taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId),
-                mCompatUIConfiguration, mMainExecutor);
+                mCompatUIConfiguration, mMainExecutor, this::onInitialReachabilityEduDismissed);
     }
 
+    private void onInitialReachabilityEduDismissed(@NonNull TaskInfo taskInfo,
+            @NonNull ShellTaskOrganizer.TaskListener taskListener) {
+        // We need to update the UI otherwise it will not be shown until the user relaunches the app
+        createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener);
+    }
+
+    private void createOrUpdateUserAspectRatioSettingsLayout(@NonNull TaskInfo taskInfo,
+            @Nullable ShellTaskOrganizer.TaskListener taskListener) {
+        if (mUserAspectRatioSettingsLayout != null) {
+            if (mUserAspectRatioSettingsLayout.needsToBeRecreated(taskInfo, taskListener)) {
+                mUserAspectRatioSettingsLayout.release();
+                mUserAspectRatioSettingsLayout = null;
+            } else {
+                // UI already exists, update the UI layout.
+                if (!mUserAspectRatioSettingsLayout.updateCompatInfo(taskInfo, taskListener,
+                        showOnDisplay(mUserAspectRatioSettingsLayout.getDisplayId()))) {
+                    mUserAspectRatioSettingsLayout.release();
+                    mUserAspectRatioSettingsLayout = null;
+                }
+                return;
+            }
+        }
+
+        // Create a new UI layout.
+        final Context context = getOrCreateDisplayContext(taskInfo.displayId);
+        if (context == null) {
+            return;
+        }
+        final UserAspectRatioSettingsWindowManager newLayout =
+                createUserAspectRatioSettingsWindowManager(context, taskInfo, taskListener);
+        if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) {
+            // The new layout is eligible to be shown, add it the active layouts.
+            mUserAspectRatioSettingsLayout = newLayout;
+        }
+    }
+
+    @VisibleForTesting
+    @NonNull
+    UserAspectRatioSettingsWindowManager createUserAspectRatioSettingsWindowManager(
+            @NonNull Context context, @NonNull TaskInfo taskInfo,
+            @Nullable ShellTaskOrganizer.TaskListener taskListener) {
+        return new UserAspectRatioSettingsWindowManager(context, taskInfo, mSyncQueue,
+                taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId),
+                mCompatUIHintsState, this::launchUserAspectRatioSettings, mMainExecutor);
+    }
+
+    private void launchUserAspectRatioSettings(
+            @NonNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener) {
+        final Intent intent = new Intent(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        mContext.startActivity(intent);
+    }
 
     private void removeLayouts(int taskId) {
-        final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId);
-        if (layout != null) {
-            layout.release();
+        final CompatUIWindowManager compatLayout = mActiveCompatLayouts.get(taskId);
+        if (compatLayout != null) {
+            compatLayout.release();
             mActiveCompatLayouts.remove(taskId);
         }
 
@@ -502,6 +589,12 @@
             mActiveReachabilityEduLayout.release();
             mActiveReachabilityEduLayout = null;
         }
+
+        if (mUserAspectRatioSettingsLayout != null
+                && mUserAspectRatioSettingsLayout.getTaskId() == taskId) {
+            mUserAspectRatioSettingsLayout.release();
+            mUserAspectRatioSettingsLayout = null;
+        }
     }
 
     private Context getOrCreateDisplayContext(int displayId) {
@@ -557,6 +650,10 @@
         if (mActiveReachabilityEduLayout != null && condition.test(mActiveReachabilityEduLayout)) {
             callback.accept(mActiveReachabilityEduLayout);
         }
+        if (mUserAspectRatioSettingsLayout != null && condition.test(
+                mUserAspectRatioSettingsLayout)) {
+            callback.accept(mUserAspectRatioSettingsLayout);
+        }
     }
 
     /** An implementation of {@link OnInsetsChangedListener} for a given display id. */
@@ -591,4 +688,14 @@
             insetsChanged(insetsState);
         }
     }
+
+    /**
+     * A class holding the state of the compat UI hints, which is shared between all compat UI
+     * window managers.
+     */
+    static class CompatUIHintsState {
+        boolean mHasShownSizeCompatHint;
+        boolean mHasShownCameraCompatHint;
+        boolean mHasShownUserAspectRatioSettingsButtonHint;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 065806d..ce3c509 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -38,6 +38,7 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIController.CompatUICallback;
+import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
 
 import java.util.function.Consumer;
 
@@ -235,15 +236,4 @@
         return mCameraCompatControlState != CAMERA_COMPAT_CONTROL_HIDDEN
                 && mCameraCompatControlState != CAMERA_COMPAT_CONTROL_DISMISSED;
     }
-
-    /**
-     * A class holding the state of the compat UI hints, which is shared between all compat UI
-     * window managers.
-     */
-    static class CompatUIHintsState {
-        @VisibleForTesting
-        boolean mHasShownSizeCompatHint;
-        @VisibleForTesting
-        boolean mHasShownCameraCompatHint;
-    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
index 95bb1fe..9de3f9d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
@@ -36,6 +36,8 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 
+import java.util.function.BiConsumer;
+
 /**
  * Window manager for the reachability education
  */
@@ -73,6 +75,8 @@
     // we need to animate them.
     private boolean mHasLetterboxSizeChanged;
 
+    private final BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnDismissCallback;
+
     @Nullable
     @VisibleForTesting
     ReachabilityEduLayout mLayout;
@@ -80,7 +84,8 @@
     ReachabilityEduWindowManager(Context context, TaskInfo taskInfo,
             SyncTransactionQueue syncQueue,
             ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout,
-            CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor) {
+            CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor,
+            BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> onDismissCallback) {
         super(context, taskInfo, syncQueue, taskListener, displayLayout);
         mIsActivityLetterboxed = taskInfo.isLetterboxDoubleTapEnabled;
         mLetterboxVerticalPosition = taskInfo.topActivityLetterboxVerticalPosition;
@@ -89,6 +94,7 @@
         mTopActivityLetterboxHeight = taskInfo.topActivityLetterboxHeight;
         mCompatUIConfiguration = compatUIConfiguration;
         mMainExecutor = mainExecutor;
+        mOnDismissCallback = onDismissCallback;
     }
 
     @Override
@@ -217,13 +223,17 @@
             return;
         }
         final TaskInfo lastTaskInfo = getLastTaskInfo();
+        final boolean hasSeenHorizontalReachabilityEdu =
+                mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(lastTaskInfo);
+        final boolean hasSeenVerticalReachabilityEdu =
+                mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(lastTaskInfo);
         final boolean eligibleForDisplayHorizontalEducation = mForceUpdate
-                || !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(lastTaskInfo)
+                || !hasSeenHorizontalReachabilityEdu
                 || (mHasUserDoubleTapped
                     && (mLetterboxHorizontalPosition == REACHABILITY_LEFT_OR_UP_POSITION
                         || mLetterboxHorizontalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION));
         final boolean eligibleForDisplayVerticalEducation = mForceUpdate
-                || !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(lastTaskInfo)
+                || !hasSeenVerticalReachabilityEdu
                 || (mHasUserDoubleTapped
                     && (mLetterboxVerticalPosition == REACHABILITY_LEFT_OR_UP_POSITION
                         || mLetterboxVerticalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION));
@@ -239,6 +249,14 @@
             if (!mHasLetterboxSizeChanged) {
                 updateHideTime();
                 mMainExecutor.executeDelayed(this::hideReachability, DISAPPEAR_DELAY_MS);
+                // If reachability education has been seen for the first time, trigger callback to
+                // display aspect ratio settings button once reachability education disappears
+                if (hasShownHorizontalReachabilityEduFirstTime(hasSeenHorizontalReachabilityEdu)
+                        || hasShownVerticalReachabilityEduFirstTime(
+                        hasSeenVerticalReachabilityEdu)) {
+                    mMainExecutor.executeDelayed(this::triggerOnDismissCallback,
+                            DISAPPEAR_DELAY_MS);
+                }
             }
             mHasUserDoubleTapped = false;
         } else {
@@ -246,6 +264,38 @@
         }
     }
 
+    /**
+     * Compares the value of
+     * {@link CompatUIConfiguration#hasSeenHorizontalReachabilityEducation} before and after the
+     * layout is shown. Horizontal reachability education is considered seen for the first time if
+     * prior to viewing the layout,
+     * {@link CompatUIConfiguration#hasSeenHorizontalReachabilityEducation} is {@code false}
+     * but becomes {@code true} once the current layout is shown.
+     */
+    private boolean hasShownHorizontalReachabilityEduFirstTime(
+            boolean previouslyShownHorizontalReachabilityEducation) {
+        return !previouslyShownHorizontalReachabilityEducation
+                && mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(getLastTaskInfo());
+    }
+
+    /**
+     * Compares the value of
+     * {@link CompatUIConfiguration#hasSeenVerticalReachabilityEducation} before and after the
+     * layout is shown. Horizontal reachability education is considered seen for the first time if
+     * prior to viewing the layout,
+     * {@link CompatUIConfiguration#hasSeenVerticalReachabilityEducation} is {@code false}
+     * but becomes {@code true} once the current layout is shown.
+     */
+    private boolean hasShownVerticalReachabilityEduFirstTime(
+            boolean previouslyShownVerticalReachabilityEducation) {
+        return !previouslyShownVerticalReachabilityEducation
+                && mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(getLastTaskInfo());
+    }
+
+    private void triggerOnDismissCallback() {
+        mOnDismissCallback.accept(getLastTaskInfo(), getTaskListener());
+    }
+
     private void hideReachability() {
         if (mLayout == null || !shouldHideEducation()) {
             return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java
new file mode 100644
index 0000000..5eeb3b6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2023 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.compatui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.annotation.IdRes;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.wm.shell.R;
+
+/**
+ * Layout for the user aspect ratio button which opens the app list page in settings
+ * and allows users to change apps aspect ratio.
+ */
+public class UserAspectRatioSettingsLayout extends LinearLayout {
+
+    private static final float ALPHA_FULL_TRANSPARENT = 0f;
+
+    private static final float ALPHA_FULL_OPAQUE = 1f;
+
+    private static final long VISIBILITY_ANIMATION_DURATION_MS = 50;
+
+    private static final String ALPHA_PROPERTY_NAME = "alpha";
+
+    private UserAspectRatioSettingsWindowManager mWindowManager;
+
+    public UserAspectRatioSettingsLayout(Context context) {
+        this(context, null);
+    }
+
+    public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    void inject(@NonNull UserAspectRatioSettingsWindowManager windowManager) {
+        mWindowManager = windowManager;
+    }
+
+    void setUserAspectRatioSettingsHintVisibility(boolean show) {
+        setViewVisibility(R.id.user_aspect_ratio_settings_hint, show);
+    }
+
+    void setUserAspectRatioButtonVisibility(boolean show) {
+        setViewVisibility(R.id.user_aspect_ratio_settings_button, show);
+        // Hint should never be visible without button.
+        if (!show) {
+            setUserAspectRatioSettingsHintVisibility(/* show= */ false);
+        }
+    }
+
+    private void setViewVisibility(@IdRes int resId, boolean show) {
+        final View view = findViewById(resId);
+        int visibility = show ? View.VISIBLE : View.GONE;
+        if (view.getVisibility() == visibility) {
+            return;
+        }
+        if (show) {
+            showItem(view);
+        } else {
+            view.setVisibility(visibility);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        // Need to relayout after changes like hiding / showing a hint since they affect size.
+        // Doing this directly in setUserAspectRatioButtonVisibility can result in flaky animation.
+        mWindowManager.relayout();
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        final ImageButton userAspectRatioButton =
+                findViewById(R.id.user_aspect_ratio_settings_button);
+        userAspectRatioButton.setOnClickListener(
+                view -> mWindowManager.onUserAspectRatioSettingsButtonClicked());
+        userAspectRatioButton.setOnLongClickListener(view -> {
+            mWindowManager.onUserAspectRatioSettingsButtonLongClicked();
+            return true;
+        });
+
+        final LinearLayout sizeCompatHint = findViewById(R.id.user_aspect_ratio_settings_hint);
+        ((TextView) sizeCompatHint.findViewById(R.id.compat_mode_hint_text))
+                .setText(R.string.user_aspect_ratio_settings_button_hint);
+        sizeCompatHint.setOnClickListener(
+                view -> setUserAspectRatioSettingsHintVisibility(/* show= */ false));
+    }
+
+    private void showItem(@NonNull View view) {
+        view.setVisibility(View.VISIBLE);
+        final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, ALPHA_PROPERTY_NAME,
+                ALPHA_FULL_TRANSPARENT, ALPHA_FULL_OPAQUE);
+        fadeIn.setDuration(VISIBILITY_ANIMATION_DURATION_MS);
+        fadeIn.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                view.setVisibility(View.VISIBLE);
+            }
+        });
+        fadeIn.start();
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
new file mode 100644
index 0000000..bd53dc7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2023 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.compatui;
+
+import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.TaskInfo;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Window manager for the user aspect ratio settings button which allows users to go to
+ * app settings and change apps aspect ratio.
+ */
+class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract {
+
+    private static final long SHOW_USER_ASPECT_RATIO_BUTTON_DELAY_MS = 500L;
+
+    private static final long HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS = 4000L;
+
+    private long mNextButtonHideTimeMs = -1L;
+
+    private final BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnButtonClicked;
+
+    private final ShellExecutor mShellExecutor;
+
+    @VisibleForTesting
+    @NonNull
+    final CompatUIHintsState mCompatUIHintsState;
+
+    @Nullable
+    private UserAspectRatioSettingsLayout mLayout;
+
+    // Remember the last reported states in case visibility changes due to keyguard or IME updates.
+    @VisibleForTesting
+    boolean mHasUserAspectRatioSettingsButton;
+
+    UserAspectRatioSettingsWindowManager(@NonNull Context context, @NonNull TaskInfo taskInfo,
+            @NonNull SyncTransactionQueue syncQueue,
+            @Nullable ShellTaskOrganizer.TaskListener taskListener,
+            @NonNull DisplayLayout displayLayout, @NonNull CompatUIHintsState compatUIHintsState,
+            @NonNull BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> onButtonClicked,
+            @NonNull ShellExecutor shellExecutor) {
+        super(context, taskInfo, syncQueue, taskListener, displayLayout);
+        mShellExecutor = shellExecutor;
+        mHasUserAspectRatioSettingsButton = getHasUserAspectRatioSettingsButton(taskInfo);
+        mCompatUIHintsState = compatUIHintsState;
+        mOnButtonClicked = onButtonClicked;
+    }
+
+    @Override
+    protected int getZOrder() {
+        return TASK_CHILD_LAYER_COMPAT_UI + 1;
+    }
+
+    @Override
+    protected @Nullable View getLayout() {
+        return mLayout;
+    }
+
+    @Override
+    protected void removeLayout() {
+        mLayout = null;
+    }
+
+    @Override
+    protected boolean eligibleToShowLayout() {
+        return mHasUserAspectRatioSettingsButton;
+    }
+
+    @Override
+    protected View createLayout() {
+        mLayout = inflateLayout();
+        mLayout.inject(this);
+
+        updateVisibilityOfViews();
+
+        return mLayout;
+    }
+
+    @VisibleForTesting
+    UserAspectRatioSettingsLayout inflateLayout() {
+        return (UserAspectRatioSettingsLayout) LayoutInflater.from(mContext).inflate(
+                R.layout.user_aspect_ratio_settings_layout, null);
+    }
+
+    @Override
+    public boolean updateCompatInfo(@NonNull TaskInfo taskInfo,
+            @NonNull ShellTaskOrganizer.TaskListener taskListener, boolean canShow) {
+        final boolean prevHasUserAspectRatioSettingsButton = mHasUserAspectRatioSettingsButton;
+        mHasUserAspectRatioSettingsButton = getHasUserAspectRatioSettingsButton(taskInfo);
+
+        if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) {
+            return false;
+        }
+
+        if (prevHasUserAspectRatioSettingsButton != mHasUserAspectRatioSettingsButton) {
+            updateVisibilityOfViews();
+        }
+        return true;
+    }
+
+    /** Called when the user aspect ratio settings button is clicked. */
+    void onUserAspectRatioSettingsButtonClicked() {
+        mOnButtonClicked.accept(getLastTaskInfo(), getTaskListener());
+    }
+
+    /** Called when the user aspect ratio settings button is long clicked. */
+    void onUserAspectRatioSettingsButtonLongClicked() {
+        if (mLayout == null) {
+            return;
+        }
+        mLayout.setUserAspectRatioSettingsHintVisibility(/* show= */ true);
+        mNextButtonHideTimeMs = updateHideTime(HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS);
+        mShellExecutor.executeDelayed(this::hideUserAspectRatioButton,
+                HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS);
+    }
+
+    @Override
+    @VisibleForTesting
+    public void updateSurfacePosition() {
+        if (mLayout == null) {
+            return;
+        }
+        // Position of the button in the container coordinate.
+        final Rect taskBounds = getTaskBounds();
+        final Rect taskStableBounds = getTaskStableBounds();
+        final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+                ? taskStableBounds.left - taskBounds.left
+                : taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth();
+        final int positionY = taskStableBounds.bottom - taskBounds.top
+                - mLayout.getMeasuredHeight();
+        updateSurfacePosition(positionX, positionY);
+    }
+
+    @VisibleForTesting
+    void updateVisibilityOfViews() {
+        if (mHasUserAspectRatioSettingsButton) {
+            mShellExecutor.executeDelayed(this::showUserAspectRatioButton,
+                    SHOW_USER_ASPECT_RATIO_BUTTON_DELAY_MS);
+            mNextButtonHideTimeMs = updateHideTime(HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS);
+            mShellExecutor.executeDelayed(this::hideUserAspectRatioButton,
+                    HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS);
+        } else {
+            mShellExecutor.removeCallbacks(this::showUserAspectRatioButton);
+            mShellExecutor.execute(this::hideUserAspectRatioButton);
+        }
+    }
+
+    private void showUserAspectRatioButton() {
+        if (mLayout == null) {
+            return;
+        }
+        mLayout.setUserAspectRatioButtonVisibility(true);
+        // Only show by default for the first time.
+        if (!mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint) {
+            mLayout.setUserAspectRatioSettingsHintVisibility(/* show= */ true);
+            mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true;
+        }
+    }
+
+    private void hideUserAspectRatioButton() {
+        if (mLayout == null || !isHideDelayReached(mNextButtonHideTimeMs)) {
+            return;
+        }
+        mLayout.setUserAspectRatioButtonVisibility(false);
+    }
+
+    private boolean isHideDelayReached(long nextHideTime) {
+        return SystemClock.uptimeMillis() >= nextHideTime;
+    }
+
+    private long updateHideTime(long hideDelay) {
+        return SystemClock.uptimeMillis() + hideDelay;
+    }
+
+    private boolean getHasUserAspectRatioSettingsButton(@NonNull TaskInfo taskInfo) {
+        return  taskInfo.topActivityEligibleForUserAspectRatioButton
+                && (taskInfo.topActivityBoundsLetterboxed
+                    || taskInfo.isUserFullscreenOverrideEnabled);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index a6501f0..efc69ebd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -58,6 +58,8 @@
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
+import dagger.Lazy;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -66,8 +68,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import dagger.Lazy;
-
 /**
  * Tests for {@link CompatUIController}.
  *
@@ -82,21 +82,36 @@
 
     private CompatUIController mController;
     private ShellInit mShellInit;
-    private @Mock ShellController mMockShellController;
-    private @Mock DisplayController mMockDisplayController;
-    private @Mock DisplayInsetsController mMockDisplayInsetsController;
-    private @Mock DisplayLayout mMockDisplayLayout;
-    private @Mock DisplayImeController mMockImeController;
-    private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener;
-    private @Mock SyncTransactionQueue mMockSyncQueue;
-    private @Mock ShellExecutor mMockExecutor;
-    private @Mock Lazy<Transitions> mMockTransitionsLazy;
-    private @Mock CompatUIWindowManager mMockCompatLayout;
-    private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout;
-    private @Mock RestartDialogWindowManager mMockRestartDialogLayout;
-    private @Mock DockStateReader mDockStateReader;
-    private @Mock CompatUIConfiguration mCompatUIConfiguration;
-    private @Mock CompatUIShellCommandHandler mCompatUIShellCommandHandler;
+    @Mock
+    private ShellController mMockShellController;
+    @Mock
+    private DisplayController mMockDisplayController;
+    @Mock
+    private DisplayInsetsController mMockDisplayInsetsController;
+    @Mock
+    private DisplayLayout mMockDisplayLayout;
+    @Mock
+    private DisplayImeController mMockImeController;
+    @Mock
+    private ShellTaskOrganizer.TaskListener mMockTaskListener;
+    @Mock
+    private SyncTransactionQueue mMockSyncQueue;
+    @Mock
+    private ShellExecutor mMockExecutor;
+    @Mock
+    private Lazy<Transitions> mMockTransitionsLazy;
+    @Mock
+    private CompatUIWindowManager mMockCompatLayout;
+    @Mock
+    private LetterboxEduWindowManager mMockLetterboxEduLayout;
+    @Mock
+    private RestartDialogWindowManager mMockRestartDialogLayout;
+    @Mock
+    private DockStateReader mDockStateReader;
+    @Mock
+    private CompatUIConfiguration mCompatUIConfiguration;
+    @Mock
+    private CompatUIShellCommandHandler mCompatUIShellCommandHandler;
 
     @Captor
     ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index 5f294d5..3bce2b8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -44,7 +44,7 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
+import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
 
 import junit.framework.Assert;
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index 78c3cbd..4c837e6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -53,7 +53,7 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
+import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
 
 import junit.framework.Assert;
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
index 973a99c..a802f15a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
@@ -40,6 +40,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.function.BiConsumer;
+
 /**
  * Tests for {@link ReachabilityEduWindowManager}.
  *
@@ -57,6 +59,8 @@
     private CompatUIConfiguration mCompatUIConfiguration;
     @Mock
     private DisplayLayout mDisplayLayout;
+    @Mock
+    private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnDismissCallback;
     private TestShellExecutor mExecutor;
     private TaskInfo mTaskInfo;
     private ReachabilityEduWindowManager mWindowManager;
@@ -104,6 +108,7 @@
 
     private ReachabilityEduWindowManager createReachabilityEduWindowManager(TaskInfo taskInfo) {
         return new ReachabilityEduWindowManager(mContext, taskInfo, mSyncTransactionQueue,
-                mTaskListener, mDisplayLayout, mCompatUIConfiguration, mExecutor);
+                mTaskListener, mDisplayLayout, mCompatUIConfiguration, mExecutor,
+                mOnDismissCallback);
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
new file mode 100644
index 0000000..1fee153
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 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.compatui;
+
+import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityManager;
+import android.app.TaskInfo;
+import android.app.TaskInfo.CameraCompatControlState;
+import android.content.ComponentName;
+import android.testing.AndroidTestingRunner;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.SurfaceControlViewHost;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Tests for {@link UserAspectRatioSettingsLayout}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:UserAspectRatioSettingsLayoutTest
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class UserAspectRatioSettingsLayoutTest extends ShellTestCase {
+
+    private static final int TASK_ID = 1;
+
+    @Mock
+    private SyncTransactionQueue mSyncTransactionQueue;
+    @Mock
+    private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener>
+            mOnUserAspectRatioSettingsButtonClicked;
+    @Mock
+    private ShellTaskOrganizer.TaskListener mTaskListener;
+    @Mock
+    private SurfaceControlViewHost mViewHost;
+    @Captor
+    private ArgumentCaptor<ShellTaskOrganizer.TaskListener> mUserAspectRatioTaskListenerCaptor;
+    @Captor
+    private ArgumentCaptor<TaskInfo> mUserAspectRationTaskInfoCaptor;
+
+    private UserAspectRatioSettingsWindowManager mWindowManager;
+    private UserAspectRatioSettingsLayout mLayout;
+    private TaskInfo mTaskInfo;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+        mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo,
+                mSyncTransactionQueue, mTaskListener, new DisplayLayout(),
+                new CompatUIController.CompatUIHintsState(),
+                mOnUserAspectRatioSettingsButtonClicked, new TestShellExecutor());
+
+        mLayout = (UserAspectRatioSettingsLayout) LayoutInflater.from(mContext).inflate(
+                R.layout.user_aspect_ratio_settings_layout, null);
+        mLayout.inject(mWindowManager);
+
+        spyOn(mWindowManager);
+        spyOn(mLayout);
+        doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost();
+        doReturn(mLayout).when(mWindowManager).inflateLayout();
+    }
+
+    @Test
+    public void testOnClickForUserAspectRatioSettingsButton() {
+        final ImageButton button = mLayout.findViewById(R.id.user_aspect_ratio_settings_button);
+        button.performClick();
+
+        verify(mWindowManager).onUserAspectRatioSettingsButtonClicked();
+        verify(mOnUserAspectRatioSettingsButtonClicked).accept(
+                mUserAspectRationTaskInfoCaptor.capture(),
+                mUserAspectRatioTaskListenerCaptor.capture());
+        final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result =
+                new Pair<>(mUserAspectRationTaskInfoCaptor.getValue(),
+                        mUserAspectRatioTaskListenerCaptor.getValue());
+        Assert.assertEquals(mTaskInfo, result.first);
+        Assert.assertEquals(mTaskListener, result.second);
+    }
+
+    @Test
+    public void testOnLongClickForUserAspectRatioButton() {
+        doNothing().when(mWindowManager).onUserAspectRatioSettingsButtonLongClicked();
+
+        final ImageButton button = mLayout.findViewById(R.id.user_aspect_ratio_settings_button);
+        button.performLongClick();
+
+        verify(mWindowManager).onUserAspectRatioSettingsButtonLongClicked();
+    }
+
+    @Test
+    public void testOnClickForUserAspectRatioSettingsHint() {
+        mWindowManager.mHasUserAspectRatioSettingsButton = true;
+        mWindowManager.createLayout(/* canShow= */ true);
+        final LinearLayout sizeCompatHint = mLayout.findViewById(
+                R.id.user_aspect_ratio_settings_hint);
+        sizeCompatHint.performClick();
+
+        verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ false);
+    }
+
+    private static TaskInfo createTaskInfo(boolean hasSizeCompat,
+            @CameraCompatControlState int cameraCompatControlState) {
+        ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+        taskInfo.taskId = TASK_ID;
+        taskInfo.topActivityInSizeCompat = hasSizeCompat;
+        taskInfo.cameraCompatControlState = cameraCompatControlState;
+        taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity");
+        return taskInfo;
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
new file mode 100644
index 0000000..b48538c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2023 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.compatui;
+
+import static android.view.WindowInsets.Type.navigationBars;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityManager;
+import android.app.TaskInfo;
+import android.content.ComponentName;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.util.Pair;
+import android.view.DisplayInfo;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+/**
+ * Tests for {@link UserAspectRatioSettingsWindowManager}.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:UserAspectRatioSettingsWindowManagerTest
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase {
+
+    private static final int TASK_ID = 1;
+
+    @Mock private SyncTransactionQueue mSyncTransactionQueue;
+    @Mock
+    private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener>
+            mOnUserAspectRatioSettingsButtonClicked;
+    @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
+    @Mock private UserAspectRatioSettingsLayout mLayout;
+    @Mock private SurfaceControlViewHost mViewHost;
+    @Captor
+    private ArgumentCaptor<ShellTaskOrganizer.TaskListener> mUserAspectRatioTaskListenerCaptor;
+    @Captor
+    private ArgumentCaptor<TaskInfo> mUserAspectRationTaskInfoCaptor;
+
+    private final Set<String> mPackageNameCache = new HashSet<>();
+
+    private UserAspectRatioSettingsWindowManager mWindowManager;
+    private TaskInfo mTaskInfo;
+
+    private TestShellExecutor mExecutor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mExecutor = new TestShellExecutor();
+        mTaskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
+                false, /* topActivityBoundsLetterboxed */ true);
+        mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo,
+                mSyncTransactionQueue, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
+                mOnUserAspectRatioSettingsButtonClicked, mExecutor);
+        spyOn(mWindowManager);
+        doReturn(mLayout).when(mWindowManager).inflateLayout();
+        doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost();
+    }
+
+    @Test
+    public void testCreateUserAspectRatioButton() {
+        // Doesn't create layout if show is false.
+        mWindowManager.mHasUserAspectRatioSettingsButton = true;
+        assertTrue(mWindowManager.createLayout(/* canShow= */ false));
+
+        verify(mWindowManager, never()).inflateLayout();
+
+        // Doesn't create hint popup.
+        mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true;
+        assertTrue(mWindowManager.createLayout(/* canShow= */ true));
+
+        verify(mWindowManager).inflateLayout();
+        mExecutor.flushAll();
+        verify(mLayout).setUserAspectRatioButtonVisibility(/* show= */ true);
+        verify(mLayout, never()).setUserAspectRatioSettingsHintVisibility(/* show= */ true);
+
+        // Creates hint popup.
+        clearInvocations(mWindowManager);
+        clearInvocations(mLayout);
+        mWindowManager.release();
+        mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = false;
+        assertTrue(mWindowManager.createLayout(/* canShow= */ true));
+
+        verify(mWindowManager).inflateLayout();
+        assertNotNull(mLayout);
+        mExecutor.flushAll();
+        verify(mLayout).setUserAspectRatioButtonVisibility(/* show= */ true);
+        verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ true);
+        assertTrue(mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint);
+
+        // Returns false and doesn't create layout if mHasUserAspectRatioSettingsButton is false.
+        clearInvocations(mWindowManager);
+        mWindowManager.release();
+        mWindowManager.mHasUserAspectRatioSettingsButton = false;
+        assertFalse(mWindowManager.createLayout(/* canShow= */ true));
+
+        verify(mWindowManager, never()).inflateLayout();
+    }
+
+    @Test
+    public void testRelease() {
+        mWindowManager.mHasUserAspectRatioSettingsButton = true;
+        mWindowManager.createLayout(/* canShow= */ true);
+
+        verify(mWindowManager).inflateLayout();
+
+        mWindowManager.release();
+
+        verify(mViewHost).release();
+    }
+
+    @Test
+    public void testUpdateCompatInfo() {
+        mWindowManager.mHasUserAspectRatioSettingsButton = true;
+        mWindowManager.createLayout(/* canShow= */ true);
+
+        // No diff
+        clearInvocations(mWindowManager);
+        TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
+                true, /* topActivityBoundsLetterboxed */ true);
+        assertTrue(mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true));
+
+        verify(mWindowManager, never()).updateSurfacePosition();
+        verify(mWindowManager, never()).release();
+        verify(mWindowManager, never()).createLayout(anyBoolean());
+
+
+        // Change task listener, recreate button.
+        clearInvocations(mWindowManager);
+        final ShellTaskOrganizer.TaskListener newTaskListener = mock(
+                ShellTaskOrganizer.TaskListener.class);
+        assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
+
+        verify(mWindowManager).release();
+        verify(mWindowManager).createLayout(/* canShow= */ true);
+
+        // Change has eligibleForUserAspectRatioButton to false, dispose the component
+        clearInvocations(mWindowManager);
+        clearInvocations(mLayout);
+        taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
+                false, /* topActivityBoundsLetterboxed */ true);
+        assertFalse(
+                mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
+        verify(mWindowManager).release();
+    }
+
+    @Test
+    public void testUpdateCompatInfoLayoutNotInflatedYet() {
+        mWindowManager.mHasUserAspectRatioSettingsButton = true;
+        mWindowManager.createLayout(/* canShow= */ false);
+
+        verify(mWindowManager, never()).inflateLayout();
+
+        // Change topActivityInSizeCompat to false and pass canShow true, layout shouldn't be
+        // inflated
+        clearInvocations(mWindowManager);
+        TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
+                false, /* topActivityBoundsLetterboxed */ true);
+        mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
+
+        verify(mWindowManager, never()).inflateLayout();
+
+        // Change topActivityInSizeCompat to true and pass canShow true, layout should be inflated.
+        clearInvocations(mWindowManager);
+        taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
+                true, /* topActivityBoundsLetterboxed */ true);
+        mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
+
+        verify(mWindowManager).inflateLayout();
+    }
+
+    @Test
+    public void testUpdateDisplayLayout() {
+        final DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.logicalWidth = 1000;
+        displayInfo.logicalHeight = 2000;
+        final DisplayLayout displayLayout1 = new DisplayLayout(displayInfo,
+                mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false);
+
+        mWindowManager.updateDisplayLayout(displayLayout1);
+        verify(mWindowManager).updateSurfacePosition();
+
+        // No update if the display bounds is the same.
+        clearInvocations(mWindowManager);
+        final DisplayLayout displayLayout2 = new DisplayLayout(displayInfo,
+                mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false);
+        mWindowManager.updateDisplayLayout(displayLayout2);
+        verify(mWindowManager, never()).updateSurfacePosition();
+    }
+
+    @Test
+    public void testUpdateDisplayLayoutInsets() {
+        final DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.logicalWidth = 1000;
+        displayInfo.logicalHeight = 2000;
+        final DisplayLayout displayLayout = new DisplayLayout(displayInfo,
+                mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false);
+
+        mWindowManager.updateDisplayLayout(displayLayout);
+        verify(mWindowManager).updateSurfacePosition();
+
+        // Update if the insets change on the existing display layout
+        clearInvocations(mWindowManager);
+        InsetsState insetsState = new InsetsState();
+        insetsState.setDisplayFrame(new Rect(0, 0, 1000, 2000));
+        InsetsSource insetsSource = new InsetsSource(
+                InsetsSource.createId(null, 0, navigationBars()), navigationBars());
+        insetsSource.setFrame(0, 1800, 1000, 2000);
+        insetsState.addSource(insetsSource);
+        displayLayout.setInsets(mContext.getResources(), insetsState);
+        mWindowManager.updateDisplayLayout(displayLayout);
+        verify(mWindowManager).updateSurfacePosition();
+    }
+
+    @Test
+    public void testUpdateVisibility() {
+        // Create button if it is not created.
+        mWindowManager.removeLayout();
+        mWindowManager.mHasUserAspectRatioSettingsButton = true;
+        mWindowManager.updateVisibility(/* canShow= */ true);
+
+        verify(mWindowManager).createLayout(/* canShow= */ true);
+
+        // Hide button.
+        clearInvocations(mWindowManager);
+        doReturn(View.VISIBLE).when(mLayout).getVisibility();
+        mWindowManager.updateVisibility(/* canShow= */ false);
+
+        verify(mWindowManager, never()).createLayout(anyBoolean());
+        verify(mLayout).setVisibility(View.GONE);
+
+        // Show button.
+        doReturn(View.GONE).when(mLayout).getVisibility();
+        mWindowManager.updateVisibility(/* canShow= */ true);
+
+        verify(mWindowManager, never()).createLayout(anyBoolean());
+        verify(mLayout).setVisibility(View.VISIBLE);
+    }
+
+    @Test
+    public void testAttachToParentSurface() {
+        final SurfaceControl.Builder b = new SurfaceControl.Builder();
+        mWindowManager.attachToParentSurface(b);
+
+        verify(mTaskListener).attachChildSurfaceToTask(TASK_ID, b);
+    }
+
+    @Test
+    public void testOnUserAspectRatioButtonClicked() {
+        mWindowManager.onUserAspectRatioSettingsButtonClicked();
+
+        verify(mOnUserAspectRatioSettingsButtonClicked).accept(
+                mUserAspectRationTaskInfoCaptor.capture(),
+                mUserAspectRatioTaskListenerCaptor.capture());
+        final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result =
+                new Pair<>(mUserAspectRationTaskInfoCaptor.getValue(),
+                        mUserAspectRatioTaskListenerCaptor.getValue());
+        Assert.assertEquals(mTaskInfo, result.first);
+        Assert.assertEquals(mTaskListener, result.second);
+    }
+
+    @Test
+    public void testOnUserAspectRatioButtonLongClicked_showHint() {
+       // Not create hint popup.
+        mWindowManager.mHasUserAspectRatioSettingsButton = true;
+        mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true;
+        mWindowManager.createLayout(/* canShow= */ true);
+
+        verify(mWindowManager).inflateLayout();
+        verify(mLayout, never()).setUserAspectRatioSettingsHintVisibility(/* show= */ true);
+
+        mWindowManager.onUserAspectRatioSettingsButtonLongClicked();
+
+        verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ true);
+    }
+
+    @Test
+    public void testWhenDockedStateHasChanged_needsToBeRecreated() {
+        ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
+        newTaskInfo.configuration.uiMode |= Configuration.UI_MODE_TYPE_DESK;
+
+        Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener));
+    }
+
+    private static TaskInfo createTaskInfo(boolean eligibleForUserAspectRatioButton,
+            boolean topActivityBoundsLetterboxed) {
+        ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+        taskInfo.taskId = TASK_ID;
+        taskInfo.topActivityEligibleForUserAspectRatioButton = eligibleForUserAspectRatioButton;
+        taskInfo.topActivityBoundsLetterboxed = topActivityBoundsLetterboxed;
+        taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK;
+        taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity");
+        return taskInfo;
+    }
+}
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 6164f29f..4aad6e7 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -917,4 +917,7 @@
     "$packageName" part that will be replaced by the code with the package name of the target app.
     -->
     <string name="config_appStoreAppLinkTemplate" translatable="false"></string>
+
+    <!-- Flag controlling whether visual query attention detection has been enabled. -->
+    <bool name="config_enableVisualQueryAttentionDetection">false</bool>
 </resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index b81e081..e3f9de1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -102,6 +102,12 @@
         super.onViewAttached();
         mView.setKeyDownListener(mKeyDownListener);
         mEmergencyButtonController.setEmergencyButtonCallback(mEmergencyButtonCallback);
+        // if the user is currently locked out, enforce it.
+        long deadline = mLockPatternUtils.getLockoutAttemptDeadline(
+                KeyguardUpdateMonitor.getCurrentUser());
+        if (shouldLockout(deadline)) {
+            handleAttemptLockout(deadline);
+        }
     }
 
     @Override
@@ -278,12 +284,6 @@
     @Override
     public void onResume(int reason) {
         mResumed = true;
-        // if the user is currently locked out, enforce it.
-        long deadline = mLockPatternUtils.getLockoutAttemptDeadline(
-                KeyguardUpdateMonitor.getCurrentUser());
-        if (shouldLockout(deadline)) {
-            handleAttemptLockout(deadline);
-        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 20e4656..42dbc48 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -30,13 +30,13 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
 import com.android.systemui.R;
+import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
+import com.android.systemui.bouncer.ui.BouncerMessageView;
+import com.android.systemui.bouncer.ui.binder.BouncerMessageViewBinder;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
-import com.android.systemui.bouncer.ui.BouncerMessageView;
-import com.android.systemui.bouncer.ui.binder.BouncerMessageViewBinder;
 import com.android.systemui.log.BouncerLogger;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.ViewController;
@@ -95,6 +95,12 @@
     @CallSuper
     protected void onViewAttached() {
         updateMessageAreaVisibility();
+        if (TextUtils.isEmpty(mMessageAreaController.getMessage())
+                && getInitialMessageResId() != 0) {
+            mMessageAreaController.setMessage(
+                    mView.getResources().getString(getInitialMessageResId()),
+                    /* animate= */ false);
+        }
     }
 
     private void updateMessageAreaVisibility() {
@@ -147,12 +153,6 @@
     }
 
     public void startAppearAnimation() {
-        if (TextUtils.isEmpty(mMessageAreaController.getMessage())
-                && getInitialMessageResId() != 0) {
-            mMessageAreaController.setMessage(
-                    mView.getResources().getString(getInitialMessageResId()),
-                    /* animate= */ false);
-        }
         mView.startAppearAnimation();
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 49f788c..a30b447 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -238,6 +238,12 @@
         }
         mView.onDevicePostureChanged(mPostureController.getDevicePosture());
         mPostureController.addCallback(mPostureCallback);
+        // if the user is currently locked out, enforce it.
+        long deadline = mLockPatternUtils.getLockoutAttemptDeadline(
+                KeyguardUpdateMonitor.getCurrentUser());
+        if (deadline != 0) {
+            handleAttemptLockout(deadline);
+        }
     }
 
     @Override
@@ -268,12 +274,6 @@
     @Override
     public void onResume(int reason) {
         super.onResume(reason);
-        // if the user is currently locked out, enforce it.
-        long deadline = mLockPatternUtils.getLockoutAttemptDeadline(
-                KeyguardUpdateMonitor.getCurrentUser());
-        if (deadline != 0) {
-            handleAttemptLockout(deadline);
-        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index b3e08c0..574a059 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -79,6 +79,10 @@
         mPasswordEntry.setUserActivityListener(this::onUserInput);
         mView.onDevicePostureChanged(mPostureController.getDevicePosture());
         mPostureController.addCallback(mPostureCallback);
+        if (mFeatureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)) {
+            mPasswordEntry.setUsePinShapes(true);
+            updateAutoConfirmationState();
+        }
     }
 
     protected void onUserInput() {
@@ -100,10 +104,6 @@
 
     @Override
     public void startAppearAnimation() {
-        if (mFeatureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)) {
-            mPasswordEntry.setUsePinShapes(true);
-            updateAutoConfirmationState();
-        }
         super.startAppearAnimation();
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index d9a1dc6..94d26bd 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -146,8 +146,19 @@
     private int mLastOrientation;
 
     private SecurityMode mCurrentSecurityMode = SecurityMode.Invalid;
+    private int mCurrentUser = UserHandle.USER_NULL;
     private UserSwitcherController.UserSwitchCallback mUserSwitchCallback =
-            () -> showPrimarySecurityScreen(false);
+            new UserSwitcherController.UserSwitchCallback() {
+        @Override
+        public void onUserSwitched() {
+            if (mCurrentUser == KeyguardUpdateMonitor.getCurrentUser()) {
+                return;
+            }
+            mCurrentUser = KeyguardUpdateMonitor.getCurrentUser();
+            showPrimarySecurityScreen(false);
+            reinflateViewFlipper((l) -> {});
+        }
+    };
 
     @VisibleForTesting
     final Gefingerpoken mGlobalTouchListener = new Gefingerpoken() {
@@ -343,7 +354,6 @@
                 @Override
                 public void onThemeChanged() {
                     reloadColors();
-                    reset();
                 }
 
                 @Override
@@ -1164,7 +1174,7 @@
     }
 
     private void reloadColors() {
-        reinflateViewFlipper(controller -> mView.reloadColors());
+        mView.reloadColors();
     }
 
     /** Handles density or font scale changes. */
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index 2b83e6b..590056f 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -23,6 +23,8 @@
 import android.util.Log;
 
 import com.android.internal.app.AssistUtils;
+import com.android.internal.app.IVisualQueryDetectionAttentionListener;
+import com.android.internal.app.IVisualQueryRecognitionStatusListener;
 import com.android.internal.app.IVoiceInteractionSessionListener;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -39,10 +41,13 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.settings.SecureSettings;
 
-import javax.inject.Inject;
-
 import dagger.Lazy;
 
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
 /**
  * Class to manage everything related to assist in SystemUI.
  */
@@ -78,6 +83,18 @@
         void hide();
     }
 
+    /**
+     * An interface for a listener that receives notification that visual query attention has
+     * either been gained or lost.
+     */
+    public interface VisualQueryAttentionListener {
+        /** Called when visual query attention has been gained. */
+        void onAttentionGained();
+
+        /** Called when visual query attention has been lost. */
+        void onAttentionLost();
+    }
+
     private static final String TAG = "AssistManager";
 
     // Note that VERBOSE logging may leak PII (e.g. transcription contents).
@@ -127,6 +144,23 @@
     private final SecureSettings mSecureSettings;
 
     private final DeviceProvisionedController mDeviceProvisionedController;
+
+    private final List<VisualQueryAttentionListener> mVisualQueryAttentionListeners =
+            new ArrayList<>();
+
+    private final IVisualQueryDetectionAttentionListener mVisualQueryDetectionAttentionListener =
+            new IVisualQueryDetectionAttentionListener.Stub() {
+        @Override
+        public void onAttentionGained() {
+            mVisualQueryAttentionListeners.forEach(VisualQueryAttentionListener::onAttentionGained);
+        }
+
+        @Override
+        public void onAttentionLost() {
+            mVisualQueryAttentionListeners.forEach(VisualQueryAttentionListener::onAttentionLost);
+        }
+    };
+
     private final CommandQueue mCommandQueue;
     protected final AssistUtils mAssistUtils;
 
@@ -157,6 +191,7 @@
         mSecureSettings = secureSettings;
 
         registerVoiceInteractionSessionListener();
+        registerVisualQueryRecognitionStatusListener();
 
         mUiController = defaultUiController;
 
@@ -266,6 +301,24 @@
         mAssistUtils.hideCurrentSession();
     }
 
+    /**
+     * Add the given {@link VisualQueryAttentionListener} to the list of listeners awaiting
+     * notification of gaining/losing visual query attention.
+     */
+    public void addVisualQueryAttentionListener(VisualQueryAttentionListener listener) {
+        if (!mVisualQueryAttentionListeners.contains(listener)) {
+            mVisualQueryAttentionListeners.add(listener);
+        }
+    }
+
+    /**
+     * Remove the given {@link VisualQueryAttentionListener} from the list of listeners awaiting
+     * notification of gaining/losing visual query attention.
+     */
+    public void removeVisualQueryAttentionListener(VisualQueryAttentionListener listener) {
+        mVisualQueryAttentionListeners.remove(listener);
+    }
+
     private void startAssistInternal(Bundle args, @NonNull ComponentName assistComponent,
             boolean isService) {
         if (isService) {
@@ -326,6 +379,27 @@
                 null, null);
     }
 
+    private void registerVisualQueryRecognitionStatusListener() {
+        if (!mContext.getResources()
+                .getBoolean(R.bool.config_enableVisualQueryAttentionDetection)) {
+            return;
+        }
+
+        mAssistUtils.subscribeVisualQueryRecognitionStatus(
+                new IVisualQueryRecognitionStatusListener.Stub() {
+                    @Override
+                    public void onStartPerceiving() {
+                        mAssistUtils.enableVisualQueryDetection(
+                                mVisualQueryDetectionAttentionListener);
+                    }
+
+                    @Override
+                    public void onStopPerceiving() {
+                        mAssistUtils.disableVisualQueryDetection();
+                    }
+                });
+    }
+
     public void launchVoiceAssistFromKeyguard() {
         mAssistUtils.launchVoiceAssistFromKeyguard();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java
index c889ac2..4dd97d5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java
@@ -16,10 +16,9 @@
 
 package com.android.systemui.dreams.conditions;
 
-import com.android.internal.app.AssistUtils;
-import com.android.internal.app.IVisualQueryDetectionAttentionListener;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.assist.AssistManager.VisualQueryAttentionListener;
 import com.android.systemui.dagger.qualifiers.Application;
-import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.shared.condition.Condition;
 
 import javax.inject.Inject;
@@ -30,12 +29,10 @@
  * {@link AssistantAttentionCondition} provides a signal when assistant has the user's attention.
  */
 public class AssistantAttentionCondition extends Condition {
-    private final DreamOverlayStateController mDreamOverlayStateController;
-    private final AssistUtils mAssistUtils;
-    private boolean mEnabled;
+    private final AssistManager mAssistManager;
 
-    private final IVisualQueryDetectionAttentionListener mVisualQueryDetectionAttentionListener =
-            new IVisualQueryDetectionAttentionListener.Stub() {
+    private final VisualQueryAttentionListener mVisualQueryAttentionListener =
+            new VisualQueryAttentionListener() {
         @Override
         public void onAttentionGained() {
             updateCondition(true);
@@ -47,59 +44,26 @@
         }
     };
 
-    private final DreamOverlayStateController.Callback mCallback =
-            new DreamOverlayStateController.Callback() {
-        @Override
-        public void onStateChanged() {
-            if (mDreamOverlayStateController.isDreamOverlayStatusBarVisible()) {
-                enableVisualQueryDetection();
-            } else {
-                disableVisualQueryDetection();
-            }
-        }
-    };
-
     @Inject
     public AssistantAttentionCondition(
             @Application CoroutineScope scope,
-            DreamOverlayStateController dreamOverlayStateController,
-            AssistUtils assistUtils) {
+            AssistManager assistManager) {
         super(scope);
-        mDreamOverlayStateController = dreamOverlayStateController;
-        mAssistUtils = assistUtils;
+        mAssistManager = assistManager;
     }
 
     @Override
     protected void start() {
-        mDreamOverlayStateController.addCallback(mCallback);
+        mAssistManager.addVisualQueryAttentionListener(mVisualQueryAttentionListener);
     }
 
     @Override
     protected void stop() {
-        disableVisualQueryDetection();
-        mDreamOverlayStateController.removeCallback(mCallback);
+        mAssistManager.removeVisualQueryAttentionListener(mVisualQueryAttentionListener);
     }
 
     @Override
     protected int getStartStrategy() {
         return START_EAGERLY;
     }
-
-    private void enableVisualQueryDetection() {
-        if (mEnabled) {
-            return;
-        }
-        mEnabled = true;
-        mAssistUtils.enableVisualQueryDetection(mVisualQueryDetectionAttentionListener);
-    }
-
-    private void disableVisualQueryDetection() {
-        if (!mEnabled) {
-            return;
-        }
-        mEnabled = false;
-        mAssistUtils.disableVisualQueryDetection();
-        // Make sure the condition is set to false as well.
-        updateCondition(false);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 5b56223..9393754 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -195,7 +195,7 @@
     // TODO(b/294110497): Tracking Bug
     @JvmField
     val ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS =
-        unreleasedFlag("enable_wallet_contextual_loyalty_cards", teamfood = true)
+        releasedFlag("enable_wallet_contextual_loyalty_cards")
 
     // TODO(b/242908637): Tracking Bug
     @JvmField val WALLPAPER_FULLSCREEN_PREVIEW = releasedFlag("wallpaper_fullscreen_preview")
@@ -386,8 +386,7 @@
     @JvmField val NEW_BLUETOOTH_REPOSITORY = releasedFlag("new_bluetooth_repository")
 
     // TODO(b/292533677): Tracking Bug
-    val WIFI_TRACKER_LIB_FOR_WIFI_ICON =
-        unreleasedFlag("wifi_tracker_lib_for_wifi_icon", teamfood = true)
+    val WIFI_TRACKER_LIB_FOR_WIFI_ICON = releasedFlag("wifi_tracker_lib_for_wifi_icon")
 
     // TODO(b/293863612): Tracking Bug
     @JvmField val INCOMPATIBLE_CHARGING_BATTERY_ICON =
@@ -787,4 +786,8 @@
     /** Enable the share wifi button in Quick Settings internet dialog. */
     @JvmField
     val SHARE_WIFI_QS_BUTTON = unreleasedFlag("share_wifi_qs_button")
+
+    /** Enable haptic slider component in the brightness slider */
+    @JvmField
+    val HAPTIC_BRIGHTNESS_SLIDER = unreleasedFlag("haptic_brightness_slider")
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
index 677d3ff..c894d91 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
@@ -180,8 +180,9 @@
     }
 
     @Test
-    public void testResume() {
-        mKeyguardAbsKeyInputViewController.onResume(KeyguardSecurityView.VIEW_REVEALED);
+    public void testOnViewAttached() {
+        reset(mLockPatternUtils);
+        mKeyguardAbsKeyInputViewController.onViewAttached();
         verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index 1a9260c..3a94730 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -20,6 +20,7 @@
 import android.testing.TestableLooper
 import android.view.inputmethod.InputMethodManager
 import android.widget.EditText
+import android.widget.ImageView
 import androidx.test.filters.SmallTest
 import com.android.internal.util.LatencyTracker
 import com.android.internal.widget.LockPatternUtils
@@ -29,6 +30,7 @@
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.mockito.whenever
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -36,6 +38,7 @@
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
@@ -45,104 +48,109 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class KeyguardPasswordViewControllerTest : SysuiTestCase() {
-  @Mock private lateinit var keyguardPasswordView: KeyguardPasswordView
-  @Mock private lateinit var passwordEntry: EditText
-  @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
-  @Mock lateinit var securityMode: KeyguardSecurityModel.SecurityMode
-  @Mock lateinit var lockPatternUtils: LockPatternUtils
-  @Mock lateinit var keyguardSecurityCallback: KeyguardSecurityCallback
-  @Mock lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory
-  @Mock lateinit var latencyTracker: LatencyTracker
-  @Mock lateinit var inputMethodManager: InputMethodManager
-  @Mock lateinit var emergencyButtonController: EmergencyButtonController
-  @Mock lateinit var mainExecutor: DelayableExecutor
-  @Mock lateinit var falsingCollector: FalsingCollector
-  @Mock lateinit var keyguardViewController: KeyguardViewController
-  @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
-  @Mock
-  private lateinit var mKeyguardMessageAreaController:
-      KeyguardMessageAreaController<BouncerKeyguardMessageArea>
+    @Mock private lateinit var keyguardPasswordView: KeyguardPasswordView
+    @Mock private lateinit var passwordEntry: EditText
+    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock lateinit var securityMode: KeyguardSecurityModel.SecurityMode
+    @Mock lateinit var lockPatternUtils: LockPatternUtils
+    @Mock lateinit var keyguardSecurityCallback: KeyguardSecurityCallback
+    @Mock lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory
+    @Mock lateinit var latencyTracker: LatencyTracker
+    @Mock lateinit var inputMethodManager: InputMethodManager
+    @Mock lateinit var emergencyButtonController: EmergencyButtonController
+    @Mock lateinit var mainExecutor: DelayableExecutor
+    @Mock lateinit var falsingCollector: FalsingCollector
+    @Mock lateinit var keyguardViewController: KeyguardViewController
+    @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea
+    @Mock
+    private lateinit var mKeyguardMessageAreaController:
+        KeyguardMessageAreaController<BouncerKeyguardMessageArea>
 
-  private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController
+    private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController
 
-  @Before
-  fun setup() {
-    MockitoAnnotations.initMocks(this)
-    Mockito.`when`(
-            keyguardPasswordView.requireViewById<BouncerKeyguardMessageArea>(
-                R.id.bouncer_message_area))
-        .thenReturn(mKeyguardMessageArea)
-    Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea))
-        .thenReturn(mKeyguardMessageAreaController)
-    Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry)
-    Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry))
-        .thenReturn(passwordEntry)
-    `when`(keyguardPasswordView.resources).thenReturn(context.resources)
-    val fakeFeatureFlags = FakeFeatureFlags()
-    fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
-    keyguardPasswordViewController =
-        KeyguardPasswordViewController(
-            keyguardPasswordView,
-            keyguardUpdateMonitor,
-            securityMode,
-            lockPatternUtils,
-            keyguardSecurityCallback,
-            messageAreaControllerFactory,
-            latencyTracker,
-            inputMethodManager,
-            emergencyButtonController,
-            mainExecutor,
-            mContext.resources,
-            falsingCollector,
-            keyguardViewController,
-            fakeFeatureFlags)
-  }
-
-  @Test
-  fun testFocusWhenBouncerIsShown() {
-    Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true)
-    Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
-    keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
-    keyguardPasswordView.post {
-      verify(keyguardPasswordView).requestFocus()
-      verify(keyguardPasswordView).showKeyboard()
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        Mockito.`when`(
+                keyguardPasswordView.requireViewById<BouncerKeyguardMessageArea>(
+                    R.id.bouncer_message_area
+                )
+            )
+            .thenReturn(mKeyguardMessageArea)
+        Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea))
+            .thenReturn(mKeyguardMessageAreaController)
+        Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry)
+        Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry))
+            .thenReturn(passwordEntry)
+        whenever(keyguardPasswordView.findViewById<ImageView>(R.id.switch_ime_button))
+            .thenReturn(mock(ImageView::class.java))
+        `when`(keyguardPasswordView.resources).thenReturn(context.resources)
+        val fakeFeatureFlags = FakeFeatureFlags()
+        fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
+        keyguardPasswordViewController =
+            KeyguardPasswordViewController(
+                keyguardPasswordView,
+                keyguardUpdateMonitor,
+                securityMode,
+                lockPatternUtils,
+                keyguardSecurityCallback,
+                messageAreaControllerFactory,
+                latencyTracker,
+                inputMethodManager,
+                emergencyButtonController,
+                mainExecutor,
+                mContext.resources,
+                falsingCollector,
+                keyguardViewController,
+                fakeFeatureFlags
+            )
     }
-  }
 
-  @Test
-  fun testDoNotFocusWhenBouncerIsHidden() {
-    Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false)
-    Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
-    keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
-    verify(keyguardPasswordView, never()).requestFocus()
-  }
-
-  @Test
-  fun testHideKeyboardWhenOnPause() {
-    keyguardPasswordViewController.onPause()
-    keyguardPasswordView.post {
-      verify(keyguardPasswordView).clearFocus()
-      verify(keyguardPasswordView).hideKeyboard()
+    @Test
+    fun testFocusWhenBouncerIsShown() {
+        Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true)
+        Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
+        keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
+        keyguardPasswordView.post {
+            verify(keyguardPasswordView).requestFocus()
+            verify(keyguardPasswordView).showKeyboard()
+        }
     }
-  }
 
-  @Test
-  fun startAppearAnimation() {
-    keyguardPasswordViewController.startAppearAnimation()
-    verify(mKeyguardMessageAreaController)
-        .setMessage(context.resources.getString(R.string.keyguard_enter_your_password), false)
-  }
+    @Test
+    fun testDoNotFocusWhenBouncerIsHidden() {
+        Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false)
+        Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true)
+        keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
+        verify(keyguardPasswordView, never()).requestFocus()
+    }
 
-  @Test
-  fun startAppearAnimation_withExistingMessage() {
-    `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
-    keyguardPasswordViewController.startAppearAnimation()
-    verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean())
-  }
+    @Test
+    fun testHideKeyboardWhenOnPause() {
+        keyguardPasswordViewController.onPause()
+        keyguardPasswordView.post {
+            verify(keyguardPasswordView).clearFocus()
+            verify(keyguardPasswordView).hideKeyboard()
+        }
+    }
 
-  @Test
-  fun testMessageIsSetWhenReset() {
-    keyguardPasswordViewController.resetState()
-    verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
-  }
+    @Test
+    fun testOnViewAttached() {
+        keyguardPasswordViewController.onViewAttached()
+        verify(mKeyguardMessageAreaController)
+            .setMessage(context.resources.getString(R.string.keyguard_enter_your_password), false)
+    }
+
+    @Test
+    fun testOnViewAttached_withExistingMessage() {
+        `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+        keyguardPasswordViewController.onViewAttached()
+        verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean())
+    }
+
+    @Test
+    fun testMessageIsSetWhenReset() {
+        keyguardPasswordViewController.resetState()
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index 9f7ab7b..1acd676 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -46,6 +46,7 @@
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
@@ -54,35 +55,35 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class KeyguardPatternViewControllerTest : SysuiTestCase() {
-   private lateinit var mKeyguardPatternView: KeyguardPatternView
+    private lateinit var mKeyguardPatternView: KeyguardPatternView
 
-  @Mock private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor
 
-  @Mock private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode
+    @Mock private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode
 
-  @Mock private lateinit var mLockPatternUtils: LockPatternUtils
+    @Mock private lateinit var mLockPatternUtils: LockPatternUtils
 
-  @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback
+    @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback
 
-  @Mock private lateinit var mLatencyTracker: LatencyTracker
-  private var mFalsingCollector: FalsingCollector = FalsingCollectorFake()
+    @Mock private lateinit var mLatencyTracker: LatencyTracker
+    private var mFalsingCollector: FalsingCollector = FalsingCollectorFake()
 
-  @Mock private lateinit var mEmergencyButtonController: EmergencyButtonController
+    @Mock private lateinit var mEmergencyButtonController: EmergencyButtonController
 
-  @Mock
-  private lateinit var mKeyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory
+    @Mock
+    private lateinit var mKeyguardMessageAreaControllerFactory:
+        KeyguardMessageAreaController.Factory
 
-  @Mock
-  private lateinit var mKeyguardMessageAreaController:
-      KeyguardMessageAreaController<BouncerKeyguardMessageArea>
+    @Mock
+    private lateinit var mKeyguardMessageAreaController:
+        KeyguardMessageAreaController<BouncerKeyguardMessageArea>
 
-  @Mock private lateinit var mPostureController: DevicePostureController
+    @Mock private lateinit var mPostureController: DevicePostureController
 
-  private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
-  private lateinit var fakeFeatureFlags: FakeFeatureFlags
+    private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
+    private lateinit var fakeFeatureFlags: FakeFeatureFlags
 
-  @Captor
-  lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
+    @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
 
     @Before
     fun setup() {
@@ -91,9 +92,8 @@
             .thenReturn(mKeyguardMessageAreaController)
         fakeFeatureFlags = FakeFeatureFlags()
         fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, false)
-        mKeyguardPatternView = View.inflate(mContext, R.layout.keyguard_pattern_view, null)
-                as KeyguardPatternView
-
+        mKeyguardPatternView =
+            View.inflate(mContext, R.layout.keyguard_pattern_view, null) as KeyguardPatternView
 
         mKeyguardPatternViewController =
             KeyguardPatternViewController(
@@ -125,8 +125,7 @@
     @Test
     fun onDevicePostureChanged_deviceOpened_propagatedToPatternView() {
         overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f)
-        whenever(mPostureController.devicePosture)
-                .thenReturn(DEVICE_POSTURE_HALF_OPENED)
+        whenever(mPostureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
 
         mKeyguardPatternViewController.onViewAttached()
 
@@ -159,39 +158,37 @@
         return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio)
     }
 
-  @Test
-  fun withFeatureFlagOn_oldMessage_isHidden() {
-    fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
+    @Test
+    fun withFeatureFlagOn_oldMessage_isHidden() {
+        fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
 
-    mKeyguardPatternViewController.onViewAttached()
+        mKeyguardPatternViewController.onViewAttached()
 
-    verify<KeyguardMessageAreaController<*>>(mKeyguardMessageAreaController).disable()
-  }
+        verify<KeyguardMessageAreaController<*>>(mKeyguardMessageAreaController).disable()
+    }
 
-  @Test
-  fun onPause_resetsText() {
-    mKeyguardPatternViewController.init()
-    mKeyguardPatternViewController.onPause()
-    verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
-  }
+    @Test
+    fun onPause_resetsText() {
+        mKeyguardPatternViewController.init()
+        mKeyguardPatternViewController.onPause()
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
+    }
 
-  @Test
-  fun startAppearAnimation() {
-    mKeyguardPatternViewController.startAppearAnimation()
-    verify(mKeyguardMessageAreaController)
-        .setMessage(context.resources.getString(R.string.keyguard_enter_your_pattern), false)
-  }
+    @Test
+    fun testOnViewAttached() {
+        reset(mKeyguardMessageAreaController)
+        reset(mLockPatternUtils)
+        mKeyguardPatternViewController.onViewAttached()
+        verify(mKeyguardMessageAreaController)
+            .setMessage(context.resources.getString(R.string.keyguard_enter_your_pattern), false)
+        verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt())
+    }
 
-  @Test
-  fun startAppearAnimation_withExistingMessage() {
-    `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
-    mKeyguardPatternViewController.startAppearAnimation()
-    verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean())
-  }
-
-  @Test
-  fun resume() {
-    mKeyguardPatternViewController.onResume(KeyguardSecurityView.VIEW_REVEALED)
-    verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt())
-  }
+    @Test
+    fun testOnViewAttached_withExistingMessage() {
+        reset(mKeyguardMessageAreaController)
+        `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+        mKeyguardPatternViewController.onViewAttached()
+        verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean())
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index cf86c21..efe1955 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -100,6 +100,8 @@
                 .thenReturn(mDeleteButton);
         when(mPinBasedInputView.findViewById(R.id.key_enter))
                 .thenReturn(mOkButton);
+
+        when(mPinBasedInputView.getResources()).thenReturn(getContext().getResources());
         FakeFeatureFlags featureFlags = new FakeFeatureFlags();
         featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true);
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 309d9e0..80fd721 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -185,27 +185,27 @@
     }
 
     @Test
-    fun startAppearAnimation() {
+    fun testOnViewAttached() {
         val pinViewController = constructPinViewController(mockKeyguardPinView)
 
-        pinViewController.startAppearAnimation()
+        pinViewController.onViewAttached()
 
         verify(keyguardMessageAreaController)
             .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
     }
 
     @Test
-    fun startAppearAnimation_withExistingMessage() {
+    fun testOnViewAttached_withExistingMessage() {
         val pinViewController = constructPinViewController(mockKeyguardPinView)
         Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.")
 
-        pinViewController.startAppearAnimation()
+        pinViewController.onViewAttached()
 
         verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean())
     }
 
     @Test
-    fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() {
+    fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() {
         val pinViewController = constructPinViewController(mockKeyguardPinView)
         `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
@@ -213,7 +213,7 @@
         `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3)
         `when`(passwordTextView.text).thenReturn("")
 
-        pinViewController.startAppearAnimation()
+        pinViewController.onViewAttached()
 
         verify(deleteButton).visibility = View.INVISIBLE
         verify(enterButton).visibility = View.INVISIBLE
@@ -222,7 +222,7 @@
     }
 
     @Test
-    fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() {
+    fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() {
         val pinViewController = constructPinViewController(mockKeyguardPinView)
         `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
@@ -230,7 +230,7 @@
         `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6)
         `when`(passwordTextView.text).thenReturn("")
 
-        pinViewController.startAppearAnimation()
+        pinViewController.onViewAttached()
 
         verify(deleteButton).visibility = View.VISIBLE
         verify(enterButton).visibility = View.VISIBLE
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 17bb3d5..63c51e4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -64,6 +64,8 @@
 import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argThat
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.settings.GlobalSettings
@@ -577,18 +579,7 @@
             ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
         underTest.onViewAttached()
         verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture())
-        clearInvocations(viewFlipperController)
         configurationListenerArgumentCaptor.value.onThemeChanged()
-        verify(viewFlipperController).clearViews()
-        verify(viewFlipperController)
-            .asynchronouslyInflateView(
-                eq(SecurityMode.PIN),
-                any(),
-                onViewInflatedCallbackArgumentCaptor.capture()
-            )
-        onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController)
-        verify(view).reset()
-        verify(viewFlipperController).reset()
         verify(view).reloadColors()
     }
 
@@ -598,16 +589,7 @@
             ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
         underTest.onViewAttached()
         verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture())
-        clearInvocations(viewFlipperController)
         configurationListenerArgumentCaptor.value.onUiModeChanged()
-        verify(viewFlipperController).clearViews()
-        verify(viewFlipperController)
-            .asynchronouslyInflateView(
-                eq(SecurityMode.PIN),
-                any(),
-                onViewInflatedCallbackArgumentCaptor.capture()
-            )
-        onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController)
         verify(view).reloadColors()
     }
 
@@ -876,6 +858,17 @@
         verify(userSwitcher).setAlpha(0f)
     }
 
+    @Test
+    fun testOnUserSwitched() {
+        val userSwitchCallbackArgumentCaptor =
+            argumentCaptor<UserSwitcherController.UserSwitchCallback>()
+        underTest.onViewAttached()
+        verify(userSwitcherController)
+            .addUserSwitchCallback(capture(userSwitchCallbackArgumentCaptor))
+        userSwitchCallbackArgumentCaptor.value.onUserSwitched()
+        verify(viewFlipperController).asynchronouslyInflateView(any(), any(), any())
+    }
+
     private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener
         get() {
             underTest.onViewAttached()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
index a3acc78..291dda25 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
@@ -97,6 +97,8 @@
     @Test
     fun onViewAttached() {
         underTest.onViewAttached()
+        verify(keyguardMessageAreaController)
+            .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
     }
 
     @Test
@@ -120,8 +122,6 @@
     @Test
     fun startAppearAnimation() {
         underTest.startAppearAnimation()
-        verify(keyguardMessageAreaController)
-            .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
index efcf4dd..626faa6 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
@@ -98,6 +98,8 @@
         underTest.onViewAttached()
         Mockito.verify(keyguardUpdateMonitor)
             .registerCallback(any(KeyguardUpdateMonitorCallback::class.java))
+        Mockito.verify(keyguardMessageAreaController)
+            .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
     }
 
     @Test
@@ -120,8 +122,6 @@
     @Test
     fun startAppearAnimation() {
         underTest.startAppearAnimation()
-        Mockito.verify(keyguardMessageAreaController)
-            .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
index 07cb5d8..6a17889 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
@@ -22,17 +22,14 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
-import android.os.RemoteException;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.app.AssistUtils;
-import com.android.internal.app.IVisualQueryDetectionAttentionListener;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.assist.AssistManager.VisualQueryAttentionListener;
 import com.android.systemui.shared.condition.Condition;
 
 import org.junit.Before;
@@ -50,9 +47,7 @@
     @Mock
     Condition.Callback mCallback;
     @Mock
-    AssistUtils mAssistUtils;
-    @Mock
-    DreamOverlayStateController mDreamOverlayStateController;
+    AssistManager mAssistManager;
     @Mock
     CoroutineScope mScope;
 
@@ -62,55 +57,34 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
-        mAssistantAttentionCondition =
-                new AssistantAttentionCondition(mScope, mDreamOverlayStateController, mAssistUtils);
+        mAssistantAttentionCondition = new AssistantAttentionCondition(mScope, mAssistManager);
         // Adding a callback also starts the condition.
         mAssistantAttentionCondition.addCallback(mCallback);
     }
 
     @Test
     public void testEnableVisualQueryDetection() {
-        final ArgumentCaptor<DreamOverlayStateController.Callback> argumentCaptor =
-                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
-        verify(mDreamOverlayStateController).addCallback(argumentCaptor.capture());
-
-        when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true);
-        argumentCaptor.getValue().onStateChanged();
-
-        verify(mAssistUtils).enableVisualQueryDetection(any());
+        verify(mAssistManager).addVisualQueryAttentionListener(
+                any(VisualQueryAttentionListener.class));
     }
 
     @Test
     public void testDisableVisualQueryDetection() {
-        final ArgumentCaptor<DreamOverlayStateController.Callback> argumentCaptor =
-                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
-        verify(mDreamOverlayStateController).addCallback(argumentCaptor.capture());
-
-        when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true);
-        argumentCaptor.getValue().onStateChanged();
-        when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(false);
-        argumentCaptor.getValue().onStateChanged();
-
-        verify(mAssistUtils).disableVisualQueryDetection();
+        mAssistantAttentionCondition.stop();
+        verify(mAssistManager).removeVisualQueryAttentionListener(
+                any(VisualQueryAttentionListener.class));
     }
 
     @Test
-    public void testAttentionChangedTriggersCondition() throws RemoteException {
-        final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCaptor =
-                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
-        verify(mDreamOverlayStateController).addCallback(callbackCaptor.capture());
+    public void testAttentionChangedTriggersCondition() {
+        final ArgumentCaptor<VisualQueryAttentionListener> argumentCaptor =
+                ArgumentCaptor.forClass(VisualQueryAttentionListener.class);
+        verify(mAssistManager).addVisualQueryAttentionListener(argumentCaptor.capture());
 
-        when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true);
-        callbackCaptor.getValue().onStateChanged();
-
-        final ArgumentCaptor<IVisualQueryDetectionAttentionListener> listenerCaptor =
-                ArgumentCaptor.forClass(IVisualQueryDetectionAttentionListener.class);
-        verify(mAssistUtils).enableVisualQueryDetection(listenerCaptor.capture());
-
-        listenerCaptor.getValue().onAttentionGained();
+        argumentCaptor.getValue().onAttentionGained();
         assertThat(mAssistantAttentionCondition.isConditionMet()).isTrue();
 
-        listenerCaptor.getValue().onAttentionLost();
+        argumentCaptor.getValue().onAttentionLost();
         assertThat(mAssistantAttentionCondition.isConditionMet()).isFalse();
 
         verify(mCallback, times(2)).onConditionChanged(eq(mAssistantAttentionCondition));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 823155b..b8f2cab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -81,7 +81,7 @@
 import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
-import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -136,8 +136,8 @@
     private StatusBarWindowStateController mStatusBarWindowStateController;
     @Mock
     private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @ClassRule
-    public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+    @Rule
+    public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
 
     private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index ef39ff8..79feb41 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -82,7 +82,7 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -111,8 +111,8 @@
     private BlockingQueueIntentReceiver mReceiver;
     private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake();
 
-    @ClassRule
-    public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+    @Rule
+    public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
 
     @Before
     public void setUp() throws Exception {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 2828eb2..21e4f5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -39,7 +39,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.animation.AnimatorTestRule;
 import android.app.KeyguardManager;
 import android.content.res.Configuration;
 import android.media.AudioManager;
@@ -86,8 +85,6 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class VolumeDialogImplTest extends SysuiTestCase {
-    private static final AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule();
-
     VolumeDialogImpl mDialog;
     View mActiveRinger;
     View mDrawerContainer;
@@ -753,7 +750,6 @@
     public void teardown() {
         cleanUp(mDialog);
         setOrientation(mOriginalOrientation);
-        sAnimatorTestRule.advanceTimeBy(mLongestHideShowAnimationDuration);
         mTestableLooper.moveTimeForward(mLongestHideShowAnimationDuration);
         mTestableLooper.processAllMessages();
         reset(mPostureController);
diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
index 19c68e8..41dbc14 100644
--- a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
+++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
@@ -49,7 +49,7 @@
  * public class SampleAnimatorTest {
  *
  *     {@literal @}Rule
- *     public AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule();
+ *     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
  *
  *     {@literal @}UiThreadTest
  *     {@literal @}Test
@@ -58,7 +58,7 @@
  *         animator.setDuration(1000L);
  *         assertThat(animator.getAnimatedValue(), is(0));
  *         animator.start();
- *         sAnimatorTestRule.advanceTimeBy(500L);
+ *         mAnimatorTestRule.advanceTimeBy(500L);
  *         assertThat(animator.getAnimatedValue(), is(500));
  *     }
  * }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index ba242ec..01786be 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -969,8 +969,10 @@
             final Rect innerFrame = hasInheritedLetterboxBehavior()
                     ? mActivityRecord.getBounds() : w.getFrame();
             mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint);
-            // We need to notify Shell that letterbox position has changed.
-            mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
+            if (mDoubleTapEvent) {
+                // We need to notify Shell that letterbox position has changed.
+                mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
+            }
         } else if (mLetterbox != null) {
             mLetterbox.hide();
         }
@@ -1242,6 +1244,7 @@
                                 ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT
                                 : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
             logLetterboxPositionChange(changeToLog);
+            mDoubleTapEvent = true;
         } else if (mLetterbox.getInnerFrame().right < x) {
             // Moving to the next stop on the right side of the app window: left > center > right.
             mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
@@ -1252,8 +1255,8 @@
                                 ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT
                                 : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
             logLetterboxPositionChange(changeToLog);
+            mDoubleTapEvent = true;
         }
-        mDoubleTapEvent = true;
         // TODO(197549949): Add animation for transition.
         mActivityRecord.recomputeConfiguration();
     }
@@ -1281,6 +1284,7 @@
                                 ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP
                                 : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
             logLetterboxPositionChange(changeToLog);
+            mDoubleTapEvent = true;
         } else if (mLetterbox.getInnerFrame().bottom < y) {
             // Moving to the next stop on the bottom side of the app window: top > center > bottom.
             mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
@@ -1291,8 +1295,8 @@
                                 ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM
                                 : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
             logLetterboxPositionChange(changeToLog);
+            mDoubleTapEvent = true;
         }
-        mDoubleTapEvent = true;
         // TODO(197549949): Add animation for transition.
         mActivityRecord.recomputeConfiguration();
     }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 8fbaac2..0f0189e61 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3459,6 +3459,8 @@
         info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
         info.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET;
         info.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET;
+        info.isUserFullscreenOverrideEnabled = top != null
+                && top.mLetterboxUiController.shouldApplyUserFullscreenOverride();
         info.isFromLetterboxDoubleTap = top != null && top.mLetterboxUiController.isFromDoubleTap();
         if (info.isLetterboxDoubleTapEnabled) {
             info.topActivityLetterboxWidth = top.getBounds().width();