Merge "Fix getOrientation from adjacent TaskFragments" into tm-qpr-dev
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 9fab3a1..25eddf8 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -66,8 +66,10 @@
     <!-- Multi-Window strings -->
     <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed in split-screen and that things might crash/not work properly [CHAR LIMIT=NONE] -->
     <string name="dock_forced_resizable">App may not work with split-screen.</string>
-    <!-- Warning message when we try to dock a non-resizeable task and launch it in fullscreen instead. -->
+    <!-- Warning message when we try to dock a non-resizeable task and launch it in fullscreen instead  [CHAR LIMIT=NONE] -->
     <string name="dock_non_resizeble_failed_to_dock_text">App does not support split-screen.</string>
+    <!-- Warning message when we try to dock an app not supporting multiple instances split into multiple sides [CHAR LIMIT=NONE] -->
+    <string name="dock_multi_instances_not_supported_text">This app can only be opened in 1 window.</string>
     <!-- Text that gets shown on top of current activity to inform the user that the system force-resized the current activity to be displayed on a secondary display and that things might crash/not work properly [CHAR LIMIT=NONE] -->
     <string name="forced_resizable_secondary_display">App may not work on a secondary display.</string>
     <!-- Warning message when we try to launch a non-resizeable activity on a secondary display and launch it on the primary instead. -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index 1474754..e8b0f02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -76,6 +76,7 @@
     private GestureDetector mDoubleTapDetector;
     private boolean mInteractive;
     private boolean mSetTouchRegion = true;
+    private int mLastDraggingPosition;
 
     /**
      * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with
@@ -298,6 +299,7 @@
                 }
                 if (mMoving) {
                     final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
+                    mLastDraggingPosition = position;
                     mSplitLayout.updateDivideBounds(position);
                 }
                 break;
@@ -372,6 +374,15 @@
                 "Set divider bar %s from %s", interactive ? "interactive" : "non-interactive",
                 from);
         mInteractive = interactive;
+        if (!mInteractive && mMoving) {
+            final int position = mSplitLayout.getDividePosition();
+            mSplitLayout.flingDividePosition(
+                    mLastDraggingPosition,
+                    position,
+                    mSplitLayout.FLING_RESIZE_DURATION,
+                    () -> mSplitLayout.setDividePosition(position, true /* applyLayoutChange */));
+            mMoving = false;
+        }
         releaseTouching();
         mHandle.setVisibility(mInteractive ? View.VISIBLE : View.INVISIBLE);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index ec9e6f7..ae49616 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -81,7 +81,7 @@
     public static final int PARALLAX_DISMISSING = 1;
     public static final int PARALLAX_ALIGN_CENTER = 2;
 
-    private static final int FLING_RESIZE_DURATION = 250;
+    public static final int FLING_RESIZE_DURATION = 250;
     private static final int FLING_SWITCH_DURATION = 350;
     private static final int FLING_ENTER_DURATION = 450;
     private static final int FLING_EXIT_DURATION = 450;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 1774dd5..a79ac45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -58,6 +58,7 @@
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.view.WindowManager;
+import android.widget.Toast;
 import android.window.RemoteTransition;
 import android.window.WindowContainerTransaction;
 
@@ -597,6 +598,10 @@
             } else if (isSplitScreenVisible()) {
                 mStageCoordinator.switchSplitPosition("startIntent");
                 return;
+            } else {
+                Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
+                        Toast.LENGTH_SHORT).show();
+                return;
             }
         }
 
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index b24ce12..6c7cab5 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -24,6 +24,7 @@
     <!-- margin from keyguard status bar to clock. For split shade it should be
          keyguard_split_shade_top_margin - status_bar_header_height_keyguard = 8dp -->
     <dimen name="keyguard_clock_top_margin">8dp</dimen>
+    <dimen name="keyguard_smartspace_top_offset">0dp</dimen>
 
     <!-- QS-->
     <dimen name="qs_panel_padding_top">16dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9ed62cb..3e7b0f1 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1321,6 +1321,9 @@
     <!-- QR Code Scanner label, title [CHAR LIMIT=32] -->
     <string name="qr_code_scanner_title">QR code scanner</string>
 
+    <!-- QR Code Scanner Secondary label when GMS Core is Updating -->
+    <string name="qr_code_scanner_updating_secondary_label">Updating</string>
+
     <!-- Name of the work status bar icon. -->
     <string name="status_bar_work">Work profile</string>
 
@@ -2679,4 +2682,50 @@
 
     <!-- Time format for the Dream Time Complication for 24-hour time format [CHAR LIMIT=NONE] -->
     <string name="dream_time_complication_24_hr_time_format">kk:mm</string>
+
+    <!--
+    Template for an action that opens a specific app. [CHAR LIMIT=16]
+    -->
+    <string name="keyguard_affordance_enablement_dialog_action_template">Open <xliff:g id="appName" example="Wallet">%1$s</xliff:g></string>
+
+    <!--
+    Template for a message shown right before a list of instructions that tell the user what to do
+    in order to enable a shortcut to a specific app. [CHAR LIMIT=NONE]
+    -->
+    <string name="keyguard_affordance_enablement_dialog_message">To add the <xliff:g id="appName" example="Wallet">%1$s</xliff:g> app as a shortcut, make sure</string>
+
+    <!--
+    Requirement for the wallet app to be available for the user to use. This is shown as part of a
+    bulleted list of requirements. When all requirements are met, the app can be accessed through a
+    shortcut button on the lock screen. [CHAR LIMIT=NONE].
+    -->
+    <string name="keyguard_affordance_enablement_dialog_wallet_instruction_1">&#8226; The app is set up</string>
+
+    <!--
+    Requirement for the wallet app to be available for the user to use. This is shown as part of a
+    bulleted list of requirements. When all requirements are met, the app can be accessed through a
+    shortcut button on the lock screen. [CHAR LIMIT=NONE].
+    -->
+    <string name="keyguard_affordance_enablement_dialog_wallet_instruction_2">&#8226; At least one card has been added to Wallet</string>
+
+    <!--
+    Requirement for the QR code scanner functionality to be available for the user to use. This is
+    shown as part of a bulleted list of requirements. When all requirements are met, the piece of
+    functionality can be accessed through a shortcut button on the lock screen. [CHAR LIMIT=NONE].
+    -->
+    <string name="keyguard_affordance_enablement_dialog_qr_scanner_instruction">&#8226; Install a camera app</string>
+
+    <!--
+    Requirement for the home app to be available for the user to use. This is shown as part of a
+    bulleted list of requirements. When all requirements are met, the app can be accessed through a
+    shortcut button on the lock screen. [CHAR LIMIT=NONE].
+    -->
+    <string name="keyguard_affordance_enablement_dialog_home_instruction_1">&#8226; The app is set up</string>
+
+    <!--
+    Requirement for the home app to be available for the user to use. This is shown as part of a
+    bulleted list of requirements. When all requirements are met, the app can be accessed through a
+    shortcut button on the lock screen. [CHAR LIMIT=NONE].
+    -->
+    <string name="keyguard_affordance_enablement_dialog_home_instruction_2">&#8226; At least one device is available</string>
 </resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
index 71469a3..98d8d3e 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
@@ -67,6 +67,8 @@
     object AffordanceTable {
         const val TABLE_NAME = "affordances"
         val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
+        const val ENABLEMENT_INSTRUCTIONS_DELIMITER = "]["
+        const val COMPONENT_NAME_SEPARATOR = "/"
 
         object Columns {
             /** String. Unique ID for this affordance. */
@@ -78,6 +80,25 @@
              * ID from the system UI package.
              */
             const val ICON = "icon"
+            /** Integer. `1` if the affordance is enabled or `0` if it disabled. */
+            const val IS_ENABLED = "is_enabled"
+            /**
+             * String. List of strings, delimited by [ENABLEMENT_INSTRUCTIONS_DELIMITER] to be shown
+             * to the user if the affordance is disabled and the user selects the affordance. The
+             * first one is a title while the rest are the steps needed to re-enable the affordance.
+             */
+            const val ENABLEMENT_INSTRUCTIONS = "enablement_instructions"
+            /**
+             * String. Optional label for a button that, when clicked, opens a destination activity
+             * where the user can re-enable the disabled affordance.
+             */
+            const val ENABLEMENT_ACTION_TEXT = "enablement_action_text"
+            /**
+             * String. Optional package name and activity action string, delimited by
+             * [COMPONENT_NAME_SEPARATOR] to use with an `Intent` to start an activity that opens a
+             * destination where the user can re-enable the disabled affordance.
+             */
+            const val ENABLEMENT_COMPONENT_NAME = "enablement_action_intent"
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardConstants.java b/packages/SystemUI/src/com/android/keyguard/KeyguardConstants.java
index b2658c9..a5b62b6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardConstants.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardConstants.java
@@ -29,5 +29,4 @@
      */
     public static final boolean DEBUG = Log.isLoggable("Keyguard", Log.DEBUG);
     public static final boolean DEBUG_SIM_STATES = true;
-    public static final boolean DEBUG_BIOMETRIC_WAKELOCK = true;
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 88477aaa..331497e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -845,6 +845,7 @@
                     + " triggered while waiting for cancellation, removing watchdog");
             mHandler.removeCallbacks(mFpCancelNotReceived);
         }
+        mLogger.d("handleFingerprintAuthFailed");
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -974,6 +975,7 @@
             stopListeningForFace(FACE_AUTH_STOPPED_FP_LOCKED_OUT);
         }
 
+        mLogger.logFingerprintError(msgId, errString);
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -3900,6 +3902,8 @@
             pw.println("    listening: actual=" + mFaceRunningState
                     + " expected=(" + (shouldListenForFace() ? 1 : 0));
             pw.println("    strongAuthFlags=" + Integer.toHexString(strongAuthFlags));
+            pw.println("    isNonStrongBiometricAllowedAfterIdleTimeout="
+                    + mStrongAuthTracker.isNonStrongBiometricAllowedAfterIdleTimeout(userId));
             pw.println("    trustManaged=" + getUserTrustIsManaged(userId));
             pw.println("    mFaceLockedOutPermanent=" + mFaceLockedOutPermanent);
             pw.println("    enabledByUser=" + mBiometricEnabledForUser.get(userId));
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
index 6264ce7..2bb75aa 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
@@ -17,7 +17,7 @@
 package com.android.keyguard.logging
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.dagger.BiometricMessagesLog
+import com.android.systemui.log.dagger.BiometricLog
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel.DEBUG
 import javax.inject.Inject
@@ -26,7 +26,7 @@
 @SysUISingleton
 class FaceMessageDeferralLogger
 @Inject
