Merge "Show captions on tablets even when display windowing mode is fullscreen." into tm-qpr-dev
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index ec1c57d..f7d4be3 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -830,29 +830,8 @@
      *     .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1.0f, 100)
      *     .compose();}</pre>
      *
-     * <p>Composition elements can also be {@link VibrationEffect} instances, including other
-     * compositions, and off durations, which are periods of time when the vibrator will be
-     * turned off. Here is an example of a composition that "warms up" with a light tap,
-     * a stronger double tap, then repeats a vibration pattern indefinitely:
-     *
-     * <pre>
-     * {@code VibrationEffect repeatingEffect = VibrationEffect.startComposition()
-     *     .addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK)
-     *     .addOffDuration(Duration.ofMillis(10))
-     *     .addEffect(VibrationEffect.createPredefined(VibrationEffect.EFFECT_DOUBLE_CLICK))
-     *     .addOffDuration(Duration.ofMillis(50))
-     *     .addEffect(VibrationEffect.createWaveform(pattern, repeatIndex))
-     *     .compose();}</pre>
-     *
      * <p>When choosing to play a composed effect, you should check that individual components are
-     * supported by the device by using the appropriate vibrator method:
-     *
-     * <ul>
-     *     <li>Primitive support can be checked using {@link Vibrator#arePrimitivesSupported}.
-     *     <li>Effect support can be checked using {@link Vibrator#areEffectsSupported}.
-     *     <li>Amplitude control for one-shot and waveforms with amplitude values can be checked
-     *         using {@link Vibrator#hasAmplitudeControl}.
-     * </ul>
+     * supported by the device by using {@link Vibrator#arePrimitivesSupported}.
      *
      * @see VibrationEffect#startComposition()
      */
@@ -1021,9 +1000,6 @@
          *
          * @param primitiveId The primitive to add
          * @return This {@link Composition} object to enable adding multiple elements in one chain.
-         *
-         * @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently
-         * ending with a repeating effect.
          */
         @NonNull
         public Composition addPrimitive(@PrimitiveType int primitiveId) {
@@ -1038,9 +1014,6 @@
          * @param primitiveId The primitive to add
          * @param scale The scale to apply to the intensity of the primitive.
          * @return This {@link Composition} object to enable adding multiple elements in one chain.
-         *
-         * @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently
-         * ending with a repeating effect.
          */
         @NonNull
         public Composition addPrimitive(@PrimitiveType int primitiveId,
@@ -1056,9 +1029,6 @@
          * @param delay The amount of time in milliseconds to wait before playing this primitive,
          *              starting at the time the previous element in this composition is finished.
          * @return This {@link Composition} object to enable adding multiple elements in one chain.
-         *
-         * @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently
-         * ending with a repeating effect.
          */
         @NonNull
         public Composition addPrimitive(@PrimitiveType int primitiveId,
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index ec6b4ac..e4caa38 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -3064,7 +3064,8 @@
                 // WindowManagerService has reported back a frame from a configuration not yet
                 // handled by the client. In this case, we need to accept the configuration so we
                 // do not lay out and draw with the wrong configuration.
-                if (!mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration)) {
+                if (mRelayoutRequested
+                        && !mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration)) {
                     if (DEBUG_CONFIGURATION) Log.v(mTag, "Visible with new config: "
                             + mPendingMergedConfiguration.getMergedConfiguration());
                     performConfigurationChange(new MergedConfiguration(mPendingMergedConfiguration),
@@ -8085,7 +8086,8 @@
         final int measuredWidth = mView.getMeasuredWidth();
         final int measuredHeight = mView.getMeasuredHeight();
         final boolean relayoutAsync;
-        if (LOCAL_LAYOUT && !mFirst && viewVisibility == mViewVisibility
+        if (LOCAL_LAYOUT
+                && (mViewFrameInfo.flags & FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED) == 0
                 && mWindowAttributes.type != TYPE_APPLICATION_STARTING
                 && mSyncSeqId <= mLastSyncSeqId
                 && winConfigFromAm.diff(winConfigFromWm, false /* compareUndefined */) == 0) {
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 285a407..fadad99 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -709,7 +709,7 @@
         }
 
         getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
-        resumeBlink();
+        makeBlink();
     }
 
     void onDetachedFromWindow() {
@@ -1685,17 +1685,12 @@
 
     void onWindowFocusChanged(boolean hasWindowFocus) {
         if (hasWindowFocus) {
-            if (mBlink != null) {
-                mBlink.uncancel();
-                makeBlink();
-            }
+            resumeBlink();
             if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
                 refreshTextActionMode();
             }
         } else {
-            if (mBlink != null) {
-                mBlink.cancel();
-            }
+            suspendBlink();
             if (mInputContentType != null) {
                 mInputContentType.enterDown = false;
             }
@@ -2851,7 +2846,8 @@
      * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
      */
     private boolean shouldBlink() {
-        if (!isCursorVisible() || !mTextView.isFocused()) return false;
+        if (!isCursorVisible() || !mTextView.isFocused()
+                || mTextView.getWindowVisibility() != mTextView.VISIBLE) return false;
 
         final int start = mTextView.getSelectionStart();
         if (start < 0) return false;
@@ -2873,6 +2869,17 @@
         }
     }
 
+    /**
+     *
+     * @return whether the Blink runnable is blinking or not, if null return false.
+     * @hide
+     */
+    @VisibleForTesting
+    public boolean isBlinking() {
+        if (mBlink == null) return false;
+        return !mBlink.mCancelled;
+    }
+
     private class Blink implements Runnable {
         private boolean mCancelled;
 
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 8ca763e..33ea2e4 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -110,11 +110,11 @@
     /** The container is an input-method window. */
     public static final int FLAG_IS_INPUT_METHOD = 1 << 8;
 
-    /** The container is ActivityEmbedding embedded. */
-    public static final int FLAG_IS_EMBEDDED = 1 << 9;
+    /** The container is in a Task with embedded activity. */
+    public static final int FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY = 1 << 9;
 
-    /** The first unused bit. This can be used by remotes to attach custom flags to this change. */
-    public static final int FLAG_FIRST_CUSTOM = 1 << 10;
+    /** The container fills its parent Task before and after the transition. */
+    public static final int FLAG_FILLS_TASK = 1 << 10;
 
     /** The container is going to show IME on its task after the transition. */
     public static final int FLAG_WILL_IME_SHOWN = 1 << 11;
@@ -125,6 +125,9 @@
     /** The container attaches work profile thumbnail for cross profile animation. */
     public static final int FLAG_CROSS_PROFILE_WORK_THUMBNAIL = 1 << 13;
 
+    /** The first unused bit. This can be used by remotes to attach custom flags to this change. */
+    public static final int FLAG_FIRST_CUSTOM = 1 << 14;
+
     /** @hide */
     @IntDef(prefix = { "FLAG_" }, value = {
             FLAG_NONE,
@@ -137,9 +140,12 @@
             FLAG_OCCLUDES_KEYGUARD,
             FLAG_DISPLAY_HAS_ALERT_WINDOWS,
             FLAG_IS_INPUT_METHOD,
-            FLAG_IS_EMBEDDED,
-            FLAG_FIRST_CUSTOM,
-            FLAG_WILL_IME_SHOWN
+            FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY,
+            FLAG_FILLS_TASK,
+            FLAG_WILL_IME_SHOWN,
+            FLAG_CROSS_PROFILE_OWNER_THUMBNAIL,
+            FLAG_CROSS_PROFILE_WORK_THUMBNAIL,
+            FLAG_FIRST_CUSTOM
     })
     public @interface ChangeFlags {}
 
@@ -322,28 +328,31 @@
             sb.append("IS_INPUT_METHOD");
         }
         if ((flags & FLAG_TRANSLUCENT) != 0) {
-            sb.append((sb.length() == 0 ? "" : "|") + "TRANSLUCENT");
+            sb.append(sb.length() == 0 ? "" : "|").append("TRANSLUCENT");
         }
         if ((flags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
-            sb.append((sb.length() == 0 ? "" : "|") + "STARTING_WINDOW_TRANSFER");
+            sb.append(sb.length() == 0 ? "" : "|").append("STARTING_WINDOW_TRANSFER");
         }
         if ((flags & FLAG_IS_VOICE_INTERACTION) != 0) {
-            sb.append((sb.length() == 0 ? "" : "|") + "IS_VOICE_INTERACTION");
+            sb.append(sb.length() == 0 ? "" : "|").append("IS_VOICE_INTERACTION");
         }
         if ((flags & FLAG_IS_DISPLAY) != 0) {
-            sb.append((sb.length() == 0 ? "" : "|") + "IS_DISPLAY");
+            sb.append(sb.length() == 0 ? "" : "|").append("IS_DISPLAY");
         }
         if ((flags & FLAG_OCCLUDES_KEYGUARD) != 0) {
-            sb.append((sb.length() == 0 ? "" : "|") + "OCCLUDES_KEYGUARD");
+            sb.append(sb.length() == 0 ? "" : "|").append("OCCLUDES_KEYGUARD");
         }
         if ((flags & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) {
-            sb.append((sb.length() == 0 ? "" : "|") + "DISPLAY_HAS_ALERT_WINDOWS");
+            sb.append(sb.length() == 0 ? "" : "|").append("DISPLAY_HAS_ALERT_WINDOWS");
         }
-        if ((flags & FLAG_IS_EMBEDDED) != 0) {
-            sb.append((sb.length() == 0 ? "" : "|") + "IS_EMBEDDED");
+        if ((flags & FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY) != 0) {
+            sb.append(sb.length() == 0 ? "" : "|").append("IN_TASK_WITH_EMBEDDED_ACTIVITY");
+        }
+        if ((flags & FLAG_FILLS_TASK) != 0) {
+            sb.append(sb.length() == 0 ? "" : "|").append("FILLS_TASK");
         }
         if ((flags & FLAG_FIRST_CUSTOM) != 0) {
-            sb.append((sb.length() == 0 ? "" : "|") + "FIRST_CUSTOM");
+            sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM");
         }
         return sb.toString();
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
index a8764e0..d76ad3d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
@@ -517,7 +517,9 @@
         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
     }
 
-    ActivityManager.RunningTaskInfo getTaskInfo() {
+    /** Returns the task info for the task in the TaskView. */
+    @Nullable
+    public ActivityManager.RunningTaskInfo getTaskInfo() {
         return mTaskInfo;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index e0004fc..521a65c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -16,7 +16,8 @@
 
 package com.android.wm.shell.activityembedding;
 
-import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
+import static android.window.TransitionInfo.FLAG_FILLS_TASK;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 
 import static java.util.Objects.requireNonNull;
 
@@ -84,12 +85,23 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        // TODO(b/207070762) Handle AE animation as a part of other transitions.
-        // Only handle the transition if all containers are embedded.
+        boolean containsEmbeddingSplit = false;
         for (TransitionInfo.Change change : info.getChanges()) {
-            if (!isEmbedded(change)) {
+            if (!change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) {
+                // Only animate the transition if all changes are in a Task with ActivityEmbedding.
                 return false;
             }
+            if (!containsEmbeddingSplit && !change.hasFlags(FLAG_FILLS_TASK)) {
+                // Whether the Task contains any ActivityEmbedding split before or after the
+                // transition.
+                containsEmbeddingSplit = true;
+            }
+        }
+        if (!containsEmbeddingSplit) {
+            // Let the system to play the default animation if there is no ActivityEmbedding split
+            // window. This allows to play the app customized animation when there is no embedding,
+            // such as the device is in a folded state.
+            return false;
         }
 
         // Start ActivityEmbedding animation.
@@ -119,8 +131,4 @@
         }
         callback.onTransitionFinished(null /* wct */, null /* wctCB */);
     }
-
-    private static boolean isEmbedded(@NonNull TransitionInfo.Change change) {
-        return (change.getFlags() & FLAG_IS_EMBEDDED) != 0;
-    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index aeaf6ed..be100bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -1270,7 +1270,7 @@
         }
         final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION);
         final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext))
-                && mExpandedBubble != null;
+                && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null;
         if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
             Log.d(TAG, "Show manage edu: " + shouldShow);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
index 063dac3..ab194df 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt
@@ -24,8 +24,8 @@
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup
-import android.view.WindowManager
 import android.view.WindowInsets
+import android.view.WindowManager
 import android.widget.FrameLayout
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY
@@ -41,6 +41,7 @@
 
     var circle = DismissCircleView(context)
     var isShowing = false
+    var targetSizeResId: Int
 
     private val animator = PhysicsAnimator.getInstance(circle)
     private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY)
@@ -70,7 +71,8 @@
         setVisibility(View.INVISIBLE)
         setBackgroundDrawable(gradientDrawable)
 
-        val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size)
+        targetSizeResId = R.dimen.dismiss_circle_size
+        val targetSize: Int = resources.getDimensionPixelSize(targetSizeResId)
         addView(circle, LayoutParams(targetSize, targetSize,
                 Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL))
         // start with circle offscreen so it's animated up
@@ -126,7 +128,7 @@
         layoutParams.height = resources.getDimensionPixelSize(
                 R.dimen.floating_dismiss_gradient_height)
 
-        val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size)
+        val targetSize = resources.getDimensionPixelSize(targetSizeResId)
         circle.layoutParams.width = targetSize
         circle.layoutParams.height = targetSize
         circle.requestLayout()
@@ -153,4 +155,4 @@
         setPadding(0, 0, 0, navInset.bottom +
                 resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin))
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 18ce364..247ba60 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -599,12 +599,12 @@
             Context context, ShellInit shellInit,
             ShellTaskOrganizer shellTaskOrganizer,
             RootDisplayAreaOrganizer rootDisplayAreaOrganizer,
-            @ShellMainThread Handler mainHandler
+            @ShellMainThread Handler mainHandler,
+            Transitions transitions
     ) {
         if (DesktopMode.IS_SUPPORTED) {
             return Optional.of(new DesktopModeController(context, shellInit, shellTaskOrganizer,
-                    rootDisplayAreaOrganizer,
-                    mainHandler));
+                    rootDisplayAreaOrganizer, mainHandler, transitions));
         } else {
             return Optional.empty();
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index 7d34ea4..c07ce10 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
 
@@ -37,6 +38,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
 
 /**
  * Handles windowing changes when desktop mode system setting changes
@@ -47,15 +49,18 @@
     private final ShellTaskOrganizer mShellTaskOrganizer;
     private final RootDisplayAreaOrganizer mRootDisplayAreaOrganizer;
     private final SettingsObserver mSettingsObserver;
+    private final Transitions mTransitions;
 
     public DesktopModeController(Context context, ShellInit shellInit,
             ShellTaskOrganizer shellTaskOrganizer,
             RootDisplayAreaOrganizer rootDisplayAreaOrganizer,
-            @ShellMainThread Handler mainHandler) {
+            @ShellMainThread Handler mainHandler,
+            Transitions transitions) {
         mContext = context;
         mShellTaskOrganizer = shellTaskOrganizer;
         mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer;
         mSettingsObserver = new SettingsObserver(mContext, mainHandler);
+        mTransitions = transitions;
         shellInit.addInitCallback(this::onInit, this);
     }
 
@@ -89,7 +94,11 @@
         }
         wct.merge(mRootDisplayAreaOrganizer.prepareWindowingModeChange(displayId,
                 targetWindowingMode), true /* transfer */);
-        mRootDisplayAreaOrganizer.applyTransaction(wct);
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mTransitions.startTransition(TRANSIT_CHANGE, wct, null);
+        } else {
+            mRootDisplayAreaOrganizer.applyTransaction(wct);
+        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index 3740a1b..7d1259a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -256,7 +256,7 @@
             windowDecor =
                     mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId);
         }
-        if (mWindowDecorViewModelOptional.isPresent()) {
+        if (mWindowDecorViewModelOptional.isPresent() && windowDecor != null) {
             mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition(
                     taskInfo, startT, finishT, windowDecor);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
index b0080b2..e7ec15e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
@@ -45,11 +45,6 @@
                 iconProvider);
     }
 
-    @Override
-    void dismiss(WindowContainerTransaction wct, boolean toTop) {
-        deactivate(wct, toTop);
-    }
-
     boolean isActive() {
         return mIsActive;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
index 86efbe0..8639b36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
@@ -42,11 +42,6 @@
                 iconProvider);
     }
 
-    @Override
-    void dismiss(WindowContainerTransaction wct, boolean toTop) {
-        removeAllTasks(wct, toTop);
-    }
-
     boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) {
         if (mChildrenTaskInfo.size() == 0) return false;
         wct.reparentTasks(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 946dfde..db35b48 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -123,6 +123,7 @@
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
 import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason;
+import com.android.wm.shell.transition.DefaultMixedHandler;
 import com.android.wm.shell.transition.LegacyTransitions;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.util.SplitBounds;
@@ -203,6 +204,8 @@
     @StageType
     private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
 
+    private DefaultMixedHandler mMixedHandler;
+
     private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
             new SplitWindowManager.ParentContainerCallbacks() {
                 @Override
@@ -326,6 +329,10 @@
         transitions.addHandler(this);
     }
 
+    public void setMixedHandler(DefaultMixedHandler mixedHandler) {
+        mMixedHandler = mixedHandler;
+    }
+
     @VisibleForTesting
     SplitScreenTransitions getSplitTransitions() {
         return mSplitTransitions;
@@ -927,13 +934,10 @@
             // Expand to top side split as full screen for fading out decor animation and dismiss
             // another side split(Moving its children to bottom).
             mIsExiting = true;
-            final StageTaskListener tempFullStage = childrenToTop;
-            final StageTaskListener dismissStage = mMainStage == childrenToTop
-                    ? mSideStage : mMainStage;
-            tempFullStage.resetBounds(wct);
-            wct.setSmallestScreenWidthDp(tempFullStage.mRootTaskInfo.token,
+            childrenToTop.resetBounds(wct);
+            wct.reorder(childrenToTop.mRootTaskInfo.token, true);
+            wct.setSmallestScreenWidthDp(childrenToTop.mRootTaskInfo.token,
                     SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
-            dismissStage.dismiss(wct, false /* toTop */);
         }
         mSyncQueue.queue(wct);
         mSyncQueue.runInSync(t -> {
@@ -950,7 +954,8 @@
                 childrenToTop.fadeOutDecor(() -> {
                     WindowContainerTransaction finishedWCT = new WindowContainerTransaction();
                     mIsExiting = false;
-                    childrenToTop.dismiss(finishedWCT, true /* toTop */);
+                    mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */);
+                    mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */);
                     finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */);
                     finishedWCT.setForceTranslucent(mRootTaskInfo.token, true);
                     finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
@@ -1879,8 +1884,25 @@
 
             // Use normal animations.
             return false;
+        } else if (mMixedHandler != null && hasDisplayChange(info)) {
+            // A display-change has been un-expectedly inserted into the transition. Redirect
+            // handling to the mixed-handler to deal with splitting it up.
+            if (mMixedHandler.animatePendingSplitWithDisplayChange(transition, info,
+                    startTransaction, finishTransaction, finishCallback)) {
+                return true;
+            }
         }
 
+        return startPendingAnimation(transition, info, startTransaction, finishTransaction,
+                finishCallback);
+    }
+
+    /** Starts the pending transition animation. */
+    public boolean startPendingAnimation(@NonNull IBinder transition,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
         boolean shouldAnimate = true;
         if (mSplitTransitions.isPendingEnter(transition)) {
             shouldAnimate = startPendingEnterAnimation(
@@ -1899,6 +1921,15 @@
         return true;
     }
 
+    private boolean hasDisplayChange(TransitionInfo info) {
+        boolean has = false;
+        for (int iC = 0; iC < info.getChanges().size() && !has; ++iC) {
+            final TransitionInfo.Change change = info.getChanges().get(iC);
+            has = change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0;
+        }
+        return has;
+    }
+
     /** Called to clean-up state and do house-keeping after the animation is done. */
     public void onTransitionAnimationComplete() {
         // If still playing, let it finish.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 1af9415..6b90eab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -106,11 +106,6 @@
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
     }
 
-    /**
-     * General function for dismiss this stage.
-     */
-    void dismiss(WindowContainerTransaction wct, boolean toTop) {}
-
     int getChildCount() {
         return mChildrenTaskInfo.size();
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index bcf4fbd..3cba929 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.transition;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
@@ -57,6 +58,9 @@
     private static class MixedTransition {
         static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
 
+        /** Both the display and split-state (enter/exit) is changing */
+        static final int TYPE_DISPLAY_AND_SPLIT_CHANGE = 2;
+
         /** The default animation for this mixed transition. */
         static final int ANIM_TYPE_DEFAULT = 0;
 
@@ -69,6 +73,7 @@
 
         Transitions.TransitionFinishCallback mFinishCallback = null;
         Transitions.TransitionHandler mLeftoversHandler = null;
+        WindowContainerTransaction mFinishWCT = null;
 
         /**
          * Mixed transitions are made up of multiple "parts". This keeps track of how many
@@ -95,6 +100,9 @@
                 mPipHandler = pipTouchHandlerOptional.get().getTransitionHandler();
                 mSplitHandler = splitScreenControllerOptional.get().getTransitionHandler();
                 mPlayer.addHandler(this);
+                if (mSplitHandler != null) {
+                    mSplitHandler.setMixedHandler(this);
+                }
             }, this);
         }
     }
@@ -122,10 +130,12 @@
     }
 
     private TransitionInfo subCopy(@NonNull TransitionInfo info,
-            @WindowManager.TransitionType int newType) {
-        final TransitionInfo out = new TransitionInfo(newType, info.getFlags());
-        for (int i = 0; i < info.getChanges().size(); ++i) {
-            out.getChanges().add(info.getChanges().get(i));
+            @WindowManager.TransitionType int newType, boolean withChanges) {
+        final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0);
+        if (withChanges) {
+            for (int i = 0; i < info.getChanges().size(); ++i) {
+                out.getChanges().add(info.getChanges().get(i));
+            }
         }
         out.setRootLeash(info.getRootLeash(), info.getRootOffset().x, info.getRootOffset().y);
         out.setAnimationOptions(info.getAnimationOptions());
@@ -157,6 +167,8 @@
         if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
             return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction,
                     finishCallback);
+        } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
+            return false;
         } else {
             mActiveTransitions.remove(mixed);
             throw new IllegalStateException("Starting mixed animation without a known mixed type? "
@@ -173,7 +185,7 @@
                 + "entering PIP while Split-Screen is active.");
         TransitionInfo.Change pipChange = null;
         TransitionInfo.Change wallpaper = null;
-        final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK);
+        final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
         boolean homeIsOpening = false;
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             TransitionInfo.Change change = info.getChanges().get(i);
@@ -254,6 +266,87 @@
         return true;
     }
 
+    private void unlinkMissingParents(TransitionInfo from) {
+        for (int i = 0; i < from.getChanges().size(); ++i) {
+            final TransitionInfo.Change chg = from.getChanges().get(i);
+            if (chg.getParent() == null) continue;
+            if (from.getChange(chg.getParent()) == null) {
+                from.getChanges().get(i).setParent(null);
+            }
+        }
+    }
+
+    private boolean isWithinTask(TransitionInfo info, TransitionInfo.Change chg) {
+        TransitionInfo.Change curr = chg;
+        while (curr != null) {
+            if (curr.getTaskInfo() != null) return true;
+            if (curr.getParent() == null) break;
+            curr = info.getChange(curr.getParent());
+        }
+        return false;
+    }
+
+    /**
+     * This is intended to be called by SplitCoordinator as a helper to mix an already-pending
+     * split transition with a display-change. The use-case for this is when a display
+     * change/rotation gets collected into a split-screen enter/exit transition which has already
+     * been claimed by StageCoordinator.handleRequest . This happens during launcher tests.
+     */
+    public boolean animatePendingSplitWithDisplayChange(@NonNull IBinder transition,
+            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT,
+            @NonNull SurfaceControl.Transaction finishT,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        final TransitionInfo everythingElse = subCopy(info, info.getType(), true /* withChanges */);
+        final TransitionInfo displayPart = subCopy(info, TRANSIT_CHANGE, false /* withChanges */);
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (isWithinTask(info, change)) continue;
+            displayPart.addChange(change);
+            everythingElse.getChanges().remove(i);
+        }
+        if (displayPart.getChanges().isEmpty()) return false;
+        unlinkMissingParents(everythingElse);
+        final MixedTransition mixed = new MixedTransition(
+                MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition);
+        mixed.mFinishCallback = finishCallback;
+        mActiveTransitions.add(mixed);
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change "
+                + "and split change.");
+        // We need to split the transition into 2 parts: the split part and the display part.
+        mixed.mInFlightSubAnimations = 2;
+
+        Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
+            --mixed.mInFlightSubAnimations;
+            if (wctCB != null) {
+                throw new IllegalArgumentException("Can't mix transitions that require finish"
+                        + " sync callback");
+            }
+            if (wct != null) {
+                if (mixed.mFinishWCT == null) {
+                    mixed.mFinishWCT = wct;
+                } else {
+                    mixed.mFinishWCT.merge(wct, true /* transfer */);
+                }
+            }
+            if (mixed.mInFlightSubAnimations > 0) return;
+            mActiveTransitions.remove(mixed);
+            mixed.mFinishCallback.onTransitionFinished(mixed.mFinishWCT, null /* wctCB */);
+        };
+
+        // Dispatch the display change. This will most-likely be taken by the default handler.
+        // Do this first since the first handler used will apply the startT; the display change
+        // needs to take a screenshot before that happens so we need it to be the first handler.
+        mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, displayPart,
+                startT, finishT, finishCB, mSplitHandler);
+
+        // Note: at this point, startT has probably already been applied, so we are basically
+        // giving splitHandler an empty startT. This is currently OK because display-change will
+        // grab a screenshot and paste it on top anyways.
+        mSplitHandler.startPendingAnimation(
+                transition, everythingElse, startT, finishT, finishCB);
+        return true;
+    }
+
     @Override
     public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@@ -279,6 +372,8 @@
                 } else {
                     mPipHandler.end();
                 }
+            } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
+                // queue
             } else {
                 throw new IllegalStateException("Playing a mixed transition with unknown type? "
                         + mixed.mType);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
index b2e45a6..a7234c1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -17,7 +17,7 @@
 package com.android.wm.shell.activityembedding;
 
 import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -57,7 +57,7 @@
     public void testStartAnimation() {
         final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
         final TransitionInfo.Change embeddingChange = createChange();
-        embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+        embeddingChange.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
         info.addChange(embeddingChange);
         doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
index 84befdd..3792e83 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
@@ -16,6 +16,9 @@
 
 package com.android.wm.shell.activityembedding;
 
+import static android.window.TransitionInfo.FLAG_FILLS_TASK;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
 import static org.junit.Assert.assertNotNull;
@@ -24,6 +27,8 @@
 
 import android.animation.Animator;
 import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.graphics.Rect;
 import android.os.IBinder;
 import android.view.SurfaceControl;
 import android.window.TransitionInfo;
@@ -80,4 +85,23 @@
         return new TransitionInfo.Change(mock(WindowContainerToken.class),
                 mock(SurfaceControl.class));
     }
+
+    /**
+     * Creates a mock {@link TransitionInfo.Change} with
+     * {@link TransitionInfo#FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY} flag.
+     */
+    static TransitionInfo.Change createEmbeddedChange(@NonNull Rect startBounds,
+            @NonNull Rect endBounds, @NonNull Rect taskBounds) {
+        final TransitionInfo.Change change = createChange();
+        change.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
+        change.setStartAbsBounds(startBounds);
+        change.setEndAbsBounds(endBounds);
+        if (taskBounds.width() == startBounds.width()
+                && taskBounds.height() == startBounds.height()
+                && taskBounds.width() == endBounds.width()
+                && taskBounds.height() == endBounds.height()) {
+            change.setFlags(FLAG_FILLS_TASK);
+        }
+        return change;
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
index cf43b00..baecf6f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -17,7 +17,6 @@
 package com.android.wm.shell.activityembedding;
 
 import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 
@@ -29,6 +28,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
+import android.graphics.Rect;
 import android.window.TransitionInfo;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -48,6 +48,10 @@
 @RunWith(AndroidJUnit4.class)
 public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimationTestBase {
 
+    private static final Rect TASK_BOUNDS = new Rect(0, 0, 1000, 500);
+    private static final Rect EMBEDDED_LEFT_BOUNDS = new Rect(0, 0, 500, 500);
+    private static final Rect EMBEDDED_RIGHT_BOUNDS = new Rect(500, 0, 1000, 500);
+
     @Before
     public void setup() {
         super.setUp();
@@ -77,13 +81,13 @@
     @Test
     public void testStartAnimation_containsNonActivityEmbeddingChange() {
         final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
-        final TransitionInfo.Change embeddingChange = createChange();
-        embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+        final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS,
+                EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
         final TransitionInfo.Change nonEmbeddingChange = createChange();
         info.addChange(embeddingChange);
         info.addChange(nonEmbeddingChange);
 
-        // No-op
+        // No-op because it contains non-embedded change.
         assertFalse(mController.startAnimation(mTransition, info, mStartTransaction,
                 mFinishTransaction, mFinishCallback));
         verify(mAnimRunner, never()).startAnimation(any(), any(), any(), any());
@@ -93,13 +97,65 @@
     }
 
     @Test
-    public void testStartAnimation_onlyActivityEmbeddingChange() {
+    public void testStartAnimation_containsOnlyFillTaskActivityEmbeddingChange() {
         final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
-        final TransitionInfo.Change embeddingChange = createChange();
-        embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+        final TransitionInfo.Change embeddingChange = createEmbeddedChange(TASK_BOUNDS, TASK_BOUNDS,
+                TASK_BOUNDS);
         info.addChange(embeddingChange);
 
-        // No-op
+        // No-op because it only contains embedded change that fills the Task. We will let the
+        // default handler to animate such transition.
+        assertFalse(mController.startAnimation(mTransition, info, mStartTransaction,
+                mFinishTransaction, mFinishCallback));
+        verify(mAnimRunner, never()).startAnimation(any(), any(), any(), any());
+        verifyNoMoreInteractions(mStartTransaction);
+        verifyNoMoreInteractions(mFinishTransaction);
+        verifyNoMoreInteractions(mFinishCallback);
+    }
+
+    @Test
+    public void testStartAnimation_containsActivityEmbeddingSplitChange() {
+        // Change that occupies only part of the Task.
+        final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+        final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS,
+                EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
+        info.addChange(embeddingChange);
+
+        // ActivityEmbeddingController will handle such transition.
+        assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
+                mFinishTransaction, mFinishCallback));
+        verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction,
+                mFinishTransaction);
+        verify(mStartTransaction).apply();
+        verifyNoMoreInteractions(mFinishTransaction);
+    }
+
+    @Test
+    public void testStartAnimation_containsChangeEnterActivityEmbeddingSplit() {
+        // Change that is entering ActivityEmbedding split.
+        final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+        final TransitionInfo.Change embeddingChange = createEmbeddedChange(TASK_BOUNDS,
+                EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
+        info.addChange(embeddingChange);
+
+        // ActivityEmbeddingController will handle such transition.
+        assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
+                mFinishTransaction, mFinishCallback));
+        verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction,
+                mFinishTransaction);
+        verify(mStartTransaction).apply();
+        verifyNoMoreInteractions(mFinishTransaction);
+    }
+
+    @Test
+    public void testStartAnimation_containsChangeExitActivityEmbeddingSplit() {
+        // Change that is exiting ActivityEmbedding split.
+        final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+        final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS,
+                TASK_BOUNDS, TASK_BOUNDS);
+        info.addChange(embeddingChange);
+
+        // ActivityEmbeddingController will handle such transition.
         assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
                 mFinishTransaction, mFinishCallback));
         verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction,
@@ -115,8 +171,8 @@
                 () -> mController.onAnimationFinished(mTransition));
 
         final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
-        final TransitionInfo.Change embeddingChange = createChange();
-        embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+        final TransitionInfo.Change embeddingChange = createEmbeddedChange(EMBEDDED_LEFT_BOUNDS,
+                EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS);
         info.addChange(embeddingChange);
         mController.startAnimation(mTransition, info, mStartTransaction,
                 mFinishTransaction, mFinishCallback);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index ef532e4..5779425 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -43,6 +43,7 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -63,6 +64,8 @@
     private ShellExecutor mTestExecutor;
     @Mock
     private Handler mMockHandler;
+    @Mock
+    private Transitions mMockTransitions;
 
     private DesktopModeController mController;
     private ShellInit mShellInit;
@@ -72,7 +75,7 @@
         mShellInit = Mockito.spy(new ShellInit(mTestExecutor));
 
         mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer,
-                mRootDisplayAreaOrganizer, mMockHandler);
+                mRootDisplayAreaOrganizer, mMockHandler, mMockTransitions);
 
         mShellInit.init();
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index ea9390e..9240abf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -225,7 +225,6 @@
         mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME);
         verify(mMainStage).reorderChild(eq(testTaskId), eq(true),
                 any(WindowContainerTransaction.class));
-        verify(mSideStage).dismiss(any(WindowContainerTransaction.class), eq(false));
         verify(mMainStage).resetBounds(any(WindowContainerTransaction.class));
     }
 
@@ -239,7 +238,6 @@
         verify(mSideStage).reorderChild(eq(testTaskId), eq(true),
                 any(WindowContainerTransaction.class));
         verify(mSideStage).resetBounds(any(WindowContainerTransaction.class));
-        verify(mMainStage).dismiss(any(WindowContainerTransaction.class), eq(false));
     }
 
     @Test
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
index 3152e65..fa056e2b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
@@ -16,6 +16,22 @@
 
 package com.android.settingslib.bluetooth;
 
+import static android.bluetooth.BluetoothAdapter.STATE_CONNECTED;
+import static android.bluetooth.BluetoothAdapter.STATE_CONNECTING;
+import static android.bluetooth.BluetoothAdapter.STATE_DISCONNECTED;
+import static android.bluetooth.BluetoothAdapter.STATE_DISCONNECTING;
+import static android.bluetooth.BluetoothAdapter.STATE_OFF;
+import static android.bluetooth.BluetoothAdapter.STATE_ON;
+import static android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF;
+import static android.bluetooth.BluetoothAdapter.STATE_TURNING_ON;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * BluetoothCallback provides a callback interface for the settings
@@ -33,7 +49,7 @@
      * {@link android.bluetooth.BluetoothAdapter#STATE_ON},
      * {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_OFF}.
      */
-    default void onBluetoothStateChanged(int bluetoothState) {}
+    default void onBluetoothStateChanged(@AdapterState int bluetoothState) {}
 
     /**
      * It will be called when the local Bluetooth adapter has started
@@ -54,14 +70,14 @@
      *
      * @param cachedDevice the Bluetooth device.
      */
-    default void onDeviceAdded(CachedBluetoothDevice cachedDevice) {}
+    default void onDeviceAdded(@NonNull CachedBluetoothDevice cachedDevice) {}
 
     /**
      * It will be called when requiring to remove a remote device from CachedBluetoothDevice list
      *
      * @param cachedDevice the Bluetooth device.
      */
-    default void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {}
+    default void onDeviceDeleted(@NonNull CachedBluetoothDevice cachedDevice) {}
 
     /**
      * It will be called when bond state of a remote device is changed.
@@ -73,7 +89,8 @@
      * {@link android.bluetooth.BluetoothDevice#BOND_BONDING},
      * {@link android.bluetooth.BluetoothDevice#BOND_BONDED}.
      */
-    default void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {}
+    default void onDeviceBondStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice, int bondState) {}
 
     /**
      * It will be called in following situations:
@@ -89,7 +106,9 @@
      * {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTED},
      * {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTING}.
      */