-constructor(@BiometricMessagesLog private val logBuffer: LogBuffer) :
+constructor(@BiometricLog private val logBuffer: LogBuffer) :
     BiometricMessageDeferralLogger(logBuffer, "FaceMessageDeferralLogger")
 
 open class BiometricMessageDeferralLogger(
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt
new file mode 100644
index 0000000..bc0bd8c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricUnlockLogger.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard.logging
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.BiometricLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_DISMISS_BOUNCER
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_NONE
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_ONLY_WAKE
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_SHOW_BOUNCER
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_UNLOCK_COLLAPSING
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "BiometricUnlockLogger"
+
+/** Helper class for logging for [com.android.systemui.statusbar.phone.BiometricUnlockController] */
+@SysUISingleton
+class BiometricUnlockLogger @Inject constructor(@BiometricLog private val logBuffer: LogBuffer) {
+    fun i(@CompileTimeConstant msg: String) = log(msg, INFO)
+    fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+    fun log(@CompileTimeConstant msg: String, level: LogLevel) = logBuffer.log(TAG, level, msg)
+
+    fun logStartWakeAndUnlock(mode: Int) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { int1 = mode },
+            { "startWakeAndUnlock(${wakeAndUnlockModeToString(int1)})" }
+        )
+    }
+
+    fun logUdfpsAttemptThresholdMet(consecutiveFailedAttempts: Int) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            { int1 = consecutiveFailedAttempts },
+            { "udfpsAttemptThresholdMet consecutiveFailedAttempts=$int1" }
+        )
+    }
+
+    fun logCalculateModeForFingerprintUnlockingAllowed(
+        deviceInteractive: Boolean,
+        keyguardShowing: Boolean,
+        deviceDreaming: Boolean
+    ) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                bool1 = deviceInteractive
+                bool2 = keyguardShowing
+                bool3 = deviceDreaming
+            },
+            {
+                "calculateModeForFingerprint unlockingAllowed=true" +
+                    " deviceInteractive=$bool1 isKeyguardShowing=$bool2" +
+                    " deviceDreaming=$bool3"
+            }
+        )
+    }
+
+    fun logCalculateModeForFingerprintUnlockingNotAllowed(
+        strongBiometric: Boolean,
+        strongAuthFlags: Int,
+        nonStrongBiometricAllowed: Boolean,
+        deviceInteractive: Boolean,
+        keyguardShowing: Boolean
+    ) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = strongAuthFlags
+                bool1 = strongBiometric
+                bool2 = nonStrongBiometricAllowed
+                bool3 = deviceInteractive
+                bool4 = keyguardShowing
+            },
+            {
+                "calculateModeForFingerprint unlockingAllowed=false" +
+                    " strongBiometric=$bool1 strongAuthFlags=$int1" +
+                    " nonStrongBiometricAllowed=$bool2" +
+                    " deviceInteractive=$bool3 isKeyguardShowing=$bool4"
+            }
+        )
+    }
+
+    fun logCalculateModeForPassiveAuthUnlockingAllowed(
+        deviceInteractive: Boolean,
+        keyguardShowing: Boolean,
+        deviceDreaming: Boolean,
+        bypass: Boolean
+    ) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                bool1 = deviceInteractive
+                bool2 = keyguardShowing
+                bool3 = deviceDreaming
+                bool4 = bypass
+            },
+            {
+                "calculateModeForPassiveAuth unlockingAllowed=true" +
+                    " deviceInteractive=$bool1 isKeyguardShowing=$bool2" +
+                    " deviceDreaming=$bool3 bypass=$bool4"
+            }
+        )
+    }
+
+    fun logCalculateModeForPassiveAuthUnlockingNotAllowed(
+        strongBiometric: Boolean,
+        strongAuthFlags: Int,
+        nonStrongBiometricAllowed: Boolean,
+        deviceInteractive: Boolean,
+        keyguardShowing: Boolean,
+        bypass: Boolean
+    ) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = if (strongBiometric) 1 else 0
+                int2 = strongAuthFlags
+                bool1 = nonStrongBiometricAllowed
+                bool2 = deviceInteractive
+                bool3 = keyguardShowing
+                bool4 = bypass
+            },
+            {
+                "calculateModeForPassiveAuth unlockingAllowed=false" +
+                    " strongBiometric=${int1 == 1}" +
+                    " strongAuthFlags=$int2 nonStrongBiometricAllowed=$bool1" +
+                    " deviceInteractive=$bool2 isKeyguardShowing=$bool3 bypass=$bool4"
+            }
+        )
+    }
+}
+
+private fun wakeAndUnlockModeToString(mode: Int): String {
+    return when (mode) {
+        MODE_NONE -> "MODE_NONE"
+        MODE_WAKE_AND_UNLOCK -> "MODE_WAKE_AND_UNLOCK"
+        MODE_WAKE_AND_UNLOCK_PULSING -> "MODE_WAKE_AND_UNLOCK_PULSING"
+        MODE_SHOW_BOUNCER -> "MODE_SHOW_BOUNCER"
+        MODE_ONLY_WAKE -> "MODE_ONLY_WAKE"
+        MODE_UNLOCK_COLLAPSING -> "MODE_UNLOCK_COLLAPSING"
+        MODE_WAKE_AND_UNLOCK_FROM_DREAM -> "MODE_WAKE_AND_UNLOCK_FROM_DREAM"
+        MODE_DISMISS_BOUNCER -> "MODE_DISMISS_BOUNCER"
+        else -> "UNKNOWN{$mode}"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 6763700..1f6441a 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -161,6 +161,13 @@
         }, {"Fingerprint auth successful: userId: $int1, isStrongBiometric: $bool1"})
     }
 
+    fun logFingerprintError(msgId: Int, originalErrMsg: String) {
+        logBuffer.log(TAG, DEBUG, {
+            str1 = originalErrMsg
+            int1 = msgId
+        }, { "Fingerprint error received: $str1 msgId= $int1" })
+    }
+
     fun logInvalidSubId(subId: Int) {
         logBuffer.log(TAG, INFO,
                 { int1 = subId },
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
index bfc60c1..29febb6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
 import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
 import javax.inject.Inject
+import kotlinx.coroutines.runBlocking
 
 class KeyguardQuickAffordanceProvider :
     ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
@@ -118,9 +119,9 @@
         sortOrder: String?,
     ): Cursor? {
         return when (uriMatcher.match(uri)) {
-            MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
+            MATCH_CODE_ALL_AFFORDANCES -> runBlocking { queryAffordances() }
             MATCH_CODE_ALL_SLOTS -> querySlots()
-            MATCH_CODE_ALL_SELECTIONS -> querySelections()
+            MATCH_CODE_ALL_SELECTIONS -> runBlocking { querySelections() }
             MATCH_CODE_ALL_FLAGS -> queryFlags()
             else -> null
         }
@@ -194,7 +195,7 @@
         }
     }
 
-    private fun querySelections(): Cursor {
+    private suspend fun querySelections(): Cursor {
         return MatrixCursor(
                 arrayOf(
                     Contract.SelectionTable.Columns.SLOT_ID,
@@ -219,12 +220,16 @@
             }
     }
 
-    private fun queryAffordances(): Cursor {
+    private suspend fun queryAffordances(): Cursor {
         return MatrixCursor(
                 arrayOf(
                     Contract.AffordanceTable.Columns.ID,
                     Contract.AffordanceTable.Columns.NAME,
                     Contract.AffordanceTable.Columns.ICON,
+                    Contract.AffordanceTable.Columns.IS_ENABLED,
+                    Contract.AffordanceTable.Columns.ENABLEMENT_INSTRUCTIONS,
+                    Contract.AffordanceTable.Columns.ENABLEMENT_ACTION_TEXT,
+                    Contract.AffordanceTable.Columns.ENABLEMENT_COMPONENT_NAME,
                 )
             )
             .apply {
@@ -234,6 +239,12 @@
                             representation.id,
                             representation.name,
                             representation.iconResourceId,
+                            if (representation.isEnabled) 1 else 0,
+                            representation.instructions?.joinToString(
+                                Contract.AffordanceTable.ENABLEMENT_INSTRUCTIONS_DELIMITER
+                            ),
+                            representation.actionText,
+                            representation.actionComponentName,
                         )
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index d6f521c..2558fab 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.content.Intent
 import androidx.annotation.DrawableRes
+import com.android.systemui.R
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -45,7 +46,7 @@
 class HomeControlsKeyguardQuickAffordanceConfig
 @Inject
 constructor(
-    @Application context: Context,
+    @Application private val context: Context,
     private val component: ControlsComponent,
 ) : KeyguardQuickAffordanceConfig {
 
@@ -66,6 +67,36 @@
             }
         }
 
+    override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
+        if (!component.isEnabled()) {
+            return KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+        }
+
+        val currentServices =
+            component.getControlsListingController().getOrNull()?.getCurrentServices()
+        val hasFavorites =
+            component.getControlsController().getOrNull()?.getFavorites()?.isNotEmpty() == true
+        if (currentServices.isNullOrEmpty() || !hasFavorites) {
+            return KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
+                instructions =
+                    listOf(
+                        context.getString(
+                            R.string.keyguard_affordance_enablement_dialog_message,
+                            pickerName,
+                        ),
+                        context.getString(
+                            R.string.keyguard_affordance_enablement_dialog_home_instruction_1
+                        ),
+                        context.getString(
+                            R.string.keyguard_affordance_enablement_dialog_home_instruction_2
+                        ),
+                    ),
+            )
+        }
+
+        return KeyguardQuickAffordanceConfig.PickerScreenState.Default
+    }
+
     override fun onTriggered(
         expandable: Expandable?,
     ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
index fd40d1d..4477310 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
 import kotlinx.coroutines.flow.Flow
 
 /** Defines interface that can act as data source for a single quick affordance model. */
@@ -41,6 +42,12 @@
     val lockScreenState: Flow<LockScreenState>
 
     /**
+     * Returns the [PickerScreenState] representing the affordance in the settings or selector
+     * experience.
+     */
+    suspend fun getPickerScreenState(): PickerScreenState = PickerScreenState.Default
+
+    /**
      * Notifies that the affordance was clicked by the user.
      *
      * @param expandable An [Expandable] to use when animating dialogs or activities
@@ -49,6 +56,58 @@
     fun onTriggered(expandable: Expandable?): OnTriggeredResult
 
     /**
+     * Encapsulates the state of a quick affordance within the context of the settings or selector
+     * experience.
+     */
+    sealed class PickerScreenState {
+
+        /** The picker shows the item for selecting this affordance as it normally would. */
+        object Default : PickerScreenState()
+
+        /**
+         * The picker does not show an item for selecting this affordance as it is not supported on
+         * the device at all. For example, missing hardware requirements.
+         */
+        object UnavailableOnDevice : PickerScreenState()
+
+        /**
+         * The picker shows the item for selecting this affordance as disabled. Clicking on it will
+         * show the given instructions to the user. If [actionText] and [actionComponentName] are
+         * provided (optional) a button will be shown to open an activity to help the user complete
+         * the steps described in the instructions.
+         */
+        data class Disabled(
+            /** List of human-readable instructions for setting up the quick affordance. */
+            val instructions: List<String>,
+            /**
+             * Optional text to display on a button that the user can click to start a flow to go
+             * and set up the quick affordance and make it enabled.
+             */
+            val actionText: String? = null,
+            /**
+             * Optional component name to be able to build an `Intent` that opens an `Activity` for
+             * the user to be able to set up the quick affordance and make it enabled.
+             *
+             * This is either just an action for the `Intent` or a package name and action,
+             * separated by [Contract.AffordanceTable.COMPONENT_NAME_SEPARATOR] for convenience, you
+             * can use the [componentName] function.
+             */
+            val actionComponentName: String? = null,
+        ) : PickerScreenState() {
+            init {
+                check(instructions.isNotEmpty()) { "Instructions must not be empty!" }
+                check(
+                    (actionText.isNullOrEmpty() && actionComponentName.isNullOrEmpty()) ||
+                        (!actionText.isNullOrEmpty() && !actionComponentName.isNullOrEmpty())
+                ) {
+                    "actionText and actionComponentName must either both be null/empty or both be" +
+                        " non-empty!"
+                }
+            }
+        }
+    }
+
+    /**
      * Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a
      * button on the lock-screen).
      */
@@ -83,4 +142,18 @@
             val canShowWhileLocked: Boolean,
         ) : OnTriggeredResult()
     }
+
+    companion object {
+        fun componentName(
+            packageName: String? = null,
+            action: String?,
+        ): String? {
+            return when {
+                action.isNullOrEmpty() -> null
+                !packageName.isNullOrEmpty() ->
+                    "$packageName${Contract.AffordanceTable.COMPONENT_NAME_SEPARATOR}$action"
+                else -> action
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
index 11f72ff..a96ce77 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -36,7 +36,7 @@
 class QrCodeScannerKeyguardQuickAffordanceConfig
 @Inject
 constructor(
-    @Application context: Context,
+    @Application private val context: Context,
     private val controller: QRCodeScannerController,
 ) : KeyguardQuickAffordanceConfig {
 
@@ -75,6 +75,28 @@
             }
         }
 
+    override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
+        return when {
+            !controller.isAvailableOnDevice ->
+                KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+            !controller.isAbleToOpenCameraApp ->
+                KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
+                    instructions =
+                        listOf(
+                            context.getString(
+                                R.string.keyguard_affordance_enablement_dialog_message,
+                                pickerName,
+                            ),
+                            context.getString(
+                                R.string
+                                    .keyguard_affordance_enablement_dialog_qr_scanner_instruction
+                            ),
+                        ),
+                )
+            else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default
+        }
+    }
+
     override fun onTriggered(
         expandable: Expandable?,
     ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
index 303e6a1..beb20ce 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -18,10 +18,12 @@
 package com.android.systemui.keyguard.data.quickaffordance
 
 import android.content.Context
+import android.content.Intent
 import android.graphics.drawable.Drawable
 import android.service.quickaccesswallet.GetWalletCardsError
 import android.service.quickaccesswallet.GetWalletCardsResponse
 import android.service.quickaccesswallet.QuickAccessWalletClient
+import android.service.quickaccesswallet.WalletCard
 import android.util.Log
 import com.android.systemui.R
 import com.android.systemui.animation.Expandable
@@ -31,25 +33,27 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.Companion.componentName
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.wallet.controller.QuickAccessWalletController
 import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.suspendCancellableCoroutine
 
 /** Quick access wallet quick affordance data source. */
 @SysUISingleton
 class QuickAccessWalletKeyguardQuickAffordanceConfig
 @Inject
 constructor(
-    @Application context: Context,
+    @Application private val context: Context,
     private val walletController: QuickAccessWalletController,
     private val activityStarter: ActivityStarter,
 ) : KeyguardQuickAffordanceConfig {
 
     override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
 
-    override val pickerName = context.getString(R.string.accessibility_wallet_button)
+    override val pickerName: String = context.getString(R.string.accessibility_wallet_button)
 
     override val pickerIconResourceId = R.drawable.ic_wallet_lockscreen
 
@@ -58,10 +62,11 @@
             val callback =
                 object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
                     override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) {
+                        val hasCards = response?.walletCards?.isNotEmpty() == true
                         trySendWithFailureLogging(
                             state(
                                 isFeatureEnabled = walletController.isWalletEnabled,
-                                hasCard = response?.walletCards?.isNotEmpty() == true,
+                                hasCard = hasCards,
                                 tileIcon = walletController.walletClient.tileIcon,
                             ),
                             TAG,
@@ -93,6 +98,44 @@
             }
         }
 
+    override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
+        return when {
+            !walletController.isWalletEnabled ->
+                KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+            walletController.walletClient.tileIcon == null || queryCards().isEmpty() -> {
+                val componentName =
+                    walletController.walletClient.createWalletSettingsIntent().toComponentName()
+                val actionText =
+                    if (componentName != null) {
+                        context.getString(
+                            R.string.keyguard_affordance_enablement_dialog_action_template,
+                            pickerName,
+                        )
+                    } else {
+                        null
+                    }
+                KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
+                    instructions =
+                        listOf(
+                            context.getString(
+                                R.string.keyguard_affordance_enablement_dialog_message,
+                                pickerName,
+                            ),
+                            context.getString(
+                                R.string.keyguard_affordance_enablement_dialog_wallet_instruction_1
+                            ),
+                            context.getString(
+                                R.string.keyguard_affordance_enablement_dialog_wallet_instruction_2
+                            ),
+                        ),
+                    actionText = actionText,
+                    actionComponentName = componentName,
+                )
+            }
+            else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default
+        }
+    }
+
     override fun onTriggered(
         expandable: Expandable?,
     ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
@@ -104,6 +147,24 @@
         return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
     }
 
+    private suspend fun queryCards(): List<WalletCard> {
+        return suspendCancellableCoroutine { continuation ->
+            val callback =
+                object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
+                    override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) {
+                        continuation.resumeWith(
+                            Result.success(response?.walletCards ?: emptyList())
+                        )
+                    }
+
+                    override fun onWalletCardRetrievalError(error: GetWalletCardsError?) {
+                        continuation.resumeWith(Result.success(emptyList()))
+                    }
+                }
+            walletController.queryWalletCards(callback)
+        }
+    }
+
     private fun state(
         isFeatureEnabled: Boolean,
         hasCard: Boolean,
@@ -125,6 +186,14 @@
         }
     }
 
+    private fun Intent?.toComponentName(): String? {
+        if (this == null) {
+            return null
+        }
+
+        return componentName(packageName = `package`, action = action)
+    }
+
     companion object {
         private const val TAG = "QuickAccessWalletKeyguardQuickAffordanceConfig"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
index 533b3ab..d300500 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
@@ -121,18 +121,32 @@
     }
 
     /**
-     * Returns the list of representation objects for all known affordances, regardless of what is
-     * selected. This is useful for building experiences like the picker/selector or user settings
-     * so the user can see everything that can be selected in a menu.
+     * Returns the list of representation objects for all known, device-available affordances,
+     * regardless of what is selected. This is useful for building experiences like the
+     * picker/selector or user settings so the user can see everything that can be selected in a
+     * menu.
      */
-    fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> {
-        return configs.map { config ->
-            KeyguardQuickAffordancePickerRepresentation(
-                id = config.key,
-                name = config.pickerName,
-                iconResourceId = config.pickerIconResourceId,
-            )
-        }
+    suspend fun getAffordancePickerRepresentations():
+        List<KeyguardQuickAffordancePickerRepresentation> {
+        return configs
+            .associateWith { config -> config.getPickerScreenState() }
+            .filterNot { (_, pickerState) ->
+                pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+            }
+            .map { (config, pickerState) ->
+                val disabledPickerState =
+                    pickerState as? KeyguardQuickAffordanceConfig.PickerScreenState.Disabled
+                KeyguardQuickAffordancePickerRepresentation(
+                    id = config.key,
+                    name = config.pickerName,
+                    iconResourceId = config.pickerIconResourceId,
+                    isEnabled =
+                        pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.Default,
+                    instructions = disabledPickerState?.instructions,
+                    actionText = disabledPickerState?.actionText,
+                    actionComponentName = disabledPickerState?.actionComponentName,
+                )
+            }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index c8216c5..2d94d76 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -189,7 +189,7 @@
     }
 
     /** Returns affordance IDs indexed by slot ID, for all known slots. */
-    fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
+    suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
         check(isUsingRepository)
 
         val slots = repository.get().getSlotPickerRepresentations()
@@ -310,7 +310,8 @@
         return Pair(splitUp[0], splitUp[1])
     }
 
-    fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> {
+    suspend fun getAffordancePickerRepresentations():
+        List<KeyguardQuickAffordancePickerRepresentation> {
         check(isUsingRepository)
 
         return repository.get().getAffordancePickerRepresentations()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
index 3b31dcf..84a8074 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
@@ -21,6 +21,7 @@
 import android.os.Trace
 import android.os.UserHandle
 import android.os.UserManager
+import android.view.View
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.DejankUtils
@@ -84,6 +85,7 @@
             )
         )
         repository.setPrimaryShowingSoon(false)
+        primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.VISIBLE)
     }
 
     val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull()
@@ -182,6 +184,7 @@
         repository.setPrimaryVisible(false)
         repository.setPrimaryHide(true)
         repository.setPrimaryShow(null)
+        primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE)
         Trace.endSection()
     }
 
@@ -276,11 +279,6 @@
         repository.setShowMessage(null)
     }
 