-    default void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {}
+    default void onConnectionStateChanged(
+            @Nullable CachedBluetoothDevice cachedDevice,
+            @ConnectionState int state) {}
 
     /**
      * It will be called when device been set as active for {@code bluetoothProfile}
@@ -101,7 +120,8 @@
      * @param activeDevice the active Bluetooth device.
      * @param bluetoothProfile the profile of active Bluetooth device.
      */
-    default void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {}
+    default void onActiveDeviceChanged(
+            @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {}
 
     /**
      * It will be called in following situations:
@@ -124,8 +144,10 @@
      * {@link android.bluetooth.BluetoothProfile#STATE_DISCONNECTING}.
      * @param bluetoothProfile the BluetoothProfile id.
      */
-    default void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice,
-            int state, int bluetoothProfile) {
+    default void onProfileConnectionStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice,
+            @ConnectionState int state,
+            int bluetoothProfile) {
     }
 
     /**
@@ -138,6 +160,24 @@
      *                     {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTED},
      *                     {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTED}
      */
-    default void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
-    }
+    default void onAclConnectionStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice, int state) {}
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "STATE_" }, value = {
+            STATE_DISCONNECTED,
+            STATE_CONNECTING,
+            STATE_CONNECTED,
+            STATE_DISCONNECTING,
+    })
+    @interface ConnectionState {}
+
+    @IntDef(prefix = { "STATE_" }, value = {
+            STATE_OFF,
+            STATE_TURNING_ON,
+            STATE_ON,
+            STATE_TURNING_OFF,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface AdapterState {}
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index 51812f0..a9f4e9c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -32,6 +32,7 @@
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
@@ -193,19 +194,19 @@
         return deviceAdded;
     }
 
-    void dispatchDeviceAdded(CachedBluetoothDevice cachedDevice) {
+    void dispatchDeviceAdded(@NonNull CachedBluetoothDevice cachedDevice) {
         for (BluetoothCallback callback : mCallbacks) {
             callback.onDeviceAdded(cachedDevice);
         }
     }
 
-    void dispatchDeviceRemoved(CachedBluetoothDevice cachedDevice) {
+    void dispatchDeviceRemoved(@NonNull CachedBluetoothDevice cachedDevice) {
         for (BluetoothCallback callback : mCallbacks) {
             callback.onDeviceDeleted(cachedDevice);
         }
     }
 
-    void dispatchProfileConnectionStateChanged(CachedBluetoothDevice device, int state,
+    void dispatchProfileConnectionStateChanged(@NonNull CachedBluetoothDevice device, int state,
             int bluetoothProfile) {
         for (BluetoothCallback callback : mCallbacks) {
             callback.onProfileConnectionStateChanged(device, state, bluetoothProfile);
@@ -228,7 +229,8 @@
     }
 
     @VisibleForTesting
-    void dispatchActiveDeviceChanged(CachedBluetoothDevice activeDevice,
+    void dispatchActiveDeviceChanged(
+            @Nullable CachedBluetoothDevice activeDevice,
             int bluetoothProfile) {
         for (CachedBluetoothDevice cachedDevice : mDeviceManager.getCachedDevicesCopy()) {
             boolean isActive = Objects.equals(cachedDevice, activeDevice);
@@ -239,7 +241,7 @@
         }
     }
 
-    private void dispatchAclStateChanged(CachedBluetoothDevice activeDevice, int state) {
+    private void dispatchAclStateChanged(@NonNull CachedBluetoothDevice activeDevice, int state) {
         for (BluetoothCallback callback : mCallbacks) {
             callback.onAclConnectionStateChanged(activeDevice, state);
         }
@@ -456,6 +458,7 @@
                 Log.w(TAG, "ActiveDeviceChangedHandler: action is null");
                 return;
             }
+            @Nullable
             CachedBluetoothDevice activeDevice = mDeviceManager.findDevice(device);
             int bluetoothProfile = 0;
             if (Objects.equals(action, BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index 7d4dcf8..ebabdf57 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -352,8 +352,11 @@
          * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after
          * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
          * before the cancellation.
+         *
+         * If this launch animation affected the occlusion state of the keyguard, WM will provide
+         * us with [newKeyguardOccludedState] so that we can set the occluded state appropriately.
          */
-        fun onLaunchAnimationCancelled() {}
+        fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {}
     }
 
     @VisibleForTesting
@@ -667,7 +670,7 @@
             removeTimeout()
             context.mainExecutor.execute {
                 animation?.cancel()
-                controller.onLaunchAnimationCancelled()
+                controller.onLaunchAnimationCancelled(newKeyguardOccludedState = isKeyguardOccluded)
             }
         }
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index eac5d275..9656b8a 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -238,7 +238,7 @@
                 }
             }
 
-            override fun onLaunchAnimationCancelled() {
+            override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
                 controller.onLaunchAnimationCancelled()
                 enableDialogDismiss()
                 dialog.dismiss()
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten1.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten1.jpeg
new file mode 100644
index 0000000..6241b0b
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/drawable/kitten1.jpeg
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten2.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten2.jpeg
new file mode 100644
index 0000000..870ef13
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/drawable/kitten2.jpeg
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten3.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten3.jpeg
new file mode 100644
index 0000000..bb7261c
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/drawable/kitten3.jpeg
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten4.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten4.jpeg
new file mode 100644
index 0000000..e34b7dd
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/drawable/kitten4.jpeg
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten5.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten5.jpeg
new file mode 100644
index 0000000..9cde24b
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/drawable/kitten5.jpeg
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten6.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten6.jpeg
new file mode 100644
index 0000000..17825b6
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/res/drawable/kitten6.jpeg
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
index 2e6456b..6805bf8 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
@@ -28,25 +28,25 @@
 
 /** The gallery app screens. */
 object GalleryAppScreens {
-    val Typography = ChildScreen("typography") { TypographyScreen() }
-    val MaterialColors = ChildScreen("material_colors") { MaterialColorsScreen() }
-    val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
-    val Buttons = ChildScreen("buttons") { ButtonsScreen() }
-    val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
+    private val Typography = ChildScreen("typography") { TypographyScreen() }
+    private val MaterialColors = ChildScreen("material_colors") { MaterialColorsScreen() }
+    private val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
+    private val Buttons = ChildScreen("buttons") { ButtonsScreen() }
+    private val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
 
-    val PeopleEmpty =
+    private val PeopleEmpty =
         ChildScreen("people_empty") { navController ->
             EmptyPeopleScreen(onResult = { navController.popBackStack() })
         }
-    val PeopleFew =
+    private val PeopleFew =
         ChildScreen("people_few") { navController ->
             FewPeopleScreen(onResult = { navController.popBackStack() })
         }
-    val PeopleFull =
+    private val PeopleFull =
         ChildScreen("people_full") { navController ->
             FullPeopleScreen(onResult = { navController.popBackStack() })
         }
-    val People =
+    private val People =
         ParentScreen(
             "people",
             mapOf(
@@ -55,6 +55,52 @@
                 "Full" to PeopleFull,
             )
         )
+    private val UserSwitcherSingleUser =
+        ChildScreen("user_switcher_single") { navController ->
+            UserSwitcherScreen(
+                userCount = 1,
+                onFinished = navController::popBackStack,
+            )
+        }
+    private val UserSwitcherThreeUsers =
+        ChildScreen("user_switcher_three") { navController ->
+            UserSwitcherScreen(
+                userCount = 3,
+                onFinished = navController::popBackStack,
+            )
+        }
+    private val UserSwitcherFourUsers =
+        ChildScreen("user_switcher_four") { navController ->
+            UserSwitcherScreen(
+                userCount = 4,
+                onFinished = navController::popBackStack,
+            )
+        }
+    private val UserSwitcherFiveUsers =
+        ChildScreen("user_switcher_five") { navController ->
+            UserSwitcherScreen(
+                userCount = 5,
+                onFinished = navController::popBackStack,
+            )
+        }
+    private val UserSwitcherSixUsers =
+        ChildScreen("user_switcher_six") { navController ->
+            UserSwitcherScreen(
+                userCount = 6,
+                onFinished = navController::popBackStack,
+            )
+        }
+    private val UserSwitcher =
+        ParentScreen(
+            "user_switcher",
+            mapOf(
+                "Single" to UserSwitcherSingleUser,
+                "Three" to UserSwitcherThreeUsers,
+                "Four" to UserSwitcherFourUsers,
+                "Five" to UserSwitcherFiveUsers,
+                "Six" to UserSwitcherSixUsers,
+            )
+        )
 
     val Home =
         ParentScreen(
@@ -66,6 +112,7 @@
                 "Example feature" to ExampleFeature,
                 "Buttons" to Buttons,
                 "People" to People,
+                "User Switcher" to UserSwitcher,
             )
         )
 }
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/UserSwitcherScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/UserSwitcherScreen.kt
new file mode 100644
index 0000000..fe9707d
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/UserSwitcherScreen.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.systemui.compose.gallery
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import com.android.systemui.user.Fakes.fakeUserSwitcherViewModel
+import com.android.systemui.user.ui.compose.UserSwitcherScreen
+
+@Composable
+fun UserSwitcherScreen(
+    userCount: Int,
+    onFinished: () -> Unit,
+) {
+    val context = LocalContext.current.applicationContext
+    UserSwitcherScreen(
+        viewModel = fakeUserSwitcherViewModel(context, userCount = userCount),
+        onFinished = onFinished,
+    )
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/user/Fakes.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/user/Fakes.kt
index 02d76f4..91a73ea 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/user/Fakes.kt
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/user/Fakes.kt
@@ -58,12 +58,29 @@
                                     (0 until userCount).map { index ->
                                         UserModel(
                                             id = index,
-                                            name = Text.Loaded("user_$index"),
+                                            name =
+                                                Text.Loaded(
+                                                    when (index % 6) {
+                                                        0 -> "Ross Geller"
+                                                        1 -> "Phoebe Buffay"
+                                                        2 -> "Monica Geller"
+                                                        3 -> "Rachel Greene"
+                                                        4 -> "Chandler Bing"
+                                                        else -> "Joey Tribbiani"
+                                                    }
+                                                ),
                                             image =
                                                 checkNotNull(
                                                     AppCompatResources.getDrawable(
                                                         context,
-                                                        R.drawable.ic_avatar_guest_user
+                                                        when (index % 6) {
+                                                            0 -> R.drawable.kitten1
+                                                            1 -> R.drawable.kitten2
+                                                            2 -> R.drawable.kitten3
+                                                            3 -> R.drawable.kitten4
+                                                            4 -> R.drawable.kitten5
+                                                            else -> R.drawable.kitten6
+                                                        },
                                                     )
                                                 ),
                                             isSelected = index == 0,
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index 689938a..8182484 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -498,7 +498,6 @@
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
index 1aaf19e..c50340c 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
@@ -147,10 +147,10 @@
     }
 
     /**
-     * Listener that is alerted when a double tap is required to confirm a single tap.
+     * Listener that is alerted when an additional tap is required to confirm a single tap.
      **/
     interface FalsingTapListener {
-        void onDoubleTapRequired();
+        void onAdditionalTapRequired();
     }
 
     /** Passed to {@link FalsingManager#onProximityEvent}. */
diff --git a/packages/SystemUI/res-keyguard/drawable/kg_bouncer_secondary_button.xml b/packages/SystemUI/res-keyguard/drawable/kg_bouncer_secondary_button.xml
new file mode 100644
index 0000000..732fc57
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/kg_bouncer_secondary_button.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:insetTop="6dp"
+    android:insetBottom="6dp">
+    <ripple android:color="?android:attr/colorControlHighlight">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <solid android:color="?android:attr/textColorSecondary"/>
+                <corners android:radius="24dp"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <corners android:radius="24dp"/>
+                <solid android:color="@android:color/transparent"/>
+                <stroke android:color="?androidprv:attr/colorAccentTertiary"
+                    android:width="1dp"
+                    />
+                <padding android:left="16dp"
+                    android:top="12dp"
+                    android:right="16dp"
+                    android:bottom="12dp"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
diff --git a/packages/SystemUI/res-keyguard/drawable/qs_invert_colors_icon_off.xml b/packages/SystemUI/res-keyguard/drawable/qs_invert_colors_icon_off.xml
new file mode 100644
index 0000000..a347123
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/qs_invert_colors_icon_off.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="17"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M7.38 -0.28 C5.69,-2.81 1.5,-8.5 0.01,-7.32 C0.03,-6.22 -0.06,6.16 -0.06,7.78 C1.56,7.81 5.44,7.19 7.37,2.06 C7.37,1.21 7.35,1.69 7.35,1 C7.35,0.09 7.38,0.59 7.38,-0.28c "
+                    android:valueTo="M6 -2.38 C1.81,-6.69 -0.5,-10 -1.72,-7.1 C-1.73,-6.22 -1.73,5.88 -1.7,7.16 C0.25,8.45 3,7.75 5.47,4.91 C6.33,3.91 7.21,2.42 7.24,1.13 C7.26,-0.05 6.87,-1.49 6,-2.38c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="33"
+                    android:propertyName="pathData"
+                    android:startOffset="17"
+                    android:valueFrom="M6 -2.38 C1.81,-6.69 -0.5,-10 -1.72,-7.1 C-1.73,-6.22 -1.73,5.88 -1.7,7.16 C0.25,8.45 3,7.75 5.47,4.91 C6.33,3.91 7.21,2.42 7.24,1.13 C7.26,-0.05 6.87,-1.49 6,-2.38c "
+                    android:valueTo="M4.94 -3.95 C-0.31,-9.06 0.52,-9.42 -5.75,-3.22 C-8.31,-0.69 -7.05,3 -6.94,3.22 C-3.63,9.31 2.63,9.5 5.94,4.59 C6.61,3.6 7.43,1.16 7.12,0 C6.72,-1.45 6.01,-2.9 4.94,-3.95c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="67"
+                    android:propertyName="pathData"
+                    android:startOffset="50"
+                    android:valueFrom="M4.94 -3.95 C-0.31,-9.06 0.52,-9.42 -5.75,-3.22 C-8.31,-0.69 -7.05,3 -6.94,3.22 C-3.63,9.31 2.63,9.5 5.94,4.59 C6.61,3.6 7.43,1.16 7.12,0 C6.72,-1.45 6.01,-2.9 4.94,-3.95c "
+                    android:valueTo="M3.07 -5.83 C-0.44,-10.25 -2.56,-6.87 -6.11,-2.6 C-7.03,-1.49 -7.23,1.85 -7.18,2.06 C-5.84,7.25 -0.33,9.23 3.14,6.75 C3.17,5.1 3.17,3.58 3.22,0 C3.25,-3.04 3.1,-4.1 3.07,-5.83c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="pathData"
+                    android:startOffset="117"
+                    android:valueFrom="M3.07 -5.83 C-0.44,-10.25 -2.56,-6.87 -6.11,-2.6 C-7.03,-1.49 -7.23,1.85 -7.18,2.06 C-5.84,7.25 -0.33,9.23 3.14,6.75 C3.17,5.1 3.17,3.58 3.22,0 C3.25,-3.04 3.1,-4.1 3.07,-5.83c "
+                    android:valueTo="M0 -8.18 C-1.81,-7.06 -3.89,-5.21 -5.95,-2.81 C-7.56,-0.94 -7.37,0.88 -7.37,1.09 C-7.37,6.31 -2.19,7.99 0,7.99 C0,6.34 -0.01,3.91 -0.01,0 C-0.01,-3.91 0,-6 0,-8.18c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="433"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="24">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="12"
+                    android:translateY="12">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#edf2eb"
+                        android:fillType="nonZero"
+                        android:pathData=" M7.38 -0.28 C5.69,-2.81 1.5,-8.5 0.01,-7.32 C0.03,-6.22 -0.06,6.16 -0.06,7.78 C1.56,7.81 5.44,7.19 7.37,2.06 C7.37,1.21 7.35,1.69 7.35,1 C7.35,0.09 7.38,0.59 7.38,-0.28c " />
+                    <path
+                        android:name="_R_G_L_0_G_D_1_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M0 -7.19 C0,-7.19 2.93,-4.32 4.38,-2.87 C5.25,-2 5.88,-0.62 5.88,1.19 C5.88,4.5 2.8,6.97 0,7 C-2.8,7.03 -6,4.37 -6,1.13 C-6,-0.43 -5.38,-1.9 -4.25,-3.01 C-4.25,-3.01 0,-7.19 0,-7.19  M-5.65 -4.44 C-5.65,-4.44 -5.65,-4.44 -5.65,-4.44 C-7.1,-3.01 -8,-1.04 -8,1.13 C-8,5.48 -4.42,9 0,9 C4.42,9 8,5.48 8,1.13 C8,-1.04 7.1,-3.01 5.65,-4.44 C5.65,-4.44 5.65,-4.44 5.65,-4.44 C5.65,-4.44 0,-10 0,-10 C0,-10 -5.65,-4.44 -5.65,-4.44c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/qs_invert_colors_icon_on.xml b/packages/SystemUI/res-keyguard/drawable/qs_invert_colors_icon_on.xml
new file mode 100644
index 0000000..c5609d8
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/qs_invert_colors_icon_on.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="100"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M0 -7.49 C-2.58,-7.49 -7.47,-2.59 -7.47,0.57 C-7.47,0.77 -7.37,0.88 -7.37,1.09 C-7.37,6.31 -2.19,7.55 0,7.55 C0,5.91 -0.01,3.91 -0.01,0 C-0.01,-3.91 0,-5.31 0,-7.49c "
+                    android:valueTo="M3.11 -6.83 C-1.37,-10.12 -6.04,-3.87 -7.22,0.09 C-7.26,0.29 -7.23,1.85 -7.18,2.06 C-5.84,7.25 -0.33,9.23 3.14,6.75 C3.17,5.1 3.17,3.58 3.22,0 C3.25,-3.04 3.14,-5.1 3.11,-6.83c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="67"
+                    android:propertyName="pathData"
+                    android:startOffset="100"
+                    android:valueFrom="M3.11 -6.83 C-1.37,-10.12 -6.04,-3.87 -7.22,0.09 C-7.26,0.29 -7.23,1.85 -7.18,2.06 C-5.84,7.25 -0.33,9.23 3.14,6.75 C3.17,5.1 3.17,3.58 3.22,0 C3.25,-3.04 3.14,-5.1 3.11,-6.83c "
+                    android:valueTo="M5 -3.95 C-0.25,-9.87 -0.47,-8.74 -5,-3.63 C-7.94,-0.31 -7.05,3 -6.94,3.22 C-3.63,9.31 2.94,9.19 7,3.59 C7.06,1.94 7,3.19 7.12,0 C7.19,-2 5.06,-2.75 5,-3.95c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="67"
+                    android:propertyName="pathData"
+                    android:startOffset="167"
+                    android:valueFrom="M5 -3.95 C-0.25,-9.87 -0.47,-8.74 -5,-3.63 C-7.94,-0.31 -7.05,3 -6.94,3.22 C-3.63,9.31 2.94,9.19 7,3.59 C7.06,1.94 7,3.19 7.12,0 C7.19,-2 5.06,-2.75 5,-3.95c "
+                    android:valueTo="M4.82 -3.81 C1,-7.87 0.75,-9.37 -1.72,-7.1 C-1.73,-6.22 -1.73,5.88 -1.7,7.16 C0.25,8.45 4.38,7.38 6.4,3.22 C6.77,2.46 7,1.69 6.74,0 C6.56,-1.16 5.85,-2.71 4.82,-3.81c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="100"
+                    android:propertyName="pathData"
+                    android:startOffset="233"
+                    android:valueFrom="M4.82 -3.81 C1,-7.87 0.75,-9.37 -1.72,-7.1 C-1.73,-6.22 -1.73,5.88 -1.7,7.16 C0.25,8.45 4.38,7.38 6.4,3.22 C6.77,2.46 7,1.69 6.74,0 C6.56,-1.16 5.85,-2.71 4.82,-3.81c "
+                    android:valueTo="M5.63 -2.97 C3.65,-4.93 0.75,-8.37 0.01,-7.32 C0.03,-6.22 -0.03,5.66 -0.03,7.28 C1.59,7.31 4.13,7.94 6.31,3.44 C6.68,2.66 7,1.37 6.87,0.06 C6.77,-0.84 6.13,-2.47 5.63,-2.97c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.2,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="433"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="24">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="12"
+                    android:translateY="12">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#edf2eb"
+                        android:fillType="nonZero"
+                        android:pathData=" M0 -7.49 C-2.58,-7.49 -7.47,-2.59 -7.47,0.57 C-7.47,0.77 -7.37,0.88 -7.37,1.09 C-7.37,6.31 -2.19,7.55 0,7.55 C0,5.91 -0.01,3.91 -0.01,0 C-0.01,-3.91 0,-5.31 0,-7.49c " />
+                    <path
+                        android:name="_R_G_L_0_G_D_1_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M0 -7.19 C0,-7.19 2.93,-4.32 4.38,-2.87 C5.25,-2 5.88,-0.62 5.88,1.19 C5.88,4.5 2.8,6.97 0,7 C-2.8,7.03 -6,4.37 -6,1.13 C-6,-0.43 -5.38,-1.9 -4.25,-3.01 C-4.25,-3.01 0,-7.19 0,-7.19  M-5.65 -4.44 C-5.65,-4.44 -5.65,-4.44 -5.65,-4.44 C-7.1,-3.01 -8,-1.04 -8,1.13 C-8,5.48 -4.42,9 0,9 C4.42,9 8,5.48 8,1.13 C8,-1.04 7.1,-3.01 5.65,-4.44 C5.65,-4.44 5.65,-4.44 5.65,-4.44 C5.65,-4.44 0,-10 0,-10 C0,-10 -5.65,-4.44 -5.65,-4.44c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_esim_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_esim_area.xml
index db508c9..67fd5b9 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_esim_area.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_esim_area.xml
@@ -17,15 +17,17 @@
 */
 -->
 
-<!-- This contains disable esim buttonas shared by sim_pin/sim_puk screens -->
-<com.android.keyguard.KeyguardEsimArea
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<!-- This contains disable eSim buttons shared by sim_pin/sim_puk screens -->
+<com.android.keyguard.KeyguardEsimArea xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/keyguard_disable_esim"
+    style="@style/Keyguard.TextView"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:id="@+id/keyguard_disable_esim"
-    android:visibility="gone"
+    android:background="@drawable/kg_bouncer_secondary_button"
+    android:drawablePadding="10dp"
+    android:drawableStart="@drawable/ic_no_sim"
+    android:drawableTint="?android:attr/textColorPrimary"
     android:text="@string/disable_carrier_button_text"
-    style="@style/Keyguard.TextView"
-    android:textAppearance="?android:attr/textAppearanceMedium"
+    android:textColor="?android:attr/textColorPrimary"
     android:textSize="@dimen/kg_status_line_font_size"
-    android:textAllCaps="@bool/kg_use_all_caps" />
+    android:visibility="gone" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml
index f2fe520..7db0fe9 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_sim_pin_view.xml
@@ -47,8 +47,7 @@
         <include layout="@layout/keyguard_esim_area"
              android:id="@+id/keyguard_esim_area"
              android:layout_width="wrap_content"
-             android:layout_height="wrap_content"
-             android:layout_marginTop="@dimen/eca_overlap" />
+             android:layout_height="wrap_content" />
         <RelativeLayout
                 android:id="@+id/row0"
                 android:layout_width="match_parent"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml
index a21ec29..422bd4c 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_sim_puk_view.xml
@@ -51,8 +51,7 @@
         <include layout="@layout/keyguard_esim_area"
             android:id="@+id/keyguard_esim_area"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/eca_overlap" />
+            android:layout_height="wrap_content" />
 
         <RelativeLayout
                 android:id="@+id/row0"
diff --git a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
index e80cfaf..a1068c6 100644
--- a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
@@ -31,8 +31,4 @@
     <!-- Overload default clock widget parameters -->
     <dimen name="widget_big_font_size">100dp</dimen>
     <dimen name="widget_label_font_size">18sp</dimen>
-
-    <!-- EmergencyCarrierArea overlap - amount to overlap the emergency button and carrier text.
-         Should be 0 on devices with plenty of room (e.g. tablets) -->
-    <dimen name="eca_overlap">0dip</dimen>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index ac131ae..46f6ab2 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -44,10 +44,6 @@
     <dimen name="keyguard_eca_top_margin">18dp</dimen>
     <dimen name="keyguard_eca_bottom_margin">12dp</dimen>
 
-    <!-- EmergencyCarrierArea overlap - amount to overlap the emergency button and carrier text.
-         Should be 0 on devices with plenty of room (e.g. tablets) -->
-    <dimen name="eca_overlap">-10dip</dimen>
-
     <!-- Slice header -->
     <dimen name="widget_title_font_size">20dp</dimen>
     <dimen name="widget_title_line_height">24dp</dimen>
diff --git a/packages/SystemUI/res/drawable/ic_no_sim.xml b/packages/SystemUI/res/drawable/ic_no_sim.xml
new file mode 100644
index 0000000..ccfb6ea
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_no_sim.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M20,17.175 L18,15.175V4Q18,4 18,4Q18,4 18,4H10.85L8.85,6L7.4,4.6L10,2H18Q18.825,2 19.413,2.587Q20,3.175 20,4ZM20.5,23.3 L6,8.8V20Q6,20 6,20Q6,20 6,20H18Q18,20 18,20Q18,20 18,20V17.975L20,19.975V20Q20,20.825 19.413,21.413Q18.825,22 18,22H6Q5.175,22 4.588,21.413Q4,20.825 4,20V8L4.6,7.4L0.7,3.5L2.125,2.1L21.9,21.875ZM13.525,10.675Q13.525,10.675 13.525,10.675Q13.525,10.675 13.525,10.675ZM11.65,14.475Q11.65,14.475 11.65,14.475Q11.65,14.475 11.65,14.475Q11.65,14.475 11.65,14.475Q11.65,14.475 11.65,14.475Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/qs_data_saver_icon_off.xml b/packages/SystemUI/res/drawable/qs_data_saver_icon_off.xml
new file mode 100644
index 0000000..57777a6
--- /dev/null
+++ b/packages/SystemUI/res/drawable/qs_data_saver_icon_off.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="133"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="150"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="24">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:pivotY="1"
+                    android:rotation="180"
+                    android:translateX="12"
+                    android:translateY="10.984">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M3 2 C3,2 1,2 1,2 C1,2 1,4 1,4 C1,4 -1,4 -1,4 C-1,4 -1,2 -1,2 C-1,2 -3,2 -3,2 C-3,2 -3,0 -3,0 C-3,0 -1,0 -1,0 C-1,0 -1,-2 -1,-2 C-1,-2 1,-2 1,-2 C1,-2 1,0 1,0 C1,0 3,0 3,0 C3,0 3,2 3,2c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:rotation="1440"
+                    android:translateX="12"
+                    android:translateY="12">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M9.15 4.05 C9.15,4.05 6.55,2.55 6.55,2.55 C6.7,2.13 6.81,1.71 6.89,1.29 C6.96,0.86 7,0.43 7,0 C7,-1.77 6.43,-3.31 5.3,-4.62 C4.17,-5.94 2.73,-6.72 1,-6.95 C1,-6.95 1,-9.95 1,-9.95 C3.57,-9.7 5.71,-8.62 7.43,-6.72 C9.14,-4.82 10,-2.58 10,0 C10,0.7 9.94,1.39 9.81,2.08 C9.69,2.76 9.47,3.42 9.15,4.05c  M0 10 C-1.38,10 -2.68,9.74 -3.9,9.21 C-5.12,8.69 -6.17,7.98 -7.07,7.08 C-7.97,6.18 -8.69,5.12 -9.21,3.9 C-9.74,2.68 -10,1.38 -10,0 C-10,-2.58 -9.14,-4.82 -7.42,-6.72 C-5.71,-8.62 -3.57,-9.7 -1,-9.95 C-1,-9.95 -1,-6.95 -1,-6.95 C-2.73,-6.72 -4.17,-5.94 -5.3,-4.62 C-6.43,-3.31 -7,-1.77 -7,0 C-7,1.95 -6.32,3.6 -4.96,4.96 C-3.6,6.32 -1.95,7 0,7 C1.07,7 2.08,6.78 3.04,6.33 C4,5.88 4.82,5.23 5.5,4.4 C5.5,4.4 8.1,5.9 8.1,5.9 C7.15,7.2 5.97,8.21 4.55,8.93 C3.13,9.64 1.62,10 0,10c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
diff --git a/packages/SystemUI/res/drawable/qs_data_saver_icon_on.xml b/packages/SystemUI/res/drawable/qs_data_saver_icon_on.xml
new file mode 100644
index 0000000..b376413
--- /dev/null
+++ b/packages/SystemUI/res/drawable/qs_data_saver_icon_on.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M1.02 2 C1.02,2 1,2 1,2 C1,2 1,1.98 1,1.98 C1,1.98 -1,1.98 -1,1.98 C-1,1.98 -1,2 -1,2 C-1,2 -1,2 -1,2 C-1,2 -1,0 -1,0 C-1,0 -1,0 -1,0 C-1,0 -1,0 -1,0 C-1,0 1,0 1,0 C1,0 1,0 1,0 C1,0 1.02,0 1.02,0 C1.02,0 1.02,2 1.02,2c "
+                    android:valueTo="M3 2 C3,2 1,2 1,2 C1,2 1,4 1,4 C1,4 -1,4 -1,4 C-1,4 -1,2 -1,2 C-1,2 -3,2 -3,2 C-3,2 -3,0 -3,0 C-3,0 -1,0 -1,0 C-1,0 -1,-2 -1,-2 C-1,-2 1,-2 1,-2 C1,-2 1,0 1,0 C1,0 3,0 3,0 C3,0 3,2 3,2c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.2,0.2 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="613.191"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.086,0.422 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="417"
+                    android:propertyName="rotation"
+                    android:startOffset="500"
+                    android:valueFrom="613.191"
+                    android:valueTo="720"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.334,1.012 0.833,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="933"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="24">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:translateX="12"
+                    android:translateY="10.75">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M1.02 2 C1.02,2 1,2 1,2 C1,2 1,1.98 1,1.98 C1,1.98 -1,1.98 -1,1.98 C-1,1.98 -1,2 -1,2 C-1,2 -1,2 -1,2 C-1,2 -1,0 -1,0 C-1,0 -1,0 -1,0 C-1,0 -1,0 -1,0 C-1,0 1,0 1,0 C1,0 1,0 1,0 C1,0 1.02,0 1.02,0 C1.02,0 1.02,2 1.02,2c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:rotation="0"
+                    android:translateX="12"
+                    android:translateY="12">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M9.15 4.05 C9.15,4.05 6.55,2.55 6.55,2.55 C6.7,2.13 6.81,1.71 6.89,1.29 C6.96,0.86 7,0.43 7,0 C7,-1.77 6.43,-3.31 5.3,-4.62 C4.17,-5.94 2.73,-6.72 1,-6.95 C1,-6.95 1,-9.95 1,-9.95 C3.57,-9.7 5.71,-8.62 7.43,-6.72 C9.14,-4.82 10,-2.58 10,0 C10,0.7 9.94,1.39 9.81,2.08 C9.69,2.76 9.47,3.42 9.15,4.05c  M0 10 C-1.38,10 -2.68,9.74 -3.9,9.21 C-5.12,8.69 -6.17,7.98 -7.07,7.08 C-7.97,6.18 -8.69,5.12 -9.21,3.9 C-9.74,2.68 -10,1.38 -10,0 C-10,-2.58 -9.14,-4.82 -7.42,-6.72 C-5.71,-8.62 -3.57,-9.7 -1,-9.95 C-1,-9.95 -1,-6.95 -1,-6.95 C-2.73,-6.72 -4.17,-5.94 -5.3,-4.62 C-6.43,-3.31 -7,-1.77 -7,0 C-7,1.95 -6.32,3.6 -4.96,4.96 C-3.6,6.32 -1.95,7 0,7 C1.07,7 2.08,6.78 3.04,6.33 C4,5.88 4.82,5.23 5.5,4.4 C5.5,4.4 8.1,5.9 8.1,5.9 C7.15,7.2 5.97,8.21 4.55,8.93 C3.13,9.64 1.62,10 0,10c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
diff --git a/packages/SystemUI/res/drawable/qs_nightlight_icon_off.xml b/packages/SystemUI/res/drawable/qs_nightlight_icon_off.xml
new file mode 100644
index 0000000..0769a85
--- /dev/null
+++ b/packages/SystemUI/res/drawable/qs_nightlight_icon_off.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+    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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="283"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="12.125"
+                    android:valueTo="12.312"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="283"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="-45"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="300"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="24">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_0_G_T_1"
+                    android:rotation="-45"
+                    android:translateX="12.875"
+                    android:translateY="12.125">
+                    <group
+                        android:name="_R_G_L_0_G"
+                        android:translateX="-2.375">
+                        <path
+                            android:name="_R_G_L_0_G_D_0_P_0"
+                            android:pathData=" M3.33 8.62 C2.09,8.68 0.59,8.47 -0.64,7.95 C-1.65,7.53 -2.75,6.9 -3.59,6.09 C-4.38,5.32 -5.19,4.17 -5.61,3.08 C-6.04,1.99 -6.25,0.83 -6.25,-0.39 C-6.25,-1.72 -5.98,-2.85 -5.55,-3.94 C-5.13,-5.02 -4.37,-6 -3.59,-6.78 C-2.63,-7.75 -1.63,-8.28 -0.52,-8.77 C0.48,-9.2 2.04,-9.41 3.15,-9.38 C4.22,-9.35 4.94,-9.16 5.81,-8.79 C5.95,-8.73 6.28,-8.6 6.18,-8.55 C3.44,-6.63 1.83,-3.66 1.83,-0.39 C1.83,2.53 3.47,5.72 6.15,7.86 C6.16,7.9 6.03,7.97 5.91,8.04 C5.12,8.44 4.31,8.56 3.33,8.62c "
+                            android:strokeAlpha="1"
+                            android:strokeColor="#ffffff"
+                            android:strokeLineCap="round"
+                            android:strokeLineJoin="round"
+                            android:strokeWidth="2" />
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
diff --git a/packages/SystemUI/res/drawable/qs_nightlight_icon_on.xml b/packages/SystemUI/res/drawable/qs_nightlight_icon_on.xml
new file mode 100644
index 0000000..5ffe262
--- /dev/null
+++ b/packages/SystemUI/res/drawable/qs_nightlight_icon_on.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+    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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="400"
+                    android:propertyName="translateY"
+                    android:startOffset="0"
+                    android:valueFrom="12.312"
+                    android:valueTo="12.125"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="400"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="-45"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="417"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="24">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_0_G_T_1"
+                    android:rotation="0"
+                    android:translateX="12.875"
+                    android:translateY="12.312">
+                    <group
+                        android:name="_R_G_L_0_G"
+                        android:translateX="-2.375">
+                        <path
+                            android:name="_R_G_L_0_G_D_0_P_0"
+                            android:pathData=" M3.33 8.62 C2.09,8.68 0.59,8.47 -0.64,7.95 C-1.65,7.53 -2.75,6.9 -3.59,6.09 C-4.38,5.32 -5.19,4.17 -5.61,3.08 C-6.04,1.99 -6.25,0.83 -6.25,-0.39 C-6.25,-1.72 -5.98,-2.85 -5.55,-3.94 C-5.13,-5.02 -4.37,-6 -3.6,-6.78 C-2.63,-7.75 -1.63,-8.28 -0.52,-8.77 C0.48,-9.2 2.04,-9.41 3.14,-9.38 C4.22,-9.35 4.94,-9.16 5.81,-8.79 C5.94,-8.73 6.28,-8.6 6.18,-8.55 C3.44,-6.63 1.83,-3.66 1.83,-0.39 C1.83,2.53 3.47,5.72 6.15,7.86 C6.16,7.9 6.03,7.97 5.91,8.04 C5.13,8.43 4.31,8.56 3.33,8.62c "
+                            android:strokeAlpha="1"
+                            android:strokeColor="#ffffff"
+                            android:strokeLineCap="round"
+                            android:strokeLineJoin="round"
+                            android:strokeWidth="2" />
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index c827e21..75baeef 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -241,6 +241,10 @@
     <color name="dream_overlay_aqi_very_unhealthy">#AD1457</color>
     <color name="dream_overlay_aqi_hazardous">#880E4F</color>
     <color name="dream_overlay_aqi_unknown">#BDC1C6</color>
+
+    <!-- Dream overlay text shadows -->
     <color name="dream_overlay_clock_key_text_shadow_color">#4D000000</color>
     <color name="dream_overlay_clock_ambient_text_shadow_color">#4D000000</color>
+    <color name="dream_overlay_status_bar_key_text_shadow_color">#66000000</color>
+    <color name="dream_overlay_status_bar_ambient_text_shadow_color">#59000000</color>
 </resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index a802723..37549c9 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -611,6 +611,33 @@
          2 - Override the setting to never bypass keyguard -->
     <integer name="config_face_unlock_bypass_override">0</integer>
 
+    <!-- Messages that should NOT be shown to the user during face authentication on keyguard.
+         This includes both lockscreen and bouncer. This should be used to hide messages that may be
+         too chatty or messages that the user can't do much about. Entries are defined in
+         android.hardware.biometrics.face@1.0 types.hal.
+
+         Although not visibly shown to the user, these acquired messages (sent per face auth frame)
+         are still counted towards the total frames to determine whether a deferred message
+         (see config_face_help_msgs_defer_until_timeout) meets the threshold % of frames to show on
+         face timeout. -->
+     <integer-array name="config_face_acquire_device_entry_ignorelist" translatable="false" >
+    </integer-array>
+
+    <!-- Which face help messages to defer until face auth times out. If face auth is cancelled
+         or ends on another error, then the message is never surfaced. May also never surface
+         if it doesn't meet a threshold % of authentication frames specified by.
+         config_face_help_msgs_defer_until_timeout_threshold. -->
+    <integer-array name="config_face_help_msgs_defer_until_timeout">
+    </integer-array>
+
+    <!-- Percentage of face auth frames received required to show a deferred message at
+         FACE_ERROR_TIMEOUT. See config_face_help_msgs_defer_until_timeout for messages
+         that are deferred.-->
+    <item name="config_face_help_msgs_defer_until_timeout_threshold"
+          translatable="false" format="float" type="dimen">
+        .75
+    </item>
+
     <!-- Which face help messages to surface when fingerprint is also enrolled.
          Message ids correspond with the acquired ids in BiometricFaceConstants -->
     <integer-array name="config_face_help_msgs_when_fingerprint_enrolled">
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 1b169e2..eb6c457 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1547,10 +1547,20 @@
     <dimen name="broadcast_dialog_btn_text_size">16sp</dimen>
     <dimen name="broadcast_dialog_btn_minHeight">44dp</dimen>
     <dimen name="broadcast_dialog_margin">16dp</dimen>
+
+    <!-- Shadow for dream overlay clock complication -->
     <dimen name="dream_overlay_clock_key_text_shadow_dx">0dp</dimen>
     <dimen name="dream_overlay_clock_key_text_shadow_dy">0dp</dimen>
-    <dimen name="dream_overlay_clock_key_text_shadow_radius">5dp</dimen>
+    <dimen name="dream_overlay_clock_key_text_shadow_radius">3dp</dimen>
     <dimen name="dream_overlay_clock_ambient_text_shadow_dx">0dp</dimen>
     <dimen name="dream_overlay_clock_ambient_text_shadow_dy">0dp</dimen>
     <dimen name="dream_overlay_clock_ambient_text_shadow_radius">1dp</dimen>
+
+    <!-- Shadow for dream overlay status bar complications -->
+    <dimen name="dream_overlay_status_bar_key_text_shadow_dx">0.5dp</dimen>
+    <dimen name="dream_overlay_status_bar_key_text_shadow_dy">0.5dp</dimen>
+    <dimen name="dream_overlay_status_bar_key_text_shadow_radius">1dp</dimen>
+    <dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen>
+    <dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen>
+    <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen>
 </resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index b3b75f6..34e2e83 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -151,8 +151,6 @@
         // relayout if the text didn't actually change.
         if (!TextUtils.equals(text, formattedText)) {
             text = formattedText
-            lastTextUpdate = getTimestamp()
-
             // Because the TextLayout may mutate under the hood as a result of the new text, we
             // notify the TextAnimator that it may have changed and request a measure/layout. A
             // crash will occur on the next invocation of setTextStyle if the layout is mutated
@@ -161,6 +159,7 @@
                 textAnimator?.updateLayout(layout)
             }
             requestLayout()
+            lastTextUpdate = getTimestamp()
         }
     }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index b5e5766..19ac2e4 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -90,6 +90,9 @@
     override lateinit var animations: ClockAnimations
         private set
 
+    private var smallRegionDarkness = false
+    private var largeRegionDarkness = false
+
     init {
         val parent = FrameLayout(ctx)
 
@@ -148,8 +151,14 @@
                 smallClockIsDark: Boolean,
                 largeClockIsDark: Boolean
         ) {
-            updateClockColor(smallClock, smallClockIsDark)
-            updateClockColor(largeClock, largeClockIsDark)
+            if (smallRegionDarkness != smallClockIsDark) {
+                smallRegionDarkness = smallClockIsDark
+                updateClockColor(smallClock, smallClockIsDark)
+            }
+            if (largeRegionDarkness != largeClockIsDark) {
+                largeRegionDarkness = largeClockIsDark
+                updateClockColor(largeClock, largeClockIsDark)
+            }
         }
 
         override fun onLocaleChanged(locale: Locale) {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
new file mode 100644
index 0000000..30c062b
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.shared.system;
+
+import android.graphics.HardwareRenderer;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Message;
+import android.os.Trace;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import android.view.View;
+import android.view.ViewRootImpl;
+
+import java.util.function.Consumer;
+
+/**
+ * Helper class to apply surface transactions in sync with RenderThread.
+ *
+ * NOTE: This is a modification of {@link android.view.SyncRtSurfaceTransactionApplier}, we can't 
+ *       currently reference that class from the shared lib as it is hidden.
+ */
+public class SyncRtSurfaceTransactionApplierCompat {
+
+    public static final int FLAG_ALL = 0xffffffff;
+    public static final int FLAG_ALPHA = 1;
+    public static final int FLAG_MATRIX = 1 << 1;
+    public static final int FLAG_WINDOW_CROP = 1 << 2;
+    public static final int FLAG_LAYER = 1 << 3;
+    public static final int FLAG_CORNER_RADIUS = 1 << 4;
+    public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5;
+    public static final int FLAG_VISIBILITY = 1 << 6;
+    public static final int FLAG_RELATIVE_LAYER = 1 << 7;
+    public static final int FLAG_SHADOW_RADIUS = 1 << 8;
+
+    private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0;
+
+    private final SurfaceControl mBarrierSurfaceControl;
+    private final ViewRootImpl mTargetViewRootImpl;
+    private final Handler mApplyHandler;
+
+    private int mSequenceNumber = 0;
+    private int mPendingSequenceNumber = 0;
+    private Runnable mAfterApplyCallback;
+
+    /**
+     * @param targetView The view in the surface that acts as synchronization anchor.
+     */
+    public SyncRtSurfaceTransactionApplierCompat(View targetView) {
+        mTargetViewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
+        mBarrierSurfaceControl = mTargetViewRootImpl != null
+            ? mTargetViewRootImpl.getSurfaceControl() : null;
+
+        mApplyHandler = new Handler(new Callback() {
+            @Override
+            public boolean handleMessage(Message msg) {
+                if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) {
+                    onApplyMessage(msg.arg1);
+                    return true;
+                }
+                return false;
+            }
+        });
+    }
+
+    private void onApplyMessage(int seqNo) {
+        mSequenceNumber = seqNo;
+        if (mSequenceNumber == mPendingSequenceNumber && mAfterApplyCallback != null) {
+            Runnable r = mAfterApplyCallback;
+            mAfterApplyCallback = null;
+            r.run();
+        }
+    }
+
+    /**
+     * Schedules applying surface parameters on the next frame.
+     *
+     * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into
+     *               this method to avoid synchronization issues.
+     */
+    public void scheduleApply(final SyncRtSurfaceTransactionApplierCompat.SurfaceParams... params) {
+        if (mTargetViewRootImpl == null || mTargetViewRootImpl.getView() == null) {
+            return;
+        }
+
+        mPendingSequenceNumber++;
+        final int toApplySeqNo = mPendingSequenceNumber;
+        mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() {
+            @Override
+            public void onFrameDraw(long frame) {
+                if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) {
+                    Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
+                            .sendToTarget();
+                    return;
+                }
+                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Sync transaction frameNumber=" + frame);
+                Transaction t = new Transaction();
+                for (int i = params.length - 1; i >= 0; i--) {
+                    SyncRtSurfaceTransactionApplierCompat.SurfaceParams surfaceParams =
+                            params[i];
+                    surfaceParams.applyTo(t);
+                }
+                if (mTargetViewRootImpl != null) {
+                    mTargetViewRootImpl.mergeWithNextTransaction(t, frame);
+                } else {
+                    t.apply();
+                }
+                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+                Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
+                        .sendToTarget();
+            }
+        });
+
+        // Make sure a frame gets scheduled.
+        mTargetViewRootImpl.getView().invalidate();
+    }
+
+    /**
+     * Calls the runnable when any pending apply calls have completed
+     */
+    public void addAfterApplyCallback(final Runnable afterApplyCallback) {
+        if (mSequenceNumber == mPendingSequenceNumber) {
+            afterApplyCallback.run();
+        } else {
+            if (mAfterApplyCallback == null) {
+                mAfterApplyCallback = afterApplyCallback;
+            } else {
+                final Runnable oldCallback = mAfterApplyCallback;
+                mAfterApplyCallback = new Runnable() {
+                    @Override
+                    public void run() {
+                        afterApplyCallback.run();
+                        oldCallback.run();
+                    }
+                };
+            }
+        }
+    }
+
+    public static void applyParams(TransactionCompat t,
+            SyncRtSurfaceTransactionApplierCompat.SurfaceParams params) {
+        params.applyTo(t.mTransaction);
+    }
+
+    /**
+     * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is
+     * attached if necessary.
+     */
+    public static void create(final View targetView,
+            final Consumer<SyncRtSurfaceTransactionApplierCompat> callback) {
+        if (targetView == null) {
+            // No target view, no applier
+            callback.accept(null);
+        } else if (targetView.getViewRootImpl() != null) {
+            // Already attached, we're good to go
+            callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
+        } else {
+            // Haven't been attached before we can get the view root
+            targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View v) {
+                    targetView.removeOnAttachStateChangeListener(this);
+                    callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
+                }
+
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                    // Do nothing
+                }
+            });
+        }
+    }
+
+    public static class SurfaceParams {
+        public static class Builder {
+            final SurfaceControl surface;
+            int flags;
+            float alpha;
+            float cornerRadius;
+            int backgroundBlurRadius;
+            Matrix matrix;
+            Rect windowCrop;
+            int layer;
+            SurfaceControl relativeTo;
+            int relativeLayer;
+            boolean visible;
+            float shadowRadius;
+
+            /**
+             * @param surface The surface to modify.
+             */
+            public Builder(SurfaceControl surface) {
+                this.surface = surface;
+            }
+
+            /**
+             * @param alpha The alpha value to apply to the surface.
+             * @return this Builder
+             */
+            public Builder withAlpha(float alpha) {
+                this.alpha = alpha;
+                flags |= FLAG_ALPHA;
+                return this;
+            }
+
+            /**
+             * @param matrix The matrix to apply to the surface.
+             * @return this Builder
+             */
+            public Builder withMatrix(Matrix matrix) {
+                this.matrix = new Matrix(matrix);
+                flags |= FLAG_MATRIX;
+                return this;
+            }
+
+            /**
+             * @param windowCrop The window crop to apply to the surface.
+             * @return this Builder
+             */
+            public Builder withWindowCrop(Rect windowCrop) {
+                this.windowCrop = new Rect(windowCrop);
+                flags |= FLAG_WINDOW_CROP;
+                return this;
+            }
+
+            /**
+             * @param layer The layer to assign the surface.
+             * @return this Builder
+             */
+            public Builder withLayer(int layer) {
+                this.layer = layer;
+                flags |= FLAG_LAYER;
+                return this;
+            }
+
+            /**
+             * @param relativeTo The surface that's set relative layer to.
+             * @param relativeLayer The relative layer.
+             * @return this Builder
+             */
+            public Builder withRelativeLayerTo(SurfaceControl relativeTo, int relativeLayer) {
+                this.relativeTo = relativeTo;
+                this.relativeLayer = relativeLayer;
+                flags |= FLAG_RELATIVE_LAYER;
+                return this;
+            }
+
+            /**
+             * @param radius the Radius for rounded corners to apply to the surface.
+             * @return this Builder
+             */
+            public Builder withCornerRadius(float radius) {
+                this.cornerRadius = radius;
+                flags |= FLAG_CORNER_RADIUS;
+                return this;
+            }
+
+            /**
+             * @param radius the Radius for the shadows to apply to the surface.
+             * @return this Builder
+             */
+            public Builder withShadowRadius(float radius) {
+                this.shadowRadius = radius;
+                flags |= FLAG_SHADOW_RADIUS;
+                return this;
+            }
+
+            /**
+             * @param radius the Radius for blur to apply to the background surfaces.
+             * @return this Builder
+             */
+            public Builder withBackgroundBlur(int radius) {
+                this.backgroundBlurRadius = radius;
+                flags |= FLAG_BACKGROUND_BLUR_RADIUS;
+                return this;
+            }
+
+            /**
+             * @param visible The visibility to apply to the surface.
+             * @return this Builder
+             */
+            public Builder withVisibility(boolean visible) {
+                this.visible = visible;
+                flags |= FLAG_VISIBILITY;
+                return this;
+            }
+
+            /**
+             * @return a new SurfaceParams instance
+             */
+            public SurfaceParams build() {
+                return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer,
+                        relativeTo, relativeLayer, cornerRadius, backgroundBlurRadius, visible,
+                        shadowRadius);
+            }
+        }
+
+        private SurfaceParams(SurfaceControl surface, int flags, float alpha, Matrix matrix,
+                Rect windowCrop, int layer, SurfaceControl relativeTo, int relativeLayer,
+                float cornerRadius, int backgroundBlurRadius, boolean visible, float shadowRadius) {
+            this.flags = flags;
+            this.surface = surface;
+            this.alpha = alpha;
+            this.matrix = matrix;
+            this.windowCrop = windowCrop;
+            this.layer = layer;
+            this.relativeTo = relativeTo;
+            this.relativeLayer = relativeLayer;
+            this.cornerRadius = cornerRadius;
+            this.backgroundBlurRadius = backgroundBlurRadius;
+            this.visible = visible;
+            this.shadowRadius = shadowRadius;
+        }
+
+        private final int flags;
+        private final float[] mTmpValues = new float[9];
+
+        public final SurfaceControl surface;
+        public final float alpha;
+        public final float cornerRadius;
+        public final int backgroundBlurRadius;
+        public final Matrix matrix;
+        public final Rect windowCrop;
+        public final int layer;
+        public final SurfaceControl relativeTo;
+        public final int relativeLayer;
+        public final boolean visible;
+        public final float shadowRadius;
+
+        public void applyTo(SurfaceControl.Transaction t) {
+            if ((flags & FLAG_MATRIX) != 0) {
+                t.setMatrix(surface, matrix, mTmpValues);
+            }
+            if ((flags & FLAG_WINDOW_CROP) != 0) {
+                t.setWindowCrop(surface, windowCrop);
+            }
+            if ((flags & FLAG_ALPHA) != 0) {
+                t.setAlpha(surface, alpha);
+            }
+            if ((flags & FLAG_LAYER) != 0) {
+                t.setLayer(surface, layer);
+            }
+            if ((flags & FLAG_CORNER_RADIUS) != 0) {
+                t.setCornerRadius(surface, cornerRadius);
+            }
+            if ((flags & FLAG_BACKGROUND_BLUR_RADIUS) != 0) {
+                t.setBackgroundBlurRadius(surface, backgroundBlurRadius);
+            }
+            if ((flags & FLAG_VISIBILITY) != 0) {
+                if (visible) {
+                    t.show(surface);
+                } else {
+                    t.hide(surface);
+                }
+            }
+            if ((flags & FLAG_RELATIVE_LAYER) != 0) {
+                t.setRelativeLayer(surface, relativeTo, relativeLayer);
+            }
+            if ((flags & FLAG_SHADOW_RADIUS) != 0) {
+                t.setShadowRadius(surface, shadowRadius);
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
new file mode 100644
index 0000000..43a882a5
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.shared.system;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+public class TransactionCompat {
+
+    final Transaction mTransaction;
+
+    final float[] mTmpValues = new float[9];
+
+    public TransactionCompat() {
+        mTransaction = new Transaction();
+    }
+
+    public void apply() {
+        mTransaction.apply();
+    }
+
+    public TransactionCompat show(SurfaceControl surfaceControl) {
+        mTransaction.show(surfaceControl);
+        return this;
+    }
+
+    public TransactionCompat hide(SurfaceControl surfaceControl) {
+        mTransaction.hide(surfaceControl);
+        return this;
+    }
+
+    public TransactionCompat setPosition(SurfaceControl surfaceControl, float x, float y) {
+        mTransaction.setPosition(surfaceControl, x, y);
+        return this;
+    }
+
+    public TransactionCompat setSize(SurfaceControl surfaceControl, int w, int h) {
+        mTransaction.setBufferSize(surfaceControl, w, h);
+        return this;
+    }
+
+    public TransactionCompat setLayer(SurfaceControl surfaceControl, int z) {
+        mTransaction.setLayer(surfaceControl, z);
+        return this;
+    }
+
+    public TransactionCompat setAlpha(SurfaceControl surfaceControl, float alpha) {
+        mTransaction.setAlpha(surfaceControl, alpha);
+        return this;
+    }
+
+    public TransactionCompat setOpaque(SurfaceControl surfaceControl, boolean opaque) {
+        mTransaction.setOpaque(surfaceControl, opaque);
+        return this;
+    }
+
+    public TransactionCompat setMatrix(SurfaceControl surfaceControl, float dsdx, float dtdx,
+            float dtdy, float dsdy) {
+        mTransaction.setMatrix(surfaceControl, dsdx, dtdx, dtdy, dsdy);
+        return this;
+    }
+
+    public TransactionCompat setMatrix(SurfaceControl surfaceControl, Matrix matrix) {
+        mTransaction.setMatrix(surfaceControl, matrix, mTmpValues);
+        return this;
+    }
+
+    public TransactionCompat setWindowCrop(SurfaceControl surfaceControl, Rect crop) {
+        mTransaction.setWindowCrop(surfaceControl, crop);
+        return this;
+    }
+
+    public TransactionCompat setCornerRadius(SurfaceControl surfaceControl, float radius) {
+        mTransaction.setCornerRadius(surfaceControl, radius);
+        return this;
+    }
+
+    public TransactionCompat setBackgroundBlurRadius(SurfaceControl surfaceControl, int radius) {
+        mTransaction.setBackgroundBlurRadius(surfaceControl, radius);
+        return this;
+    }
+
+    public TransactionCompat setColor(SurfaceControl surfaceControl, float[] color) {
+        mTransaction.setColor(surfaceControl, color);
+        return this;
+    }
+
+    public static void setRelativeLayer(Transaction t, SurfaceControl surfaceControl,
+            SurfaceControl relativeTo, int z) {
+        t.setRelativeLayer(surfaceControl, relativeTo, z);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt
new file mode 100644
index 0000000..b4bfca1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt
@@ -0,0 +1,54 @@
+/*
+ * 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
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageView
+import androidx.core.graphics.drawable.DrawableCompat
+import com.android.systemui.R
+
+abstract class KeyguardSimInputView(context: Context, attrs: AttributeSet) :
+    KeyguardPinBasedInputView(context, attrs) {
+    private var simImageView: ImageView? = null
+    private var disableESimButton: KeyguardEsimArea? = null
+
+    override fun onFinishInflate() {
+        simImageView = findViewById(R.id.keyguard_sim)
+        disableESimButton = findViewById(R.id.keyguard_esim_area)
+        super.onFinishInflate()
+    }
+
+    /** Set UI state based on whether there is a locked eSim card */
+    fun setESimLocked(isESimLocked: Boolean, subId: Int) {
+        disableESimButton?.setSubscriptionId(subId)
+        disableESimButton?.visibility = if (isESimLocked) VISIBLE else GONE
+        simImageView?.visibility = if (isESimLocked) GONE else VISIBLE
+    }
+
+    override fun reloadColors() {
+        super.reloadColors()
+        val customAttrs = intArrayOf(android.R.attr.textColorSecondary)
+        val a = context.obtainStyledAttributes(customAttrs)
+        val imageColor = a.getColor(0, 0)
+        a.recycle()
+        simImageView?.let {
+            val wrappedDrawable = DrawableCompat.wrap(it.drawable)
+            DrawableCompat.setTint(wrappedDrawable, imageColor)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java
index ae9d3df..9d17015 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java
@@ -18,21 +18,14 @@
 
 import android.content.Context;
 import android.content.res.Configuration;
-import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-
-import androidx.core.graphics.drawable.DrawableCompat;
 
 import com.android.systemui.R;
 
 /**
  * Displays a PIN pad for unlocking.
  */
-public class KeyguardSimPinView extends KeyguardPinBasedInputView {
-    private ImageView mSimImageView;
+public class KeyguardSimPinView extends KeyguardSimInputView {
     public static final String TAG = "KeyguardSimPinView";
 
     public KeyguardSimPinView(Context context) {
@@ -43,12 +36,6 @@
         super(context, attrs);
     }
 
-    public void setEsimLocked(boolean locked, int subscriptionId) {
-        KeyguardEsimArea esimButton = findViewById(R.id.keyguard_esim_area);
-        esimButton.setSubscriptionId(subscriptionId);
-        esimButton.setVisibility(locked ? View.VISIBLE : View.GONE);
-    }
-
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
@@ -68,7 +55,6 @@
 
     @Override
     protected void onFinishInflate() {
-        mSimImageView = findViewById(R.id.keyguard_sim);
         super.onFinishInflate();
 
         if (mEcaView instanceof EmergencyCarrierArea) {
@@ -86,17 +72,4 @@
         return getContext().getString(
                 com.android.internal.R.string.keyguard_accessibility_sim_pin_unlock);
     }
-
-    @Override
-    public void reloadColors() {
-        super.reloadColors();
-
-        int[] customAttrs = {android.R.attr.textColorSecondary};
-        TypedArray a = getContext().obtainStyledAttributes(customAttrs);
-        int imageColor = a.getColor(0, 0);
-        a.recycle();
-        Drawable wrappedDrawable = DrawableCompat.wrap(mSimImageView.getDrawable());
-        DrawableCompat.setTint(wrappedDrawable, imageColor);
-    }
 }
-
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
index ecd88e6..76f7d78 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
@@ -105,7 +105,7 @@
             showDefaultMessage();
         }
 
-        mView.setEsimLocked(KeyguardEsimArea.isEsimLocked(mView.getContext(), mSubId), mSubId);
+        mView.setESimLocked(KeyguardEsimArea.isEsimLocked(mView.getContext(), mSubId), mSubId);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java
index c0971bf..5f45fe3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java
@@ -19,13 +19,8 @@
 import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat;
 
 import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.util.Log;
-import android.widget.ImageView;
-
-import androidx.core.graphics.drawable.DrawableCompat;
 
 import com.android.systemui.R;
 
@@ -33,8 +28,7 @@
 /**
  * Displays a PIN pad for entering a PUK (Pin Unlock Kode) provided by a carrier.
  */
-public class KeyguardSimPukView extends KeyguardPinBasedInputView {
-    private ImageView mSimImageView;
+public class KeyguardSimPukView extends KeyguardSimInputView {
     private static final boolean DEBUG = KeyguardConstants.DEBUG;
     public static final String TAG = "KeyguardSimPukView";
 
@@ -86,7 +80,6 @@
 
     @Override
     protected void onFinishInflate() {
-        mSimImageView = findViewById(R.id.keyguard_sim);
         super.onFinishInflate();
 
         if (mEcaView instanceof EmergencyCarrierArea) {
@@ -104,18 +97,4 @@
         return getContext().getString(
                 com.android.internal.R.string.keyguard_accessibility_sim_puk_unlock);
     }
-
-    @Override
-    public void reloadColors() {
-        super.reloadColors();
-
-        int[] customAttrs = {android.R.attr.textColorSecondary};
-        TypedArray a = getContext().obtainStyledAttributes(customAttrs);
-        int imageColor = a.getColor(0, 0);
-        a.recycle();
-        Drawable wrappedDrawable = DrawableCompat.wrap(mSimImageView.getDrawable());
-        DrawableCompat.setTint(wrappedDrawable, imageColor);
-    }
 }
-
-
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
index 203f9b6..d8cffd7 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
@@ -30,7 +30,6 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.Log;
-import android.view.View;
 import android.view.WindowManager;
 import android.widget.ImageView;
 
@@ -173,11 +172,9 @@
             if (mShowDefaultMessage) {
                 showDefaultMessage();
             }
-            boolean isEsimLocked = KeyguardEsimArea.isEsimLocked(mView.getContext(), mSubId);
 
-            KeyguardEsimArea esimButton = mView.findViewById(R.id.keyguard_esim_area);
-            esimButton.setSubscriptionId(mSubId);
-            esimButton.setVisibility(isEsimLocked ? View.VISIBLE : View.GONE);
+            mView.setESimLocked(KeyguardEsimArea.isEsimLocked(mView.getContext(), mSubId), mSubId);
+
             mPasswordEntry.requestFocus();
         }
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 763b29e..32c1cf9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -157,6 +157,7 @@
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -167,6 +168,7 @@
 import java.util.TimeZone;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -281,6 +283,7 @@
     private final AuthController mAuthController;
     private final StatusBarStateController mStatusBarStateController;
     private final UiEventLogger mUiEventLogger;
+    private final Set<Integer> mFaceAcquiredInfoIgnoreList;
     private int mStatusBarState;
     private final StatusBarStateController.StateListener mStatusBarStateControllerListener =
             new StatusBarStateController.StateListener() {
@@ -1023,6 +1026,7 @@
 
     private void handleFaceAuthFailed() {
         Assert.isMainThread();
+        mLogger.d("onFaceAuthFailed");
         mFaceCancelSignal = null;
         setFaceRunningState(BIOMETRIC_STATE_STOPPED);
         for (int i = 0; i < mCallbacks.size(); i++) {
@@ -1639,6 +1643,9 @@
 
                 @Override
                 public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
+                    if (mFaceAcquiredInfoIgnoreList.contains(helpMsgId)) {
+                        return;
+                    }
                     handleFaceHelp(helpMsgId, helpString.toString());
                 }
 
@@ -1931,6 +1938,11 @@
         mActiveUnlockConfig.setKeyguardUpdateMonitor(this);
         mWakeOnFingerprintAcquiredStart = context.getResources()
                         .getBoolean(com.android.internal.R.bool.kg_wake_on_acquire_start);
+        mFaceAcquiredInfoIgnoreList = Arrays.stream(
+                mContext.getResources().getIntArray(
+                        R.array.config_face_acquire_device_entry_ignorelist))
+                .boxed()
+                .collect(Collectors.toSet());
 
         mHandler = new Handler(mainLooper) {
             @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
new file mode 100644
index 0000000..2c2ab7b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.LogBuffer
+import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.dagger.BiometricMessagesLog
+import javax.inject.Inject
+
+/** Helper class for logging for [com.android.systemui.biometrics.FaceHelpMessageDeferral] */
+@SysUISingleton
+class FaceMessageDeferralLogger
+@Inject
+constructor(@BiometricMessagesLog private val logBuffer: LogBuffer) :
+    BiometricMessageDeferralLogger(logBuffer, "FaceMessageDeferralLogger")
+
+open class BiometricMessageDeferralLogger(
+    private val logBuffer: LogBuffer,
+    private val tag: String
+) {
+    fun reset() {
+        logBuffer.log(tag, DEBUG, "reset")
+    }
+
+    fun logUpdateMessage(acquiredInfo: Int, helpString: String) {
+        logBuffer.log(
+            tag,
+            DEBUG,
+            {
+                int1 = acquiredInfo
+                str1 = helpString
+            },
+            { "updateMessage acquiredInfo=$int1 helpString=$str1" }
+        )
+    }
+
+    fun logFrameProcessed(
+        acquiredInfo: Int,
+        totalFrames: Int,
+        mostFrequentAcquiredInfoToDefer: String? // may not meet the threshold
+    ) {
+        logBuffer.log(
+            tag,
+            DEBUG,
+            {
+                int1 = acquiredInfo
+                int2 = totalFrames
+                str1 = mostFrequentAcquiredInfoToDefer
+            },
+            {
+                "frameProcessed acquiredInfo=$int1 totalFrames=$int2 " +
+                    "messageToShowOnTimeout=$str1"
+            }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricMessageDeferral.kt b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricMessageDeferral.kt
deleted file mode 100644
index f2d8aaa..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricMessageDeferral.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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.systemui.biometrics
-
-/**
- * Provides whether an acquired error message should be shown immediately when its received (see
- * [shouldDefer]) or should be shown when the biometric error is received [getDeferredMessage].
- * @property excludedMessages messages that are excluded from counts
- * @property messagesToDefer messages that shouldn't show immediately when received, but may be
- * shown later if the message is the most frequent message processed and meets [THRESHOLD]
- * percentage of all messages (excluding [excludedMessages])
- */
-class BiometricMessageDeferral(
-    private val excludedMessages: Set<Int>,
-    private val messagesToDefer: Set<Int>
-) {
-    private val msgCounts: MutableMap<Int, Int> = HashMap() // msgId => frequency of msg
-    private val msgIdToCharSequence: MutableMap<Int, CharSequence> = HashMap() // msgId => message
-    private var totalRelevantMessages = 0
-    private var mostFrequentMsgIdToDefer: Int? = null
-
-    /** Reset all saved counts. */
-    fun reset() {
-        totalRelevantMessages = 0
-        msgCounts.clear()
-        msgIdToCharSequence.clear()
-    }
-
-    /** Whether the given message should be deferred instead of being shown immediately. */
-    fun shouldDefer(acquiredMsgId: Int): Boolean {
-        return messagesToDefer.contains(acquiredMsgId)
-    }
-
-    /**
-     * Adds the acquiredMsgId to the counts if it's not in [excludedMessages]. We still count
-     * messages that shouldn't be deferred in these counts.
-     */
-    fun processMessage(acquiredMsgId: Int, helpString: CharSequence) {
-        if (excludedMessages.contains(acquiredMsgId)) {
-            return
-        }
-
-        totalRelevantMessages++
-        msgIdToCharSequence[acquiredMsgId] = helpString
-
-        val newAcquiredMsgCount = msgCounts.getOrDefault(acquiredMsgId, 0) + 1
-        msgCounts[acquiredMsgId] = newAcquiredMsgCount
-        if (
-            messagesToDefer.contains(acquiredMsgId) &&
-                (mostFrequentMsgIdToDefer == null ||
-                    newAcquiredMsgCount > msgCounts.getOrDefault(mostFrequentMsgIdToDefer!!, 0))
-        ) {
-            mostFrequentMsgIdToDefer = acquiredMsgId
-        }
-    }
-
-    /**
-     * Get the most frequent deferred message that meets the [THRESHOLD] percentage of processed
-     * messages excluding [excludedMessages].
-     * @return null if no messages have been deferred OR deferred messages didn't meet the
-     * [THRESHOLD] percentage of messages to show.
-     */
-    fun getDeferredMessage(): CharSequence? {
-        mostFrequentMsgIdToDefer?.let {
-            if (msgCounts.getOrDefault(it, 0) > (THRESHOLD * totalRelevantMessages)) {
-                return msgIdToCharSequence[mostFrequentMsgIdToDefer]
-            }
-        }
-
-        return null
-    }
-    companion object {
-        const val THRESHOLD = .5f
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt
new file mode 100644
index 0000000..fabc1c1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.systemui.biometrics
+
+import android.content.res.Resources
+import com.android.keyguard.logging.BiometricMessageDeferralLogger
+import com.android.keyguard.logging.FaceMessageDeferralLogger
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import java.io.PrintWriter
+import java.util.*
+import javax.inject.Inject
+
+/**
+ * Provides whether a face acquired help message should be shown immediately when its received or
+ * should be shown when face auth times out. See [updateMessage] and [getDeferredMessage].
+ */
+@SysUISingleton
+class FaceHelpMessageDeferral
+@Inject
+constructor(
+    @Main resources: Resources,
+    logBuffer: FaceMessageDeferralLogger,
+    dumpManager: DumpManager
+) :
+    BiometricMessageDeferral(
+        resources.getIntArray(R.array.config_face_help_msgs_defer_until_timeout).toHashSet(),
+        resources.getFloat(R.dimen.config_face_help_msgs_defer_until_timeout_threshold),
+        logBuffer,
+        dumpManager
+    )
+
+/**
+ * @property messagesToDefer messages that shouldn't show immediately when received, but may be
+ * shown later if the message is the most frequent acquiredInfo processed and meets [threshold]
+ * percentage of all passed acquired frames.
+ */
+open class BiometricMessageDeferral(
+    private val messagesToDefer: Set<Int>,
+    private val threshold: Float,
+    private val logBuffer: BiometricMessageDeferralLogger,
+    dumpManager: DumpManager
+) : Dumpable {
+    private val acquiredInfoToFrequency: MutableMap<Int, Int> = HashMap()
+    private val acquiredInfoToHelpString: MutableMap<Int, String> = HashMap()
+    private var mostFrequentAcquiredInfoToDefer: Int? = null
+    private var totalFrames = 0
+
+    init {
+        dumpManager.registerDumpable(this.javaClass.name, this)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("messagesToDefer=$messagesToDefer")
+        pw.println("totalFrames=$totalFrames")
+        pw.println("threshold=$threshold")
+    }
+
+    /** Reset all saved counts. */
+    fun reset() {
+        totalFrames = 0
+        mostFrequentAcquiredInfoToDefer = null
+        acquiredInfoToFrequency.clear()
+        acquiredInfoToHelpString.clear()
+        logBuffer.reset()
+    }
+
+    /** Updates the message associated with the acquiredInfo if it's a message we may defer. */
+    fun updateMessage(acquiredInfo: Int, helpString: String) {
+        if (!messagesToDefer.contains(acquiredInfo)) {
+            return
+        }
+        if (!Objects.equals(acquiredInfoToHelpString[acquiredInfo], helpString)) {
+            logBuffer.logUpdateMessage(acquiredInfo, helpString)
+            acquiredInfoToHelpString[acquiredInfo] = helpString
+        }
+    }
+
+    /** Whether the given message should be deferred instead of being shown immediately. */
+    fun shouldDefer(acquiredMsgId: Int): Boolean {
+        return messagesToDefer.contains(acquiredMsgId)
+    }
+
+    /** Adds the acquiredInfo frame to the counts. We account for all frames. */
+    fun processFrame(acquiredInfo: Int) {
+        if (messagesToDefer.isEmpty()) {
+            return
+        }
+
+        totalFrames++
+
+        val newAcquiredInfoCount = acquiredInfoToFrequency.getOrDefault(acquiredInfo, 0) + 1
+        acquiredInfoToFrequency[acquiredInfo] = newAcquiredInfoCount
+        if (
+            messagesToDefer.contains(acquiredInfo) &&
+                (mostFrequentAcquiredInfoToDefer == null ||
+                    newAcquiredInfoCount >
+                        acquiredInfoToFrequency.getOrDefault(mostFrequentAcquiredInfoToDefer!!, 0))
+        ) {
+            mostFrequentAcquiredInfoToDefer = acquiredInfo
+        }
+
+        logBuffer.logFrameProcessed(
+            acquiredInfo,
+            totalFrames,
+            mostFrequentAcquiredInfoToDefer?.toString()
+        )
+    }
+
+    /**
+     * Get the most frequent deferred message that meets the [threshold] percentage of processed
+     * frames.
+     * @return null if no acquiredInfo have been deferred OR deferred messages didn't meet the
+     * [threshold] percentage.
+     */
+    fun getDeferredMessage(): CharSequence? {
+        mostFrequentAcquiredInfoToDefer?.let {
+            if (acquiredInfoToFrequency.getOrDefault(it, 0) > (threshold * totalFrames)) {
+                return acquiredInfoToHelpString[it]
+            }
+        }
+        return null
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
new file mode 100644
index 0000000..96af42b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.systemui.bluetooth
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.BluetoothLog
+import javax.inject.Inject
+
+/** Helper class for logging bluetooth events. */
+@SysUISingleton
+class BluetoothLogger @Inject constructor(@BluetoothLog private val logBuffer: LogBuffer) {
+    fun logActiveDeviceChanged(address: String?, profileId: Int) =
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = address
+                int1 = profileId
+            },
+            { "ActiveDeviceChanged. address=$str1 profileId=$int1" }
+        )
+
+    fun logDeviceConnectionStateChanged(address: String?, state: String) =
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = address
+                str2 = state
+            },
+            { "DeviceConnectionStateChanged. address=$str1 state=$str2" }
+        )
+
+    fun logAclConnectionStateChanged(address: String, state: String) =
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = address
+                str2 = state
+            },
+            { "AclConnectionStateChanged. address=$str1 state=$str2" }
+        )
+
+    fun logProfileConnectionStateChanged(address: String?, state: String, profileId: Int) =
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = address
+                str2 = state
+                int1 = profileId
+            },
+            { "ProfileConnectionStateChanged. address=$str1 state=$str2 profileId=$int1" }
+        )
+
+    fun logStateChange(state: String) =
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = state },
+            { "BluetoothStateChanged. state=$str1" }
+        )
+
+    fun logBondStateChange(address: String, state: Int) =
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = address
+                int1 = state
+            },
+            { "DeviceBondStateChanged. address=$str1 state=$int1" }
+        )
+
+    fun logDeviceAdded(address: String) =
+        logBuffer.log(TAG, LogLevel.DEBUG, { str1 = address }, { "DeviceAdded. address=$str1" })
+
+    fun logDeviceDeleted(address: String) =
+        logBuffer.log(TAG, LogLevel.DEBUG, { str1 = address }, { "DeviceDeleted. address=$str1" })
+
+    fun logDeviceAttributesChanged() =
+        logBuffer.log(TAG, LogLevel.DEBUG, {}, { "DeviceAttributesChanged." })
+}
+
+private const val TAG = "BluetoothLog"
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
index 31a2134..bfbf37a 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
@@ -291,7 +291,7 @@
                         FalsingClassifier.Result.falsed(
                                 0, getClass().getSimpleName(), "bad history"));
                 logDebug("False Single Tap: true (bad history)");
-                mFalsingTapListeners.forEach(FalsingTapListener::onDoubleTapRequired);
+                mFalsingTapListeners.forEach(FalsingTapListener::onAdditionalTapRequired);
                 return true;
             } else {
                 mPriorResults = getPassedResult(0.1);
@@ -321,7 +321,7 @@
                 mHistoryTracker.falseBelief(),
                 mHistoryTracker.falseConfidence());
         mPriorResults = Collections.singleton(result);
-        logDebug("False Double Tap: " + result.isFalse());
+        logDebug("False Double Tap: " + result.isFalse() + " reason=" + result.getReason());
         return result.isFalse();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
index 23d87ff..f5f9655 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
@@ -303,9 +303,7 @@
 
     @Override
     public void onTouchEvent(MotionEvent ev) {
-        if (!mKeyguardStateController.isShowing()
-                || (mStatusBarStateController.isDozing()
-                    && !mStatusBarStateController.isPulsing())) {
+        if (!mKeyguardStateController.isShowing()) {
             avoidGesture();
             return;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/drawable/CircularDrawable.kt b/packages/SystemUI/src/com/android/systemui/common/ui/drawable/CircularDrawable.kt
new file mode 100644
index 0000000..ec71c38
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/drawable/CircularDrawable.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.systemui.common.ui.drawable
+
+import android.graphics.Canvas
+import android.graphics.Path
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.DrawableWrapper
+import kotlin.math.min
+
+/** Renders the wrapped [Drawable] as a circle. */
+class CircularDrawable(
+    drawable: Drawable,
+) : DrawableWrapper(drawable) {
+    private val path: Path by lazy { Path() }
+
+    override fun onBoundsChange(bounds: Rect) {
+        super.onBoundsChange(bounds)
+        updateClipPath()
+    }
+
+    override fun draw(canvas: Canvas) {
+        canvas.save()
+        canvas.clipPath(path)
+        drawable?.draw(canvas)
+        canvas.restore()
+    }
+
+    private fun updateClipPath() {
+        path.reset()
+        path.addCircle(
+            bounds.centerX().toFloat(),
+            bounds.centerY().toFloat(),
+            min(bounds.width(), bounds.height()) / 2f,
+            Path.Direction.CW
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextClock.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextClock.java
index 653f4dc..789ebc5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextClock.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextClock.java
@@ -17,24 +17,21 @@
 package com.android.systemui.dreams.complication;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.util.AttributeSet;
 import android.widget.TextClock;
 
 import com.android.systemui.R;
+import com.android.systemui.dreams.complication.DoubleShadowTextHelper.ShadowInfo;
+
+import kotlin.Unit;
 
 /**
  * Extension of {@link TextClock} which draws two shadows on the text (ambient and key shadows)
  */
 public class DoubleShadowTextClock extends TextClock {
-    private final float mAmbientShadowBlur;
-    private final int mAmbientShadowColor;
-    private final float mKeyShadowBlur;
-    private final float mKeyShadowOffsetX;
-    private final float mKeyShadowOffsetY;
-    private final int mKeyShadowColor;
-    private final float mAmbientShadowOffsetX;
-    private final float mAmbientShadowOffsetY;
+    private final DoubleShadowTextHelper mShadowHelper;
 
     public DoubleShadowTextClock(Context context) {
         this(context, null);
@@ -46,38 +43,28 @@
 
     public DoubleShadowTextClock(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-        mKeyShadowBlur = context.getResources()
-                .getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_radius);
-        mKeyShadowOffsetX = context.getResources()
-                .getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_dx);
-        mKeyShadowOffsetY = context.getResources()
-                .getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_dy);
-        mKeyShadowColor = context.getResources().getColor(
-                R.color.dream_overlay_clock_key_text_shadow_color);
-        mAmbientShadowBlur = context.getResources()
-                .getDimensionPixelSize(R.dimen.dream_overlay_clock_ambient_text_shadow_radius);
-        mAmbientShadowColor = context.getResources().getColor(
-                R.color.dream_overlay_clock_ambient_text_shadow_color);
-        mAmbientShadowOffsetX = context.getResources()
-                .getDimensionPixelSize(R.dimen.dream_overlay_clock_ambient_text_shadow_dx);
-        mAmbientShadowOffsetY = context.getResources()
-                .getDimensionPixelSize(R.dimen.dream_overlay_clock_ambient_text_shadow_dy);
+
+        final Resources resources = context.getResources();
+        final ShadowInfo keyShadowInfo = new ShadowInfo(
+                resources.getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_radius),
+                resources.getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_dx),
+                resources.getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_dy),
+                resources.getColor(R.color.dream_overlay_clock_key_text_shadow_color));
+
+        final ShadowInfo ambientShadowInfo = new ShadowInfo(
+                resources.getDimensionPixelSize(
+                        R.dimen.dream_overlay_clock_ambient_text_shadow_radius),
+                resources.getDimensionPixelSize(R.dimen.dream_overlay_clock_ambient_text_shadow_dx),
+                resources.getDimensionPixelSize(R.dimen.dream_overlay_clock_ambient_text_shadow_dy),
+                resources.getColor(R.color.dream_overlay_clock_ambient_text_shadow_color));
+        mShadowHelper = new DoubleShadowTextHelper(keyShadowInfo, ambientShadowInfo);
     }
 
     @Override
     public void onDraw(Canvas canvas) {
-        // We enhance the shadow by drawing the shadow twice
-        getPaint().setShadowLayer(mAmbientShadowBlur, mAmbientShadowOffsetX, mAmbientShadowOffsetY,
-                mAmbientShadowColor);
-        super.onDraw(canvas);
-        canvas.save();
-        canvas.clipRect(getScrollX(), getScrollY() + getExtendedPaddingTop(),
-                getScrollX() + getWidth(),
-                getScrollY() + getHeight());
-
-        getPaint().setShadowLayer(
-                mKeyShadowBlur, mKeyShadowOffsetX, mKeyShadowOffsetY, mKeyShadowColor);
-        super.onDraw(canvas);
-        canvas.restore();
+        mShadowHelper.applyShadows(this, canvas, () -> {
+            super.onDraw(canvas);
+            return Unit.INSTANCE;
+        });
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextHelper.kt b/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextHelper.kt
new file mode 100644
index 0000000..b1dc5a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextHelper.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.systemui.dreams.complication
+
+import android.graphics.Canvas
+import android.widget.TextView
+import androidx.annotation.ColorInt
+
+class DoubleShadowTextHelper
+constructor(
+    private val keyShadowInfo: ShadowInfo,
+    private val ambientShadowInfo: ShadowInfo,
+) {
+    data class ShadowInfo(
+        val blur: Float,
+        val offsetX: Float = 0f,
+        val offsetY: Float = 0f,
+        @ColorInt val color: Int
+    )
+
+    fun applyShadows(view: TextView, canvas: Canvas, onDrawCallback: () -> Unit) {
+        // We enhance the shadow by drawing the shadow twice
+        view.paint.setShadowLayer(
+            ambientShadowInfo.blur,
+            ambientShadowInfo.offsetX,
+            ambientShadowInfo.offsetY,
+            ambientShadowInfo.color
+        )
+        onDrawCallback()
+        canvas.save()
+        canvas.clipRect(
+            view.scrollX,
+            view.scrollY + view.extendedPaddingTop,
+            view.scrollX + view.width,
+            view.scrollY + view.height
+        )
+
+        view.paint.setShadowLayer(
+            keyShadowInfo.blur,
+            keyShadowInfo.offsetX,
+            keyShadowInfo.offsetY,
+            keyShadowInfo.color
+        )
+        onDrawCallback()
+        canvas.restore()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextView.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextView.java
new file mode 100644
index 0000000..cf7e312
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextView.java
@@ -0,0 +1,76 @@
+/*
+ * 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.systemui.dreams.complication;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+
+import kotlin.Unit;
+
+/**
+ * Extension of {@link TextView} which draws two shadows on the text (ambient and key shadows}
+ */
+public class DoubleShadowTextView extends TextView {
+    private final DoubleShadowTextHelper mShadowHelper;
+
+    public DoubleShadowTextView(Context context) {
+        this(context, null);
+    }
+
+    public DoubleShadowTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DoubleShadowTextView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        final Resources resources = context.getResources();
+        final DoubleShadowTextHelper.ShadowInfo
+                keyShadowInfo = new DoubleShadowTextHelper.ShadowInfo(
+                resources.getDimensionPixelSize(
+                        R.dimen.dream_overlay_status_bar_key_text_shadow_radius),
+                resources.getDimensionPixelSize(
+                        R.dimen.dream_overlay_status_bar_key_text_shadow_dx),
+                resources.getDimensionPixelSize(
+                        R.dimen.dream_overlay_status_bar_key_text_shadow_dy),
+                resources.getColor(R.color.dream_overlay_status_bar_key_text_shadow_color));
+
+        final DoubleShadowTextHelper.ShadowInfo
+                ambientShadowInfo = new DoubleShadowTextHelper.ShadowInfo(
+                resources.getDimensionPixelSize(
+                        R.dimen.dream_overlay_status_bar_ambient_text_shadow_radius),
+                resources.getDimensionPixelSize(
+                        R.dimen.dream_overlay_status_bar_ambient_text_shadow_dx),
+                resources.getDimensionPixelSize(
+                        R.dimen.dream_overlay_status_bar_ambient_text_shadow_dy),
+                resources.getColor(R.color.dream_overlay_status_bar_ambient_text_shadow_color));
+        mShadowHelper = new DoubleShadowTextHelper(keyShadowInfo, ambientShadowInfo);
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        mShadowHelper.applyShadows(this, canvas, () -> {
+            super.onDraw(canvas);
+            return Unit.INSTANCE;
+        });
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
index a3dc779..568143c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
@@ -42,6 +42,8 @@
 import android.util.Slog;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
+
 import com.android.settingslib.bluetooth.BluetoothCallback;
 import com.android.settingslib.bluetooth.BluetoothUtils;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -601,13 +603,14 @@
 
     private final class BluetoothCallbackHandler implements BluetoothCallback {
         @Override
-        public void onBluetoothStateChanged(int bluetoothState) {
+        public void onBluetoothStateChanged(@BluetoothCallback.AdapterState int bluetoothState) {
             mHandler.obtainMessage(MSG_ON_BLUETOOTH_STATE_CHANGED,
                     bluetoothState, 0).sendToTarget();
         }
 
         @Override
-        public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
+        public void onDeviceBondStateChanged(
+                @NonNull CachedBluetoothDevice cachedDevice, int bondState) {
             mHandler.obtainMessage(MSG_ON_DEVICE_BOND_STATE_CHANGED,
                     bondState, 0, cachedDevice).sendToTarget();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 2da9232..ddcd053 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -202,7 +202,8 @@
         }
     }
 
-    // Wrap Keyguard going away animation
+    // Wrap Keyguard going away animation.
+    // Note: Also used for wrapping occlude by Dream animation. It works (with some redundancy).
     private static IRemoteTransition wrap(IRemoteAnimationRunner runner) {
         return new IRemoteTransition.Stub() {
             final ArrayMap<IBinder, IRemoteTransitionFinishedCallback> mFinishCallbacks =
@@ -388,6 +389,27 @@
             f = new TransitionFilter();
             f.mTypeSet = new int[]{TRANSIT_KEYGUARD_UNOCCLUDE};
             mShellTransitions.registerRemote(f, unoccludeTransition);
+
+            Slog.d(TAG, "KeyguardService registerRemote: TRANSIT_KEYGUARD_OCCLUDE for DREAM");
+            // Register for occluding by Dream
+            f = new TransitionFilter();
+            f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED;
+            f.mRequirements = new TransitionFilter.Requirement[]{
+                    new TransitionFilter.Requirement(), new TransitionFilter.Requirement()};
+            // First require at-least one app of type DREAM showing that occludes.
+            f.mRequirements[0].mActivityType = WindowConfiguration.ACTIVITY_TYPE_DREAM;
+            f.mRequirements[0].mMustBeIndependent = false;
+            f.mRequirements[0].mFlags = FLAG_OCCLUDES_KEYGUARD;
+            f.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+            // Then require that we aren't closing any occludes (because this would mean a
+            // regular task->task or activity->activity animation not involving keyguard).
+            f.mRequirements[1].mNot = true;
+            f.mRequirements[1].mMustBeIndependent = false;
+            f.mRequirements[1].mFlags = FLAG_OCCLUDES_KEYGUARD;
+            f.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+            mShellTransitions.registerRemote(f, new RemoteTransition(
+                    wrap(mKeyguardViewMediator.getOccludeByDreamAnimationRunner()),
+                    getIApplicationThread()));
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index bd75ab2..6f38f4f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -241,9 +241,8 @@
      */
     @VisibleForTesting
     var surfaceTransactionApplier: SyncRtSurfaceTransactionApplier? = null
-    private var surfaceBehindRemoteAnimationTarget: RemoteAnimationTarget? = null
+    private var surfaceBehindRemoteAnimationTargets: Array<RemoteAnimationTarget>? = null
     private var surfaceBehindRemoteAnimationStartTime: Long = 0
-    private var surfaceBehindParams: SyncRtSurfaceTransactionApplier.SurfaceParams? = null
 
     /**
      * Alpha value applied to [surfaceBehindRemoteAnimationTarget], which is the surface of the
@@ -458,7 +457,7 @@
      * (fingerprint, tap, etc.) and the keyguard is going away.
      */
     fun notifyStartSurfaceBehindRemoteAnimation(
-        target: RemoteAnimationTarget,
+        targets: Array<RemoteAnimationTarget>,
         startTime: Long,
         requestedShowSurfaceBehindKeyguard: Boolean
     ) {
@@ -467,10 +466,7 @@
                     keyguardViewController.viewRootImpl.view)
         }
 
-        // New animation, new params.
-        surfaceBehindParams = null
-
-        surfaceBehindRemoteAnimationTarget = target
+        surfaceBehindRemoteAnimationTargets = targets
         surfaceBehindRemoteAnimationStartTime = startTime
 
         // If we specifically requested that the surface behind be made visible (vs. it being made
@@ -597,7 +593,7 @@
      * keyguard dismiss amount and the method of dismissal.
      */
     private fun updateSurfaceBehindAppearAmount() {
-        if (surfaceBehindRemoteAnimationTarget == null) {
+        if (surfaceBehindRemoteAnimationTargets == null) {
             return
         }
 
@@ -715,63 +711,60 @@
      * cancelled).
      */
     fun setSurfaceBehindAppearAmount(amount: Float) {
-        if (surfaceBehindRemoteAnimationTarget == null) {
-            return
-        }
+        surfaceBehindRemoteAnimationTargets?.forEach { surfaceBehindRemoteAnimationTarget ->
+            val surfaceHeight: Int = surfaceBehindRemoteAnimationTarget.screenSpaceBounds.height()
 
-        // Otherwise, animate in the surface's scale/transltion.
-        val surfaceHeight: Int = surfaceBehindRemoteAnimationTarget!!.screenSpaceBounds.height()
+            var scaleFactor = (SURFACE_BEHIND_START_SCALE_FACTOR +
+                    (1f - SURFACE_BEHIND_START_SCALE_FACTOR) *
+                    MathUtils.clamp(amount, 0f, 1f))
 
-        var scaleFactor = (SURFACE_BEHIND_START_SCALE_FACTOR +
-                (1f - SURFACE_BEHIND_START_SCALE_FACTOR) *
-                MathUtils.clamp(amount, 0f, 1f))
-
-        // If we're dismissing via swipe to the Launcher, we'll play in-window scale animations, so
-        // don't also scale the window.
-        if (keyguardStateController.isDismissingFromSwipe &&
-                willUnlockWithInWindowLauncherAnimations) {
-            scaleFactor = 1f
-        }
-
-        // Scale up from a point at the center-bottom of the surface.
-        surfaceBehindMatrix.setScale(
-            scaleFactor,
-            scaleFactor,
-            surfaceBehindRemoteAnimationTarget!!.screenSpaceBounds.width() / 2f,
-            surfaceHeight * SURFACE_BEHIND_SCALE_PIVOT_Y
-        )
-
-        // Translate up from the bottom.
-        surfaceBehindMatrix.postTranslate(
-            0f,
-            surfaceHeight * SURFACE_BEHIND_START_TRANSLATION_Y * (1f - amount)
-        )
-
-        // If we're snapping the keyguard back, immediately begin fading it out.
-        val animationAlpha =
-            if (keyguardStateController.isSnappingKeyguardBackAfterSwipe) amount
-            else surfaceBehindAlpha
-
-        // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is unable
-        // to draw
-        val sc: SurfaceControl? = surfaceBehindRemoteAnimationTarget?.leash
-        if (keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE &&
-            sc?.isValid == true) {
-            with(SurfaceControl.Transaction()) {
-                setMatrix(sc, surfaceBehindMatrix, tmpFloat)
-                setCornerRadius(sc, roundedCornerRadius)
-                setAlpha(sc, animationAlpha)
-                apply()
+            // If we're dismissing via swipe to the Launcher, we'll play in-window scale animations,
+            // so don't also scale the window.
+            if (keyguardStateController.isDismissingFromSwipe &&
+                    willUnlockWithInWindowLauncherAnimations) {
+                scaleFactor = 1f
             }
-        } else {
-            applyParamsToSurface(
-                SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(
-                    surfaceBehindRemoteAnimationTarget!!.leash)
-                    .withMatrix(surfaceBehindMatrix)
-                    .withCornerRadius(roundedCornerRadius)
-                    .withAlpha(animationAlpha)
-                    .build()
+
+            // Translate up from the bottom.
+            surfaceBehindMatrix.setTranslate(
+                    surfaceBehindRemoteAnimationTarget.screenSpaceBounds.left.toFloat(),
+                    surfaceHeight * SURFACE_BEHIND_START_TRANSLATION_Y * (1f - amount)
             )
+
+            // Scale up from a point at the center-bottom of the surface.
+            surfaceBehindMatrix.postScale(
+                    scaleFactor,
+                    scaleFactor,
+                    keyguardViewController.viewRootImpl.width / 2f,
+                    surfaceHeight * SURFACE_BEHIND_SCALE_PIVOT_Y
+            )
+
+            // If we're snapping the keyguard back, immediately begin fading it out.
+            val animationAlpha =
+                    if (keyguardStateController.isSnappingKeyguardBackAfterSwipe) amount
+                    else surfaceBehindAlpha
+
+            // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is
+            // unable to draw
+            val sc: SurfaceControl? = surfaceBehindRemoteAnimationTarget.leash
+            if (keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE &&
+                    sc?.isValid == true) {
+                with(SurfaceControl.Transaction()) {
+                    setMatrix(sc, surfaceBehindMatrix, tmpFloat)
+                    setCornerRadius(sc, roundedCornerRadius)
+                    setAlpha(sc, animationAlpha)
+                    apply()
+                }
+            } else {
+                applyParamsToSurface(
+                        SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(
+                                surfaceBehindRemoteAnimationTarget.leash)
+                                .withMatrix(surfaceBehindMatrix)
+                                .withCornerRadius(roundedCornerRadius)
+                                .withAlpha(animationAlpha)
+                                .build()
+                )
+            }
         }
     }
 
@@ -796,8 +789,7 @@
         launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */)
 
         // That target is no longer valid since the animation finished, null it out.
-        surfaceBehindRemoteAnimationTarget = null
-        surfaceBehindParams = null
+        surfaceBehindRemoteAnimationTargets = null
 
         playingCannedUnlockAnimation = false
         willUnlockWithInWindowLauncherAnimations = false
@@ -829,7 +821,6 @@
 
     private fun applyParamsToSurface(params: SyncRtSurfaceTransactionApplier.SurfaceParams) {
         surfaceTransactionApplier!!.scheduleApply(params)
-        surfaceBehindParams = params
     }
 
     private fun fadeInSurfaceBehind() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index d4e0f061..be1d162 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -831,7 +831,7 @@
                 public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {}
 
                 @Override
-                public void onLaunchAnimationCancelled() {
+                public void onLaunchAnimationCancelled(@Nullable Boolean newKeyguardOccludedState) {
                     Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: "
                             + mOccluded);
                 }
@@ -2587,18 +2587,10 @@
                 mInteractionJankMonitor.begin(
                         createInteractionJankMonitorConf("DismissPanel"));
 
-                // Apply the opening animation on root task if exists
-                RemoteAnimationTarget aniTarget = apps[0];
-                for (RemoteAnimationTarget tmpTarget : apps) {
-                    if (tmpTarget.taskId != -1 && !tmpTarget.hasAnimatingParent) {
-                        aniTarget = tmpTarget;
-                        break;
-                    }
-                }
                 // Pass the surface and metadata to the unlock animation controller.
                 mKeyguardUnlockAnimationControllerLazy.get()
                         .notifyStartSurfaceBehindRemoteAnimation(
-                                aniTarget, startTime, mSurfaceBehindRemoteAnimationRequested);
+                                apps, startTime, mSurfaceBehindRemoteAnimationRequested);
             } else {
                 mInteractionJankMonitor.begin(
                         createInteractionJankMonitorConf("RemoteAnimationDisabled"));
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
new file mode 100644
index 0000000..7f1ad6d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
@@ -0,0 +1,33 @@
+/*
+ * 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.systemui.log.dagger;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * A {@link com.android.systemui.log.LogBuffer} for BiometricMessages processing such as
+ * {@link com.android.systemui.biometrics.FaceHelpMessageDeferral}
+ */
+@Qualifier
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+public @interface BiometricMessagesLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BluetoothLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/BluetoothLog.kt
new file mode 100644
index 0000000..4887b6a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BluetoothLog.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for bluetooth. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class BluetoothLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardUpdateMonitorLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardUpdateMonitorLog.kt
index 323ee219..b551125 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardUpdateMonitorLog.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardUpdateMonitorLog.kt
@@ -1,4 +1,9 @@
 package com.android.systemui.log.dagger
 
+import javax.inject.Qualifier
+
 /** A [com.android.systemui.log.LogBuffer] for KeyguardUpdateMonitor. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
 annotation class KeyguardUpdateMonitorLog
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 c2a8764..5612c22 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -287,6 +287,17 @@
     }
 
     /**
+     * Provides a {@link LogBuffer} for use by
+     * {@link com.android.systemui.biometrics.FaceHelpMessageDeferral}.
+     */
+    @Provides
+    @SysUISingleton
+    @BiometricMessagesLog
+    public static LogBuffer provideBiometricMessagesLogBuffer(LogBufferFactory factory) {
+        return factory.create("BiometricMessagesLog", 150);
+    }
+
+    /**
      * Provides a {@link LogBuffer} for use by the status bar network controller.
      */
     @Provides
@@ -305,4 +316,14 @@
     public static LogBuffer provideKeyguardUpdateMonitorLogBuffer(LogBufferFactory factory) {
         return factory.create("KeyguardUpdateMonitorLog", 200);
     }
+
+    /**
+     * Provides a {@link LogBuffer} for bluetooth-related logs.
+     */
+    @Provides
+    @SysUISingleton
+    @BluetoothLog
+    public static LogBuffer providerBluetoothLogBuffer(LogBufferFactory factory) {
+        return factory.create("BluetoothLog", 50);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
index f03fbcb..b237f2d 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
@@ -19,7 +19,6 @@
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.android.systemui.log.LogBuffer;
-import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
@@ -27,7 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for events processed by {@link ConnectivityInfoProcessor}
+ * A {@link LogBuffer} for status bar connectivity events.
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
index 88f6f3d..6bc94cd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
@@ -60,6 +60,8 @@
             linePaint.strokeWidth = value
         }
 
+    // Enables a transition region where the amplitude
+    // of the wave is reduced linearly across it.
     var transitionEnabled = true
         set(value) {
             field = value
@@ -116,44 +118,40 @@
         }
 
         val progress = level / 10_000f
-        val totalProgressPx = bounds.width() * progress
-        val waveProgressPx = bounds.width() * (
+        val totalWidth = bounds.width().toFloat()
+        val totalProgressPx = totalWidth * progress
+        val waveProgressPx = totalWidth * (
             if (!transitionEnabled || progress > matchedWaveEndpoint) progress else
             lerp(minWaveEndpoint, matchedWaveEndpoint, lerpInv(0f, matchedWaveEndpoint, progress)))
 
         // Build Wiggly Path
-        val waveStart = -phaseOffset
-        val waveEnd = waveProgressPx
-        val transitionLength = if (transitionEnabled) transitionPeriods * waveLength else 0.01f
+        val waveStart = -phaseOffset - waveLength / 2f
+        val waveEnd = if (transitionEnabled) totalWidth else waveProgressPx
 
         // helper function, computes amplitude for wave segment
         val computeAmplitude: (Float, Float) -> Float = { x, sign ->
-            sign * heightFraction * lineAmplitude *
-                    lerpInvSat(waveEnd, waveEnd - transitionLength, x)
+            if (transitionEnabled) {
+                val length = transitionPeriods * waveLength
+                val coeff = lerpInvSat(
+                    waveProgressPx + length / 2f,
+                    waveProgressPx - length / 2f,
+                    x)
+                sign * heightFraction * lineAmplitude * coeff
+            } else {
+                sign * heightFraction * lineAmplitude
+            }
         }
 
-        var currentX = waveEnd
-        var waveSign = if (phaseOffset < waveLength / 2) 1f else -1f
+        // Reset path object to the start
         path.rewind()
+        path.moveTo(waveStart, 0f)
 
-        // Draw flat line from end to wave endpoint
-        path.moveTo(bounds.width().toFloat(), 0f)
-        path.lineTo(waveEnd, 0f)
-
-        // First wave has shortened wavelength
-        // approx quarter wave gets us to first wave peak
-        // shouldn't be big enough to notice it's not a sin wave
-        currentX -= phaseOffset % (waveLength / 2)
-        val controlRatio = 0.25f
+        // Build the wave, incrementing by half the wavelength each time
+        var currentX = waveStart
+        var waveSign = 1f
         var currentAmp = computeAmplitude(currentX, waveSign)
-        path.cubicTo(
-            waveEnd, currentAmp * controlRatio,
-            lerp(currentX, waveEnd, controlRatio), currentAmp,
-            currentX, currentAmp)
-
-        // Other waves have full wavelength
-        val dist = -1 * waveLength / 2f
-        while (currentX > waveStart) {
+        val dist = waveLength / 2f
+        while (currentX < waveEnd) {
             waveSign = -waveSign
             val nextX = currentX + dist
             val midX = currentX + dist / 2
@@ -166,34 +164,35 @@
             currentX = nextX
         }
 
-        // Draw path; clip to progress position
+        // translate to the start position of the progress bar for all draw commands
+        val clipTop = lineAmplitude + strokeWidth
         canvas.save()
         canvas.translate(bounds.left.toFloat(), bounds.centerY().toFloat())
-        canvas.clipRect(
-                0f,
-                -lineAmplitude - strokeWidth,
-                totalProgressPx,
-                lineAmplitude + strokeWidth)
+
+        // Draw path up to progress position
+        canvas.save()
+        canvas.clipRect(0f, -1f * clipTop, totalProgressPx, clipTop)
         canvas.drawPath(path, wavePaint)
         canvas.restore()
 
-        // Draw path; clip between progression position & far edge
-        canvas.save()
-        canvas.translate(bounds.left.toFloat(), bounds.centerY().toFloat())
-        canvas.clipRect(
-                totalProgressPx,
-                -lineAmplitude - strokeWidth,
-                bounds.width().toFloat(),
-                lineAmplitude + strokeWidth)
-        canvas.drawPath(path, linePaint)
-        canvas.restore()
+        if (transitionEnabled) {
+            // If there's a smooth transition, we draw the rest of the
+            // path in a different color (using different clip params)
+            canvas.save()
+            canvas.clipRect(totalProgressPx, -1f * clipTop, totalWidth, clipTop)
+            canvas.drawPath(path, linePaint)
+            canvas.restore()
+        } else {
+            // No transition, just draw a flat line to the end of the region.
+            // The discontinuity is hidden by the progress bar thumb shape.
+            canvas.drawLine(totalProgressPx, 0f, totalWidth, 0f, linePaint)
+        }
 
         // Draw round line cap at the beginning of the wave
-        val startAmp = cos(abs(waveEnd - phaseOffset) / waveLength * TWO_PI)
-        canvas.drawPoint(
-                bounds.left.toFloat(),
-                bounds.centerY() + startAmp * lineAmplitude * heightFraction,
-                wavePaint)
+        val startAmp = cos(abs(waveStart) / waveLength * TWO_PI)
+        canvas.drawPoint(0f, startAmp * lineAmplitude * heightFraction, wavePaint)
+
+        canvas.restore()
     }
 
     override fun getOpacity(): Int {
@@ -233,4 +232,4 @@
         linePaint.color = ColorUtils.setAlphaComponent(tintColor,
                 (DISABLED_ALPHA * (alpha / 255f)).toInt())
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
index 6fe06e0..7d3e82c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
@@ -107,7 +107,8 @@
         SysUiStatsLog.write(
                 SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT,
                 SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__ADJUST_VOLUME,
-                getInteractionDeviceType(source));
+                getInteractionDeviceType(source),
+                getLoggingPackageName());
     }
 
     /**
@@ -121,7 +122,8 @@
         SysUiStatsLog.write(
                 SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT,
                 SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__STOP_CASTING,
-                SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__UNKNOWN_TYPE);
+                SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__UNKNOWN_TYPE,
+                getLoggingPackageName());
     }
 
     /**
@@ -135,7 +137,8 @@
         SysUiStatsLog.write(
                 SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT,
                 SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__EXPANSION,
-                getInteractionDeviceType(source));
+                getInteractionDeviceType(source),
+                getLoggingPackageName());
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
index aa10f7e..b565f3c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -18,38 +18,57 @@
 
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel
+import com.android.systemui.temporarydisplay.TemporaryViewLogger
 
 /**
  * A logger for media tap-to-transfer events.
  *
- * @property deviceTypeTag the type of device triggering the logs -- "Sender" or "Receiver".
+ * @param deviceTypeTag the type of device triggering the logs -- "Sender" or "Receiver".
  */
 class MediaTttLogger(
-    private val deviceTypeTag: String,
-    private val buffer: LogBuffer
-){
+    deviceTypeTag: String,
+    buffer: LogBuffer
+) : TemporaryViewLogger(buffer, BASE_TAG + deviceTypeTag) {
     /** Logs a change in the chip state for the given [mediaRouteId]. */
-    fun logStateChange(stateName: String, mediaRouteId: String) {
+    fun logStateChange(stateName: String, mediaRouteId: String, packageName: String?) {
         buffer.log(
-            BASE_TAG + deviceTypeTag,
+            tag,
             LogLevel.DEBUG,
             {
                 str1 = stateName
                 str2 = mediaRouteId
+                str3 = packageName
             },
-            { "State changed to $str1 for ID=$str2" }
+            { "State changed to $str1 for ID=$str2 package=$str3" }
         )
     }
 
-    /** Logs that we removed the chip for the given [reason]. */
-    fun logChipRemoval(reason: String) {
+    /** Logs that we couldn't find information for [packageName]. */
+    fun logPackageNotFound(packageName: String) {
         buffer.log(
-            BASE_TAG + deviceTypeTag,
+            tag,
             LogLevel.DEBUG,
-            { str1 = reason },
-            { "Chip removed due to $str1" }
+            { str1 = packageName },
+            { "Package $str1 could not be found" }
         )
     }
+
+    /**
+     * Logs that a removal request has been bypassed (ignored).
+     *
+     * @param removalReason the reason that the chip removal was requested.
+     * @param bypassReason the reason that the request was bypassed.
+     */
+    fun logRemovalBypass(removalReason: String, bypassReason: String) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            {
+                str1 = removalReason
+                str2 = bypassReason
+            },
+            { "Chip removal requested due to $str1; however, removal was ignored because $str2" })
+    }
 }
 
 private const val BASE_TAG = "MediaTtt"
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
new file mode 100644
index 0000000..792ae7c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.systemui.media.taptotransfer.common
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import com.android.internal.widget.CachingIconView
+import com.android.settingslib.Utils
+import com.android.systemui.R
+
+/** Utility methods for media tap-to-transfer. */
+class MediaTttUtils {
+    companion object {
+        // Used in CTS tests UpdateMediaTapToTransferSenderDisplayTest and
+        // UpdateMediaTapToTransferReceiverDisplayTest
+        const val WINDOW_TITLE = "Media Transfer Chip View"
+        const val WAKE_REASON = "MEDIA_TRANSFER_ACTIVATED"
+
+        /**
+         * Returns the information needed to display the icon.
+         *
+         * The information will either contain app name and icon of the app playing media, or a
+         * default name and icon if we can't find the app name/icon.
+         *
+         * @param appPackageName the package name of the app playing the media.
+         * @param logger the logger to use for any errors.
+         */
+        fun getIconInfoFromPackageName(
+            context: Context,
+            appPackageName: String?,
+            logger: MediaTttLogger
+        ): IconInfo {
+            if (appPackageName != null) {
+                try {
+                    val contentDescription =
+                        context.packageManager
+                            .getApplicationInfo(
+                                appPackageName,
+                                PackageManager.ApplicationInfoFlags.of(0)
+                            )
+                            .loadLabel(context.packageManager)
+                            .toString()
+                    return IconInfo(
+                        contentDescription,
+                        drawable = context.packageManager.getApplicationIcon(appPackageName),
+                        isAppIcon = true
+                    )
+                } catch (e: PackageManager.NameNotFoundException) {
+                    logger.logPackageNotFound(appPackageName)
+                }
+            }
+            return IconInfo(
+                contentDescription =
+                    context.getString(R.string.media_output_dialog_unknown_launch_app_name),
+                drawable =
+                    context.resources.getDrawable(R.drawable.ic_cast).apply {
+                        this.setTint(
+                            Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary)
+                        )
+                    },
+                isAppIcon = false
+            )
+        }
+
+        /**
+         * Sets an icon to be displayed by the given view.
+         *
+         * @param iconSize the size in pixels that the icon should be. If null, the size of
+         * [appIconView] will not be adjusted.
+         */
+        fun setIcon(
+            appIconView: CachingIconView,
+            icon: Drawable,
+            iconContentDescription: CharSequence,
+            iconSize: Int? = null,
+        ) {
+            iconSize?.let { size ->
+                val lp = appIconView.layoutParams
+                lp.width = size
+                lp.height = size
+                appIconView.layoutParams = lp
+            }
+
+            appIconView.contentDescription = iconContentDescription
+            appIconView.setImageDrawable(icon)
+        }
+    }
+}
+
+data class IconInfo(
+    val contentDescription: String,
+    val drawable: Drawable,
+    /**
+     * True if [drawable] is the app's icon, and false if [drawable] is some generic default icon.
+     */
+    val isAppIcon: Boolean
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 5d6d683..dfd9e22 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.media.taptotransfer.common.MediaTttUtils
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS
@@ -61,7 +62,7 @@
         powerManager: PowerManager,
         @Main private val mainHandler: Handler,
         private val uiEventLogger: MediaTttReceiverUiEventLogger,
-) : TemporaryViewDisplayController<ChipReceiverInfo>(
+) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger>(
         context,
         logger,
         windowManager,
@@ -70,6 +71,8 @@
         configurationController,
         powerManager,
         R.layout.media_ttt_chip_receiver,
+        MediaTttUtils.WINDOW_TITLE,
+        MediaTttUtils.WAKE_REASON,
 ) {
     @SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
     override val windowLayoutParams = commonWindowLayoutParams.apply {
@@ -107,7 +110,7 @@
     ) {
         val chipState: ChipStateReceiver? = ChipStateReceiver.getReceiverStateFromId(displayState)
         val stateName = chipState?.name ?: "Invalid"
-        logger.logStateChange(stateName, routeInfo.id)
+        logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
 
         if (chipState == null) {
             Log.e(RECEIVER_TAG, "Unhandled MediaTransferReceiverState $displayState")
@@ -137,13 +140,26 @@
 
     override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) {
         super.updateView(newInfo, currentView)
-        val iconName = setIcon(
-                currentView,
-                newInfo.routeInfo.clientPackageName,
-                newInfo.appIconDrawableOverride,
-                newInfo.appNameOverride
+
+        val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
+            context, newInfo.routeInfo.clientPackageName, logger
         )
-        currentView.contentDescription = iconName
+        val iconDrawable = newInfo.appIconDrawableOverride ?: iconInfo.drawable
+        val iconContentDescription = newInfo.appNameOverride ?: iconInfo.contentDescription
+        val iconSize = context.resources.getDimensionPixelSize(
+            if (iconInfo.isAppIcon) {
+                R.dimen.media_ttt_icon_size_receiver
+            } else {
+                R.dimen.media_ttt_generic_icon_size_receiver
+            }
+        )
+
+        MediaTttUtils.setIcon(
+            currentView.requireViewById(R.id.app_icon),
+            iconDrawable,
+            iconContentDescription,
+            iconSize,
+        )
     }
 
     override fun animateViewIn(view: ViewGroup) {
@@ -161,15 +177,6 @@
         startRipple(view.requireViewById(R.id.ripple))
     }
 
-    override fun getIconSize(isAppIcon: Boolean): Int? =
-        context.resources.getDimensionPixelSize(
-            if (isAppIcon) {
-                R.dimen.media_ttt_icon_size_receiver
-            } else {
-                R.dimen.media_ttt_generic_icon_size_receiver
-            }
-        )
-
     /** Returns the amount that the chip will be translated by in its intro animation. */
     private fun getTranslationAmount(): Int {
         return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation)
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index bde588c..4379d25 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -34,8 +34,7 @@
  * @property stateInt the integer from [StatusBarManager] corresponding with this state.
  * @property stringResId the res ID of the string that should be displayed in the chip. Null if the
  *   state should not have the chip be displayed.
- * @property isMidTransfer true if the state represents that a transfer is currently ongoing.
- * @property isTransferFailure true if the state represents that the transfer has failed.
+ * @property transferStatus the transfer status that the chip state represents.
  * @property timeout the amount of time this chip should display on the screen before it times out
  *   and disappears.
  */
@@ -43,8 +42,7 @@
     @StatusBarManager.MediaTransferSenderState val stateInt: Int,
     val uiEvent: UiEventLogger.UiEventEnum,
     @StringRes val stringResId: Int?,
-    val isMidTransfer: Boolean = false,
-    val isTransferFailure: Boolean = false,
+    val transferStatus: TransferStatus,
     val timeout: Long = DEFAULT_TIMEOUT_MILLIS
 ) {
     /**
@@ -56,6 +54,7 @@
         StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST,
         R.string.media_move_closer_to_start_cast,
+        transferStatus = TransferStatus.NOT_STARTED,
     ),
 
     /**
@@ -68,6 +67,7 @@
         StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST,
         R.string.media_move_closer_to_end_cast,
+        transferStatus = TransferStatus.NOT_STARTED,
     ),
 
     /**
@@ -78,7 +78,7 @@
         StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED,
         R.string.media_transfer_playing_different_device,
-        isMidTransfer = true,
+        transferStatus = TransferStatus.IN_PROGRESS,
         timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
     ),
 
@@ -90,7 +90,7 @@
         StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
         R.string.media_transfer_playing_this_device,
-        isMidTransfer = true,
+        transferStatus = TransferStatus.IN_PROGRESS,
         timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
     ),
 
@@ -100,7 +100,8 @@
     TRANSFER_TO_RECEIVER_SUCCEEDED(
         StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED,
-        R.string.media_transfer_playing_different_device
+        R.string.media_transfer_playing_different_device,
+        transferStatus = TransferStatus.SUCCEEDED,
     ) {
         override fun undoClickListener(
             controllerSender: MediaTttChipControllerSender,
@@ -135,7 +136,8 @@
     TRANSFER_TO_THIS_DEVICE_SUCCEEDED(
         StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
-        R.string.media_transfer_playing_this_device
+        R.string.media_transfer_playing_this_device,
+        transferStatus = TransferStatus.SUCCEEDED,
     ) {
         override fun undoClickListener(
             controllerSender: MediaTttChipControllerSender,
@@ -169,7 +171,7 @@
         StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED,
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED,
         R.string.media_transfer_failed,
-        isTransferFailure = true
+        transferStatus = TransferStatus.FAILED,
     ),
 
     /** A state representing that a transfer back to this device has failed. */
@@ -177,14 +179,15 @@
         StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED,
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED,
         R.string.media_transfer_failed,
-        isTransferFailure = true
+        transferStatus = TransferStatus.FAILED,
     ),
 
     /** A state representing that this device is far away from any receiver device. */
     FAR_FROM_RECEIVER(
         StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER,
-        stringResId = null
+        stringResId = null,
+        transferStatus = TransferStatus.TOO_FAR,
     );
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index 0c1ebd7..e539f3f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.media.taptotransfer.common.MediaTttUtils
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.TemporaryDisplayRemovalReason
@@ -57,7 +58,7 @@
         configurationController: ConfigurationController,
         powerManager: PowerManager,
         private val uiEventLogger: MediaTttSenderUiEventLogger
-) : TemporaryViewDisplayController<ChipSenderInfo>(
+) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>(
         context,
         logger,
         windowManager,
@@ -66,6 +67,8 @@
         configurationController,
         powerManager,
         R.layout.media_ttt_chip,
+        MediaTttUtils.WINDOW_TITLE,
+        MediaTttUtils.WAKE_REASON,
 ) {
     override val windowLayoutParams = commonWindowLayoutParams.apply {
         gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
@@ -94,7 +97,7 @@
     ) {
         val chipState: ChipStateSender? = ChipStateSender.getSenderStateFromId(displayState)
         val stateName = chipState?.name ?: "Invalid"
-        logger.logStateChange(stateName, routeInfo.id)
+        logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
 
         if (chipState == null) {
             Log.e(SENDER_TAG, "Unhandled MediaTransferSenderState $displayState")
@@ -103,7 +106,7 @@
         uiEventLogger.logSenderStateChange(chipState)
 
         if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
-            removeView(removalReason = ChipStateSender.FAR_FROM_RECEIVER::class.simpleName!!)
+            removeView(removalReason = ChipStateSender.FAR_FROM_RECEIVER.name)
         } else {
             displayView(ChipSenderInfo(chipState, routeInfo, undoCallback))
         }
@@ -118,7 +121,14 @@
         val chipState = newInfo.state
 
         // App icon
-        val iconName = setIcon(currentView, newInfo.routeInfo.clientPackageName)
+        val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
+            context, newInfo.routeInfo.clientPackageName, logger
+        )
+        MediaTttUtils.setIcon(
+            currentView.requireViewById(R.id.app_icon),
+            iconInfo.drawable,
+            iconInfo.contentDescription
+        )
 
         // Text
         val otherDeviceName = newInfo.routeInfo.name.toString()
@@ -127,7 +137,7 @@
 
         // Loading
         currentView.requireViewById<View>(R.id.loading).visibility =
-            chipState.isMidTransfer.visibleIfTrue()
+            (chipState.transferStatus == TransferStatus.IN_PROGRESS).visibleIfTrue()
 
         // Undo
         val undoView = currentView.requireViewById<View>(R.id.undo)
@@ -139,12 +149,12 @@
 
         // Failure
         currentView.requireViewById<View>(R.id.failure_icon).visibility =
-            chipState.isTransferFailure.visibleIfTrue()
+            (chipState.transferStatus == TransferStatus.FAILED).visibleIfTrue()
 
         // For accessibility
         currentView.requireViewById<ViewGroup>(
                 R.id.media_ttt_sender_chip_inner
-        ).contentDescription = "$iconName $chipText"
+        ).contentDescription = "${iconInfo.contentDescription} $chipText"
     }
 
     override fun animateViewIn(view: ViewGroup) {
@@ -162,10 +172,17 @@
     }
 
     override fun removeView(removalReason: String) {
-        // Don't remove the chip if we're mid-transfer since the user should still be able to
-        // see the status of the transfer. (But do remove it if it's finally timed out.)
-        if (info?.state?.isMidTransfer == true &&
-                removalReason != TemporaryDisplayRemovalReason.REASON_TIMEOUT) {
+        // Don't remove the chip if we're in progress or succeeded, since the user should still be
+        // able to see the status of the transfer. (But do remove it if it's finally timed out.)
+        val transferStatus = info?.state?.transferStatus
+        if (
+            (transferStatus == TransferStatus.IN_PROGRESS ||
+                transferStatus == TransferStatus.SUCCEEDED) &&
+            removalReason != TemporaryDisplayRemovalReason.REASON_TIMEOUT
+        ) {
+            logger.logRemovalBypass(
+                removalReason, bypassReason = "transferStatus=${transferStatus.name}"
+            )
             return
         }
         super.removeView(removalReason)
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
new file mode 100644
index 0000000..f15720d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.systemui.media.taptotransfer.sender
+
+/** Represents the different possible transfer states that we could be in. */
+enum class TransferStatus {
+    /** The transfer hasn't started yet. */
+    NOT_STARTED,
+    /** The transfer is currently ongoing but hasn't completed yet. */
+    IN_PROGRESS,
+    /** The transfer has completed successfully. */
+    SUCCEEDED,
+    /** The transfer has completed with a failure. */
+    FAILED,
+    /** The device is too far away to do a transfer. */
+    TOO_FAR,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index 5842665..e9a6c25 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -50,6 +50,7 @@
     protected int mIconSizePx;
     private boolean mAnimationEnabled = true;
     private int mState = -1;
+    private boolean mDisabledByPolicy = false;
     private int mTint;
     @Nullable
     private QSTile.Icon mLastIcon;
@@ -159,14 +160,10 @@
     }
 
     protected void setIcon(ImageView iv, QSTile.State state, boolean allowAnimations) {
-        if (state.disabledByPolicy) {
-            iv.setColorFilter(getContext().getColor(R.color.qs_tile_disabled_color));
-        } else {
-            iv.clearColorFilter();
-        }
-        if (state.state != mState) {
+        if (state.state != mState || state.disabledByPolicy != mDisabledByPolicy) {
             int color = getColor(state);
             mState = state.state;
+            mDisabledByPolicy = state.disabledByPolicy;
             if (mTint != 0 && allowAnimations && shouldAnimate(iv)) {
                 animateGrayScale(mTint, color, iv, () -> updateIcon(iv, state, allowAnimations));
             } else {
@@ -241,8 +238,8 @@
      */
     private static int getIconColorForState(Context context, QSTile.State state) {
         if (state.disabledByPolicy || state.state == Tile.STATE_UNAVAILABLE) {
-            return Utils.applyAlpha(QSTileViewImpl.UNAVAILABLE_ALPHA,
-                    Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary));
+            return Utils.getColorAttrDefaultColor(
+                    context, com.android.internal.R.attr.textColorTertiary);
         } else if (state.state == Tile.STATE_INACTIVE) {
             return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary);
         } else if (state.state == Tile.STATE_ACTIVE) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 163ee2a..972b243 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -100,14 +100,15 @@
             Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.textColorOnAccent)
     private val colorLabelInactive =
             Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary)
-    private val colorLabelUnavailable = Utils.applyAlpha(UNAVAILABLE_ALPHA, colorLabelInactive)
+    private val colorLabelUnavailable =
+        Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.textColorTertiary)
 
     private val colorSecondaryLabelActive =
             Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondaryInverse)
     private val colorSecondaryLabelInactive =
             Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary)
     private val colorSecondaryLabelUnavailable =
-            Utils.applyAlpha(UNAVAILABLE_ALPHA, colorSecondaryLabelInactive)
+        Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.textColorTertiary)
 
     private lateinit var label: TextView
     protected lateinit var secondaryLabel: TextView
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
index c2a82a7..a31500c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
@@ -49,7 +49,6 @@
 /** Quick settings tile: Invert colors **/
 public class ColorInversionTile extends QSTileImpl<BooleanState> {
 
-    private final Icon mIcon = ResourceIcon.get(drawable.ic_invert_colors);
     private final SettingObserver mSetting;
 
     @Inject
@@ -123,7 +122,9 @@
         state.value = enabled;
         state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
         state.label = mContext.getString(R.string.quick_settings_inversion_label);
-        state.icon = mIcon;
+        state.icon = ResourceIcon.get(state.value
+                ? drawable.qs_invert_colors_icon_on
+                : drawable.qs_invert_colors_icon_off);
         state.expandedAccessibilityClassName = Switch.class.getName();
         state.contentDescription = state.label;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
index 9fdf594..2fc99f3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
@@ -128,13 +128,13 @@
 
     @Override
     protected void handleUpdateState(BooleanState state, Object arg) {
-        state.value = arg instanceof Boolean ? (Boolean) arg
+        state.value = arg instanceof Boolean ? ((Boolean) arg).booleanValue()
                 : mDataSaverController.isDataSaverEnabled();
         state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
         state.label = mContext.getString(R.string.data_saver);
         state.contentDescription = state.label;
-        state.icon = ResourceIcon.get(state.value ? R.drawable.ic_data_saver
-                : R.drawable.ic_data_saver_off);
+        state.icon = ResourceIcon.get(state.value ? R.drawable.qs_data_saver_icon_on
+                : R.drawable.qs_data_saver_icon_off);
         state.expandedAccessibilityClassName = Switch.class.getName();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
index 81813db..0e9f659 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
@@ -144,9 +144,10 @@
     protected void handleUpdateState(BooleanState state, Object arg) {
         state.value = mManager.isNightDisplayActivated();
         state.label = mContext.getString(R.string.quick_settings_night_display_label);
-        state.icon = ResourceIcon.get(com.android.internal.R.drawable.ic_qs_night_display_on);
         state.expandedAccessibilityClassName = Switch.class.getName();
         state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
+        state.icon = ResourceIcon.get(state.value ? R.drawable.qs_nightlight_icon_on
+                : R.drawable.qs_nightlight_icon_off);
         state.secondaryLabel = getSecondaryLabel(state.value);
         state.contentDescription = TextUtils.isEmpty(state.secondaryLabel)
                 ? state.label
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 3429b9d..1011a6d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -33,6 +33,7 @@
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
 import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
+import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
 import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED;
@@ -62,6 +63,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.PowerManager;
+import android.os.Process;
 import android.os.Trace;
 import android.os.UserManager;
 import android.os.VibrationEffect;
@@ -236,6 +238,9 @@
     private static final boolean SPEW_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
     private static final boolean DEBUG_DRAWABLE = false;
 
+    private static final VibrationEffect ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT =
+            VibrationEffect.get(VibrationEffect.EFFECT_STRENGTH_MEDIUM, false);
+
     /**
      * The parallax amount of the quick settings translation when dragging down the panel
      */
@@ -682,14 +687,22 @@
 
     private final FalsingTapListener mFalsingTapListener = new FalsingTapListener() {
         @Override
-        public void onDoubleTapRequired() {
+        public void onAdditionalTapRequired() {
             if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
                 mTapAgainViewController.show();
             } else {
                 mKeyguardIndicationController.showTransientIndication(
                         R.string.notification_tap_again);
             }
-            mVibratorHelper.vibrate(VibrationEffect.EFFECT_STRENGTH_MEDIUM);
+
+            if (!mStatusBarStateController.isDozing()) {
+                mVibratorHelper.vibrate(
+                        Process.myUid(),
+                        mView.getContext().getPackageName(),
+                        ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT,
+                        "falsing-additional-tap-required",
+                        TOUCH_VIBRATION_ATTRIBUTES);
+            }
         }
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index abafecc..8d74a09 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -73,7 +73,7 @@
     private final AmbientState mAmbientState;
     private final PulsingGestureListener mPulsingGestureListener;
 
-    private GestureDetector mGestureDetector;
+    private GestureDetector mPulsingWakeupGestureHandler;
     private View mBrightnessMirror;
     private boolean mTouchActive;
     private boolean mTouchCancelled;
@@ -149,7 +149,8 @@
     /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */
     public void setupExpandedStatusBar() {
         mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller);
-        mGestureDetector = new GestureDetector(mView.getContext(), mPulsingGestureListener);
+        mPulsingWakeupGestureHandler = new GestureDetector(mView.getContext(),
+                mPulsingGestureListener);
 
         mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() {
             @Override
@@ -196,7 +197,7 @@
                 }
 
                 mFalsingCollector.onTouchEvent(ev);
-                mGestureDetector.onTouchEvent(ev);
+                mPulsingWakeupGestureHandler.onTouchEvent(ev);
                 mStatusBarKeyguardViewManager.onTouch(ev);
                 if (mBrightnessMirror != null
                         && mBrightnessMirror.getVisibility() == View.VISIBLE) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index 9b3fe92..084b7dc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
@@ -36,12 +37,12 @@
 
 /**
  * If tap and/or double tap to wake is enabled, this gestureListener will wake the display on
- * tap/double tap when the device is pulsing (AoD 2). Taps are gated by the proximity sensor and
- * falsing manager.
+ * tap/double tap when the device is pulsing (AoD2) or transitioning to AoD. Taps are gated by the
+ * proximity sensor and falsing manager.
  *
- * Touches go through the [NotificationShadeWindowViewController] when the device is pulsing.
- * Otherwise, if the device is dozing and NOT pulsing, wake-ups are handled by
- * [com.android.systemui.doze.DozeSensors].
+ * Touches go through the [NotificationShadeWindowViewController] when the device is dozing but the
+ * screen is still ON and not in the true AoD display state. When the device is in the true AoD
+ * display state, wake-ups are handled by [com.android.systemui.doze.DozeSensors].
  */
 @CentralSurfacesComponent.CentralSurfacesScope
 class PulsingGestureListener @Inject constructor(
@@ -75,12 +76,12 @@
         dumpManager.registerDumpable(this)
     }
 
-    override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
-        if (statusBarStateController.isPulsing &&
+    override fun onSingleTapUp(e: MotionEvent): Boolean {
+        if (statusBarStateController.isDozing &&
                 singleTapEnabled &&
                 !dockManager.isDocked &&
                 !falsingManager.isProximityNear &&
-                !falsingManager.isFalseTap(FalsingManager.MODERATE_PENALTY)
+                !falsingManager.isFalseTap(LOW_PENALTY)
         ) {
             centralSurfaces.wakeUpIfDozing(
                     SystemClock.uptimeMillis(),
@@ -91,8 +92,15 @@
         return false
     }
 
-    override fun onDoubleTap(e: MotionEvent): Boolean {
-        if (statusBarStateController.isPulsing &&
+    /**
+     * Receives [MotionEvent.ACTION_DOWN], [MotionEvent.ACTION_MOVE], and [MotionEvent.ACTION_UP]
+     * motion events for a double tap.
+     */
+    override fun onDoubleTapEvent(e: MotionEvent): Boolean {
+        // React to the [MotionEvent.ACTION_UP] event after double tap is detected. Falsing
+        // checks MUST be on the ACTION_UP event.
+        if (e.actionMasked == MotionEvent.ACTION_UP &&
+                statusBarStateController.isDozing &&
                 (doubleTapEnabled || singleTapEnabled) &&
                 !falsingManager.isProximityNear &&
                 !falsingManager.isFalseDoubleTap
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 408c61f..e06c977 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -19,9 +19,6 @@
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_MANAGEMENT_DISCLOSURE;
 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_NAMED_MANAGEMENT_DISCLOSURE;
-import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_FIRST_FRAME_RECEIVED;
-import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_GOOD;
-import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_START;
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK;
 import static android.hardware.biometrics.BiometricSourceType.FACE;
 import static android.view.View.GONE;
@@ -53,6 +50,7 @@
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Color;
+import android.hardware.biometrics.BiometricFaceConstants;
 import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
@@ -82,7 +80,7 @@
 import com.android.settingslib.Utils;
 import com.android.settingslib.fuelgauge.BatteryStatus;
 import com.android.systemui.R;
-import com.android.systemui.biometrics.BiometricMessageDeferral;
+import com.android.systemui.biometrics.FaceHelpMessageDeferral;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -184,6 +182,7 @@
     private long mChargingTimeRemaining;
     private String mBiometricErrorMessageToShowOnScreenOn;
     private final Set<Integer> mCoExFaceAcquisitionMsgIdsToShow;
+    private final FaceHelpMessageDeferral mFaceAcquiredMessageDeferral;
     private boolean mInited;
 
     private KeyguardUpdateMonitorCallback mUpdateMonitorCallback;
@@ -233,7 +232,8 @@
             LockPatternUtils lockPatternUtils,
             ScreenLifecycle screenLifecycle,
             KeyguardBypassController keyguardBypassController,
-            AccessibilityManager accessibilityManager) {
+            AccessibilityManager accessibilityManager,
+            FaceHelpMessageDeferral faceHelpMessageDeferral) {
         mContext = context;
         mBroadcastDispatcher = broadcastDispatcher;
         mDevicePolicyManager = devicePolicyManager;
@@ -254,6 +254,7 @@
         mScreenLifecycle = screenLifecycle;
         mScreenLifecycle.addObserver(mScreenObserver);
 
+        mFaceAcquiredMessageDeferral = faceHelpMessageDeferral;
         mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>();
         int[] msgIds = context.getResources().getIntArray(
                 com.android.systemui.R.array.config_face_help_msgs_when_fingerprint_enrolled);
@@ -1041,8 +1042,22 @@
         }
 
         @Override
+        public void onBiometricAcquired(BiometricSourceType biometricSourceType, int acquireInfo) {
+            if (biometricSourceType == FACE) {
+                mFaceAcquiredMessageDeferral.processFrame(acquireInfo);
+            }
+        }
+
+        @Override
         public void onBiometricHelp(int msgId, String helpString,
                 BiometricSourceType biometricSourceType) {
+            if (biometricSourceType == FACE) {
+                mFaceAcquiredMessageDeferral.updateMessage(msgId, helpString);
+                if (mFaceAcquiredMessageDeferral.shouldDefer(msgId)) {
+                    return;
+                }
+            }
+
             // TODO(b/141025588): refactor to reduce repetition of code/comments
             // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong
             // as long as primary auth, i.e. PIN/pattern/password, is not required), so it's ok to
@@ -1053,17 +1068,6 @@
                 return;
             }
 
-            if (biometricSourceType == FACE) {
-                if (msgId == KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED) {
-                    mFaceAcquiredMessageDeferral.reset();
-                } else {
-                    mFaceAcquiredMessageDeferral.processMessage(msgId, helpString);
-                    if (mFaceAcquiredMessageDeferral.shouldDefer(msgId)) {
-                        return;
-                    }
-                }
-            }
-
             final boolean faceAuthSoftError = biometricSourceType == FACE
                     && msgId != BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
             final boolean faceAuthFailed = biometricSourceType == FACE
@@ -1109,11 +1113,23 @@
         }
 
         @Override
+        public void onBiometricAuthFailed(BiometricSourceType biometricSourceType) {
+            if (biometricSourceType == FACE) {
+                mFaceAcquiredMessageDeferral.reset();
+            }
+        }
+
+        @Override
         public void onBiometricError(int msgId, String errString,
                 BiometricSourceType biometricSourceType) {
             CharSequence deferredFaceMessage = null;
             if (biometricSourceType == FACE) {
-                deferredFaceMessage = mFaceAcquiredMessageDeferral.getDeferredMessage();
+                if (msgId == BiometricFaceConstants.FACE_ERROR_TIMEOUT) {
+                    deferredFaceMessage = mFaceAcquiredMessageDeferral.getDeferredMessage();
+                    if (DEBUG) {
+                        Log.d(TAG, "showDeferredFaceMessage msgId=" + deferredFaceMessage);
+                    }
+                }
                 mFaceAcquiredMessageDeferral.reset();
             }
 
@@ -1308,14 +1324,4 @@
             }
         }
     };
-
-    private final BiometricMessageDeferral mFaceAcquiredMessageDeferral =
-            new BiometricMessageDeferral(
-                    Set.of(
-                            FACE_ACQUIRED_GOOD,
-                            FACE_ACQUIRED_START,
-                            FACE_ACQUIRED_FIRST_FRAME_RECEIVED
-                    ),
-                    Set.of(FACE_ACQUIRED_TOO_DARK)
-            );
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
index c74621d..258f9fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
@@ -38,7 +38,7 @@
 public class VibratorHelper {
 
     private final Vibrator mVibrator;
-    private static final VibrationAttributes TOUCH_VIBRATION_ATTRIBUTES =
+    public static final VibrationAttributes TOUCH_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_TOUCH);
     private final Executor mExecutor;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
index 31b21c9..553826d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
@@ -136,7 +136,7 @@
         headsUpManager.removeNotification(notificationKey, true /* releaseImmediately */, animate)
     }
 
-    override fun onLaunchAnimationCancelled() {
+    override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
         // TODO(b/184121838): Should we call InteractionJankMonitor.cancel if the animation started
         // here?
         notificationShadeWindowViewController.setExpandAnimationRunning(false)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 820f6e4..0b63bbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1737,13 +1737,18 @@
                 }
 
                 @Override
-                public void onLaunchAnimationCancelled() {
+                public void onLaunchAnimationCancelled(@Nullable Boolean newKeyguardOccludedState) {
+                    if (newKeyguardOccludedState != null) {
+                        mKeyguardViewMediator.setOccluded(
+                                newKeyguardOccludedState, false /* animate */);
+                    }
+
                     // Set mIsLaunchingActivityOverLockscreen to false before actually finishing the
                     // animation so that we can assume that mIsLaunchingActivityOverLockscreen
                     // being true means that we will collapse the shade (or at least run the
                     // post collapse runnables) later on.
                     CentralSurfacesImpl.this.mIsLaunchingActivityOverLockscreen = false;
-                    getDelegate().onLaunchAnimationCancelled();
+                    getDelegate().onLaunchAnimationCancelled(newKeyguardOccludedState);
                 }
             };
         } else if (dismissShade) {
@@ -3623,6 +3628,7 @@
             dismissVolumeDialog();
             mWakeUpCoordinator.setFullyAwake(false);
             mKeyguardBypassController.onStartedGoingToSleep();
+            mStatusBarTouchableRegionManager.updateTouchableRegion();
 
             // The unlocked screen off and fold to aod animations might use our LightRevealScrim -
             // we need to be expanded for it to be visible.
@@ -3651,6 +3657,7 @@
                 // once we fully woke up.
                 updateRevealEffect(true /* wakingUp */);
                 updateNotificationPanelTouchState();
+                mStatusBarTouchableRegionManager.updateTouchableRegion();
 
                 // If we are waking up during the screen off animation, we should undo making the
                 // expanded visible (we did that so the LightRevealScrim would be visible).
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
index c092216..ee948c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -51,7 +51,7 @@
         centralSurfaces.notificationPanelViewController.applyLaunchAnimationProgress(linearProgress)
     }
 
-    override fun onLaunchAnimationCancelled() {
+    override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
         delegate.onLaunchAnimationCancelled()
         centralSurfaces.notificationPanelViewController.setIsLaunchAnimationRunning(false)
         centralSurfaces.onLaunchAnimationCancelled(isLaunchForActivity)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index 75dac1a..d9c0293 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -55,6 +55,7 @@
     private final Context mContext;
     private final HeadsUpManagerPhone mHeadsUpManager;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
+    private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
 
     private boolean mIsStatusBarExpanded = false;
     private boolean mShouldAdjustInsets = false;
@@ -72,7 +73,8 @@
             Context context,
             NotificationShadeWindowController notificationShadeWindowController,
             ConfigurationController configurationController,
-            HeadsUpManagerPhone headsUpManager
+            HeadsUpManagerPhone headsUpManager,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController
     ) {
         mContext = context;
         initResources();
@@ -115,6 +117,8 @@
         mNotificationShadeWindowController.setForcePluginOpenListener((forceOpen) -> {
             updateTouchableRegion();
         });
+
+        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
     }
 
     protected void setup(
@@ -179,7 +183,7 @@
     /**
      * Set the touchable portion of the status bar based on what elements are visible.
      */
-    private void updateTouchableRegion() {
+    public void updateTouchableRegion() {
         boolean hasCutoutInset = (mNotificationShadeWindowView != null)
                 && (mNotificationShadeWindowView.getRootWindowInsets() != null)
                 && (mNotificationShadeWindowView.getRootWindowInsets().getDisplayCutout() != null);
@@ -242,12 +246,25 @@
         touchableRegion.union(bounds);
     }
 
+    /**
+     * Helper to let us know when calculating the region is not needed because we know the entire
+     * screen needs to be touchable.
+     */
+    private boolean shouldMakeEntireScreenTouchable() {
+        // The touchable region is always the full area when expanded, whether we're showing the
+        // shade or the bouncer. It's also fully touchable when the screen off animation is playing
+        // since we don't want stray touches to go through the light reveal scrim to whatever is
+        // underneath.
+        return mIsStatusBarExpanded
+                || mCentralSurfaces.isBouncerShowing()
+                || mUnlockedScreenOffAnimationController.isAnimationPlaying();
+    }
+
     private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener =
             new OnComputeInternalInsetsListener() {
         @Override
         public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
-            if (mIsStatusBarExpanded || mCentralSurfaces.isBouncerShowing()) {
-                // The touchable region is always the full area when expanded
+            if (shouldMakeEntireScreenTouchable()) {
                 return;
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
deleted file mode 100644
index fe84674..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.systemui.statusbar.pipeline
-
-import android.content.Context
-import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
-import javax.inject.Inject
-import javax.inject.Provider
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-
-/**
- * A temporary object that collects on [WifiViewModel] flows for debugging purposes.
- *
- * This will eventually get migrated to a view binder that will use the flow outputs to set state on
- * views. For now, this just collects on flows so that the information gets logged.
- */
-@SysUISingleton
-class ConnectivityInfoProcessor @Inject constructor(
-        context: Context,
-        // TODO(b/238425913): Don't use the application scope; instead, use the status bar view's
-        // scope so we only do work when there's UI that cares about it.
-        @Application private val scope: CoroutineScope,
-        private val statusBarPipelineFlags: StatusBarPipelineFlags,
-        private val wifiViewModelProvider: Provider<WifiViewModel>,
-) : CoreStartable(context) {
-    override fun start() {
-        if (!statusBarPipelineFlags.isNewPipelineBackendEnabled()) {
-            return
-        }
-        // TODO(b/238425913): The view binder should do this instead. For now, do it here so we can
-        // see the logs.
-        scope.launch {
-            wifiViewModelProvider.get().isActivityInVisible.collect { }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 681dc6f..9a7c3fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -16,25 +16,15 @@
 
 package com.android.systemui.statusbar.pipeline.dagger
 
-import com.android.systemui.CoreStartable
-import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl
 import dagger.Binds
 import dagger.Module
-import dagger.multibindings.ClassKey
-import dagger.multibindings.IntoMap
 
 @Module
 abstract class StatusBarPipelineModule {
-    /** Inject into ConnectivityInfoProcessor. */
-    @Binds
-    @IntoMap
-    @ClassKey(ConnectivityInfoProcessor::class)
-    abstract fun bindConnectivityInfoProcessor(cip: ConnectivityInfoProcessor): CoreStartable
-
     @Binds
     abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index e7fa6d2..aae0f93 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -27,7 +27,6 @@
 import android.os.Message;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 
@@ -37,6 +36,7 @@
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.systemui.bluetooth.BluetoothLogger;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -57,9 +57,9 @@
 public class BluetoothControllerImpl implements BluetoothController, BluetoothCallback,
         CachedBluetoothDevice.Callback, LocalBluetoothProfileManager.ServiceListener {
     private static final String TAG = "BluetoothController";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private final DumpManager mDumpManager;
+    private final BluetoothLogger mLogger;
     private final LocalBluetoothManager mLocalBluetoothManager;
     private final UserManager mUserManager;
     private final int mCurrentUser;
@@ -70,6 +70,7 @@
     private final List<CachedBluetoothDevice> mConnectedDevices = new ArrayList<>();
 
     private boolean mEnabled;
+    @ConnectionState
     private int mConnectionState = BluetoothAdapter.STATE_DISCONNECTED;
     private boolean mAudioProfileOnly;
     private boolean mIsActive;
@@ -83,10 +84,12 @@
     public BluetoothControllerImpl(
             Context context,
             DumpManager dumpManager,
+            BluetoothLogger logger,
             @Background Looper bgLooper,
             @Main Looper mainLooper,
             @Nullable LocalBluetoothManager localBluetoothManager) {
         mDumpManager = dumpManager;
+        mLogger = logger;
         mLocalBluetoothManager = localBluetoothManager;
         mBgHandler = new Handler(bgLooper);
         mHandler = new H(mainLooper);
@@ -116,7 +119,7 @@
             return;
         }
         pw.print("  mEnabled="); pw.println(mEnabled);
-        pw.print("  mConnectionState="); pw.println(stateToString(mConnectionState));
+        pw.print("  mConnectionState="); pw.println(connectionStateToString(mConnectionState));
         pw.print("  mAudioProfileOnly="); pw.println(mAudioProfileOnly);
         pw.print("  mIsActive="); pw.println(mIsActive);
         pw.print("  mConnectedDevices="); pw.println(getConnectedDevices());
@@ -127,7 +130,7 @@
         }
     }
 
-    private static String stateToString(int state) {
+    private static String connectionStateToString(@ConnectionState int state) {
         switch (state) {
             case BluetoothAdapter.STATE_CONNECTED:
                 return "CONNECTED";
@@ -320,8 +323,8 @@
     }
 
     @Override
-    public void onBluetoothStateChanged(int bluetoothState) {
-        if (DEBUG) Log.d(TAG, "BluetoothStateChanged=" + stateToString(bluetoothState));
+    public void onBluetoothStateChanged(@AdapterState int bluetoothState) {
+        mLogger.logStateChange(BluetoothAdapter.nameForState(bluetoothState));
         mEnabled = bluetoothState == BluetoothAdapter.STATE_ON
                 || bluetoothState == BluetoothAdapter.STATE_TURNING_ON;
         mState = bluetoothState;
@@ -330,24 +333,25 @@
     }
 
     @Override
-    public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
-        if (DEBUG) Log.d(TAG, "DeviceAdded=" + cachedDevice.getAddress());
+    public void onDeviceAdded(@NonNull CachedBluetoothDevice cachedDevice) {
+        mLogger.logDeviceAdded(cachedDevice.getAddress());
         cachedDevice.registerCallback(this);
         updateConnected();
         mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
     }
 
     @Override
-    public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
-        if (DEBUG) Log.d(TAG, "DeviceDeleted=" + cachedDevice.getAddress());
+    public void onDeviceDeleted(@NonNull CachedBluetoothDevice cachedDevice) {
+        mLogger.logDeviceDeleted(cachedDevice.getAddress());
         mCachedState.remove(cachedDevice);
         updateConnected();
         mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
     }
 
     @Override
-    public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
-        if (DEBUG) Log.d(TAG, "DeviceBondStateChanged=" + cachedDevice.getAddress());
+    public void onDeviceBondStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice, int bondState) {
+        mLogger.logBondStateChange(cachedDevice.getAddress(), bondState);
         mCachedState.remove(cachedDevice);
         updateConnected();
         mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
@@ -355,50 +359,47 @@
 
     @Override
     public void onDeviceAttributesChanged() {
-        if (DEBUG) Log.d(TAG, "DeviceAttributesChanged");
+        mLogger.logDeviceAttributesChanged();
         updateConnected();
         mHandler.sendEmptyMessage(H.MSG_PAIRED_DEVICES_CHANGED);
     }
 
     @Override
-    public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
-        if (DEBUG) {
-            Log.d(TAG, "ConnectionStateChanged=" + cachedDevice.getAddress() + " "
-                    + stateToString(state));
-        }
+    public void onConnectionStateChanged(
+            @Nullable CachedBluetoothDevice cachedDevice,
+            @ConnectionState int state) {
+        mLogger.logDeviceConnectionStateChanged(
+                getAddressOrNull(cachedDevice), connectionStateToString(state));
         mCachedState.remove(cachedDevice);
         updateConnected();
         mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
     }
 
     @Override
-    public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice,
-            int state, int bluetoothProfile) {
-        if (DEBUG) {
-            Log.d(TAG, "ProfileConnectionStateChanged=" + cachedDevice.getAddress() + " "
-                    + stateToString(state) + " profileId=" + bluetoothProfile);
-        }
+    public void onProfileConnectionStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice,
+            @ConnectionState int state,
+            int bluetoothProfile) {
+        mLogger.logProfileConnectionStateChanged(
+                cachedDevice.getAddress(), connectionStateToString(state), bluetoothProfile);
         mCachedState.remove(cachedDevice);
         updateConnected();
         mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
     }
 
     @Override
-    public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {
-        if (DEBUG) {
-            Log.d(TAG, "ActiveDeviceChanged=" + activeDevice.getAddress()
-                    + " profileId=" + bluetoothProfile);
-        }
+    public void onActiveDeviceChanged(
+            @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
+        mLogger.logActiveDeviceChanged(getAddressOrNull(activeDevice), bluetoothProfile);
         updateActive();
         mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
     }
 
     @Override
-    public void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
-        if (DEBUG) {
-            Log.d(TAG, "ACLConnectionStateChanged=" + cachedDevice.getAddress() + " "
-                    + stateToString(state));
-        }
+    public void onAclConnectionStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice, int state) {
+        mLogger.logAclConnectionStateChanged(
+                cachedDevice.getAddress(), connectionStateToString(state));
         mCachedState.remove(cachedDevice);
         updateConnected();
         mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
@@ -415,6 +416,11 @@
         return state;
     }
 
+    @Nullable
+    private String getAddressOrNull(@Nullable CachedBluetoothDevice device) {
+        return device == null ? null : device.getAddress();
+    }
+
     @Override
     public void onServiceConnected() {
         updateConnected();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index 62bda2c..1d5b88e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -1122,11 +1122,19 @@
         }
 
         public String getName(Context context, UserRecord item) {
+            return getName(context, item, false);
+        }
+
+        /**
+         * Returns the name for the given {@link UserRecord}.
+         */
+        public String getName(Context context, UserRecord item, boolean isTablet) {
             return LegacyUserUiHelper.getUserRecordName(
                     context,
                     item,
                     mController.isGuestUserAutoCreated(),
-                    mController.isGuestUserResetting());
+                    mController.isGuestUserResetting(),
+                    isTablet);
         }
 
         protected static ColorFilter getDisabledUserAvatarColorFilter() {
@@ -1136,8 +1144,12 @@
         }
 
         protected static Drawable getIconDrawable(Context context, UserRecord item) {
+            return getIconDrawable(context, item, false);
+        }
+        protected static Drawable getIconDrawable(Context context, UserRecord item,
+                boolean isTablet) {
             int iconRes = LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
-                    item.isAddUser, item.isGuest, item.isAddSupervisedUser);
+                    item.isAddUser, item.isGuest, item.isAddSupervisedUser, isTablet);
             return context.getDrawable(iconRes);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index 734eeec..a52e2af 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -19,12 +19,10 @@
 import android.annotation.LayoutRes
 import android.annotation.SuppressLint
 import android.content.Context
-import android.content.pm.PackageManager
 import android.graphics.PixelFormat
 import android.graphics.drawable.Drawable
 import android.os.PowerManager
 import android.os.SystemClock
-import android.util.Log
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import android.view.WindowManager
@@ -33,11 +31,7 @@
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT
 import androidx.annotation.CallSuper
-import com.android.internal.widget.CachingIconView
-import com.android.settingslib.Utils
-import com.android.systemui.R
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
 
@@ -50,17 +44,22 @@
  * The generic type T is expected to contain all the information necessary for the subclasses to
  * display the view in a certain state, since they receive <T> in [updateView].
  *
- * TODO(b/245610654): Remove all the media-specific logic from this class.
+ * @property windowTitle the title to use for the window that displays the temporary view. Should be
+ *   normally cased, like "Window Title".
+ * @property wakeReason a string used for logging if we needed to wake the screen in order to
+ *   display the temporary view. Should be screaming snake cased, like WAKE_REASON.
  */
-abstract class TemporaryViewDisplayController<T : TemporaryViewInfo>(
+abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : TemporaryViewLogger>(
     internal val context: Context,
-    internal val logger: MediaTttLogger,
+    internal val logger: U,
     internal val windowManager: WindowManager,
     @Main private val mainExecutor: DelayableExecutor,
     private val accessibilityManager: AccessibilityManager,
     private val configurationController: ConfigurationController,
     private val powerManager: PowerManager,
     @LayoutRes private val viewLayoutRes: Int,
+    private val windowTitle: String,
+    private val wakeReason: String,
 ) {
     /**
      * Window layout params that will be used as a starting point for the [windowLayoutParams] of
@@ -72,7 +71,7 @@
         height = WindowManager.LayoutParams.WRAP_CONTENT
         type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
         flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-        title = WINDOW_TITLE
+        title = windowTitle
         format = PixelFormat.TRANSLUCENT
         setTrustedOverlay()
     }
@@ -115,10 +114,10 @@
                 powerManager.wakeUp(
                         SystemClock.uptimeMillis(),
                         PowerManager.WAKE_REASON_APPLICATION,
-                        "com.android.systemui:media_tap_to_transfer_activated"
+                        "com.android.systemui:$wakeReason",
                 )
             }
-
+            logger.logChipAddition()
             inflateAndUpdateView(newInfo)
         }
 
@@ -192,80 +191,8 @@
      * appears.
      */
     open fun animateViewIn(view: ViewGroup) {}
-
-    /**
-     * Returns the size that the icon should be, or null if no size override is needed.
-     */
-    open fun getIconSize(isAppIcon: Boolean): Int? = null
-
-    /**
-     * An internal method to set the icon on the view.
-     *
-     * This is in the common superclass since both the sender and the receiver show an icon.
-     *
-     * @param appPackageName the package name of the app playing the media. Will be used to fetch
-     *   the app icon and app name if overrides aren't provided.
-     *
-     * @return the content description of the icon.
-     */
-    internal fun setIcon(
-        currentView: ViewGroup,
-        appPackageName: String?,
-        appIconDrawableOverride: Drawable? = null,
-        appNameOverride: CharSequence? = null,
-    ): CharSequence {
-        val appIconView = currentView.requireViewById<CachingIconView>(R.id.app_icon)
-        val iconInfo = getIconInfo(appPackageName)
-
-        getIconSize(iconInfo.isAppIcon)?.let { size ->
-            val lp = appIconView.layoutParams
-            lp.width = size
-            lp.height = size
-            appIconView.layoutParams = lp
-        }
-
-        appIconView.contentDescription = appNameOverride ?: iconInfo.iconName
-        appIconView.setImageDrawable(appIconDrawableOverride ?: iconInfo.icon)
-        return appIconView.contentDescription
-    }
-
-    /**
-     * Returns the information needed to display the icon.
-     *
-     * The information will either contain app name and icon of the app playing media, or a default
-     * name and icon if we can't find the app name/icon.
-     */
-    private fun getIconInfo(appPackageName: String?): IconInfo {
-        if (appPackageName != null) {
-            try {
-                return IconInfo(
-                    iconName = context.packageManager.getApplicationInfo(
-                        appPackageName, PackageManager.ApplicationInfoFlags.of(0)
-                    ).loadLabel(context.packageManager).toString(),
-                    icon = context.packageManager.getApplicationIcon(appPackageName),
-                    isAppIcon = true
-                )
-            } catch (e: PackageManager.NameNotFoundException) {
-                Log.w(TAG, "Cannot find package $appPackageName", e)
-            }
-        }
-        return IconInfo(
-            iconName = context.getString(R.string.media_output_dialog_unknown_launch_app_name),
-            icon = context.resources.getDrawable(R.drawable.ic_cast).apply {
-                this.setTint(
-                    Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary)
-                )
-            },
-            isAppIcon = false
-        )
-    }
 }
 
-// Used in CTS tests UpdateMediaTapToTransferSenderDisplayTest and
-// UpdateMediaTapToTransferReceiverDisplayTest
-private const val WINDOW_TITLE = "Media Transfer Chip View"
-private val TAG = TemporaryViewDisplayController::class.simpleName!!
-
 object TemporaryDisplayRemovalReason {
     const val REASON_TIMEOUT = "TIMEOUT"
     const val REASON_SCREEN_TAP = "SCREEN_TAP"
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
new file mode 100644
index 0000000..606a11a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.systemui.temporarydisplay
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+
+/** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */
+open class TemporaryViewLogger(
+    internal val buffer: LogBuffer,
+    internal val tag: String,
+) {
+    /** Logs that we added the chip to a new window. */
+    fun logChipAddition() {
+        buffer.log(tag, LogLevel.DEBUG, {}, { "Chip added" })
+    }
+
+    /** Logs that we removed the chip for the given [reason]. */
+    fun logChipRemoval(reason: String) {
+        buffer.log(tag, LogLevel.DEBUG, { str1 = reason }, { "Chip removed due to $str1" })
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
index d43f739..5e2dde6 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
@@ -173,8 +173,8 @@
             this,
             R.layout.user_switcher_fullscreen_popup_item,
             layoutInflater,
-            { item: UserRecord -> adapter.getName(this@UserSwitcherActivity, item) },
-            { item: UserRecord -> adapter.findUserIcon(item).mutate().apply {
+            { item: UserRecord -> adapter.getName(this@UserSwitcherActivity, item, true) },
+            { item: UserRecord -> adapter.findUserIcon(item, true).mutate().apply {
                 setTint(resources.getColor(
                     R.color.user_switcher_fullscreen_popup_item_tint,
                     getTheme()
@@ -322,6 +322,9 @@
         return flags.isEnabled(Flags.MODERN_USER_SWITCHER_ACTIVITY)
     }
 
+    /**
+     * Provides views to populate the option menu.
+     */
     private class ItemAdapter(
         val parentContext: Context,
         val resource: Int,
@@ -375,20 +378,20 @@
             return view
         }
 
-        override fun getName(context: Context, item: UserRecord): String {
+        override fun getName(context: Context, item: UserRecord, isTablet: Boolean): String {
             return if (item == manageUserRecord) {
                 getString(R.string.manage_users)
             } else {
-                super.getName(context, item)
+                super.getName(context, item, isTablet)
             }
         }
 
-        fun findUserIcon(item: UserRecord): Drawable {
+        fun findUserIcon(item: UserRecord, isTablet: Boolean = false): Drawable {
             if (item == manageUserRecord) {
                 return getDrawable(R.drawable.ic_manage_users)
             }
             if (item.info == null) {
-                return getIconDrawable(this@UserSwitcherActivity, item)
+                return getIconDrawable(this@UserSwitcherActivity, item, isTablet)
             }
             val userIcon = userManager.getUserIcon(item.info.id)
             if (userIcon != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
index 18369d9..15fdc35 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
@@ -49,8 +49,11 @@
         isAddUser: Boolean,
         isGuest: Boolean,
         isAddSupervisedUser: Boolean,
+        isTablet: Boolean = false,
     ): Int {
-        return if (isAddUser) {
+        return if (isAddUser && isTablet) {
+            R.drawable.ic_account_circle_filled
+        } else if (isAddUser) {
             R.drawable.ic_add
         } else if (isGuest) {
             R.drawable.ic_account_circle
@@ -67,6 +70,7 @@
         record: UserRecord,
         isGuestUserAutoCreated: Boolean,
         isGuestUserResetting: Boolean,
+        isTablet: Boolean = false,
     ): String {
         val resourceId: Int? = getGuestUserRecordNameResourceId(record)
         return when {
@@ -80,6 +84,7 @@
                         isGuestUserResetting = isGuestUserResetting,
                         isAddUser = record.isAddUser,
                         isAddSupervisedUser = record.isAddSupervisedUser,
+                        isTablet = isTablet,
                     )
                 )
         }
@@ -108,12 +113,14 @@
         isGuestUserResetting: Boolean,
         isAddUser: Boolean,
         isAddSupervisedUser: Boolean,
+        isTablet: Boolean = false,
     ): Int {
         check(isGuest || isAddUser || isAddSupervisedUser)
 
         return when {
             isGuest && isGuestUserAutoCreated && isGuestUserResetting ->
                 com.android.settingslib.R.string.guest_resetting
+            isGuest && isTablet -> com.android.settingslib.R.string.guest_new_guest
             isGuest && isGuestUserAutoCreated -> com.android.internal.R.string.guest_name
             isGuest -> com.android.internal.R.string.guest_name
             isAddUser -> com.android.settingslib.R.string.user_add_user
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
index 83a3d0d..d7ad3ce 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
@@ -129,7 +129,10 @@
                     viewModel.users.collect { users ->
                         val viewPool =
                             view.children.filter { it.tag == USER_VIEW_TAG }.toMutableList()
-                        viewPool.forEach { view.removeView(it) }
+                        viewPool.forEach {
+                            view.removeView(it)
+                            flowWidget.removeView(it)
+                        }
                         users.forEach { userViewModel ->
                             val userView =
                                 if (viewPool.isNotEmpty()) {
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 66ce01b..398341d 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -20,6 +20,7 @@
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import com.android.systemui.R
+import com.android.systemui.common.ui.drawable.CircularDrawable
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
@@ -130,7 +131,7 @@
         return UserViewModel(
             viewKey = model.id,
             name = model.name,
-            image = model.image,
+            image = CircularDrawable(model.image),
             isSelectionMarkerVisible = model.isSelected,
             alpha =
                 if (model.isSelectable) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
index e2790e4..a61cd23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
@@ -161,7 +161,18 @@
         runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback)
 
         waitForIdleSync()
-        verify(controller).onLaunchAnimationCancelled()
+        verify(controller).onLaunchAnimationCancelled(false /* newKeyguardOccludedState */)
+        verify(controller, never()).onLaunchAnimationStart(anyBoolean())
+    }
+
+    @Test
+    fun passesOccludedStateToLaunchAnimationCancelled_ifTrue() {
+        val runner = activityLaunchAnimator.createRunner(controller)
+        runner.onAnimationCancelled(true /* isKeyguardOccluded */)
+        runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback)
+
+        waitForIdleSync()
+        verify(controller).onLaunchAnimationCancelled(true /* newKeyguardOccludedState */)
         verify(controller, never()).onLaunchAnimationStart(anyBoolean())
     }
 
@@ -253,7 +264,7 @@
         assertOnMainThread()
     }
 
-    override fun onLaunchAnimationCancelled() {
+    override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
         assertOnMainThread()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricMessageDeferralTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricMessageDeferralTest.kt
deleted file mode 100644
index 419fedf..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricMessageDeferralTest.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * 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.systemui.biometrics
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNull
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class BiometricMessageDeferralTest : SysuiTestCase() {
-
-    @Test
-    fun testProcessNoMessages_noDeferredMessage() {
-        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf())
-
-        assertNull(biometricMessageDeferral.getDeferredMessage())
-    }
-
-    @Test
-    fun testProcessNonDeferredMessages_noDeferredMessage() {
-        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf(1, 2))
-
-        // WHEN there are no deferred messages processed
-        for (i in 0..3) {
-            biometricMessageDeferral.processMessage(4, "test")
-        }
-
-        // THEN getDeferredMessage is null
-        assertNull(biometricMessageDeferral.getDeferredMessage())
-    }
-
-    @Test
-    fun testAllProcessedMessagesWereDeferred() {
-        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf(1))
-
-        // WHEN all the processed messages are a deferred message
-        for (i in 0..3) {
-            biometricMessageDeferral.processMessage(1, "test")
-        }
-
-        // THEN deferredMessage will return the string associated with the deferred msgId
-        assertEquals("test", biometricMessageDeferral.getDeferredMessage())
-    }
-
-    @Test
-    fun testReturnsMostFrequentDeferredMessage() {
-        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf(1, 2))
-
-        // WHEN there's two msgId=1 processed and one msgId=2 processed
-        biometricMessageDeferral.processMessage(1, "msgId-1")
-        biometricMessageDeferral.processMessage(1, "msgId-1")
-        biometricMessageDeferral.processMessage(1, "msgId-1")
-        biometricMessageDeferral.processMessage(2, "msgId-2")
-
-        // THEN the most frequent deferred message is that meets the threshold is returned
-        assertEquals("msgId-1", biometricMessageDeferral.getDeferredMessage())
-    }
-
-    @Test
-    fun testDeferredMessage_mustMeetThreshold() {
-        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf(1))
-
-        // WHEN more nonDeferredMessages are shown than the deferred message
-        val totalMessages = 10
-        val nonDeferredMessagesCount =
-            (totalMessages * BiometricMessageDeferral.THRESHOLD).toInt() + 1
-        for (i in 0 until nonDeferredMessagesCount) {
-            biometricMessageDeferral.processMessage(4, "non-deferred-msg")
-        }
-        for (i in nonDeferredMessagesCount until totalMessages) {
-            biometricMessageDeferral.processMessage(1, "msgId-1")
-        }
-
-        // THEN there's no deferred message because it didn't meet the threshold
-        assertNull(biometricMessageDeferral.getDeferredMessage())
-    }
-
-    @Test
-    fun testDeferredMessage_manyExcludedMessages_getDeferredMessage() {
-        val biometricMessageDeferral = BiometricMessageDeferral(setOf(3), setOf(1))
-
-        // WHEN more excludedMessages are shown than the deferred message
-        val totalMessages = 10
-        val excludedMessagesCount = (totalMessages * BiometricMessageDeferral.THRESHOLD).toInt() + 1
-        for (i in 0 until excludedMessagesCount) {
-            biometricMessageDeferral.processMessage(3, "excluded-msg")
-        }
-        for (i in excludedMessagesCount until totalMessages) {
-            biometricMessageDeferral.processMessage(1, "msgId-1")
-        }
-
-        // THEN there IS a deferred message because the deferred msg meets the threshold amongst the
-        // non-excluded messages
-        assertEquals("msgId-1", biometricMessageDeferral.getDeferredMessage())
-    }
-
-    @Test
-    fun testResetClearsOutCounts() {
-        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf(1, 2))
-
-        // GIVEN two msgId=1 events processed
-        biometricMessageDeferral.processMessage(1, "msgId-1")
-        biometricMessageDeferral.processMessage(1, "msgId-1")
-
-        // WHEN counts are reset and then a single deferred message is processed (msgId=2)
-        biometricMessageDeferral.reset()
-        biometricMessageDeferral.processMessage(2, "msgId-2")
-
-        // THEN msgId-2 is the deferred message since the two msgId=1 events were reset
-        assertEquals("msgId-2", biometricMessageDeferral.getDeferredMessage())
-    }
-
-    @Test
-    fun testShouldDefer() {
-        // GIVEN should defer msgIds 1 and 2
-        val biometricMessageDeferral = BiometricMessageDeferral(setOf(3), setOf(1, 2))
-
-        // THEN shouldDefer returns true for ids 1 & 2
-        assertTrue(biometricMessageDeferral.shouldDefer(1))
-        assertTrue(biometricMessageDeferral.shouldDefer(2))
-
-        // THEN should defer returns false for ids 3 & 4
-        assertFalse(biometricMessageDeferral.shouldDefer(3))
-        assertFalse(biometricMessageDeferral.shouldDefer(4))
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
new file mode 100644
index 0000000..c9ccdb3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.systemui.biometrics
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.keyguard.logging.BiometricMessageDeferralLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class FaceHelpMessageDeferralTest : SysuiTestCase() {
+    val threshold = .75f
+    @Mock lateinit var logger: BiometricMessageDeferralLogger
+    @Mock lateinit var dumpManager: DumpManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun testProcessFrame_logs() {
+        val biometricMessageDeferral = createMsgDeferral(setOf(1))
+        biometricMessageDeferral.processFrame(1)
+        verify(logger).logFrameProcessed(1, 1, "1")
+    }
+
+    @Test
+    fun testUpdateMessage_logs() {
+        val biometricMessageDeferral = createMsgDeferral(setOf(1))
+        biometricMessageDeferral.updateMessage(1, "hi")
+        verify(logger).logUpdateMessage(1, "hi")
+    }
+
+    @Test
+    fun testReset_logs() {
+        val biometricMessageDeferral = createMsgDeferral(setOf(1))
+        biometricMessageDeferral.reset()
+        verify(logger).reset()
+    }
+
+    @Test
+    fun testProcessNoMessages_noDeferredMessage() {
+        val biometricMessageDeferral = createMsgDeferral(emptySet())
+
+        assertNull(biometricMessageDeferral.getDeferredMessage())
+    }
+
+    @Test
+    fun testProcessNonDeferredMessages_noDeferredMessage() {
+        val biometricMessageDeferral = createMsgDeferral(setOf(1, 2))
+
+        // WHEN there are no deferred messages processed
+        for (i in 0..3) {
+            biometricMessageDeferral.processFrame(4)
+            biometricMessageDeferral.updateMessage(4, "test")
+        }
+
+        // THEN getDeferredMessage is null
+        assertNull(biometricMessageDeferral.getDeferredMessage())
+    }
+
+    @Test
+    fun testProcessMessagesWithDeferredMessage_deferredMessageWasNeverGivenAString() {
+        val biometricMessageDeferral = createMsgDeferral(setOf(1, 2))
+
+        biometricMessageDeferral.processFrame(1)
+
+        assertNull(biometricMessageDeferral.getDeferredMessage())
+    }
+
+    @Test
+    fun testAllProcessedMessagesWereDeferred() {
+        val biometricMessageDeferral = createMsgDeferral(setOf(1))
+
+        // WHEN all the processed messages are a deferred message
+        for (i in 0..3) {
+            biometricMessageDeferral.processFrame(1)
+            biometricMessageDeferral.updateMessage(1, "test")
+        }
+
+        // THEN deferredMessage will return the string associated with the deferred msgId
+        assertEquals("test", biometricMessageDeferral.getDeferredMessage())
+    }
+
+    @Test
+    fun testReturnsMostFrequentDeferredMessage() {
+        val biometricMessageDeferral = createMsgDeferral(setOf(1, 2))
+
+        // WHEN there's 80%of the messages are msgId=1 and 20% is msgId=2
+        biometricMessageDeferral.processFrame(1)
+        biometricMessageDeferral.processFrame(1)
+        biometricMessageDeferral.processFrame(1)
+        biometricMessageDeferral.processFrame(1)
+        biometricMessageDeferral.updateMessage(1, "msgId-1")
+
+        biometricMessageDeferral.processFrame(2)
+        biometricMessageDeferral.updateMessage(2, "msgId-2")
+
+        // THEN the most frequent deferred message is that meets the threshold is returned
+        assertEquals("msgId-1", biometricMessageDeferral.getDeferredMessage())
+    }
+
+    @Test
+    fun testDeferredMessage_mustMeetThreshold() {
+        val biometricMessageDeferral = createMsgDeferral(setOf(1))
+
+        // WHEN more nonDeferredMessages are shown than the deferred message
+        val totalMessages = 10
+        val nonDeferredMessagesCount = (totalMessages * threshold).toInt() + 1
+        for (i in 0 until nonDeferredMessagesCount) {
+            biometricMessageDeferral.processFrame(4)
+            biometricMessageDeferral.updateMessage(4, "non-deferred-msg")
+        }
+        for (i in nonDeferredMessagesCount until totalMessages) {
+            biometricMessageDeferral.processFrame(1)
+            biometricMessageDeferral.updateMessage(1, "msgId-1")
+        }
+
+        // THEN there's no deferred message because it didn't meet the threshold
+        assertNull(biometricMessageDeferral.getDeferredMessage())
+    }
+
+    @Test
+    fun testResetClearsOutCounts() {
+        val biometricMessageDeferral = createMsgDeferral(setOf(1, 2))
+
+        // GIVEN two msgId=1 events processed
+        biometricMessageDeferral.processFrame(
+            1,
+        )
+        biometricMessageDeferral.updateMessage(1, "msgId-1")
+        biometricMessageDeferral.processFrame(1)
+        biometricMessageDeferral.updateMessage(1, "msgId-1")
+
+        // WHEN counts are reset and then a single deferred message is processed (msgId=2)
+        biometricMessageDeferral.reset()
+        biometricMessageDeferral.processFrame(2)
+        biometricMessageDeferral.updateMessage(2, "msgId-2")
+
+        // THEN msgId-2 is the deferred message since the two msgId=1 events were reset
+        assertEquals("msgId-2", biometricMessageDeferral.getDeferredMessage())
+    }
+
+    @Test
+    fun testShouldDefer() {
+        // GIVEN should defer msgIds 1 and 2
+        val biometricMessageDeferral = createMsgDeferral(setOf(1, 2))
+
+        // THEN shouldDefer returns true for ids 1 & 2
+        assertTrue(biometricMessageDeferral.shouldDefer(1))
+        assertTrue(biometricMessageDeferral.shouldDefer(2))
+
+        // THEN should defer returns false for ids 3 & 4
+        assertFalse(biometricMessageDeferral.shouldDefer(3))
+        assertFalse(biometricMessageDeferral.shouldDefer(4))
+    }
+
+    private fun createMsgDeferral(messagesToDefer: Set<Int>): BiometricMessageDeferral {
+        return BiometricMessageDeferral(messagesToDefer, threshold, logger, dumpManager)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
index acb5622..3e9cf1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
@@ -229,7 +229,10 @@
     }
 
     @Test
-    public void testAvoidDozingNotPulsing() {
+    public void testGestureWhenDozing() {
+        // We check the FalsingManager for taps during the transition to AoD (dozing=true,
+        // pulsing=false), so the FalsingCollector needs to continue to analyze events that occur
+        // while the device is dozing.
         MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
         MotionEvent up = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0);
 
@@ -239,13 +242,13 @@
         mFalsingCollector.onTouchEvent(down);
         verify(mFalsingDataProvider, never()).onMotionEvent(any(MotionEvent.class));
 
-        // Up event would normally flush the up event, but doesn't.
+        // Up event flushes
         mFalsingCollector.onTouchEvent(up);
-        verify(mFalsingDataProvider, never()).onMotionEvent(any(MotionEvent.class));
+        verify(mFalsingDataProvider, times(2)).onMotionEvent(any(MotionEvent.class));
     }
 
     @Test
-    public void testAvoidDozingButPulsing() {
+    public void testGestureWhenPulsing() {
         MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
         MotionEvent up = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
index eecbee5..a78c902 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
@@ -27,11 +27,11 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor.forClass
 import org.mockito.Mock
+import org.mockito.Mockito.`when`
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -60,7 +60,18 @@
     @Mock
     private lateinit var launcherUnlockAnimationController: ILauncherUnlockAnimationController.Stub
 
-    private lateinit var remoteAnimationTarget: RemoteAnimationTarget
+    private var surfaceControl1 = mock(SurfaceControl::class.java)
+    private var remoteTarget1 = RemoteAnimationTarget(
+            0 /* taskId */, 0, surfaceControl1, false, Rect(), Rect(), 0, Point(), Rect(), Rect(),
+            mock(WindowConfiguration::class.java), false, surfaceControl1, Rect(),
+            mock(ActivityManager.RunningTaskInfo::class.java), false)
+
+    private var surfaceControl2 = mock(SurfaceControl::class.java)
+    private var remoteTarget2 = RemoteAnimationTarget(
+            1 /* taskId */, 0, surfaceControl2, false, Rect(), Rect(), 0, Point(), Rect(), Rect(),
+            mock(WindowConfiguration::class.java), false, surfaceControl2, Rect(),
+            mock(ActivityManager.RunningTaskInfo::class.java), false)
+    private lateinit var remoteAnimationTargets: Array<RemoteAnimationTarget>
 
     @Before
     fun setUp() {
@@ -77,10 +88,7 @@
 
         // All of these fields are final, so we can't mock them, but are needed so that the surface
         // appear amount setter doesn't short circuit.
-        remoteAnimationTarget = RemoteAnimationTarget(
-            0, 0, null, false, Rect(), Rect(), 0, Point(), Rect(), Rect(),
-            mock(WindowConfiguration::class.java), false, mock(SurfaceControl::class.java), Rect(),
-            mock(ActivityManager.RunningTaskInfo::class.java), false)
+        remoteAnimationTargets = arrayOf(remoteTarget1)
 
         // Set the surface applier to our mock so that we can verify the arguments passed to it.
         // This applier does not have any side effects within the unlock animation controller, so
@@ -99,7 +107,7 @@
         `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
 
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
-            remoteAnimationTarget,
+            remoteAnimationTargets,
             0 /* startTime */,
             false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -130,7 +138,7 @@
         `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(false)
 
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
-            remoteAnimationTarget,
+            remoteAnimationTargets,
             0 /* startTime */,
             false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -154,7 +162,7 @@
         `when`(keyguardStateController.isFlingingToDismissKeyguard).thenReturn(false)
 
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
-            remoteAnimationTarget,
+            remoteAnimationTargets,
             0 /* startTime */,
             true /* requestedShowSurfaceBehindKeyguard */
         )
@@ -176,7 +184,7 @@
         `when`(keyguardStateController.isFlingingToDismissKeyguard).thenReturn(true)
 
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
-            remoteAnimationTarget,
+            remoteAnimationTargets,
             0 /* startTime */,
             true /* requestedShowSurfaceBehindKeyguard */
         )
@@ -196,7 +204,7 @@
     @Test
     fun playCannedUnlockAnimation_ifDidNotRequestShowSurface() {
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
-            remoteAnimationTarget,
+            remoteAnimationTargets,
             0 /* startTime */,
             false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -210,7 +218,7 @@
         `when`(notificationShadeWindowController.isLaunchingActivity).thenReturn(true)
 
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
-            remoteAnimationTarget,
+            remoteAnimationTargets,
             0 /* startTime */,
             true /* requestedShowSurfaceBehindKeyguard */
         )
@@ -225,11 +233,46 @@
         keyguardUnlockAnimationController.willUnlockWithInWindowLauncherAnimations = true
 
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
-                remoteAnimationTarget,
+                remoteAnimationTargets,
                 0 /* startTime */,
                 false /* requestedShowSurfaceBehindKeyguard */
         )
 
         assertTrue(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation())
     }
+
+    /**
+     * If we are not wake and unlocking, we expect the unlock animation to play normally.
+     */
+    @Test
+    fun surfaceAnimation_multipleTargets() {
+        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
+                arrayOf(remoteTarget1, remoteTarget2),
+                0 /* startTime */,
+                false /* requestedShowSurfaceBehindKeyguard */
+        )
+
+        // Set appear to 50%, we'll just verify that we're not applying the identity matrix which
+        // means an animation is in progress.
+        keyguardUnlockAnimationController.setSurfaceBehindAppearAmount(0.5f)
+
+        val captor = forClass(SyncRtSurfaceTransactionApplier.SurfaceParams::class.java)
+        verify(surfaceTransactionApplier, times(2)).scheduleApply(captor.capture())
+
+        val allParams = captor.allValues
+
+        val remainingTargets = mutableListOf(surfaceControl1, surfaceControl2)
+        allParams.forEach { params ->
+            assertTrue(!params.matrix.isIdentity)
+            remainingTargets.remove(params.surface)
+        }
+
+        // Make sure we called applyParams with each of the surface controls once. The order does
+        // not matter, so don't explicitly check for that.
+        assertTrue(remainingTargets.isEmpty())
+
+        // Since the animation is running, we should not have finished the remote animation.
+        verify(keyguardViewMediator, times(0)).onKeyguardExitRemoteAnimationFinished(
+                false /* cancelled */)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
index d95e5c4..1078cda 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
@@ -23,11 +23,11 @@
 import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.log.LogcatEchoTracker
 import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mockito.mock
-import java.io.PrintWriter
-import java.io.StringWriter
 
 @SmallTest
 class MediaTttLoggerTest : SysuiTestCase() {
@@ -43,32 +43,46 @@
     }
 
     @Test
-    fun logStateChange_bufferHasDeviceTypeTagAndStateNameAndId() {
+    fun logStateChange_bufferHasDeviceTypeTagAndParamInfo() {
         val stateName = "test state name"
         val id = "test id"
+        val packageName = "this.is.a.package"
 
-        logger.logStateChange(stateName, id)
+        logger.logStateChange(stateName, id, packageName)
 
-        val stringWriter = StringWriter()
-        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
-        val actualString = stringWriter.toString()
-
+        val actualString = getStringFromBuffer()
         assertThat(actualString).contains(DEVICE_TYPE_TAG)
         assertThat(actualString).contains(stateName)
         assertThat(actualString).contains(id)
+        assertThat(actualString).contains(packageName)
     }
 
     @Test
-    fun logChipRemoval_bufferHasDeviceTypeAndReason() {
-        val reason = "test reason"
-        logger.logChipRemoval(reason)
+    fun logPackageNotFound_bufferHasPackageName() {
+        val packageName = "this.is.a.package"
 
+        logger.logPackageNotFound(packageName)
+
+        val actualString = getStringFromBuffer()
+        assertThat(actualString).contains(packageName)
+    }
+
+    @Test
+    fun logRemovalBypass_bufferHasReasons() {
+        val removalReason = "fakeRemovalReason"
+        val bypassReason = "fakeBypassReason"
+
+        logger.logRemovalBypass(removalReason, bypassReason)
+
+        val actualString = getStringFromBuffer()
+        assertThat(actualString).contains(removalReason)
+        assertThat(actualString).contains(bypassReason)
+    }
+
+    private fun getStringFromBuffer(): String {
         val stringWriter = StringWriter()
         buffer.dump(PrintWriter(stringWriter), tailLength = 0)
-        val actualString = stringWriter.toString()
-
-        assertThat(actualString).contains(DEVICE_TYPE_TAG)
-        assertThat(actualString).contains(reason)
+        return stringWriter.toString()
     }
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
new file mode 100644
index 0000000..37f6434
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.systemui.media.taptotransfer.common
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.widget.FrameLayout
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.CachingIconView
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class MediaTttUtilsTest : SysuiTestCase() {
+
+    private lateinit var appIconFromPackageName: Drawable
+    @Mock private lateinit var packageManager: PackageManager
+    @Mock private lateinit var applicationInfo: ApplicationInfo
+    @Mock private lateinit var logger: MediaTttLogger
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        // Set up our package manager to give valid information for [PACKAGE_NAME] only
+        appIconFromPackageName = context.getDrawable(R.drawable.ic_cake)!!
+        whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(appIconFromPackageName)
+        whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME)
+        whenever(
+                packageManager.getApplicationInfo(any(), any<PackageManager.ApplicationInfoFlags>())
+            )
+            .thenThrow(PackageManager.NameNotFoundException())
+        whenever(
+                packageManager.getApplicationInfo(
+                    Mockito.eq(PACKAGE_NAME),
+                    any<PackageManager.ApplicationInfoFlags>()
+                )
+            )
+            .thenReturn(applicationInfo)
+        context.setMockPackageManager(packageManager)
+    }
+
+    @Test
+    fun getIconInfoFromPackageName_nullPackageName_returnsDefault() {
+        val iconInfo =
+            MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger)
+
+        assertThat(iconInfo.isAppIcon).isFalse()
+        assertThat(iconInfo.contentDescription)
+            .isEqualTo(context.getString(R.string.media_output_dialog_unknown_launch_app_name))
+    }
+
+    @Test
+    fun getIconInfoFromPackageName_invalidPackageName_returnsDefault() {
+        val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, "fakePackageName", logger)
+
+        assertThat(iconInfo.isAppIcon).isFalse()
+        assertThat(iconInfo.contentDescription)
+            .isEqualTo(context.getString(R.string.media_output_dialog_unknown_launch_app_name))
+    }
+
+    @Test
+    fun getIconInfoFromPackageName_validPackageName_returnsAppInfo() {
+        val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, PACKAGE_NAME, logger)
+
+        assertThat(iconInfo.isAppIcon).isTrue()
+        assertThat(iconInfo.drawable).isEqualTo(appIconFromPackageName)
+        assertThat(iconInfo.contentDescription).isEqualTo(APP_NAME)
+    }
+
+    @Test
+    fun setIcon_viewHasIconAndContentDescription() {
+        val view = CachingIconView(context)
+        val icon = context.getDrawable(R.drawable.ic_celebration)!!
+        val contentDescription = "Happy birthday!"
+
+        MediaTttUtils.setIcon(view, icon, contentDescription)
+
+        assertThat(view.drawable).isEqualTo(icon)
+        assertThat(view.contentDescription).isEqualTo(contentDescription)
+    }
+
+    @Test
+    fun setIcon_iconSizeNull_viewSizeDoesNotChange() {
+        val view = CachingIconView(context)
+        val size = 456
+        view.layoutParams = FrameLayout.LayoutParams(size, size)
+
+        MediaTttUtils.setIcon(view, context.getDrawable(R.drawable.ic_cake)!!, "desc")
+
+        assertThat(view.layoutParams.width).isEqualTo(size)
+        assertThat(view.layoutParams.height).isEqualTo(size)
+    }
+
+    @Test
+    fun setIcon_iconSizeProvided_viewSizeUpdates() {
+        val view = CachingIconView(context)
+        val size = 456
+        view.layoutParams = FrameLayout.LayoutParams(size, size)
+
+        val newSize = 40
+        MediaTttUtils.setIcon(
+            view,
+            context.getDrawable(R.drawable.ic_cake)!!,
+            "desc",
+            iconSize = newSize
+        )
+
+        assertThat(view.layoutParams.width).isEqualTo(newSize)
+        assertThat(view.layoutParams.height).isEqualTo(newSize)
+    }
+}
+
+private const val PACKAGE_NAME = "com.android.systemui"
+private const val APP_NAME = "Fake App Name"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index e7b4593..d41ad48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -173,37 +173,72 @@
             null
         )
 
-        verify(logger).logStateChange(any(), any())
+        verify(logger).logStateChange(any(), any(), any())
     }
 
     @Test
-    fun setIcon_isAppIcon_usesAppIconSize() {
-        controllerReceiver.displayView(getChipReceiverInfo())
+    fun updateView_noOverrides_usesInfoFromAppIcon() {
+        controllerReceiver.displayView(
+            ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appNameOverride = null)
+        )
+
+        val view = getChipView()
+        assertThat(view.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(view.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+    }
+
+    @Test
+    fun updateView_appIconOverride_usesOverride() {
+        val drawableOverride = context.getDrawable(R.drawable.ic_celebration)!!
+
+        controllerReceiver.displayView(
+            ChipReceiverInfo(routeInfo, drawableOverride, appNameOverride = null)
+        )
+
+        val view = getChipView()
+        assertThat(view.getAppIconView().drawable).isEqualTo(drawableOverride)
+    }
+
+    @Test
+    fun updateView_appNameOverride_usesOverride() {
+        val appNameOverride = "Sweet New App"
+
+        controllerReceiver.displayView(
+            ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appNameOverride)
+        )
+
+        val view = getChipView()
+        assertThat(view.getAppIconView().contentDescription).isEqualTo(appNameOverride)
+    }
+
+    @Test
+    fun updateView_isAppIcon_usesAppIconSize() {
+        controllerReceiver.displayView(getChipReceiverInfo(packageName = PACKAGE_NAME))
         val chipView = getChipView()
 
-        controllerReceiver.setIcon(chipView, PACKAGE_NAME)
         chipView.measure(
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
         )
 
-        val expectedSize = controllerReceiver.getIconSize(isAppIcon = true)
+        val expectedSize =
+            context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver)
         assertThat(chipView.getAppIconView().measuredWidth).isEqualTo(expectedSize)
         assertThat(chipView.getAppIconView().measuredHeight).isEqualTo(expectedSize)
     }
 
     @Test
-    fun setIcon_notAppIcon_usesGenericIconSize() {
-        controllerReceiver.displayView(getChipReceiverInfo())
+    fun updateView_notAppIcon_usesGenericIconSize() {
+        controllerReceiver.displayView(getChipReceiverInfo(packageName = null))
         val chipView = getChipView()
 
-        controllerReceiver.setIcon(chipView, appPackageName = null)
         chipView.measure(
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
         )
 
-        val expectedSize = controllerReceiver.getIconSize(isAppIcon = false)
+        val expectedSize =
+            context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_size_receiver)
         assertThat(chipView.getAppIconView().measuredWidth).isEqualTo(expectedSize)
         assertThat(chipView.getAppIconView().measuredHeight).isEqualTo(expectedSize)
     }
@@ -226,8 +261,13 @@
         return viewCaptor.value as ViewGroup
     }
 
-    private fun getChipReceiverInfo(): ChipReceiverInfo =
-        ChipReceiverInfo(routeInfo, null, null)
+    private fun getChipReceiverInfo(packageName: String?): ChipReceiverInfo {
+        val routeInfo = MediaRoute2Info.Builder("id", "Test route name")
+            .addFeature("feature")
+            .setClientPackageName(packageName)
+            .build()
+        return ChipReceiverInfo(routeInfo, null, null)
+    }
 
     private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
index 52b6eed..ff0faf9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
@@ -299,7 +299,7 @@
             null
         )
 
-        verify(logger).logStateChange(any(), any())
+        verify(logger).logStateChange(any(), any(), any())
     }
 
     @Test
@@ -587,15 +587,29 @@
         fakeExecutor.runAllReady()
 
         verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
     }
 
     @Test
-    fun transferToReceiverTriggeredThenFarFromReceiver_eventuallyTimesOut() {
-        val state = transferToReceiverTriggered()
-        controllerSender.displayView(state)
-        fakeClock.advanceTime(1000L)
-        controllerSender.removeView("fakeRemovalReason")
+    fun transferToReceiverTriggeredThenFarFromReceiver_viewStillDisplayed() {
+        controllerSender.displayView(transferToReceiverTriggered())
 
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+        fakeExecutor.runAllReady()
+
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+    }
+
+    @Test
+    fun transferToReceiverTriggeredThenRemoveView_eventuallyTimesOut() {
+        controllerSender.displayView(transferToReceiverTriggered())
+
+        controllerSender.removeView("fakeRemovalReason")
         fakeClock.advanceTime(TIMEOUT + 1L)
 
         verify(windowManager).removeView(any())
@@ -610,20 +624,106 @@
         fakeExecutor.runAllReady()
 
         verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
     }
 
     @Test
-    fun transferToThisDeviceTriggeredThenFarFromReceiver_eventuallyTimesOut() {
-        val state = transferToThisDeviceTriggered()
-        controllerSender.displayView(state)
-        fakeClock.advanceTime(1000L)
-        controllerSender.removeView("fakeRemovalReason")
+    fun transferToThisDeviceTriggeredThenRemoveView_eventuallyTimesOut() {
+        controllerSender.displayView(transferToThisDeviceTriggered())
 
+        controllerSender.removeView("fakeRemovalReason")
         fakeClock.advanceTime(TIMEOUT + 1L)
 
         verify(windowManager).removeView(any())
     }
 
+    @Test
+    fun transferToThisDeviceTriggeredThenFarFromReceiver_viewStillDisplayed() {
+        controllerSender.displayView(transferToThisDeviceTriggered())
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+        fakeExecutor.runAllReady()
+
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+    }
+
+    @Test
+    fun transferToReceiverSucceededThenRemoveView_viewStillDisplayed() {
+        controllerSender.displayView(transferToReceiverSucceeded())
+
+        controllerSender.removeView("fakeRemovalReason")
+        fakeExecutor.runAllReady()
+
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+    }
+
+    @Test
+    fun transferToReceiverSucceededThenRemoveView_eventuallyTimesOut() {
+        controllerSender.displayView(transferToReceiverSucceeded())
+
+        controllerSender.removeView("fakeRemovalReason")
+        fakeClock.advanceTime(TIMEOUT + 1L)
+
+        verify(windowManager).removeView(any())
+    }
+
+    @Test
+    fun transferToReceiverSucceededThenFarFromReceiver_viewStillDisplayed() {
+        controllerSender.displayView(transferToReceiverSucceeded())
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+        fakeExecutor.runAllReady()
+
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+    }
+
+    @Test
+    fun transferToThisDeviceSucceededThenRemoveView_viewStillDisplayed() {
+        controllerSender.displayView(transferToThisDeviceSucceeded())
+
+        controllerSender.removeView("fakeRemovalReason")
+        fakeExecutor.runAllReady()
+
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+    }
+
+    @Test
+    fun transferToThisDeviceSucceededThenRemoveView_eventuallyTimesOut() {
+        controllerSender.displayView(transferToThisDeviceSucceeded())
+
+        controllerSender.removeView("fakeRemovalReason")
+        fakeClock.advanceTime(TIMEOUT + 1L)
+
+        verify(windowManager).removeView(any())
+    }
+
+    @Test
+    fun transferToThisDeviceSucceededThenFarFromReceiver_viewStillDisplayed() {
+        controllerSender.displayView(transferToThisDeviceSucceeded())
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+        fakeExecutor.runAllReady()
+
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+    }
+
     private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
 
     private fun ViewGroup.getChipText(): String =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
index bf682a8..3fd2501 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
@@ -33,12 +33,15 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.settings.SecureSettings;
@@ -54,6 +57,8 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
 public class ColorInversionTileTest extends SysuiTestCase {
+    private static final Integer COLOR_INVERSION_DISABLED = 0;
+    private static final Integer COLOR_INVERSION_ENABLED = 1;
 
     @Mock
     private QSTileHost mHost;
@@ -113,4 +118,24 @@
         assertThat(IntentCaptor.getValue().getAction()).isEqualTo(
                 Settings.ACTION_COLOR_INVERSION_SETTINGS);
     }
+
+    @Test
+    public void testIcon_whenColorInversionDisabled_isOffState() {
+        QSTile.BooleanState state = new QSTile.BooleanState();
+
+        mTile.handleUpdateState(state, COLOR_INVERSION_DISABLED);
+
+        assertThat(state.icon)
+                .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_invert_colors_icon_off));
+    }
+
+    @Test
+    public void testIcon_whenColorInversionEnabled_isOnState() {
+        QSTile.BooleanState state = new QSTile.BooleanState();
+
+        mTile.handleUpdateState(state, COLOR_INVERSION_ENABLED);
+
+        assertThat(state.icon)
+                .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_invert_colors_icon_on));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
new file mode 100644
index 0000000..ce62f2d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.systemui.qs.tiles
+
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.statusbar.policy.DataSaverController
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class DataSaverTileTest : SysuiTestCase() {
+
+    @Mock private lateinit var mHost: QSHost
+    @Mock private lateinit var mMetricsLogger: MetricsLogger
+    @Mock private lateinit var mStatusBarStateController: StatusBarStateController
+    @Mock private lateinit var mActivityStarter: ActivityStarter
+    @Mock private lateinit var mQsLogger: QSLogger
+    private val falsingManager = FalsingManagerFake()
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var dataSaverController: DataSaverController
+    @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+
+    private val uiEventLogger = UiEventLoggerFake()
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var tile: DataSaverTile
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        testableLooper = TestableLooper.get(this)
+
+        Mockito.`when`(mHost.context).thenReturn(mContext)
+        Mockito.`when`(mHost.uiEventLogger).thenReturn(uiEventLogger)
+
+        tile =
+            DataSaverTile(
+                mHost,
+                testableLooper.looper,
+                Handler(testableLooper.looper),
+                falsingManager,
+                mMetricsLogger,
+                statusBarStateController,
+                activityStarter,
+                mQsLogger,
+                dataSaverController,
+                dialogLaunchAnimator
+            )
+    }
+
+    @Test
+    fun testIcon_whenDataSaverEnabled_isOnState() {
+        val state = QSTile.BooleanState()
+
+        tile.handleUpdateState(state, true)
+
+        assertThat(state.icon)
+            .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_data_saver_icon_on))
+    }
+
+    @Test
+    fun testIcon_whenDataSaverDisabled_isOffState() {
+        val state = QSTile.BooleanState()
+
+        tile.handleUpdateState(state, false)
+
+        assertThat(state.icon)
+            .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_data_saver_icon_off))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
new file mode 100644
index 0000000..188c3a3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.systemui.qs.tiles
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.statusbar.policy.LocationController
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class NightDisplayTileTest : SysuiTestCase() {
+    @Mock
+    private lateinit var mHost: QSHost
+
+    @Mock
+    private lateinit var mMetricsLogger: MetricsLogger
+
+    @Mock
+    private lateinit var mStatusBarStateController: StatusBarStateController
+
+    @Mock
+    private lateinit var mActivityStarter: ActivityStarter
+
+    @Mock
+    private lateinit var mQsLogger: QSLogger
+
+    @Mock
+    private lateinit var mLocationController: LocationController
+
+    @Mock
+    private lateinit var mColorDisplayManager: ColorDisplayManager
+
+    @Mock
+    private lateinit var mNightDisplayListenerBuilder: NightDisplayListenerModule.Builder
+
+    @Mock
+    private lateinit var mNightDisplayListener: NightDisplayListener
+
+    private lateinit var mTestableLooper: TestableLooper
+    private lateinit var mTile: NightDisplayTile
+
+    private val mUiEventLogger: UiEventLogger = UiEventLoggerFake()
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        mTestableLooper = TestableLooper.get(this)
+        whenever(mHost.context).thenReturn(mContext)
+        whenever(mHost.uiEventLogger).thenReturn(mUiEventLogger)
+        whenever(mHost.userContext).thenReturn(mContext)
+        whenever(mNightDisplayListenerBuilder.setUser(anyInt())).thenReturn(
+            mNightDisplayListenerBuilder
+        )
+        whenever(mNightDisplayListenerBuilder.build()).thenReturn(mNightDisplayListener)
+
+        mTile = NightDisplayTile(
+            mHost,
+            mTestableLooper.looper,
+            Handler(mTestableLooper.looper),
+            FalsingManagerFake(),
+            mMetricsLogger,
+            mStatusBarStateController,
+            mActivityStarter,
+            mQsLogger,
+            mLocationController,
+            mColorDisplayManager,
+            mNightDisplayListenerBuilder
+        )
+    }
+
+    @Test
+    fun testIcon_whenDisabled_showsOffState() {
+        whenever(mColorDisplayManager.isNightDisplayActivated).thenReturn(false)
+        val state = QSTile.BooleanState()
+
+        mTile.handleUpdateState(state, /* arg= */ null)
+
+        Truth.assertThat(state.icon)
+            .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_nightlight_icon_off))
+    }
+
+    @Test
+    fun testIcon_whenEnabled_showsOnState() {
+        whenever(mColorDisplayManager.isNightDisplayActivated).thenReturn(true)
+        val state = QSTile.BooleanState()
+
+        mTile.handleUpdateState(state, /* arg= */ null)
+
+        Truth.assertThat(state.icon)
+            .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_nightlight_icon_on))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index e730713..b40d5ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -1022,7 +1022,7 @@
         FalsingManager.FalsingTapListener listener = getFalsingTapListener();
         mStatusBarStateController.setState(KEYGUARD);
 
-        listener.onDoubleTapRequired();
+        listener.onAdditionalTapRequired();
 
         verify(mKeyguardIndicationController).showTransientIndication(anyInt());
     }
@@ -1032,7 +1032,7 @@
         FalsingManager.FalsingTapListener listener = getFalsingTapListener();
         mStatusBarStateController.setState(SHADE_LOCKED);
 
-        listener.onDoubleTapRequired();
+        listener.onAdditionalTapRequired();
 
         verify(mTapAgainViewController).show();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
index 97c0bb2..09add65 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -89,7 +89,7 @@
 
     @Test
     fun testGestureDetector_singleTapEnabled() {
-        whenever(statusBarStateController.isPulsing).thenReturn(true)
+        whenever(statusBarStateController.isDozing).thenReturn(true)
 
         // GIVEN tap is enabled, prox not covered
         whenever(ambientDisplayConfiguration.tapGestureEnabled(anyInt())).thenReturn(true)
@@ -100,7 +100,7 @@
         whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false)
 
         // WHEN there's a tap
-        underTest.onSingleTapConfirmed(downEv)
+        underTest.onSingleTapUp(upEv)
 
         // THEN wake up device if dozing
         verify(centralSurfaces).wakeUpIfDozing(anyLong(), anyObject(), anyString())
@@ -108,7 +108,7 @@
 
     @Test
     fun testGestureDetector_doubleTapEnabled() {
-        whenever(statusBarStateController.isPulsing).thenReturn(true)
+        whenever(statusBarStateController.isDozing).thenReturn(true)
 
         // GIVEN double tap is enabled, prox not covered
         whenever(ambientDisplayConfiguration.doubleTapGestureEnabled(anyInt())).thenReturn(true)
@@ -119,15 +119,27 @@
         whenever(falsingManager.isFalseDoubleTap).thenReturn(false)
 
         // WHEN there's a double tap
-        underTest.onDoubleTap(downEv)
+        underTest.onDoubleTapEvent(upEv)
 
         // THEN wake up device if dozing
         verify(centralSurfaces).wakeUpIfDozing(anyLong(), anyObject(), anyString())
     }
 
     @Test
+    fun testGestureDetector_doubleTapEnabled_onDownEvent_noFalsingCheck() {
+        // GIVEN tap is enabled
+        whenever(ambientDisplayConfiguration.tapGestureEnabled(anyInt())).thenReturn(true)
+
+        // WHEN there's a double tap on DOWN event
+        underTest.onDoubleTapEvent(downEv)
+
+        // THEN don't check the falsing manager, should only be checked on the UP event
+        verify(falsingManager, never()).isFalseDoubleTap()
+    }
+
+    @Test
     fun testGestureDetector_singleTapEnabled_falsing() {
-        whenever(statusBarStateController.isPulsing).thenReturn(true)
+        whenever(statusBarStateController.isDozing).thenReturn(true)
 
         // GIVEN tap is enabled, prox not covered
         whenever(ambientDisplayConfiguration.tapGestureEnabled(anyInt())).thenReturn(true)
@@ -138,29 +150,43 @@
         whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
 
         // WHEN there's a tap
-        underTest.onSingleTapConfirmed(downEv)
+        underTest.onSingleTapUp(upEv)
 
         // THEN the device doesn't wake up
         verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
     }
 
     @Test
-    fun testGestureDetector_notPulsing_noFalsingCheck() {
-        whenever(statusBarStateController.isPulsing).thenReturn(false)
+    fun testSingleTap_notDozing_noFalsingCheck() {
+        whenever(statusBarStateController.isDozing).thenReturn(false)
 
-        // GIVEN tap is enabled, prox not covered
+        // GIVEN tap is enabled
         whenever(ambientDisplayConfiguration.tapGestureEnabled(anyInt())).thenReturn(true)
         // WHEN there's a tap
-        underTest.onSingleTapConfirmed(downEv)
+        underTest.onSingleTapUp(upEv)
 
-        // THEN the falsing manager never gets a call (because the device wasn't pulsing
+        // THEN the falsing manager never gets a call (because the device wasn't dozing
+        // during the tap)
+        verify(falsingManager, never()).isFalseTap(anyInt())
+    }
+
+    @Test
+    fun testDoubleTap_notDozing_noFalsingCheck() {
+        whenever(statusBarStateController.isDozing).thenReturn(false)
+
+        // GIVEN tap is enabled
+        whenever(ambientDisplayConfiguration.tapGestureEnabled(anyInt())).thenReturn(true)
+        // WHEN there's a tap
+        underTest.onDoubleTapEvent(upEv)
+
+        // THEN the falsing manager never gets a call (because the device wasn't dozing
         // during the tap)
         verify(falsingManager, never()).isFalseTap(anyInt())
     }
 
     @Test
     fun testGestureDetector_doubleTapEnabled_falsing() {
-        whenever(statusBarStateController.isPulsing).thenReturn(true)
+        whenever(statusBarStateController.isDozing).thenReturn(true)
 
         // GIVEN double tap is enabled, prox not covered
         whenever(ambientDisplayConfiguration.doubleTapGestureEnabled(anyInt())).thenReturn(true)
@@ -170,8 +196,8 @@
         // GIVEN the falsing manager thinks the tap is a false tap
         whenever(falsingManager.isFalseDoubleTap).thenReturn(true)
 
-        // WHEN there's a tap
-        underTest.onDoubleTap(downEv)
+        // WHEN there's a double tap ACTION_UP event
+        underTest.onDoubleTapEvent(upEv)
 
         // THEN the device doesn't wake up
         verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
@@ -179,7 +205,7 @@
 
     @Test
     fun testGestureDetector_singleTapEnabled_proxCovered() {
-        whenever(statusBarStateController.isPulsing).thenReturn(true)
+        whenever(statusBarStateController.isDozing).thenReturn(true)
 
         // GIVEN tap is enabled, not a false tap based on classifiers
         whenever(ambientDisplayConfiguration.tapGestureEnabled(anyInt())).thenReturn(true)
@@ -190,7 +216,7 @@
         whenever(falsingManager.isProximityNear()).thenReturn(true)
 
         // WHEN there's a tap
-        underTest.onSingleTapConfirmed(downEv)
+        underTest.onSingleTapUp(upEv)
 
         // THEN the device doesn't wake up
         verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
@@ -198,7 +224,7 @@
 
     @Test
     fun testGestureDetector_doubleTapEnabled_proxCovered() {
-        whenever(statusBarStateController.isPulsing).thenReturn(true)
+        whenever(statusBarStateController.isDozing).thenReturn(true)
 
         // GIVEN double tap is enabled, not a false tap based on classifiers
         whenever(ambientDisplayConfiguration.doubleTapGestureEnabled(anyInt())).thenReturn(true)
@@ -209,7 +235,7 @@
         whenever(falsingManager.isProximityNear()).thenReturn(true)
 
         // WHEN there's a tap
-        underTest.onDoubleTap(downEv)
+        underTest.onDoubleTapEvent(upEv)
 
         // THEN the device doesn't wake up
         verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
@@ -227,3 +253,4 @@
 }
 
 private val downEv = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+private val upEv = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index ac8874b..945cf7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -19,8 +19,10 @@
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_DEFAULT;
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
 import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
+import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK;
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIMEOUT;
 
+import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
 import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_ALIGNMENT;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BATTERY;
@@ -86,6 +88,7 @@
 import com.android.settingslib.fuelgauge.BatteryStatus;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.FaceHelpMessageDeferral;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.KeyguardIndication;
@@ -167,6 +170,8 @@
     @Mock
     private AccessibilityManager mAccessibilityManager;
     @Mock
+    private FaceHelpMessageDeferral mFaceHelpMessageDeferral;
+    @Mock
     private ScreenLifecycle mScreenLifecycle;
     @Captor
     private ArgumentCaptor<DockManager.AlignmentStateListener> mAlignmentListener;
@@ -259,7 +264,8 @@
                 mKeyguardStateController, mStatusBarStateController, mKeyguardUpdateMonitor,
                 mDockManager, mBroadcastDispatcher, mDevicePolicyManager, mIBatteryStats,
                 mUserManager, mExecutor, mExecutor,  mFalsingManager, mLockPatternUtils,
-                mScreenLifecycle, mKeyguardBypassController, mAccessibilityManager);
+                mScreenLifecycle, mKeyguardBypassController, mAccessibilityManager,
+                mFaceHelpMessageDeferral);
         mController.init();
         mController.setIndicationArea(mIndicationArea);
         verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture());
@@ -535,7 +541,7 @@
 
         mController.setVisible(true);
         mController.getKeyguardCallback().onBiometricHelp(
-                KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED, message,
+                BIOMETRIC_HELP_FACE_NOT_RECOGNIZED, message,
                 BiometricSourceType.FACE);
         verifyIndicationMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE, message);
         reset(mRotateTextViewController);
@@ -582,7 +588,7 @@
 
         // WHEN there's a face not recognized message
         mController.getKeyguardCallback().onBiometricHelp(
-                KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED,
+                BIOMETRIC_HELP_FACE_NOT_RECOGNIZED,
                 message,
                 BiometricSourceType.FACE);
 
@@ -748,8 +754,10 @@
         when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
                 0)).thenReturn(false);
 
-        // WHEN help message received
+        // WHEN help message received and deferred message is valid
         final String helpString = "helpMsg";
+        when(mFaceHelpMessageDeferral.getDeferredMessage()).thenReturn(helpString);
+        when(mFaceHelpMessageDeferral.shouldDefer(FACE_ACQUIRED_TOO_DARK)).thenReturn(true);
         mKeyguardUpdateMonitorCallback.onBiometricHelp(
                 BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK,
                 helpString,
@@ -777,8 +785,10 @@
         when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
                 0)).thenReturn(true);
 
-        // WHEN help message received
+        // WHEN help message received and deferredMessage is valid
         final String helpString = "helpMsg";
+        when(mFaceHelpMessageDeferral.getDeferredMessage()).thenReturn(helpString);
+        when(mFaceHelpMessageDeferral.shouldDefer(FACE_ACQUIRED_TOO_DARK)).thenReturn(true);
         mKeyguardUpdateMonitorCallback.onBiometricHelp(
                 BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK,
                 helpString,
@@ -1123,7 +1133,6 @@
         verifyIndicationMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP, pressToOpen);
     }
 
-
     @Test
     public void coEx_faceSuccess_touchExplorationEnabled_showsFaceUnlockedSwipeToOpen() {
         // GIVEN bouncer isn't showing, can skip bouncer, udfps is supported, a11y enabled
@@ -1269,6 +1278,87 @@
         verifyIndicationMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE, swipeToOpen);
     }
 
+    @Test
+    public void faceOnAcquired_processFrame() {
+        createController();
+
+        // WHEN face sends an acquired message
+        final int acquireInfo = 1;
+        mKeyguardUpdateMonitorCallback.onBiometricAcquired(BiometricSourceType.FACE, acquireInfo);
+
+        // THEN face help message deferral should process the acquired frame
+        verify(mFaceHelpMessageDeferral).processFrame(acquireInfo);
+    }
+
+    @Test
+    public void fingerprintOnAcquired_noProcessFrame() {
+        createController();
+
+        // WHEN fingerprint sends an acquired message
+        mKeyguardUpdateMonitorCallback.onBiometricAcquired(BiometricSourceType.FINGERPRINT, 1);
+
+        // THEN face help message deferral should NOT process any acquired frames
+        verify(mFaceHelpMessageDeferral, never()).processFrame(anyInt());
+    }
+
+    @Test
+    public void onBiometricHelp_fingerprint_faceHelpMessageDeferralDoesNothing() {
+        createController();
+
+        // WHEN fingerprint sends an onBiometricHelp
+        mKeyguardUpdateMonitorCallback.onBiometricHelp(
+                1,
+                "placeholder",
+                BiometricSourceType.FINGERPRINT);
+
+        // THEN face help message deferral is NOT: reset, updated, or checked for shouldDefer
+        verify(mFaceHelpMessageDeferral, never()).reset();
+        verify(mFaceHelpMessageDeferral, never()).updateMessage(anyInt(), anyString());
+        verify(mFaceHelpMessageDeferral, never()).shouldDefer(anyInt());
+    }
+
+    @Test
+    public void onBiometricFailed_resetFaceHelpMessageDeferral() {
+        createController();
+
+        // WHEN face sends an onBiometricHelp BIOMETRIC_HELP_FACE_NOT_RECOGNIZED
+        mKeyguardUpdateMonitorCallback.onBiometricAuthFailed(BiometricSourceType.FACE);
+
+        // THEN face help message deferral is reset
+        verify(mFaceHelpMessageDeferral).reset();
+    }
+
+    @Test
+    public void onBiometricError_resetFaceHelpMessageDeferral() {
+        createController();
+
+        // WHEN face has an error
+        mKeyguardUpdateMonitorCallback.onBiometricError(4, "string",
+                BiometricSourceType.FACE);
+
+        // THEN face help message deferral is reset
+        verify(mFaceHelpMessageDeferral).reset();
+    }
+
+    @Test
+    public void onBiometricHelp_faceAcquiredInfo_faceHelpMessageDeferral() {
+        createController();
+
+        // WHEN face sends an onBiometricHelp BIOMETRIC_HELP_FACE_NOT_RECOGNIZED
+        final int msgId = 1;
+        final String helpString = "test";
+        mKeyguardUpdateMonitorCallback.onBiometricHelp(
+                msgId,
+                "test",
+                BiometricSourceType.FACE);
+
+        // THEN face help message deferral is NOT reset and message IS updated
+        verify(mFaceHelpMessageDeferral, never()).reset();
+        verify(mFaceHelpMessageDeferral).updateMessage(msgId, helpString);
+    }
+
+
+
     private void sendUpdateDisclosureBroadcast() {
         mBroadcastReceiver.onReceive(mContext, new Intent());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java
index 3dd36d1..d0391ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java
@@ -41,6 +41,7 @@
 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.bluetooth.BluetoothLogger;
 import com.android.systemui.dump.DumpManager;
 
 import org.junit.Before;
@@ -81,6 +82,7 @@
 
         mBluetoothControllerImpl = new BluetoothControllerImpl(mContext,
                 mMockDumpManager,
+                mock(BluetoothLogger.class),
                 mTestableLooper.getLooper(),
                 mTestableLooper.getLooper(),
                 mMockBluetoothManager);
@@ -233,4 +235,11 @@
         assertTrue(mBluetoothControllerImpl.isBluetoothAudioActive());
         assertTrue(mBluetoothControllerImpl.isBluetoothAudioProfileOnly());
     }
+
+    /** Regression test for b/246876230. */
+    @Test
+    public void testOnActiveDeviceChanged_null_noCrash() {
+        mBluetoothControllerImpl.onActiveDeviceChanged(null, BluetoothProfile.HEADSET);
+        // No assert, just need no crash.
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index e616c26..921b7ef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -17,20 +17,14 @@
 package com.android.systemui.temporarydisplay
 
 import android.content.Context
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.graphics.drawable.Drawable
 import android.os.PowerManager
-import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
-import android.widget.ImageView
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import com.android.systemui.util.concurrency.DelayableExecutor
@@ -42,9 +36,7 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
-import org.mockito.ArgumentCaptor
 import org.mockito.Mock
-import org.mockito.Mockito.eq
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
@@ -58,13 +50,8 @@
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var fakeExecutor: FakeExecutor
 
-    private lateinit var appIconFromPackageName: Drawable
     @Mock
-    private lateinit var packageManager: PackageManager
-    @Mock
-    private lateinit var applicationInfo: ApplicationInfo
-    @Mock
-    private lateinit var logger: MediaTttLogger
+    private lateinit var logger: TemporaryViewLogger
     @Mock
     private lateinit var accessibilityManager: AccessibilityManager
     @Mock
@@ -78,17 +65,6 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        appIconFromPackageName = context.getDrawable(R.drawable.ic_cake)!!
-        whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(appIconFromPackageName)
-        whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME)
-        whenever(packageManager.getApplicationInfo(
-            any(), any<PackageManager.ApplicationInfoFlags>()
-        )).thenThrow(PackageManager.NameNotFoundException())
-        whenever(packageManager.getApplicationInfo(
-            eq(PACKAGE_NAME), any<PackageManager.ApplicationInfoFlags>()
-        )).thenReturn(applicationInfo)
-        context.setMockPackageManager(packageManager)
-
         whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any()))
             .thenReturn(TIMEOUT_MS.toInt())
 
@@ -229,117 +205,8 @@
         verify(windowManager, never()).removeView(any())
     }
 
-    @Test
-    fun setIcon_nullAppIconDrawableAndNullPackageName_stillHasIcon() {
-        underTest.displayView(getState())
-        val view = getView()
-
-        underTest.setIcon(view, appPackageName = null, appIconDrawableOverride = null)
-
-        assertThat(view.getAppIconView().drawable).isNotNull()
-    }
-
-    @Test
-    fun setIcon_nullAppIconDrawableAndInvalidPackageName_stillHasIcon() {
-        underTest.displayView(getState())
-        val view = getView()
-
-        underTest.setIcon(
-            view, appPackageName = "fakePackageName", appIconDrawableOverride = null
-        )
-
-        assertThat(view.getAppIconView().drawable).isNotNull()
-    }
-
-    @Test
-    fun setIcon_nullAppIconDrawable_iconIsFromPackageName() {
-        underTest.displayView(getState())
-        val view = getView()
-
-        underTest.setIcon(view, PACKAGE_NAME, appIconDrawableOverride = null, null)
-
-        assertThat(view.getAppIconView().drawable).isEqualTo(appIconFromPackageName)
-    }
-
-    @Test
-    fun setIcon_hasAppIconDrawable_iconIsDrawable() {
-        underTest.displayView(getState())
-        val view = getView()
-
-        val drawable = context.getDrawable(R.drawable.ic_alarm)!!
-        underTest.setIcon(view, PACKAGE_NAME, drawable, null)
-
-        assertThat(view.getAppIconView().drawable).isEqualTo(drawable)
-    }
-
-    @Test
-    fun setIcon_nullAppNameAndNullPackageName_stillHasContentDescription() {
-        underTest.displayView(getState())
-        val view = getView()
-
-        underTest.setIcon(view, appPackageName = null, appNameOverride = null)
-
-        assertThat(view.getAppIconView().contentDescription.toString()).isNotEmpty()
-    }
-
-    @Test
-    fun setIcon_nullAppNameAndInvalidPackageName_stillHasContentDescription() {
-        underTest.displayView(getState())
-        val view = getView()
-
-        underTest.setIcon(
-            view, appPackageName = "fakePackageName", appNameOverride = null
-        )
-
-        assertThat(view.getAppIconView().contentDescription.toString()).isNotEmpty()
-    }
-
-    @Test
-    fun setIcon_nullAppName_iconContentDescriptionIsFromPackageName() {
-        underTest.displayView(getState())
-        val view = getView()
-
-        underTest.setIcon(view, PACKAGE_NAME, null, appNameOverride = null)
-
-        assertThat(view.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-    }
-
-    @Test
-    fun setIcon_hasAppName_iconContentDescriptionIsAppNameOverride() {
-        underTest.displayView(getState())
-        val view = getView()
-
-        val appName = "Override App Name"
-        underTest.setIcon(view, PACKAGE_NAME, null, appName)
-
-        assertThat(view.getAppIconView().contentDescription).isEqualTo(appName)
-    }
-
-    @Test
-    fun setIcon_iconSizeMatchesGetIconSize() {
-        underTest.displayView(getState())
-        val view = getView()
-
-        underTest.setIcon(view, PACKAGE_NAME)
-        view.measure(
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
-        )
-
-        assertThat(view.getAppIconView().measuredWidth).isEqualTo(ICON_SIZE)
-        assertThat(view.getAppIconView().measuredHeight).isEqualTo(ICON_SIZE)
-    }
-
     private fun getState(name: String = "name") = ViewInfo(name)
 
-    private fun getView(): ViewGroup {
-        val viewCaptor = ArgumentCaptor.forClass(View::class.java)
-        verify(windowManager).addView(viewCaptor.capture(), any())
-        return viewCaptor.value as ViewGroup
-    }
-
-    private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
-
     private fun getConfigurationListener(): ConfigurationListener {
         val callbackCaptor = argumentCaptor<ConfigurationListener>()
         verify(configurationController).addCallback(capture(callbackCaptor))
@@ -348,13 +215,13 @@
 
     inner class TestController(
         context: Context,
-        logger: MediaTttLogger,
+        logger: TemporaryViewLogger,
         windowManager: WindowManager,
         @Main mainExecutor: DelayableExecutor,
         accessibilityManager: AccessibilityManager,
         configurationController: ConfigurationController,
         powerManager: PowerManager,
-    ) : TemporaryViewDisplayController<ViewInfo>(
+    ) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger>(
         context,
         logger,
         windowManager,
@@ -363,6 +230,8 @@
         configurationController,
         powerManager,
         R.layout.media_ttt_chip,
+        "Window Title",
+        "WAKE_REASON",
     ) {
         var mostRecentViewInfo: ViewInfo? = null
 
@@ -371,7 +240,6 @@
             super.updateView(newInfo, currentView)
             mostRecentViewInfo = newInfo
         }
-        override fun getIconSize(isAppIcon: Boolean): Int = ICON_SIZE
     }
 
     inner class ViewInfo(val name: String) : TemporaryViewInfo {
@@ -379,7 +247,4 @@
     }
 }
 
-private const val PACKAGE_NAME = "com.android.systemui"
-private const val APP_NAME = "Fake App Name"
 private const val TIMEOUT_MS = 10000L
-private const val ICON_SIZE = 47
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
new file mode 100644
index 0000000..c9f2b4d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.systemui.temporarydisplay
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.log.LogcatEchoTracker
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+
+@SmallTest
+class TemporaryViewLoggerTest : SysuiTestCase() {
+    private lateinit var buffer: LogBuffer
+    private lateinit var logger: TemporaryViewLogger
+
+    @Before
+    fun setUp() {
+        buffer =
+            LogBufferFactory(DumpManager(), Mockito.mock(LogcatEchoTracker::class.java))
+                .create("buffer", 10)
+        logger = TemporaryViewLogger(buffer, TAG)
+    }
+
+    @Test
+    fun logChipAddition_bufferHasLog() {
+        logger.logChipAddition()
+
+        val stringWriter = StringWriter()
+        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
+        val actualString = stringWriter.toString()
+
+        assertThat(actualString).contains(TAG)
+    }
+
+    @Test
+    fun logChipRemoval_bufferHasTagAndReason() {
+        val reason = "test reason"
+        logger.logChipRemoval(reason)
+
+        val stringWriter = StringWriter()
+        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
+        val actualString = stringWriter.toString()
+
+        assertThat(actualString).contains(TAG)
+        assertThat(actualString).contains(reason)
+    }
+}
+
+private const val TAG = "TestTag"
diff --git a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java
index 98bae3d..811e96c 100644
--- a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java
+++ b/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java
@@ -96,6 +96,7 @@
 
         // show Alert
         mAlert = mAlertDialog.create();
+        mAlert.getWindow().setHideOverlayWindows(true);
         mAlert.show();
 
         // set Alert Timeout
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 0fac808..4d55d4e 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -1786,8 +1786,8 @@
          * from receiving events from the profile.
          */
         public boolean isPermittedForProfile(int userId) {
-            if (!mUserProfiles.canProfileUseBoundServices(userId)) {
-                return false;
+            if (!mUserProfiles.isProfileUser(userId)) {
+                return true;
             }
             DevicePolicyManager dpm =
                     (DevicePolicyManager) mContext.getSystemService(DEVICE_POLICY_SERVICE);
@@ -1862,16 +1862,16 @@
             }
         }
 
-        public boolean canProfileUseBoundServices(int userId) {
+        public boolean isProfileUser(int userId) {
             synchronized (mCurrentProfiles) {
                 UserInfo user = mCurrentProfiles.get(userId);
                 if (user == null) {
                     return false;
                 }
                 if (user.isManagedProfile() || user.isCloneProfile()) {
-                    return false;
+                    return true;
                 }
-                return true;
+                return false;
             }
         }
     }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 4e0cc61..f11801f 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1828,7 +1828,7 @@
             } else if (action.equals(Intent.ACTION_USER_SWITCHED)) {
                 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
                 mUserProfiles.updateCache(context);
-                if (mUserProfiles.canProfileUseBoundServices(userId)) {
+                if (!mUserProfiles.isProfileUser(userId)) {
                     // reload per-user settings
                     mSettingsObserver.update(null);
                     // Refresh managed services
@@ -1842,7 +1842,7 @@
                 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
                 if (userId != USER_NULL) {
                     mUserProfiles.updateCache(context);
-                    if (mUserProfiles.canProfileUseBoundServices(userId)) {
+                    if (!mUserProfiles.isProfileUser(userId)) {
                         allowDefaultApprovedServices(userId);
                     }
                 }
@@ -1860,7 +1860,7 @@
                 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
                 mUserProfiles.updateCache(context);
                 mAssistants.onUserUnlocked(userId);
-                if (mUserProfiles.canProfileUseBoundServices(userId)) {
+                if (!mUserProfiles.isProfileUser(userId)) {
                     mConditionProviders.onUserUnlocked(userId);
                     mListeners.onUserUnlocked(userId);
                     mZenModeHelper.onUserUnlocked(userId);
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index c3b4792..0915c21 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -657,6 +657,18 @@
         final String packageName = versionedPackage.getPackageName();
         final long versionCode = versionedPackage.getLongVersionCode();
 
+        if (mPm.mProtectedPackages.isPackageDataProtected(userId, packageName)) {
+            mPm.mHandler.post(() -> {
+                try {
+                    Slog.w(TAG, "Attempted to delete protected package: " + packageName);
+                    observer.onPackageDeleted(packageName,
+                            PackageManager.DELETE_FAILED_INTERNAL_ERROR, null);
+                } catch (RemoteException re) {
+                }
+            });
+            return;
+        }
+
         try {
             if (mPm.mInjector.getLocalService(ActivityTaskManagerInternal.class)
                     .isBaseOfLockedTask(packageName)) {
diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java
index 219092b..1266db5 100644
--- a/services/core/java/com/android/server/wm/AsyncRotationController.java
+++ b/services/core/java/com/android/server/wm/AsyncRotationController.java
@@ -357,7 +357,12 @@
      * or seamless transformation in a rotated display.
      */
     boolean shouldFreezeInsetsPosition(WindowState w) {
-        return mTransitionOp != OP_LEGACY && w.mTransitionController.inTransition()
+        if (TransitionController.SYNC_METHOD != BLASTSyncEngine.METHOD_BLAST) {
+            // Expect a screenshot layer has covered the screen, so it is fine to let client side
+            // insets animation runner update the position directly.
+            return false;
+        }
+        return mTransitionOp != OP_LEGACY && !mIsStartTransactionCommitted
                 && isTargetToken(w.mToken);
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index b093d25..95d71a3 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -245,6 +245,7 @@
 import com.android.internal.util.function.pooled.PooledPredicate;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.policy.WindowManagerPolicy;
+import com.android.server.wm.utils.RegionUtils;
 import com.android.server.wm.utils.RotationCache;
 import com.android.server.wm.utils.WmDisplayCutout;
 
@@ -5780,6 +5781,12 @@
 
             if (w.isVisible() && !w.inPinnedWindowingMode()) {
                 w.getKeepClearAreas(outRestricted, outUnrestricted, tmpMatrix, tmpFloat9);
+
+                if (w.mIsImWindow) {
+                    Region touchableRegion = Region.obtain();
+                    w.getEffectiveTouchableRegion(touchableRegion);
+                    RegionUtils.forEachRect(touchableRegion, rect -> outUnrestricted.add(rect));
+                }
             }
 
             // We stop traversing when we reach the base of a fullscreen app.
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 6650f43..488fe67 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -46,8 +46,9 @@
 import static android.view.WindowManager.TransitionType;
 import static android.view.WindowManager.transitTypeToString;
 import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+import static android.window.TransitionInfo.FLAG_FILLS_TASK;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
-import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
 import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD;
 import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
@@ -1873,8 +1874,10 @@
                     flags |= FLAG_WILL_IME_SHOWN;
                 }
             }
+            Task parentTask = null;
             final ActivityRecord record = wc.asActivityRecord();
             if (record != null) {
+                parentTask = record.getTask();
                 if (record.mUseTransferredAnimation) {
                     flags |= FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
                 }
@@ -1882,6 +1885,26 @@
                     flags |= FLAG_IS_VOICE_INTERACTION;
                 }
             }
+            final TaskFragment taskFragment = wc.asTaskFragment();
+            if (taskFragment != null && task == null) {
+                parentTask = taskFragment.getTask();
+            }
+            if (parentTask != null) {
+                if (parentTask.forAllLeafTaskFragments(TaskFragment::isEmbedded)) {
+                    // Whether this is in a Task with embedded activity.
+                    flags |= FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
+                }
+                final Rect taskBounds = parentTask.getBounds();
+                final Rect startBounds = mAbsoluteBounds;
+                final Rect endBounds = wc.getBounds();
+                if (taskBounds.width() == startBounds.width()
+                        && taskBounds.height() == startBounds.height()
+                        && taskBounds.width() == endBounds.width()
+                        && taskBounds.height() == endBounds.height()) {
+                    // Whether the container fills the Task bounds before and after the transition.
+                    flags |= FLAG_FILLS_TASK;
+                }
+            }
             final DisplayContent dc = wc.asDisplayContent();
             if (dc != null) {
                 flags |= FLAG_IS_DISPLAY;
@@ -1898,9 +1921,6 @@
             if (occludesKeyguard(wc)) {
                 flags |= FLAG_OCCLUDES_KEYGUARD;
             }
-            if (wc.isEmbedded()) {
-                flags |= FLAG_IS_EMBEDDED;
-            }
             return flags;
         }
     }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 49879efe..7986043 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -1704,8 +1704,8 @@
     }
 
     @Test
-    public void testInfoIsPermittedForProfile_notAllowed() {
-        when(mUserProfiles.canProfileUseBoundServices(anyInt())).thenReturn(false);
+    public void testInfoIsPermittedForProfile_notProfile() {
+        when(mUserProfiles.isProfileUser(anyInt())).thenReturn(false);
 
         IInterface service = mock(IInterface.class);
         when(service.asBinder()).thenReturn(mock(IBinder.class));
@@ -1714,12 +1714,12 @@
         services.registerSystemService(service, null, 10, 1000);
         ManagedServices.ManagedServiceInfo info = services.checkServiceTokenLocked(service);
 
-        assertFalse(info.isPermittedForProfile(0));
+        assertTrue(info.isPermittedForProfile(0));
     }
 
     @Test
-    public void testInfoIsPermittedForProfile_allows() {
-        when(mUserProfiles.canProfileUseBoundServices(anyInt())).thenReturn(true);
+    public void testInfoIsPermittedForProfile_profileAndDpmAllows() {
+        when(mUserProfiles.isProfileUser(anyInt())).thenReturn(true);
         when(mDpm.isNotificationListenerServicePermitted(anyString(), anyInt())).thenReturn(true);
 
         IInterface service = mock(IInterface.class);
@@ -1734,6 +1734,22 @@
     }
 
     @Test
+    public void testInfoIsPermittedForProfile_profileAndDpmDenies() {
+        when(mUserProfiles.isProfileUser(anyInt())).thenReturn(true);
+        when(mDpm.isNotificationListenerServicePermitted(anyString(), anyInt())).thenReturn(false);
+
+        IInterface service = mock(IInterface.class);
+        when(service.asBinder()).thenReturn(mock(IBinder.class));
+        ManagedServices services = new TestManagedServices(getContext(), mLock, mUserProfiles,
+                mIpm, APPROVAL_BY_PACKAGE);
+        services.registerSystemService(service, null, 10, 1000);
+        ManagedServices.ManagedServiceInfo info = services.checkServiceTokenLocked(service);
+        info.component = new ComponentName("a","b");
+
+        assertFalse(info.isPermittedForProfile(0));
+    }
+
+    @Test
     public void testUserProfiles_canProfileUseBoundServices_managedProfile() {
         List<UserInfo> users = new ArrayList<>();
         UserInfo profile = new UserInfo(ActivityManager.getCurrentUser(), "current", 0);
@@ -1750,9 +1766,9 @@
         ManagedServices.UserProfiles profiles = new ManagedServices.UserProfiles();
         profiles.updateCache(mContext);
 
-        assertTrue(profiles.canProfileUseBoundServices(ActivityManager.getCurrentUser()));
-        assertFalse(profiles.canProfileUseBoundServices(12));
-        assertFalse(profiles.canProfileUseBoundServices(13));
+        assertFalse(profiles.isProfileUser(ActivityManager.getCurrentUser()));
+        assertTrue(profiles.isProfileUser(12));
+        assertTrue(profiles.isProfileUser(13));
     }
 
     private void resetComponentsAndPackages() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 8f186e4..8cf32ba 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
@@ -31,7 +32,8 @@
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
+import static android.window.TransitionInfo.FLAG_FILLS_TASK;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
@@ -727,7 +729,7 @@
         assertTrue(ime.mToken.inTransition());
         assertTrue(task.inTransition());
         assertTrue(asyncRotationController.isTargetToken(decorToken));
-        assertTrue(asyncRotationController.shouldFreezeInsetsPosition(navBar));
+        assertShouldFreezeInsetsPosition(asyncRotationController, statusBar, true);
 
         screenDecor.setOrientationChanging(false);
         // Status bar finishes drawing before the start transaction. Its fade-in animation will be
@@ -742,6 +744,7 @@
         // The transaction is committed, so fade-in animation for status bar is consumed.
         transactionCommittedListener.onTransactionCommitted();
         assertFalse(asyncRotationController.isTargetToken(statusBar.mToken));
+        assertShouldFreezeInsetsPosition(asyncRotationController, navBar, false);
 
         // Navigation bar finishes drawing after the start transaction, so its fade-in animation
         // can execute directly.
@@ -777,7 +780,7 @@
         final AsyncRotationController asyncRotationController =
                 mDisplayContent.getAsyncRotationController();
         assertNotNull(asyncRotationController);
-        assertTrue(asyncRotationController.shouldFreezeInsetsPosition(statusBar));
+        assertShouldFreezeInsetsPosition(asyncRotationController, statusBar, true);
 
         statusBar.setOrientationChanging(true);
         player.startTransition();
@@ -823,7 +826,7 @@
         final AsyncRotationController asyncRotationController =
                 mDisplayContent.getAsyncRotationController();
         assertNotNull(asyncRotationController);
-        assertTrue(asyncRotationController.shouldFreezeInsetsPosition(statusBar));
+        assertShouldFreezeInsetsPosition(asyncRotationController, statusBar, true);
         assertTrue(app.getTask().inTransition());
 
         player.start();
@@ -858,6 +861,15 @@
         assertNull(mDisplayContent.getAsyncRotationController());
     }
 
+    private static void assertShouldFreezeInsetsPosition(AsyncRotationController controller,
+            WindowState w, boolean freeze) {
+        if (TransitionController.SYNC_METHOD != BLASTSyncEngine.METHOD_BLAST) {
+            // Non blast sync should never freeze insets position.
+            freeze = false;
+        }
+        assertEquals(freeze, controller.shouldFreezeInsetsPosition(w));
+    }
+
     @Test
     public void testDeferRotationForTransientLaunch() {
         final TestTransitionPlayer player = registerTestTransitionPlayer();
@@ -1062,12 +1074,14 @@
     }
 
     @Test
-    public void testIsEmbeddedChange() {
+    public void testFlagInTaskWithEmbeddedActivity() {
         final Transition transition = createTestTransition(TRANSIT_OPEN);
         final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
         final ArraySet<WindowContainer> participants = transition.mParticipants;
 
         final Task task = createTask(mDisplayContent);
+        final ActivityRecord nonEmbeddedActivity = createActivityRecord(task);
+        assertFalse(nonEmbeddedActivity.isEmbedded());
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         mAtm.mTaskFragmentOrganizerController.registerOrganizer(
                 ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()));
@@ -1082,20 +1096,72 @@
         changes.put(embeddedTf, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
         changes.put(closingActivity, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
         changes.put(openingActivity, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
+        changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(true /* vis */,
+                false /* exChg */));
         // End states.
         closingActivity.mVisibleRequested = false;
         openingActivity.mVisibleRequested = true;
+        nonEmbeddedActivity.mVisibleRequested = false;
 
         participants.add(closingActivity);
         participants.add(openingActivity);
+        participants.add(nonEmbeddedActivity);
         final ArrayList<WindowContainer> targets = Transition.calculateTargets(
                 participants, changes);
         final TransitionInfo info = Transition.calculateTransitionInfo(
                 transition.mType, 0 /* flags */, targets, changes, mMockT);
 
+        // All windows in the Task should have FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY because the Task
+        // contains embedded activity.
+        assertEquals(3, info.getChanges().size());
+        assertTrue(info.getChanges().get(0).hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY));
+        assertTrue(info.getChanges().get(1).hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY));
+        assertTrue(info.getChanges().get(2).hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY));
+    }
+
+    @Test
+    public void testFlagFillsTask() {
+        final Transition transition = createTestTransition(TRANSIT_OPEN);
+        final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+        final ArraySet<WindowContainer> participants = transition.mParticipants;
+
+        final Task task = createTask(mDisplayContent);
+        // Set to multi-windowing mode in order to set bounds.
+        task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+        final Rect taskBounds = new Rect(0, 0, 500, 1000);
+        task.setBounds(taskBounds);
+        final ActivityRecord nonEmbeddedActivity = createActivityRecord(task);
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        mAtm.mTaskFragmentOrganizerController.registerOrganizer(
+                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()));
+        final TaskFragment embeddedTf = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .createActivityCount(1)
+                .setOrganizer(organizer)
+                .build();
+        final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity();
+        // Start states.
+        changes.put(task, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
+        changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(true /* vis */,
+                false /* exChg */));
+        changes.put(embeddedTf, new Transition.ChangeInfo(false /* vis */, true /* exChg */));
+        // End states.
+        nonEmbeddedActivity.mVisibleRequested = false;
+        embeddedActivity.mVisibleRequested = true;
+        embeddedTf.setBounds(new Rect(0, 0, 500, 500));
+
+        participants.add(nonEmbeddedActivity);
+        participants.add(embeddedTf);
+        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+                participants, changes);
+        final TransitionInfo info = Transition.calculateTransitionInfo(
+                transition.mType, 0 /* flags */, targets, changes, mMockT);
+
+        // The embedded with bounds overridden should not have the flag.
         assertEquals(2, info.getChanges().size());
-        assertTrue((info.getChanges().get(0).getFlags() & FLAG_IS_EMBEDDED) != 0);
-        assertTrue((info.getChanges().get(1).getFlags() & FLAG_IS_EMBEDDED) != 0);
+        assertFalse(info.getChanges().get(0).hasFlags(FLAG_FILLS_TASK));
+        assertEquals(embeddedTf.getBounds(), info.getChanges().get(0).getEndAbsBounds());
+        assertFalse(info.getChanges().get(1).hasFlags(FLAG_FILLS_TASK));
     }
 
     @Test
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
new file mode 100644
index 0000000..d11ca49
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.helpers
+
+import android.app.Instrumentation
+import android.support.test.launcherhelper.ILauncherStrategy
+import android.support.test.launcherhelper.LauncherStrategyFactory
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Direction
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.toFlickerComponent
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+
+class GameAppHelper @JvmOverloads constructor(
+    instr: Instrumentation,
+    launcherName: String = ActivityOptions.GAME_ACTIVITY_LAUNCHER_NAME,
+    component: FlickerComponentName =
+            ActivityOptions.GAME_ACTIVITY_COMPONENT_NAME.toFlickerComponent(),
+    launcherStrategy: ILauncherStrategy =
+        LauncherStrategyFactory.getInstance(instr).launcherStrategy,
+) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+
+    /**
+     * Swipes down in the mock game app.
+     *
+     * @return true if the swipe operation is successful.
+     */
+    fun swipeDown(): Boolean {
+        val gameView = uiDevice.wait(
+            Until.findObject(By.res(getPackage(), GAME_APP_VIEW_RES)), WAIT_TIME_MS)
+        require(gameView != null) { "Mock game app view not found." }
+
+        val bound = gameView.getVisibleBounds()
+        return uiDevice.swipe(
+            bound.centerX(), bound.top, bound.centerX(), bound.centerY(), SWIPE_STEPS)
+    }
+
+    /**
+     * Switches to a recent app by quick switch gesture. This function can be used in both portrait
+     * and landscape mode.
+     *
+     * @param wmHelper Helper used to get window region.
+     * @param direction UiAutomator Direction enum to indicate the swipe direction.
+     *
+     * @return true if the swipe operation is successful.
+     */
+    fun switchToPreviousAppByQuickSwitchGesture(
+        wmHelper: WindowManagerStateHelper,
+        direction: Direction
+    ): Boolean {
+        val ratioForScreenBottom = 0.97
+        val fullView = wmHelper.getWindowRegion(component)
+        require(!fullView.isEmpty) { "Target $component view not found." }
+
+        val bound = fullView.bounds
+        val targetYPos = bound.bottom * ratioForScreenBottom
+        val endX = when (direction) {
+            Direction.LEFT -> bound.left
+            Direction.RIGHT -> bound.right
+            else -> {
+                throw IllegalStateException("Only left or right direction is allowed.")
+            }
+        }
+        return uiDevice.swipe(
+            bound.centerX(), targetYPos.toInt(), endX, targetYPos.toInt(), SWIPE_STEPS)
+    }
+
+    /**
+     * Waits for view idel with timeout, then checkes the target object whether visible on screen.
+     *
+     * @param packageName The targe application's package name.
+     * @param identifier The resource id of the target object.
+     * @param timeout The timeout duration in milliseconds.
+     *
+     * @return true if the target object exists.
+     */
+    @JvmOverloads
+    fun isTargetObjVisible(
+        packageName: String,
+        identifier: String,
+        timeout: Long = WAIT_TIME_MS
+    ): Boolean {
+        uiDevice.waitForIdle(timeout)
+        return uiDevice.hasObject(By.res(packageName, identifier))
+    }
+
+    companion object {
+        private const val GAME_APP_VIEW_RES = "container"
+        private const val WAIT_TIME_MS = 3_000L
+        private const val SWIPE_STEPS = 30
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt
new file mode 100644
index 0000000..3385784
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.server.wm.flicker.helpers
+
+import android.app.Instrumentation
+import android.support.test.launcherhelper.ILauncherStrategy
+import android.support.test.launcherhelper.LauncherStrategyFactory
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Direction
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.traces.common.FlickerComponentName
+import com.android.server.wm.traces.parser.toFlickerComponent
+
+class MailAppHelper @JvmOverloads constructor(
+        instr: Instrumentation,
+        launcherName: String = ActivityOptions.MAIL_ACTIVITY_LAUNCHER_NAME,
+        component: FlickerComponentName =
+                ActivityOptions.MAIL_ACTIVITY_COMPONENT_NAME.toFlickerComponent(),
+        launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
+                .getInstance(instr)
+                .launcherStrategy
+) : StandardAppHelper(instr, launcherName, component, launcherStrategy) {
+
+    fun openMail(rowIdx: Int) {
+        val rowSel = By.res(getPackage(), "mail_row_item_text")
+            .textEndsWith(String.format("%04d", rowIdx))
+        var row: UiObject2? = null
+        for (i in 1..1000) {
+            row = uiDevice.wait(Until.findObject(rowSel), SHORT_WAIT_TIME_MS)
+            if (row != null) break
+            scrollDown()
+        }
+        require(row != null) {""}
+        row.click()
+        uiDevice.wait(Until.gone(By.res(getPackage(), MAIL_LIST_RES_ID)), FIND_TIMEOUT)
+    }
+
+    fun scrollDown() {
+        val listView = waitForMailList()
+        listView.scroll(Direction.DOWN, 1.0f)
+    }
+
+    fun waitForMailList(): UiObject2 {
+        var sel = By.res(getPackage(), MAIL_LIST_RES_ID).scrollable(true)
+        val ret = uiDevice.wait(Until.findObject(sel), FIND_TIMEOUT)
+        require(ret != null) {""}
+        return ret
+    }
+
+    companion object {
+        private const val SHORT_WAIT_TIME_MS = 5000L
+        private const val MAIL_LIST_RES_ID = "mail_recycle_view"
+    }
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index 66ad6f1..b8ef195 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -185,5 +185,34 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+        <service
+            android:name=".AssistantInteractionSessionService"
+            android:exported="true"
+            android:permission="android.permission.BIND_VOICE_INTERACTION" />
+        <service
+            android:name=".AssistantRecognitionService"
+            android:exported="true"
+            android:label="Test Voice Interaction Service">
+            <intent-filter>
+                <action android:name="android.speech.RecognitionService" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data
+                android:name="android.speech"
+                android:resource="@xml/recognition_service" />
+        </service>
+        <service
+            android:name=".AssistantInteractionService"
+            android:exported="true"
+            android:label="Test Voice Interaction Service"
+            android:permission="android.permission.BIND_VOICE_INTERACTION">
+            <intent-filter>
+                <action android:name="android.service.voice.VoiceInteractionService" />
+            </intent-filter>
+            <meta-data
+                android:name="android.voice_interaction"
+                android:resource="@xml/interaction_service" />
+        </service>
     </application>
+    <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
 </manifest>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/assistant_session.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/assistant_session.xml
new file mode 100644
index 0000000..eb7f307
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/assistant_session.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ 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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <FrameLayout
+        android:id="@+id/vis_frame"
+        android:layout_width="match_parent"
+        android:layout_height="300dp"
+        android:layout_gravity="bottom"
+        android:background="#37474F"/>
+</FrameLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/xml/interaction_service.xml b/tests/FlickerTests/test-apps/flickerapp/res/xml/interaction_service.xml
new file mode 100644
index 0000000..2e661fb
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/xml/interaction_service.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ 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.
+  -->
+
+<voice-interaction-service xmlns:android="http://schemas.android.com/apk/res/android"
+    android:recognitionService="com.android.server.wm.flicker.testapp.AssistantRecognitionService"
+    android:sessionService="com.android.server.wm.flicker.testapp.AssistantInteractionSessionService"
+    android:supportsAssist="true" />
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/xml/recognition_service.xml b/tests/FlickerTests/test-apps/flickerapp/res/xml/recognition_service.xml
new file mode 100644
index 0000000..2e12498
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/xml/recognition_service.xml
@@ -0,0 +1,17 @@
+<!--
+  ~ 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.
+  -->
+
+<recognition-service xmlns:android="http://schemas.android.com/apk/res/android" />
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index 42a37df..45a4730 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -88,6 +88,10 @@
             new ComponentName(FLICKER_APP_PACKAGE,
                     FLICKER_APP_PACKAGE + ".NotificationActivity");
 
+    public static final String MAIL_ACTIVITY_LAUNCHER_NAME = "MailActivity";
+    public static final ComponentName MAIL_ACTIVITY_COMPONENT_NAME = new ComponentName(
+            FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".MailActivity");
+
     public static final String GAME_ACTIVITY_LAUNCHER_NAME = "GameApp";
     public static final ComponentName GAME_ACTIVITY_COMPONENT_NAME =
             new ComponentName(FLICKER_APP_PACKAGE,
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantInteractionService.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantInteractionService.java
new file mode 100644
index 0000000..d60143c
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantInteractionService.java
@@ -0,0 +1,22 @@
+/*
+ * 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.server.wm.flicker.testapp;
+
+import android.service.voice.VoiceInteractionService;
+
+public class AssistantInteractionService extends VoiceInteractionService {
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantInteractionSession.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantInteractionSession.java
new file mode 100644
index 0000000..d2c9b37
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantInteractionSession.java
@@ -0,0 +1,53 @@
+/*
+ * 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.server.wm.flicker.testapp;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.service.voice.VoiceInteractionSession;
+import android.view.View;
+import android.view.Window;
+
+public class AssistantInteractionSession extends VoiceInteractionSession {
+
+    public AssistantInteractionSession(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void onCreate() {
+        Window window = getWindow().getWindow();
+        if (window != null) {
+            window.getDecorView().setBackgroundColor(Color.TRANSPARENT);
+
+        }
+        View rootView = getLayoutInflater().inflate(R.layout.assistant_session, null);
+        setContentView(rootView);
+        setUiEnabled(false);
+    }
+
+    @Override
+    public void onShow(Bundle args, int showFlags) {
+        setUiEnabled(true);
+    }
+
+    @Override
+    public void onHide() {
+        setUiEnabled(false);
+    }
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantInteractionSessionService.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantInteractionSessionService.java
new file mode 100644
index 0000000..4d6125c
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantInteractionSessionService.java
@@ -0,0 +1,28 @@
+/*
+ * 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.server.wm.flicker.testapp;
+
+import android.os.Bundle;
+import android.service.voice.VoiceInteractionSession;
+import android.service.voice.VoiceInteractionSessionService;
+
+public class AssistantInteractionSessionService extends VoiceInteractionSessionService {
+    @Override
+    public VoiceInteractionSession onNewSession(Bundle args) {
+        return new AssistantInteractionSession(this);
+    }
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantRecognitionService.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantRecognitionService.java
new file mode 100644
index 0000000..68aae45
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/AssistantRecognitionService.java
@@ -0,0 +1,37 @@
+/*
+ * 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.server.wm.flicker.testapp;
+
+import android.content.Intent;
+import android.speech.RecognitionService;
+
+public class AssistantRecognitionService extends RecognitionService {
+    @Override
+    protected void onStartListening(Intent recognizerIntent, Callback listener) {
+
+    }
+
+    @Override
+    protected void onCancel(Callback listener) {
+
+    }
+
+    @Override
+    protected void onStopListening(Callback listener) {
+
+    }
+}