-    /** Notify that view visibility has changed. */
-    fun notifyBouncerVisibilityHasChanged(visibility: Int) {
-        primaryBouncerCallbackInteractor.dispatchVisibilityChanged(visibility)
-    }
-
     /** Notify that the resources have been updated */
     fun notifyUpdatedResources() {
         repository.setResourceUpdateRequests(false)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
index a56bc90..7d13359 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
@@ -27,4 +27,22 @@
     val id: String,
     val name: String,
     @DrawableRes val iconResourceId: Int,
+
+    /** Whether this quick affordance is enabled. */
+    val isEnabled: Boolean = true,
+
+    /** If not enabled, the list of user-visible steps to re-enable it. */
+    val instructions: List<String>? = null,
+
+    /**
+     * If not enabled, an optional label for a button that takes the user to a destination where
+     * they can re-enable it.
+     */
+    val actionText: String? = null,
+
+    /**
+     * If not enabled, an optional component name (package and action) for a button that takes the
+     * user to a destination where they can re-enable it.
+     */
+    val actionComponentName: String? = null,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
index 3c927ee..f772b17 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -88,7 +88,7 @@
                 }
             }
         view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
                 try {
                     viewModel.setBouncerViewDelegate(delegate)
                     launch {
@@ -152,7 +152,6 @@
                             val visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
                             view.visibility = visibility
                             hostViewController.onBouncerVisibilityChanged(visibility)
-                            viewModel.notifyBouncerVisibilityHasChanged(visibility)
                         }
                     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
index 503c8ba..e5d4e49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -72,10 +72,6 @@
     /** Observe whether screen is turned off. */
     val screenTurnedOff: Flow<Unit> = interactor.screenTurnedOff
 
-    /** Notify that view visibility has changed. */
-    fun notifyBouncerVisibilityHasChanged(visibility: Int) {
-        return interactor.notifyBouncerVisibilityHasChanged(visibility)
-    }
     /** Observe whether we want to update resources. */
     fun notifyUpdateResources() {
         interactor.notifyUpdatedResources()
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricLog.java
similarity index 95%
rename from packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
rename to packages/SystemUI/src/com/android/systemui/log/dagger/BiometricLog.java
index eeadf40..4b774d3 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricLog.java
@@ -29,5 +29,5 @@
 @Qualifier
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
-public @interface BiometricMessagesLog {
+public @interface BiometricLog {
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 9adb855..74d5043 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -300,9 +300,9 @@
      */
     @Provides
     @SysUISingleton
-    @BiometricMessagesLog
-    public static LogBuffer provideBiometricMessagesLogBuffer(LogBufferFactory factory) {
-        return factory.create("BiometricMessagesLog", 150);
+    @BiometricLog
+    public static LogBuffer provideBiometricLogBuffer(LogBufferFactory factory) {
+        return factory.create("BiometricLog", 200);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java
index 2c20feb..fa3f878f 100644
--- a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java
+++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java
@@ -158,14 +158,18 @@
      * Returns true if lock screen entry point for QR Code Scanner is to be enabled.
      */
     public boolean isEnabledForLockScreenButton() {
-        return mQRCodeScannerEnabled && mIntent != null && mConfigEnableLockScreenButton
-                && isActivityCallable(mIntent);
+        return mQRCodeScannerEnabled && isAbleToOpenCameraApp() && isAvailableOnDevice();
+    }
+
+    /** Returns whether the feature is available on the device. */
+    public boolean isAvailableOnDevice() {
+        return mConfigEnableLockScreenButton;
     }
 
     /**
-     * Returns true if quick settings entry point for QR Code Scanner is to be enabled.
+     * Returns true if the feature can open a camera app on the device.
      */
-    public boolean isEnabledForQuickSettings() {
+    public boolean isAbleToOpenCameraApp() {
         return mIntent != null && isActivityCallable(mIntent);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
index 376d3d8..6d50b56 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
@@ -115,8 +115,12 @@
         state.label = mContext.getString(R.string.qr_code_scanner_title);
         state.contentDescription = state.label;
         state.icon = ResourceIcon.get(R.drawable.ic_qr_code_scanner);
-        state.state = mQRCodeScannerController.isEnabledForQuickSettings() ? Tile.STATE_INACTIVE
+        state.state = mQRCodeScannerController.isAbleToOpenCameraApp() ? Tile.STATE_INACTIVE
                 : Tile.STATE_UNAVAILABLE;
+        // The assumption is that if the OEM has the QR code scanner module enabled then the scanner
+        // would go to "Unavailable" state only when GMS core is updating.
+        state.secondaryLabel = state.state == Tile.STATE_UNAVAILABLE
+                ? mContext.getString(R.string.qr_code_scanner_updating_secondary_label) : null;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index 3ae2545..65a21a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -1160,12 +1160,21 @@
                 mLogger.logParentChanged(mIterationCount, prev.getParent(), curr.getParent());
             }
 
-            if (curr.getSuppressedChanges().getParent() != null) {
-                mLogger.logParentChangeSuppressed(
+            GroupEntry currSuppressedParent = curr.getSuppressedChanges().getParent();
+            GroupEntry prevSuppressedParent = prev.getSuppressedChanges().getParent();
+            if (currSuppressedParent != null && (prevSuppressedParent == null
+                    || !prevSuppressedParent.getKey().equals(currSuppressedParent.getKey()))) {
+                mLogger.logParentChangeSuppressedStarted(
                         mIterationCount,
-                        curr.getSuppressedChanges().getParent(),
+                        currSuppressedParent,
                         curr.getParent());
             }
+            if (prevSuppressedParent != null && currSuppressedParent == null) {
+                mLogger.logParentChangeSuppressedStopped(
+                        mIterationCount,
+                        prevSuppressedParent,
+                        prev.getParent());
+            }
 
             if (curr.getSuppressedChanges().getSection() != null) {
                 mLogger.logSectionChangeSuppressed(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
index 8e052c7..4adc90a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
@@ -193,7 +193,7 @@
         })
     }
 
-    fun logParentChangeSuppressed(
+    fun logParentChangeSuppressedStarted(
         buildId: Int,
         suppressedParent: GroupEntry?,
         keepingParent: GroupEntry?
@@ -207,6 +207,21 @@
         })
     }
 
+    fun logParentChangeSuppressedStopped(
+            buildId: Int,
+            previouslySuppressedParent: GroupEntry?,
+            previouslyKeptParent: GroupEntry?
+    ) {
+        buffer.log(TAG, INFO, {
+            long1 = buildId.toLong()
+            str1 = previouslySuppressedParent?.logKey
+            str2 = previouslyKeptParent?.logKey
+        }, {
+            "(Build $long1)     Change of parent to '$str1' no longer suppressed; " +
+                    "replaced parent '$str2'"
+        })
+    }
+
     fun logGroupPruningSuppressed(
         buildId: Int,
         keepingParent: GroupEntry?
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 182d397..679bcea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -29,7 +29,6 @@
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.os.Trace;
-import android.util.Log;
 
 import androidx.annotation.Nullable;
 
@@ -41,10 +40,10 @@
 import com.android.internal.logging.UiEventLoggerImpl;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.LatencyTracker;
-import com.android.keyguard.KeyguardConstants;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.keyguard.KeyguardViewController;
+import com.android.keyguard.logging.BiometricUnlockLogger;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.biometrics.AuthController;
@@ -79,9 +78,6 @@
  */
 @SysUISingleton
 public class BiometricUnlockController extends KeyguardUpdateMonitorCallback implements Dumpable {
-
-    private static final String TAG = "BiometricUnlockCtrl";
-    private static final boolean DEBUG_BIO_WAKELOCK = KeyguardConstants.DEBUG_BIOMETRIC_WAKELOCK;
     private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000;
     private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock";
     private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl();
@@ -176,6 +172,7 @@
     private final StatusBarStateController mStatusBarStateController;
     private final LatencyTracker mLatencyTracker;
     private final VibratorHelper mVibratorHelper;
+    private final BiometricUnlockLogger mLogger;
 
     private long mLastFpFailureUptimeMillis;
     private int mNumConsecutiveFpFailures;
@@ -262,7 +259,8 @@
     private final ScreenOffAnimationController mScreenOffAnimationController;
 
     @Inject
-    public BiometricUnlockController(DozeScrimController dozeScrimController,
+    public BiometricUnlockController(
+            DozeScrimController dozeScrimController,
             KeyguardViewMediator keyguardViewMediator, ScrimController scrimController,
             ShadeController shadeController,
             NotificationShadeWindowController notificationShadeWindowController,
@@ -272,6 +270,7 @@
             KeyguardBypassController keyguardBypassController,
             MetricsLogger metricsLogger, DumpManager dumpManager,
             PowerManager powerManager,
+            BiometricUnlockLogger biometricUnlockLogger,
             NotificationMediaManager notificationMediaManager,
             WakefulnessLifecycle wakefulnessLifecycle,
             ScreenLifecycle screenLifecycle,
@@ -308,6 +307,7 @@
         mSessionTracker = sessionTracker;
         mScreenOffAnimationController = screenOffAnimationController;
         mVibratorHelper = vibrator;
+        mLogger = biometricUnlockLogger;
 
         dumpManager.registerDumpable(getClass().getName(), this);
     }
@@ -329,9 +329,7 @@
     private final Runnable mReleaseBiometricWakeLockRunnable = new Runnable() {
         @Override
         public void run() {
-            if (DEBUG_BIO_WAKELOCK) {
-                Log.i(TAG, "biometric wakelock: TIMEOUT!!");
-            }
+            mLogger.i("biometric wakelock: TIMEOUT!!");
             releaseBiometricWakeLock();
         }
     };
@@ -339,9 +337,7 @@
     private void releaseBiometricWakeLock() {
         if (mWakeLock != null) {
             mHandler.removeCallbacks(mReleaseBiometricWakeLockRunnable);
-            if (DEBUG_BIO_WAKELOCK) {
-                Log.i(TAG, "releasing biometric wakelock");
-            }
+            mLogger.i("releasing biometric wakelock");
             mWakeLock.release();
             mWakeLock = null;
         }
@@ -372,9 +368,7 @@
             Trace.beginSection("acquiring wake-and-unlock");
             mWakeLock.acquire();
             Trace.endSection();
-            if (DEBUG_BIO_WAKELOCK) {
-                Log.i(TAG, "biometric acquired, grabbing biometric wakelock");
-            }
+            mLogger.i("biometric acquired, grabbing biometric wakelock");
             mHandler.postDelayed(mReleaseBiometricWakeLockRunnable,
                     BIOMETRIC_WAKELOCK_TIMEOUT_MS);
         }
@@ -411,7 +405,7 @@
             mKeyguardViewMediator.userActivity();
             startWakeAndUnlock(biometricSourceType, isStrongBiometric);
         } else {
-            Log.d(TAG, "onBiometricAuthenticated aborted by bypass controller");
+            mLogger.d("onBiometricAuthenticated aborted by bypass controller");
         }
     }
 
@@ -427,7 +421,7 @@
     }
 
     public void startWakeAndUnlock(@WakeAndUnlockMode int mode) {
-        Log.v(TAG, "startWakeAndUnlock(" + mode + ")");
+        mLogger.logStartWakeAndUnlock(mode);
         boolean wasDeviceInteractive = mUpdateMonitor.isDeviceInteractive();
         mMode = mode;
         mHasScreenTurnedOnSinceAuthenticating = false;
@@ -442,9 +436,7 @@
         // brightness changes due to display state transitions.
         Runnable wakeUp = ()-> {
             if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
-                if (DEBUG_BIO_WAKELOCK) {
-                    Log.i(TAG, "bio wakelock: Authenticated, waking up...");
-                }
+                mLogger.i("bio wakelock: Authenticated, waking up...");
                 mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
                         "android.policy:BIOMETRIC");
             }
@@ -537,13 +529,16 @@
     }
 
     private @WakeAndUnlockMode int calculateModeForFingerprint(boolean isStrongBiometric) {
-        boolean unlockingAllowed =
+        final boolean unlockingAllowed =
                 mUpdateMonitor.isUnlockingWithBiometricAllowed(isStrongBiometric);
-        boolean deviceDreaming = mUpdateMonitor.isDreaming();
+        final boolean deviceInteractive = mUpdateMonitor.isDeviceInteractive();
+        final boolean keyguardShowing = mKeyguardStateController.isShowing();
+        final boolean deviceDreaming = mUpdateMonitor.isDreaming();
 
-        if (!mUpdateMonitor.isDeviceInteractive()) {
-            if (!mKeyguardStateController.isShowing()
-                    && !mScreenOffAnimationController.isKeyguardShowDelayed()) {
+        logCalculateModeForFingerprint(unlockingAllowed, deviceInteractive,
+                keyguardShowing, deviceDreaming, isStrongBiometric);
+        if (!deviceInteractive) {
+            if (!keyguardShowing && !mScreenOffAnimationController.isKeyguardShowDelayed()) {
                 if (mKeyguardStateController.isUnlocked()) {
                     return MODE_WAKE_AND_UNLOCK;
                 }
@@ -559,7 +554,7 @@
         if (unlockingAllowed && deviceDreaming) {
             return MODE_WAKE_AND_UNLOCK_FROM_DREAM;
         }
-        if (mKeyguardStateController.isShowing()) {
+        if (keyguardShowing) {
             if (mKeyguardViewController.primaryBouncerIsOrWillBeShowing() && unlockingAllowed) {
                 return MODE_DISMISS_BOUNCER;
             } else if (unlockingAllowed) {
@@ -571,14 +566,39 @@
         return MODE_NONE;
     }
 
+    private void logCalculateModeForFingerprint(boolean unlockingAllowed, boolean deviceInteractive,
+            boolean keyguardShowing, boolean deviceDreaming, boolean strongBiometric) {
+        if (unlockingAllowed) {
+            mLogger.logCalculateModeForFingerprintUnlockingAllowed(deviceInteractive,
+                    keyguardShowing, deviceDreaming);
+        } else {
+            // if unlocking isn't allowed, log more information about why unlocking may not
+            // have been allowed
+            final int strongAuthFlags = mUpdateMonitor.getStrongAuthTracker().getStrongAuthForUser(
+                    KeyguardUpdateMonitor.getCurrentUser());
+            final boolean nonStrongBiometricAllowed =
+                    mUpdateMonitor.getStrongAuthTracker()
+                            .isNonStrongBiometricAllowedAfterIdleTimeout(
+                                    KeyguardUpdateMonitor.getCurrentUser());
+
+            mLogger.logCalculateModeForFingerprintUnlockingNotAllowed(strongBiometric,
+                    strongAuthFlags, nonStrongBiometricAllowed, deviceInteractive, keyguardShowing);
+        }
+    }
+
     private @WakeAndUnlockMode int calculateModeForPassiveAuth(boolean isStrongBiometric) {
-        boolean unlockingAllowed =
+        final boolean deviceInteractive = mUpdateMonitor.isDeviceInteractive();
+        final boolean isKeyguardShowing = mKeyguardStateController.isShowing();
+        final boolean unlockingAllowed =
                 mUpdateMonitor.isUnlockingWithBiometricAllowed(isStrongBiometric);
-        boolean deviceDreaming = mUpdateMonitor.isDreaming();
-        boolean bypass = mKeyguardBypassController.getBypassEnabled()
+        final boolean deviceDreaming = mUpdateMonitor.isDreaming();
+        final boolean bypass = mKeyguardBypassController.getBypassEnabled()
                 || mAuthController.isUdfpsFingerDown();
-        if (!mUpdateMonitor.isDeviceInteractive()) {
-            if (!mKeyguardStateController.isShowing()) {
+
+        logCalculateModeForPassiveAuth(unlockingAllowed, deviceInteractive, isKeyguardShowing,
+                deviceDreaming, bypass, isStrongBiometric);
+        if (!deviceInteractive) {
+            if (!isKeyguardShowing) {
                 return bypass ? MODE_WAKE_AND_UNLOCK : MODE_ONLY_WAKE;
             } else if (!unlockingAllowed) {
                 return bypass ? MODE_SHOW_BOUNCER : MODE_NONE;
@@ -602,11 +622,11 @@
         if (unlockingAllowed && mKeyguardStateController.isOccluded()) {
             return MODE_UNLOCK_COLLAPSING;
         }
-        if (mKeyguardStateController.isShowing()) {
+        if (isKeyguardShowing) {
             if ((mKeyguardViewController.primaryBouncerIsOrWillBeShowing()
                     || mKeyguardBypassController.getAltBouncerShowing()) && unlockingAllowed) {
                 return MODE_DISMISS_BOUNCER;
-            } else if (unlockingAllowed && (bypass || mAuthController.isUdfpsFingerDown())) {
+            } else if (unlockingAllowed && bypass) {
                 return MODE_UNLOCK_COLLAPSING;
             } else {
                 return bypass ? MODE_SHOW_BOUNCER : MODE_NONE;
@@ -615,6 +635,28 @@
         return MODE_NONE;
     }
 
+    private void logCalculateModeForPassiveAuth(boolean unlockingAllowed,
+            boolean deviceInteractive, boolean keyguardShowing, boolean deviceDreaming,
+            boolean bypass, boolean strongBiometric) {
+        if (unlockingAllowed) {
+            mLogger.logCalculateModeForPassiveAuthUnlockingAllowed(
+                    deviceInteractive, keyguardShowing, deviceDreaming, bypass);
+        } else {
+            // if unlocking isn't allowed, log more information about why unlocking may not
+            // have been allowed
+            final int strongAuthFlags = mUpdateMonitor.getStrongAuthTracker().getStrongAuthForUser(
+                    KeyguardUpdateMonitor.getCurrentUser());
+            final boolean nonStrongBiometricAllowed =
+                    mUpdateMonitor.getStrongAuthTracker()
+                            .isNonStrongBiometricAllowedAfterIdleTimeout(
+                                    KeyguardUpdateMonitor.getCurrentUser());
+
+            mLogger.logCalculateModeForPassiveAuthUnlockingNotAllowed(
+                    strongBiometric, strongAuthFlags, nonStrongBiometricAllowed,
+                    deviceInteractive, keyguardShowing, bypass);
+        }
+    }
+
     @Override
     public void onBiometricAuthFailed(BiometricSourceType biometricSourceType) {
         mMetricsLogger.write(new LogMaker(MetricsEvent.BIOMETRIC_AUTH)
@@ -632,6 +674,7 @@
 
         if (!mVibratorHelper.hasVibrator()
                 && (!mUpdateMonitor.isDeviceInteractive() || mUpdateMonitor.isDreaming())) {
+            mLogger.d("wakeup device on authentication failure (device doesn't have a vibrator)");
             startWakeAndUnlock(MODE_ONLY_WAKE);
         } else if (biometricSourceType == BiometricSourceType.FINGERPRINT
                 && mUpdateMonitor.isUdfpsSupported()) {
@@ -644,6 +687,7 @@
             mLastFpFailureUptimeMillis = currUptimeMillis;
 
             if (mNumConsecutiveFpFailures >= UDFPS_ATTEMPTS_BEFORE_SHOW_BOUNCER) {
+                mLogger.logUdfpsAttemptThresholdMet(mNumConsecutiveFpFailures);
                 startWakeAndUnlock(MODE_SHOW_BOUNCER);
                 UI_EVENT_LOGGER.log(BiometricUiEvent.BIOMETRIC_BOUNCER_SHOWN, getSessionId());
                 mNumConsecutiveFpFailures = 0;
@@ -674,6 +718,7 @@
                 && (msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT
                 || msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT);
         if (fingerprintLockout) {
+            mLogger.d("fingerprint locked out");
             startWakeAndUnlock(MODE_SHOW_BOUNCER);
             UI_EVENT_LOGGER.log(BiometricUiEvent.BIOMETRIC_BOUNCER_SHOWN, getSessionId());
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index fcf33b4..dcbabaa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -475,7 +475,7 @@
             } else {
                 mPrimaryBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
             }
-        } else if (mKeyguardStateController.isShowing()  && !hideBouncerOverDream) {
+        } else if (mKeyguardStateController.isShowing() && !hideBouncerOverDream) {
             if (!isWakeAndUnlocking()
                     && !(mBiometricUnlockController.getMode() == MODE_DISMISS_BOUNCER)
                     && !(mBiometricUnlockController.getMode() == MODE_SHOW_BOUNCER)
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index d411e34..ae30ca0 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -94,7 +94,7 @@
                 overlayContainer = builder.build()
 
                 SurfaceControl.Transaction()
-                    .setLayer(overlayContainer, Integer.MAX_VALUE)
+                    .setLayer(overlayContainer, UNFOLD_OVERLAY_LAYER_Z_INDEX)
                     .show(overlayContainer)
                     .apply()
 
@@ -268,4 +268,12 @@
                 this.isFolded = isFolded
             }
         )
+
+    private companion object {
+        private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE
+
+        // Put the unfold overlay below the rotation animation screenshot to hide the moment
+        // when it is rotated but the rotation of the other windows hasn't happen yet
+        private const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
index 0f06de2..3655232 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
@@ -5,6 +5,7 @@
 import android.testing.TestableLooper
 import android.window.OnBackInvokedCallback
 import android.window.OnBackInvokedDispatcher
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.SmallTest
 import androidx.test.rule.ActivityTestRule
 import androidx.test.runner.intercepting.SingleActivityFactory
@@ -79,6 +80,8 @@
         activityRule.launchActivity(intent)
     }
 
+    // b/259549854 to root-cause and fix
+    @FlakyTest
     @Test
     fun testBackCallbackRegistrationAndUnregistration() {
         // 1. ensure that launching the activity results in it registering a callback
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
index c94cec6..322014a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -24,8 +24,9 @@
 import com.android.systemui.controls.dagger.ControlsComponent
 import com.android.systemui.controls.management.ControlsListingController
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import java.util.Optional
+import java.util.*
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -40,7 +41,6 @@
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -93,6 +93,14 @@
         whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
         whenever(component.getControlsListingController())
             .thenReturn(Optional.of(controlsListingController))
+        whenever(controlsListingController.getCurrentServices())
+            .thenReturn(
+                if (hasServiceInfos) {
+                    listOf(mock(), mock())
+                } else {
+                    emptyList()
+                }
+            )
         whenever(component.canShowWhileLockedSetting)
             .thenReturn(MutableStateFlow(canShowWhileLocked))
         whenever(component.getVisibility())
@@ -144,6 +152,17 @@
                     KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java
                 }
             )
+        assertThat(underTest.getPickerScreenState())
+            .isInstanceOf(
+                when {
+                    !isFeatureEnabled ->
+                        KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice::class
+                            .java
+                    hasServiceInfos && hasFavorites ->
+                        KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java
+                    else -> KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java
+                }
+            )
         job.cancel()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
index 2bd8e9a..6255980 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -24,17 +24,18 @@
 import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -134,6 +135,33 @@
             )
     }
 
+    @Test
+    fun `getPickerScreenState - enabled if configured on device - can open camera`() = runTest {
+        whenever(controller.isAvailableOnDevice).thenReturn(true)
+        whenever(controller.isAbleToOpenCameraApp).thenReturn(true)
+
+        assertThat(underTest.getPickerScreenState())
+            .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default)
+    }
+
+    @Test
+    fun `getPickerScreenState - disabled if configured on device - cannot open camera`() = runTest {
+        whenever(controller.isAvailableOnDevice).thenReturn(true)
+        whenever(controller.isAbleToOpenCameraApp).thenReturn(false)
+
+        assertThat(underTest.getPickerScreenState())
+            .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java)
+    }
+
+    @Test
+    fun `getPickerScreenState - unavailable if not configured on device`() = runTest {
+        whenever(controller.isAvailableOnDevice).thenReturn(false)
+        whenever(controller.isAbleToOpenCameraApp).thenReturn(true)
+
+        assertThat(underTest.getPickerScreenState())
+            .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+    }
+
     private fun assertVisibleState(latest: KeyguardQuickAffordanceConfig.LockScreenState?) {
         assertThat(latest)
             .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
index 5178154..d875dd9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -33,9 +33,11 @@
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.wallet.controller.QuickAccessWalletController
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -44,6 +46,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
@@ -59,7 +62,7 @@
 
         underTest =
             QuickAccessWalletKeyguardQuickAffordanceConfig(
-                mock(),
+                context,
                 walletController,
                 activityStarter,
             )
@@ -151,6 +154,44 @@
             )
     }
 
+    @Test
+    fun `getPickerScreenState - default`() = runTest {
+        setUpState()
+
+        assertThat(underTest.getPickerScreenState())
+            .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default)
+    }
+
+    @Test
+    fun `getPickerScreenState - unavailable`() = runTest {
+        setUpState(
+            isWalletEnabled = false,
+        )
+
+        assertThat(underTest.getPickerScreenState())
+            .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+    }
+
+    @Test
+    fun `getPickerScreenState - disabled when there is no icon`() = runTest {
+        setUpState(
+            hasWalletIcon = false,
+        )
+
+        assertThat(underTest.getPickerScreenState())
+            .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java)
+    }
+
+    @Test
+    fun `getPickerScreenState - disabled when there is no card`() = runTest {
+        setUpState(
+            hasSelectedCard = false,
+        )
+
+        assertThat(underTest.getPickerScreenState())
+            .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java)
+    }
+
     private fun setUpState(
         isWalletEnabled: Boolean = true,
         isWalletQuerySuccessful: Boolean = true,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
index d8a3605..bfd5190 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
@@ -133,23 +133,24 @@
         }
 
     @Test
-    fun getAffordancePickerRepresentations() {
-        assertThat(underTest.getAffordancePickerRepresentations())
-            .isEqualTo(
-                listOf(
-                    KeyguardQuickAffordancePickerRepresentation(
-                        id = config1.key,
-                        name = config1.pickerName,
-                        iconResourceId = config1.pickerIconResourceId,
-                    ),
-                    KeyguardQuickAffordancePickerRepresentation(
-                        id = config2.key,
-                        name = config2.pickerName,
-                        iconResourceId = config2.pickerIconResourceId,
-                    ),
+    fun getAffordancePickerRepresentations() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.getAffordancePickerRepresentations())
+                .isEqualTo(
+                    listOf(
+                        KeyguardQuickAffordancePickerRepresentation(
+                            id = config1.key,
+                            name = config1.pickerName,
+                            iconResourceId = config1.pickerIconResourceId,
+                        ),
+                        KeyguardQuickAffordancePickerRepresentation(
+                            id = config2.key,
+                            name = config2.pickerName,
+                            iconResourceId = config2.pickerIconResourceId,
+                        ),
+                    )
                 )
-            )
-    }
+        }
 
     @Test
     fun getSlotPickerRepresentations() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
index 559f183..a6fc13b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -19,6 +19,7 @@
 import android.os.Looper
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
+import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -106,6 +107,7 @@
         verify(repository).setPrimaryVisible(true)
         verify(repository).setPrimaryShow(any(KeyguardBouncerModel::class.java))
         verify(repository).setPrimaryShowingSoon(false)
+        verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.VISIBLE)
     }
 
     @Test
@@ -129,6 +131,7 @@
         verify(repository).setPrimaryVisible(false)
         verify(repository).setPrimaryHide(true)
         verify(repository).setPrimaryShow(null)
+        verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.INVISIBLE)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java
index 346d1e6..65210d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java
@@ -31,7 +31,6 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
@@ -133,7 +132,7 @@
                 /* enableOnLockScreen */ true);
         verifyActivityDetails(null);
         assertThat(mController.isEnabledForLockScreenButton()).isFalse();
-        assertThat(mController.isEnabledForQuickSettings()).isFalse();
+        assertThat(mController.isAbleToOpenCameraApp()).isFalse();
     }
 
     @Test
@@ -152,7 +151,7 @@
                 /* enableOnLockScreen */ true);
         verifyActivityDetails("abc/.def");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
     }
 
     @Test
@@ -162,7 +161,7 @@
                 /* enableOnLockScreen */ true);
         verifyActivityDetails("abc/.def");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
     }
 
     @Test
@@ -172,7 +171,7 @@
                 /* enableOnLockScreen */ true);
         verifyActivityDetails("abc/.def");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
     }
 
     @Test
@@ -182,7 +181,7 @@
                 /* enableOnLockScreen */ true);
         verifyActivityDetails("abc/abc.def");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
     }
 
     @Test
@@ -192,7 +191,7 @@
                 /* enableOnLockScreen */ true);
         verifyActivityDetails(null);
         assertThat(mController.isEnabledForLockScreenButton()).isFalse();
-        assertThat(mController.isEnabledForQuickSettings()).isFalse();
+        assertThat(mController.isAbleToOpenCameraApp()).isFalse();
     }
 
     @Test
@@ -202,21 +201,21 @@
                 /* enableOnLockScreen */ true);
         verifyActivityDetails("abc/.def");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
 
         mProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER,
                 "def/.ijk", false);
         verifyActivityDetails("def/.ijk");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
 
         mProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER,
                 null, false);
         verifyActivityDetails("abc/.def");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
 
         // Once from setup + twice from this function
         verify(mCallback, times(3)).onQRCodeScannerActivityChanged();
@@ -229,7 +228,7 @@
                 /* enableOnLockScreen */ true);
         verifyActivityDetails(null);
         assertThat(mController.isEnabledForLockScreenButton()).isFalse();
-        assertThat(mController.isEnabledForQuickSettings()).isFalse();
+        assertThat(mController.isAbleToOpenCameraApp()).isFalse();
 
         mProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER,
@@ -237,14 +236,14 @@
 
         verifyActivityDetails("def/.ijk");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
 
         mProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER,
                 null, false);
         verifyActivityDetails(null);
         assertThat(mController.isEnabledForLockScreenButton()).isFalse();
-        assertThat(mController.isEnabledForQuickSettings()).isFalse();
+        assertThat(mController.isAbleToOpenCameraApp()).isFalse();
         verify(mCallback, times(2)).onQRCodeScannerActivityChanged();
     }
 
@@ -296,19 +295,19 @@
                 /* enableOnLockScreen */ true);
         verifyActivityDetails("abc/.def");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
 
         mSecureSettings.putStringForUser(LOCK_SCREEN_SHOW_QR_CODE_SCANNER, "0",
                 UserHandle.USER_CURRENT);
         verifyActivityDetails("abc/.def");
         assertThat(mController.isEnabledForLockScreenButton()).isFalse();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
 
         mSecureSettings.putStringForUser(LOCK_SCREEN_SHOW_QR_CODE_SCANNER, "1",
                 UserHandle.USER_CURRENT);
         verifyActivityDetails("abc/.def");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
         // Once from setup + twice from this function
         verify(mCallback, times(3)).onQRCodeScannerPreferenceChanged();
     }
@@ -320,13 +319,13 @@
                 /* enableOnLockScreen */ true);
         verifyActivityDetails("abc/.def");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
 
         mController.unregisterQRCodeScannerChangeObservers(DEFAULT_QR_CODE_SCANNER_CHANGE,
                 QR_CODE_SCANNER_PREFERENCE_CHANGE);
         verifyActivityDetails(null);
         assertThat(mController.isEnabledForLockScreenButton()).isFalse();
-        assertThat(mController.isEnabledForQuickSettings()).isFalse();
+        assertThat(mController.isAbleToOpenCameraApp()).isFalse();
 
         // Unregister once again and make sure it affects the next register event
         mController.unregisterQRCodeScannerChangeObservers(DEFAULT_QR_CODE_SCANNER_CHANGE,
@@ -335,7 +334,7 @@
                 QR_CODE_SCANNER_PREFERENCE_CHANGE);
         verifyActivityDetails("abc/.def");
         assertThat(mController.isEnabledForLockScreenButton()).isTrue();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
     }
 
     @Test
@@ -345,7 +344,7 @@
                 /* enableOnLockScreen */ false);
         assertThat(mController.getIntent()).isNotNull();
         assertThat(mController.isEnabledForLockScreenButton()).isFalse();
-        assertThat(mController.isEnabledForQuickSettings()).isTrue();
+        assertThat(mController.isAbleToOpenCameraApp()).isTrue();
         assertThat(getSettingsQRCodeDefaultComponent()).isNull();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
index cac90a1..a1be2f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
@@ -18,6 +18,7 @@
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNull;
 
 import static org.mockito.Mockito.when;
 
@@ -108,17 +109,20 @@
 
     @Test
     public void testQRCodeTileUnavailable() {
-        when(mController.isEnabledForQuickSettings()).thenReturn(false);
+        when(mController.isAbleToOpenCameraApp()).thenReturn(false);
         QSTile.State state = new QSTile.State();
         mTile.handleUpdateState(state, null);
         assertEquals(state.state, Tile.STATE_UNAVAILABLE);
+        assertEquals(state.secondaryLabel.toString(),
+                     mContext.getString(R.string.qr_code_scanner_updating_secondary_label));
     }
 
     @Test
     public void testQRCodeTileAvailable() {
-        when(mController.isEnabledForQuickSettings()).thenReturn(true);
+        when(mController.isAbleToOpenCameraApp()).thenReturn(true);
         QSTile.State state = new QSTile.State();
         mTile.handleUpdateState(state, null);
         assertEquals(state.state, Tile.STATE_INACTIVE);
+        assertNull(state.secondaryLabel);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
index 2a3d32e..9c36be6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
@@ -344,26 +344,6 @@
     }
 
     @Test
-    fun testCheckViewsDontChangeSizeBetweenAnimationConstraints() {
-        val views = mapOf(
-                R.id.clock to "clock",
-                R.id.date to "date",
-                R.id.statusIcons to "icons",
-                R.id.privacy_container to "privacy",
-                R.id.carrier_group to "carriers",
-                R.id.batteryRemainingIcon to "battery",
-        )
-        views.forEach { (id, name) ->
-            assertWithMessage("$name changes height")
-                    .that(qqsConstraint.getConstraint(id).layout.mHeight)
-                    .isEqualTo(qsConstraint.getConstraint(id).layout.mHeight)
-            assertWithMessage("$name changes width")
-                    .that(qqsConstraint.getConstraint(id).layout.mWidth)
-                    .isEqualTo(qsConstraint.getConstraint(id).layout.mWidth)
-        }
-    }
-
-    @Test
     fun testEmptyCutoutDateIconsAreConstrainedWidth() {
         CombinedShadeHeadersConstraintManagerImpl.emptyCutoutConstraints()()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 75a3b21..d1957ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -42,6 +42,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.util.LatencyTracker;
 import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.logging.BiometricUnlockLogger;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.dump.DumpManager;
@@ -78,6 +79,8 @@
     @Mock
     private KeyguardUpdateMonitor mUpdateMonitor;
     @Mock
+    private KeyguardUpdateMonitor.StrongAuthTracker mStrongAuthTracker;
+    @Mock
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     @Mock
     private NotificationShadeWindowController mNotificationShadeWindowController;
@@ -119,6 +122,8 @@
     private ScreenOffAnimationController mScreenOffAnimationController;
     @Mock
     private VibratorHelper mVibratorHelper;
+    @Mock
+    private BiometricUnlockLogger mLogger;
     private BiometricUnlockController mBiometricUnlockController;
 
     @Before
@@ -138,12 +143,13 @@
                 mKeyguardViewMediator, mScrimController, mShadeController,
                 mNotificationShadeWindowController, mKeyguardStateController, mHandler,
                 mUpdateMonitor, res.getResources(), mKeyguardBypassController,
-                mMetricsLogger, mDumpManager, mPowerManager,
+                mMetricsLogger, mDumpManager, mPowerManager, mLogger,
                 mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
                 mAuthController, mStatusBarStateController, mKeyguardUnlockAnimationController,
                 mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper);
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
         mBiometricUnlockController.addBiometricModeListener(mBiometricModeListener);
+        when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker);
     }
 
     @Test
@@ -285,6 +291,7 @@
     @Test
     public void onBiometricAuthenticated_whenFace_andBypass_encrypted_showPrimaryBouncer() {
         reset(mUpdateMonitor);
+        when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker);
         when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true);
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
 
@@ -322,6 +329,7 @@
     @Test
     public void onBiometricAuthenticated_whenFace_noBypass_encrypted_doNothing() {
         reset(mUpdateMonitor);
+        when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker);
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
 
         when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index bee882d..d42f757 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -77,6 +77,7 @@
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
 
+import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.colorextraction.ColorExtractor;
@@ -157,6 +158,7 @@
 import java.util.List;
 import java.util.Optional;
 
+@FlakyTest
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java
index 78df983..166806b 100644
--- a/services/core/java/com/android/server/SystemServiceManager.java
+++ b/services/core/java/com/android/server/SystemServiceManager.java
@@ -357,13 +357,24 @@
      * Starts the given user.
      */
     public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId) {
-        EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId);
-
         final TargetUser targetUser = newTargetUser(userId);
         synchronized (mTargetUsers) {
+            // On Automotive / Headless System User Mode, the system user will be started twice:
+            // - Once by some external or local service that switches the system user to
+            //   the background.
+            // - Once by the ActivityManagerService, when the system is marked ready.
+            // These two events are not synchronized and the order of execution is
+            // non-deterministic. To avoid starting the system user twice, verify whether
+            // the system user has already been started by checking the mTargetUsers.
+            // TODO(b/242195409): this workaround shouldn't be necessary once we move
+            // the headless-user start logic to UserManager-land.
+            if (userId == UserHandle.USER_SYSTEM && mTargetUsers.contains(userId)) {
+                Slog.e(TAG, "Skipping starting system user twice");
+                return;
+            }
             mTargetUsers.put(userId, targetUser);
         }
-
+        EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId);
         onUser(t, USER_STARTING, /* prevUser= */ null, targetUser);
     }
 
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index fde96b9..bc083f1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -8163,15 +8163,12 @@
         mBatteryStatsService.noteEvent(BatteryStats.HistoryItem.EVENT_USER_FOREGROUND_START,
                 Integer.toString(currentUserId), currentUserId);
 
-        // On Automotive, at this point the system user has already been started and unlocked,
-        // and some of the tasks we do here have already been done. So skip those in that case.
-        // TODO(b/132262830, b/203885241): this workdound shouldn't be necessary once we move the
-        // headless-user start logic to UserManager-land
-        final boolean bootingSystemUser = currentUserId == UserHandle.USER_SYSTEM;
-
-        if (bootingSystemUser) {
-            mSystemServiceManager.onUserStarting(t, currentUserId);
-        }
+        // On Automotive / Headless System User Mode, at this point the system user has already been
+        // started and unlocked, and some of the tasks we do here have already been done. So skip
+        // those in that case. The duplicate system user start is guarded in SystemServiceManager.
+        // TODO(b/242195409): this workaround shouldn't be necessary once we move the headless-user
+        // start logic to UserManager-land.
+        mSystemServiceManager.onUserStarting(t, currentUserId);
 
         synchronized (this) {
             // Only start up encryption-aware persistent apps; once user is
@@ -8201,7 +8198,15 @@
                 t.traceEnd();
             }
 
-            if (bootingSystemUser) {
+            // Some systems - like automotive - will explicitly unlock system user then switch
+            // to a secondary user. Hence, we don't want to send duplicate broadcasts for
+            // the system user here.
+            // TODO(b/242195409): this workaround shouldn't be necessary once we move
+            // the headless-user start logic to UserManager-land.
+            final boolean isBootingSystemUser = (currentUserId == UserHandle.USER_SYSTEM)
+                    && !UserManager.isHeadlessSystemUserMode();
+
+            if (isBootingSystemUser) {
                 t.traceBegin("startHomeOnAllDisplays");
                 mAtmInternal.startHomeOnAllDisplays(currentUserId, "systemReady");
                 t.traceEnd();
@@ -8212,7 +8217,7 @@
             t.traceEnd();
 
 
-            if (bootingSystemUser) {
+            if (isBootingSystemUser) {
                 t.traceBegin("sendUserStartBroadcast");
                 final int callingUid = Binder.getCallingUid();
                 final int callingPid = Binder.getCallingPid();
@@ -8253,7 +8258,7 @@
             mAtmInternal.resumeTopActivities(false /* scheduleIdle */);
             t.traceEnd();
 
-            if (bootingSystemUser) {
+            if (isBootingSystemUser) {
                 t.traceBegin("sendUserSwitchBroadcasts");
                 mUserController.sendUserSwitchBroadcasts(-1, currentUserId);
                 t.traceEnd();
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index fc15890..66992aa 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -5261,42 +5261,7 @@
         }
         // If we are preparing an app transition, then delay changing
         // the visibility of this token until we execute that transition.
-        // Note that we ignore display frozen since we want the opening / closing transition type
-        // can be updated correctly even display frozen, and it's safe since in applyAnimation will
-        // still check DC#okToAnimate again if the transition animation is fine to apply.
-        // TODO(new-app-transition): Rewrite this logic using WM Shell.
-        final boolean recentsAnimating = isAnimating(PARENTS, ANIMATION_TYPE_RECENTS);
-        final boolean isEnteringPipWithoutVisibleChange = mWaitForEnteringPinnedMode
-                && mVisible == visible;
-        if (okToAnimate(true /* ignoreFrozen */, canTurnScreenOn())
-                && (appTransition.isTransitionSet()
-                || (recentsAnimating && !isActivityTypeHome()))
-                // If the visibility is not changed during enter PIP, we don't want to include it in
-                // app transition to affect the animation theme, because the Pip organizer will
-                // animate the entering PIP instead.
-                && !isEnteringPipWithoutVisibleChange) {
-            if (visible) {
-                displayContent.mOpeningApps.add(this);
-                mEnteringAnimation = true;
-            } else if (mVisible) {
-                displayContent.mClosingApps.add(this);
-                mEnteringAnimation = false;
-            }
-            if ((appTransition.getTransitFlags() & TRANSIT_FLAG_OPEN_BEHIND) != 0) {
-                // We're launchingBehind, add the launching activity to mOpeningApps.
-                final WindowState win = getDisplayContent().findFocusedWindow();
-                if (win != null) {
-                    final ActivityRecord focusedActivity = win.mActivityRecord;
-                    if (focusedActivity != null) {
-                        ProtoLog.d(WM_DEBUG_APP_TRANSITIONS,
-                                "TRANSIT_FLAG_OPEN_BEHIND,  adding %s to mOpeningApps",
-                                focusedActivity);
-
-                        // Force animation to be loaded.
-                        displayContent.mOpeningApps.add(focusedActivity);
-                    }
-                }
-            }
+        if (deferCommitVisibilityChange(visible)) {
             return;
         }
 
@@ -5304,6 +5269,61 @@
         updateReportedVisibilityLocked();
     }
 
+    /**
+     * Returns {@code true} if this activity is either added to opening-apps or closing-apps.
+     * Then its visibility will be committed until the transition is ready.
+     */
+    private boolean deferCommitVisibilityChange(boolean visible) {
+        if (!mDisplayContent.mAppTransition.isTransitionSet()) {
+            if (mTransitionController.isShellTransitionsEnabled()) {
+                // Shell transition doesn't use opening/closing sets.
+                return false;
+            }
+            // Defer committing visibility for non-home app which is animating by recents.
+            if (isActivityTypeHome() || !isAnimating(PARENTS, ANIMATION_TYPE_RECENTS)) {
+                return false;
+            }
+        }
+        if (mWaitForEnteringPinnedMode && mVisible == visible) {
+            // If the visibility is not changed during enter PIP, we don't want to include it in
+            // app transition to affect the animation theme, because the Pip organizer will
+            // animate the entering PIP instead.
+            return false;
+        }
+
+        // The animation will be visible soon so do not skip by screen off.
+        final boolean ignoreScreenOn = canTurnScreenOn() || mTaskSupervisor.getKeyguardController()
+                .isKeyguardGoingAway(mDisplayContent.mDisplayId);
+        // Ignore display frozen so the opening / closing transition type can be updated correctly
+        // even if the display is frozen. And it's safe since in applyAnimation will still check
+        // DC#okToAnimate again if the transition animation is fine to apply.
+        if (!okToAnimate(true /* ignoreFrozen */, ignoreScreenOn)) {
+            return false;
+        }
+        if (visible) {
+            mDisplayContent.mOpeningApps.add(this);
+            mEnteringAnimation = true;
+        } else if (mVisible) {
+            mDisplayContent.mClosingApps.add(this);
+            mEnteringAnimation = false;
+        }
+        if ((mDisplayContent.mAppTransition.getTransitFlags() & TRANSIT_FLAG_OPEN_BEHIND) != 0) {
+            // Add the launching-behind activity to mOpeningApps.
+            final WindowState win = mDisplayContent.findFocusedWindow();
+            if (win != null) {
+                final ActivityRecord focusedActivity = win.mActivityRecord;
+                if (focusedActivity != null) {
+                    ProtoLog.d(WM_DEBUG_APP_TRANSITIONS,
+                            "TRANSIT_FLAG_OPEN_BEHIND,  adding %s to mOpeningApps",
+                            focusedActivity);
+                    // Force animation to be loaded.
+                    mDisplayContent.mOpeningApps.add(focusedActivity);
+                }
+            }
+        }
+        return true;
+    }
+
     @Override
     boolean applyAnimation(LayoutParams lp, @TransitionOldType int transit, boolean enter,
             boolean isVoiceInteraction, @Nullable ArrayList<WindowContainer> sources) {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index eec7801..a3554cd 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -3037,12 +3037,6 @@
                 newParent = candidateTf;
             }
         }
-        if (newParent.asTask() == null) {
-            // only collect task-fragments.
-            // TODO(b/258095975): we probably shouldn't ever collect the parent here since it isn't
-            //                    changing. The logic that changes it should collect it.
-            newParent.mTransitionController.collect(newParent);
-        }
         if (mStartActivity.getTaskFragment() == null
                 || mStartActivity.getTaskFragment() == newParent) {
             newParent.addChild(mStartActivity, POSITION_TOP);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 38f6a53..3a936a5 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -4935,9 +4935,8 @@
     @Override
     boolean okToAnimate(boolean ignoreFrozen, boolean ignoreScreenOn) {
         return okToDisplay(ignoreFrozen, ignoreScreenOn)
-                && (mDisplayId != DEFAULT_DISPLAY
-                || mWmService.mPolicy.okToAnimate(ignoreScreenOn))
-                && getDisplayPolicy().isScreenOnFully();
+                && (mDisplayId != DEFAULT_DISPLAY || mWmService.mPolicy.okToAnimate(ignoreScreenOn))
+                && (ignoreFrozen || mDisplayPolicy.isScreenOnFully());
     }
 
     static final class TaskForResizePointSearchResult implements Predicate<Task> {
diff --git a/services/core/java/com/android/server/wm/LaunchParamsUtil.java b/services/core/java/com/android/server/wm/LaunchParamsUtil.java
index 91469a0..09a17e1 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsUtil.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsUtil.java
@@ -46,6 +46,8 @@
 
     private static final int DISPLAY_EDGE_OFFSET_DP = 27;
 
+    private static final Rect TMP_STABLE_BOUNDS = new Rect();
+
     private LaunchParamsUtil() {}
 
     /**
@@ -130,18 +132,42 @@
         return new Size(adjWidth, adjHeight);
     }
 
-    static void adjustBoundsToFitInDisplayArea(@NonNull Rect stableBounds, int layoutDirection,
+    static void adjustBoundsToFitInDisplayArea(@NonNull TaskDisplayArea displayArea,
+                                               int layoutDirection,
                                                @NonNull ActivityInfo.WindowLayout layout,
                                                @NonNull Rect inOutBounds) {
+        // Give a small margin between the window bounds and the display bounds.
+        final Rect stableBounds = TMP_STABLE_BOUNDS;
+        displayArea.getStableRect(stableBounds);
+        final float density = (float) displayArea.getConfiguration().densityDpi / DENSITY_DEFAULT;
+        final int displayEdgeOffset = (int) (DISPLAY_EDGE_OFFSET_DP * density + 0.5f);
+        stableBounds.inset(displayEdgeOffset, displayEdgeOffset);
+
         if (stableBounds.width() < inOutBounds.width()
                 || stableBounds.height() < inOutBounds.height()) {
-            // There is no way for us to fit the bounds in the displayArea without changing width
-            // or height. Just move the start to align with the displayArea.
-            final int left = layoutDirection == View.LAYOUT_DIRECTION_RTL
-                    ? stableBounds.right - inOutBounds.right + inOutBounds.left
-                    : stableBounds.left;
-            inOutBounds.offsetTo(left, stableBounds.top);
-            return;
+            final float heightShrinkRatio = stableBounds.width() / (float) inOutBounds.width();
+            final float widthShrinkRatio =
+                    stableBounds.height() / (float) inOutBounds.height();
+            final float shrinkRatio = Math.min(heightShrinkRatio, widthShrinkRatio);
+            // Minimum layout requirements.
+            final int layoutMinWidth = (layout == null) ? -1 : layout.minWidth;
+            final int layoutMinHeight = (layout == null) ? -1 : layout.minHeight;
+            int adjustedWidth = Math.max(layoutMinWidth, (int) (inOutBounds.width() * shrinkRatio));
+            int adjustedHeight = Math.max(layoutMinHeight,
+                    (int) (inOutBounds.height() * shrinkRatio));
+            if (stableBounds.width() < adjustedWidth
+                    || stableBounds.height() < adjustedHeight) {
+                // There is no way for us to fit the bounds in the displayArea without breaking min
+                // size constraints. Set the min size to make visible as much content as possible.
+                final int left = layoutDirection == View.LAYOUT_DIRECTION_RTL
+                        ? stableBounds.right - adjustedWidth
+                        : stableBounds.left;
+                inOutBounds.set(left, stableBounds.top, left + adjustedWidth,
+                        stableBounds.top + adjustedHeight);
+                return;
+            }
+            inOutBounds.set(inOutBounds.left, inOutBounds.top,
+                    inOutBounds.left + adjustedWidth, inOutBounds.top + adjustedHeight);
         }
 
         final int dx;
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 43b315c..1088a2e 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -917,6 +917,7 @@
             // Update windowing mode if necessary, e.g. launch into a different windowing mode.
             if (windowingMode != WINDOWING_MODE_UNDEFINED && candidateTask.isRootTask()
                     && candidateTask.getWindowingMode() != windowingMode) {
+                candidateTask.mTransitionController.collect(candidateTask);
                 candidateTask.setWindowingMode(windowingMode);
             }
             return candidateTask.getRootTask();
diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
index f51a173..3949952 100644
--- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
@@ -277,29 +277,31 @@
         // is set with the suggestedDisplayArea. If it is set, but the eventual TaskDisplayArea is
         // different, we should recalcuating the bounds.
         boolean hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = false;
-        if (suggestedDisplayArea.inFreeformWindowingMode()) {
-            if (launchMode == WINDOWING_MODE_PINNED) {
-                if (DEBUG) appendLog("picture-in-picture");
-            } else if (!root.isResizeable()) {
-                if (shouldLaunchUnresizableAppInFreeform(root, suggestedDisplayArea, options)) {
-                    launchMode = WINDOWING_MODE_FREEFORM;
-                    if (outParams.mBounds.isEmpty()) {
-                        getTaskBounds(root, suggestedDisplayArea, layout, launchMode,
-                                hasInitialBounds, outParams.mBounds);
-                        hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = true;
-                    }
-                    if (DEBUG) appendLog("unresizable-freeform");
-                } else {
-                    launchMode = WINDOWING_MODE_FULLSCREEN;
-                    outParams.mBounds.setEmpty();
-                    if (DEBUG) appendLog("unresizable-forced-maximize");
+        // shouldSetAsOverrideWindowingMode is set if the task needs to retain the launchMode
+        // regardless of the windowing mode of the parent.
+        boolean shouldSetAsOverrideWindowingMode = false;
+        if (launchMode == WINDOWING_MODE_PINNED) {
+            if (DEBUG) appendLog("picture-in-picture");
+        } else if (!root.isResizeable()) {
+            if (shouldLaunchUnresizableAppInFreeformInFreeformMode(root, suggestedDisplayArea,
+                    options)) {
+                launchMode = WINDOWING_MODE_UNDEFINED;
+                if (outParams.mBounds.isEmpty()) {
+                    getTaskBounds(root, suggestedDisplayArea, layout, launchMode, hasInitialBounds,
+                            outParams.mBounds);
+                    hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = true;
                 }
+                if (DEBUG) appendLog("unresizable-freeform");
+            } else {
+                launchMode = WINDOWING_MODE_FULLSCREEN;
+                outParams.mBounds.setEmpty();
+                shouldSetAsOverrideWindowingMode = true;
+                if (DEBUG) appendLog("unresizable-forced-maximize");
             }
-        } else {
-            if (DEBUG) appendLog("non-freeform-task-display-area");
         }
         // If launch mode matches display windowing mode, let it inherit from display.
         outParams.mWindowingMode = launchMode == suggestedDisplayArea.getWindowingMode()
+                && !shouldSetAsOverrideWindowingMode
                 ? WINDOWING_MODE_UNDEFINED : launchMode;
 
         if (phase == PHASE_WINDOWING_MODE) {
@@ -650,7 +652,7 @@
         inOutBounds.offset(xOffset, yOffset);
     }
 
-    private boolean shouldLaunchUnresizableAppInFreeform(ActivityRecord activity,
+    private boolean shouldLaunchUnresizableAppInFreeformInFreeformMode(ActivityRecord activity,
             TaskDisplayArea displayArea, @Nullable ActivityOptions options) {
         if (options != null && options.getLaunchWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
             // Do not launch the activity in freeform if it explicitly requested fullscreen mode.
@@ -663,8 +665,7 @@
         final int displayOrientation = orientationFromBounds(displayArea.getBounds());
         final int activityOrientation = resolveOrientation(activity, displayArea,
                 displayArea.getBounds());
-        if (displayArea.getWindowingMode() == WINDOWING_MODE_FREEFORM
-                && displayOrientation != activityOrientation) {
+        if (displayOrientation != activityOrientation) {
             return true;
         }
 
@@ -768,9 +769,10 @@
             // to the center of suggested bounds (or the displayArea if no suggested bounds). The
             // default size might be too big to center to source activity bounds in displayArea, so
             // we may need to move it back to the displayArea.
+            adjustBoundsToFitInDisplayArea(displayArea, layout, mTmpBounds);
+            inOutBounds.setEmpty();
             LaunchParamsUtil.centerBounds(displayArea, mTmpBounds.width(), mTmpBounds.height(),
                     inOutBounds);
-            adjustBoundsToFitInDisplayArea(displayArea, layout, inOutBounds);
             if (DEBUG) appendLog("freeform-size-mismatch=" + inOutBounds);
         }
 
@@ -821,8 +823,7 @@
                                                 @NonNull Rect inOutBounds) {
         final int layoutDirection = mSupervisor.mRootWindowContainer.getConfiguration()
                 .getLayoutDirection();
-        displayArea.getStableRect(mTmpStableBounds);
-        LaunchParamsUtil.adjustBoundsToFitInDisplayArea(mTmpStableBounds, layoutDirection, layout,
+        LaunchParamsUtil.adjustBoundsToFitInDisplayArea(displayArea, layoutDirection, layout,
                 inOutBounds);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index ff5622f..d3e638f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3091,6 +3091,17 @@
         assertTrue(activity.mVisibleRequested);
         assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
+
+        // There should still be animation (add to opening) if keyguard is going away while the
+        // screen is off because it will be visible after screen is turned on by unlocking.
+        mDisplayContent.mOpeningApps.remove(activity);
+        mDisplayContent.mClosingApps.remove(activity);
+        activity.commitVisibility(false /* visible */, false /* performLayout */);
+        mDisplayContent.getDisplayPolicy().screenTurnedOff();
+        final KeyguardController controller = mSupervisor.getKeyguardController();
+        doReturn(true).when(controller).isKeyguardGoingAway(anyInt());
+        activity.setVisibility(true);
+        assertTrue(mDisplayContent.mOpeningApps.contains(activity));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
index dce9f66..2a88eb0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
@@ -85,6 +85,11 @@
     private static final Rect DISPLAY_STABLE_BOUNDS = new Rect(/* left */ 100,
             /* top */ 200, /* right */ 1620, /* bottom */ 680);
 
+    private static final Rect SMALL_DISPLAY_BOUNDS = new Rect(/* left */ 0, /* top */ 0,
+            /* right */ 1000, /* bottom */ 500);
+    private static final Rect SMALL_DISPLAY_STABLE_BOUNDS = new Rect(/* left */ 100,
+            /* top */ 50, /* right */ 900, /* bottom */ 450);
+
     private ActivityRecord mActivity;
 
     private TaskLaunchParamsModifier mTarget;
@@ -820,10 +825,11 @@
     }
 
     @Test
-    public void testLaunchesPortraitUnresizableOnFreeformDisplayWithFreeformSizeCompat() {
+    public void testLaunchesPortraitUnresizableOnFreeformLandscapeDisplay() {
         mAtm.mDevEnableNonResizableMultiWindow = true;
         final TestDisplayContent freeformDisplay = createNewDisplayContent(
                 WINDOWING_MODE_FREEFORM);
+        assertTrue(freeformDisplay.getBounds().width() > freeformDisplay.getBounds().height());
         final ActivityOptions options = ActivityOptions.makeBasic();
         mCurrent.mPreferredTaskDisplayArea = freeformDisplay.getDefaultTaskDisplayArea();
         mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE;
@@ -831,12 +837,42 @@
         assertEquals(RESULT_CONTINUE,
                 new CalculateRequestBuilder().setOptions(options).calculate());
 
-        assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode,
-                WINDOWING_MODE_FREEFORM);
+        assertEquals(WINDOWING_MODE_UNDEFINED, mResult.mWindowingMode);
     }
 
     @Test
-    public void testSkipsForceMaximizingAppsOnNonFreeformDisplay() {
+    public void testLaunchesLandscapeUnresizableOnFreeformLandscapeDisplay() {
+        mAtm.mDevEnableNonResizableMultiWindow = true;
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+        assertTrue(freeformDisplay.getBounds().width() > freeformDisplay.getBounds().height());
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        mCurrent.mPreferredTaskDisplayArea = freeformDisplay.getDefaultTaskDisplayArea();
+        mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE;
+        mActivity.info.screenOrientation = SCREEN_ORIENTATION_LANDSCAPE;
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder().setOptions(options).calculate());
+
+        assertEquals(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode);
+    }
+
+    @Test
+    public void testLaunchesUndefinedUnresizableOnFreeformLandscapeDisplay() {
+        mAtm.mDevEnableNonResizableMultiWindow = true;
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+        assertTrue(freeformDisplay.getBounds().width() > freeformDisplay.getBounds().height());
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        mCurrent.mPreferredTaskDisplayArea = freeformDisplay.getDefaultTaskDisplayArea();
+        mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE;
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder().setOptions(options).calculate());
+
+        assertEquals(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode);
+    }
+
+    @Test
+    public void testForceMaximizingAppsOnNonFreeformDisplay() {
         final ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM);
         options.setLaunchBounds(new Rect(0, 0, 200, 100));
@@ -850,8 +886,9 @@
         assertEquals(RESULT_CONTINUE,
                 new CalculateRequestBuilder().setOptions(options).calculate());
 
-        assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode,
-                WINDOWING_MODE_FULLSCREEN);
+        // Non-resizable apps must be launched in fullscreen in a fullscreen display regardless of
+        // other properties.
+        assertEquals(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode);
     }
 
     @Test
@@ -1346,6 +1383,20 @@
     }
 
     @Test
+    public void testDefaultFreeformSizeShrinksOnSmallDisplay() {
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM, SMALL_DISPLAY_BOUNDS, SMALL_DISPLAY_STABLE_BOUNDS);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setOptions(options)
+                .calculate());
+
+        assertEquals(new Rect(414, 77, 587, 423), mResult.mBounds);
+    }
+
+    @Test
     public void testDefaultFreeformSizeRespectsMinAspectRatio() {
         final TestDisplayContent freeformDisplay = createNewDisplayContent(
                 WINDOWING_MODE_FREEFORM);
@@ -1535,16 +1586,15 @@
         options.setLaunchDisplayId(freeformDisplay.mDisplayId);
 
         mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM;
-        mCurrent.mBounds.set(100, 300, 1820, 1380);
+        mCurrent.mBounds.set(0, 0, 3000, 2000);
 
         mActivity.info.applicationInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
 
         assertEquals(RESULT_CONTINUE,
                 new CalculateRequestBuilder().setOptions(options).calculate());
 
-        assertTrue("Result bounds should start from app bounds's origin, but it's "
-                        + mResult.mBounds,
-                mResult.mBounds.left == 100 && mResult.mBounds.top == 200);
+        // Must shrink to fit the display while reserving aspect ratio.
+        assertEquals(new Rect(127, 227, 766, 653), mResult.mBounds);
     }
 
     @Test
@@ -1560,18 +1610,19 @@
 
         final ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+        final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder()
+                .setMinWidth(500).setMinHeight(500).build();
 
         mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM;
-        mCurrent.mBounds.set(100, 300, 1820, 1380);
+        mCurrent.mBounds.set(0, 0, 2000, 3000);
 
         mActivity.info.applicationInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
 
         assertEquals(RESULT_CONTINUE,
-                new CalculateRequestBuilder().setOptions(options).calculate());
+                new CalculateRequestBuilder().setOptions(options).setLayout(layout).calculate());
 
-        assertTrue("Result bounds should start from top-right corner of app bounds, but "
-                        + "it's " + mResult.mBounds,
-                mResult.mBounds.left == -100 && mResult.mBounds.top == 200);
+        // Must shrink to fit the display while reserving aspect ratio.
+        assertEquals(new Rect(1093, 227, 1593, 727), mResult.mBounds);
     }
 
     @Test
@@ -1746,7 +1797,7 @@
         assertEquals(RESULT_CONTINUE,
                 new CalculateRequestBuilder().setOptions(options).calculate());
 
-        assertEquals(new Rect(100, 200, 400, 500), mResult.mBounds);
+        assertEquals(new Rect(127, 227, 427, 527), mResult.mBounds);
     }
 
     @Test
@@ -1799,13 +1850,18 @@
     }
 
     private TestDisplayContent createNewDisplayContent(int windowingMode) {
+        return createNewDisplayContent(windowingMode, DISPLAY_BOUNDS, DISPLAY_STABLE_BOUNDS);
+    }
+
+    private TestDisplayContent createNewDisplayContent(int windowingMode, Rect displayBounds,
+                                                       Rect displayStableBounds) {
         final TestDisplayContent display = addNewDisplayContentAt(DisplayContent.POSITION_TOP);
         display.getDefaultTaskDisplayArea().setWindowingMode(windowingMode);
-        display.setBounds(DISPLAY_BOUNDS);
+        display.setBounds(displayBounds);
         display.getConfiguration().densityDpi = DENSITY_DEFAULT;
         display.getConfiguration().orientation = ORIENTATION_LANDSCAPE;
         configInsetsState(display.getInsetsStateController().getRawInsetsState(), display,
-                DISPLAY_STABLE_BOUNDS);
+                displayStableBounds);
         return display;
     }