Merge "Allow color mode to be NOT_SET" into tm-qpr-dev
diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java
index f7f0235..93748f8 100644
--- a/core/java/android/companion/AssociationInfo.java
+++ b/core/java/android/companion/AssociationInfo.java
@@ -55,6 +55,14 @@
 
     private final boolean mSelfManaged;
     private final boolean mNotifyOnDeviceNearby;
+
+    /**
+     * Indicates that the association has been revoked (removed), but we keep the association
+     * record for final clean up (e.g. removing the app from the list of the role holders).
+     *
+     * @see CompanionDeviceManager#disassociate(int)
+     */
+    private final boolean mRevoked;
     private final long mTimeApprovedMs;
     /**
      * A long value indicates the last time connected reported by selfManaged devices
@@ -71,7 +79,7 @@
     public AssociationInfo(int id, @UserIdInt int userId, @NonNull String packageName,
             @Nullable MacAddress macAddress, @Nullable CharSequence displayName,
             @Nullable String deviceProfile, boolean selfManaged, boolean notifyOnDeviceNearby,
-            long timeApprovedMs, long lastTimeConnectedMs) {
+            boolean revoked, long timeApprovedMs, long lastTimeConnectedMs) {
         if (id <= 0) {
             throw new IllegalArgumentException("Association ID should be greater than 0");
         }
@@ -91,6 +99,7 @@
 
         mSelfManaged = selfManaged;
         mNotifyOnDeviceNearby = notifyOnDeviceNearby;
+        mRevoked = revoked;
         mTimeApprovedMs = timeApprovedMs;
         mLastTimeConnectedMs = lastTimeConnectedMs;
     }
@@ -176,6 +185,14 @@
     }
 
     /**
+     * @return if the association has been revoked (removed).
+     * @hide
+     */
+    public boolean isRevoked() {
+        return mRevoked;
+    }
+
+    /**
      * @return the last time self reported disconnected for selfManaged only.
      * @hide
      */
@@ -244,6 +261,7 @@
                 + ", mDeviceProfile='" + mDeviceProfile + '\''
                 + ", mSelfManaged=" + mSelfManaged
                 + ", mNotifyOnDeviceNearby=" + mNotifyOnDeviceNearby
+                + ", mRevoked=" + mRevoked
                 + ", mTimeApprovedMs=" + new Date(mTimeApprovedMs)
                 + ", mLastTimeConnectedMs=" + (
                     mLastTimeConnectedMs == Long.MAX_VALUE
@@ -260,6 +278,7 @@
                 && mUserId == that.mUserId
                 && mSelfManaged == that.mSelfManaged
                 && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby
+                && mRevoked == that.mRevoked
                 && mTimeApprovedMs == that.mTimeApprovedMs
                 && mLastTimeConnectedMs == that.mLastTimeConnectedMs
                 && Objects.equals(mPackageName, that.mPackageName)
@@ -271,7 +290,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(mId, mUserId, mPackageName, mDeviceMacAddress, mDisplayName,
-                mDeviceProfile, mSelfManaged, mNotifyOnDeviceNearby, mTimeApprovedMs,
+                mDeviceProfile, mSelfManaged, mNotifyOnDeviceNearby, mRevoked, mTimeApprovedMs,
                 mLastTimeConnectedMs);
     }
 
@@ -293,6 +312,7 @@
 
         dest.writeBoolean(mSelfManaged);
         dest.writeBoolean(mNotifyOnDeviceNearby);
+        dest.writeBoolean(mRevoked);
         dest.writeLong(mTimeApprovedMs);
         dest.writeLong(mLastTimeConnectedMs);
     }
@@ -309,6 +329,7 @@
 
         mSelfManaged = in.readBoolean();
         mNotifyOnDeviceNearby = in.readBoolean();
+        mRevoked = in.readBoolean();
         mTimeApprovedMs = in.readLong();
         mLastTimeConnectedMs = in.readLong();
     }
@@ -352,11 +373,13 @@
         @NonNull
         private final AssociationInfo mOriginalInfo;
         private boolean mNotifyOnDeviceNearby;
+        private boolean mRevoked;
         private long mLastTimeConnectedMs;
 
         private Builder(@NonNull AssociationInfo info) {
             mOriginalInfo = info;
             mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby;
+            mRevoked = info.mRevoked;
             mLastTimeConnectedMs = info.mLastTimeConnectedMs;
         }
 
@@ -388,6 +411,17 @@
         }
 
         /**
+         * Should only be used by the CompanionDeviceManagerService.
+         * @hide
+         */
+        @Override
+        @NonNull
+        public Builder setRevoked(boolean revoked) {
+            mRevoked = revoked;
+            return this;
+        }
+
+        /**
          * @hide
          */
         @NonNull
@@ -401,6 +435,7 @@
                     mOriginalInfo.mDeviceProfile,
                     mOriginalInfo.mSelfManaged,
                     mNotifyOnDeviceNearby,
+                    mRevoked,
                     mOriginalInfo.mTimeApprovedMs,
                     mLastTimeConnectedMs
             );
@@ -433,5 +468,12 @@
          */
         @NonNull
         Builder setLastTimeConnected(long lastTimeConnectedMs);
+
+        /**
+         * Should only be used by the CompanionDeviceManagerService.
+         * @hide
+         */
+        @NonNull
+        Builder setRevoked(boolean revoked);
     }
 }
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index eb2d647..1df7dbc 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -24,7 +24,6 @@
 import static android.graphics.Matrix.MSKEW_Y;
 import static android.view.SurfaceControl.METADATA_WINDOW_TYPE;
 import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
-import static android.view.ViewRootImpl.LOCAL_LAYOUT;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 
 import android.animation.AnimationHandler;
@@ -41,7 +40,6 @@
 import android.app.WallpaperColors;
 import android.app.WallpaperInfo;
 import android.app.WallpaperManager;
-import android.app.WindowConfiguration;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.Intent;
@@ -260,8 +258,6 @@
         private final Point mLastSurfaceSize = new Point();
         private final Matrix mTmpMatrix = new Matrix();
         private final float[] mTmpValues = new float[9];
-        private final WindowLayout mWindowLayout = new WindowLayout();
-        private final Rect mTempRect = new Rect();
 
         final WindowManager.LayoutParams mLayout
                 = new WindowManager.LayoutParams();
@@ -1100,8 +1096,7 @@
                             | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 
                     final Configuration config = mMergedConfiguration.getMergedConfiguration();
-                    final WindowConfiguration winConfig = config.windowConfiguration;
-                    final Rect maxBounds = winConfig.getMaxBounds();
+                    final Rect maxBounds = config.windowConfiguration.getMaxBounds();
                     if (myWidth == ViewGroup.LayoutParams.MATCH_PARENT
                             && myHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
                         mLayout.width = myWidth;
@@ -1158,28 +1153,9 @@
                     } else {
                         mLayout.surfaceInsets.set(0, 0, 0, 0);
                     }
-
-                    int relayoutResult = 0;
-                    if (LOCAL_LAYOUT) {
-                        if (!mSurfaceControl.isValid()) {
-                            relayoutResult = mSession.updateVisibility(mWindow, mLayout,
-                                    View.VISIBLE, mMergedConfiguration, mSurfaceControl,
-                                    mInsetsState, mTempControls);
-                        }
-
-                        final Rect displayCutoutSafe = mTempRect;
-                        mInsetsState.getDisplayCutoutSafe(displayCutoutSafe);
-                        mWindowLayout.computeFrames(mLayout, mInsetsState, displayCutoutSafe,
-                                winConfig.getBounds(), winConfig.getWindowingMode(), mWidth,
-                                mHeight, mRequestedVisibilities, 1f /* compatScale */, mWinFrames);
-
-                        mSession.updateLayout(mWindow, mLayout, 0 /* flags */, mWinFrames, mWidth,
-                                mHeight);
-                    } else {
-                        relayoutResult = mSession.relayout(mWindow, mLayout, mWidth, mHeight,
-                                View.VISIBLE, 0, mWinFrames, mMergedConfiguration,
-                                mSurfaceControl, mInsetsState, mTempControls, mSyncSeqIdBundle);
-                    }
+                    final int relayoutResult = mSession.relayout(mWindow, mLayout, mWidth, mHeight,
+                            View.VISIBLE, 0, mWinFrames, mMergedConfiguration, mSurfaceControl,
+                            mInsetsState, mTempControls, mSyncSeqIdBundle);
 
                     final int transformHint = SurfaceControl.rotationToBufferTransform(
                             (mDisplayInstallOrientation + mDisplay.getRotation()) % 4);
@@ -1228,7 +1204,7 @@
                             null /* ignoringVisibilityState */, config.isScreenRound(),
                             false /* alwaysConsumeSystemBars */, mLayout.softInputMode,
                             mLayout.flags, SYSTEM_UI_FLAG_VISIBLE, mLayout.type,
-                            winConfig.getWindowingMode(), null /* typeSideMap */);
+                            config.windowConfiguration.getWindowingMode(), null /* typeSideMap */);
 
                     if (!fixedSize) {
                         final Rect padding = mIWallpaperEngine.mDisplayPadding;
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 4bf68e23..ef57b1d 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -109,41 +109,6 @@
             out InsetsState insetsState, out InsetsSourceControl[] activeControls,
             out Bundle bundle);
 
-    /**
-     * Changes the view visibility and the attributes of a window. This should only be called when
-     * the visibility of the root view is changed. This returns a valid surface if the root view is
-     * visible. This also returns the latest information for the caller to compute its window frame.
-     *
-     * @param window The window being updated.
-     * @param attrs If non-null, new attributes to apply to the window.
-     * @param viewVisibility Window root view's visibility.
-     * @param outMergedConfiguration New config container that holds global, override and merged
-     * config for window, if it is now becoming visible and the merged configuration has changed
-     * since it was last displayed.
-     * @param outSurfaceControl Object in which is placed the new display surface.
-     * @param outInsetsState The current insets state in the system.
-     * @param outActiveControls The insets source controls for the caller to override the insets
-     * state in the system.
-     *
-     * @return int Result flags: {@link WindowManagerGlobal#RELAYOUT_FIRST_TIME}.
-     */
-    int updateVisibility(IWindow window, in WindowManager.LayoutParams attrs, int viewVisibility,
-            out MergedConfiguration outMergedConfiguration, out SurfaceControl outSurfaceControl,
-            out InsetsState outInsetsState, out InsetsSourceControl[] outActiveControls);
-
-    /**
-     * Reports the layout results and the attributes of a window to the server.
-     *
-     * @param window The window being reported.
-     * @param attrs If non-null, new attributes to apply to the window.
-     * @param flags Request flags: {@link WindowManagerGlobal#RELAYOUT_INSETS_PENDING}.
-     * @param clientFrames the window frames computed by the client.
-     * @param requestedWidth The width the window wants to be.
-     * @param requestedHeight The height the window wants to be.
-     */
-    oneway void updateLayout(IWindow window, in WindowManager.LayoutParams attrs, int flags,
-            in ClientWindowFrames clientFrames, int requestedWidth, int requestedHeight);
-
     /*
      * Notify the window manager that an application is relaunching and
      * windows should be prepared for replacement.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 1289548..64151d9 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -75,7 +75,6 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
@@ -8038,70 +8037,20 @@
         final int requestedWidth = (int) (mView.getMeasuredWidth() * appScale + 0.5f);
         final int requestedHeight = (int) (mView.getMeasuredHeight() * appScale + 0.5f);
 
-        int relayoutResult = 0;
-        WindowConfiguration winConfig = getConfiguration().windowConfiguration;
-        if (LOCAL_LAYOUT) {
-            if (mFirst || viewVisibility != mViewVisibility) {
-                relayoutResult = mWindowSession.updateVisibility(mWindow, params, viewVisibility,
-                        mPendingMergedConfiguration, mSurfaceControl, mTempInsets, mTempControls);
-                if (mTranslator != null) {
-                    mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
-                    mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
-                }
-                mInsetsController.onStateChanged(mTempInsets);
-                mInsetsController.onControlsChanged(mTempControls);
-
-                mPendingAlwaysConsumeSystemBars =
-                        (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0;
-            }
-            final InsetsState state = mInsetsController.getState();
-            final Rect displayCutoutSafe = mTempRect;
-            state.getDisplayCutoutSafe(displayCutoutSafe);
-            if (mWindowAttributes.type == TYPE_APPLICATION_STARTING) {
-                // TODO(b/210378379): Remove the special logic.
-                // Letting starting window use the window bounds from the pending config is for the
-                // fixed rotation, because the config is not overridden before the starting window
-                // is created.
-                winConfig = mPendingMergedConfiguration.getMergedConfiguration()
-                        .windowConfiguration;
-            }
-            mWindowLayout.computeFrames(mWindowAttributes, state, displayCutoutSafe,
-                    winConfig.getBounds(), winConfig.getWindowingMode(), requestedWidth,
-                    requestedHeight, mInsetsController.getRequestedVisibilities(),
-                    1f /* compatScale */, mTmpFrames);
-
-            mWindowSession.updateLayout(mWindow, params,
-                    insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mTmpFrames,
-                    requestedWidth, requestedHeight);
-
-        } else {
-            relayoutResult = mWindowSession.relayout(mWindow, params,
-                    requestedWidth, requestedHeight, viewVisibility,
-                    insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
-                    mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
-                    mTempControls, mRelayoutBundle);
-            final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid");
-            if (maybeSyncSeqId > 0) {
-                mSyncSeqId = maybeSyncSeqId;
-            }
-
-            if (mTranslator != null) {
-                mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame);
-                mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame);
-                mTranslator.translateRectInScreenToAppWindow(mTmpFrames.attachedFrame);
-                mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
-                mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
-            }
-            mInsetsController.onStateChanged(mTempInsets);
-            mInsetsController.onControlsChanged(mTempControls);
-
-            mPendingAlwaysConsumeSystemBars =
-                    (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0;
+        int relayoutResult = mWindowSession.relayout(mWindow, params,
+                requestedWidth, requestedHeight, viewVisibility,
+                insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
+                mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
+                mTempControls, mRelayoutBundle);
+        final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid");
+        if (maybeSyncSeqId > 0) {
+            mSyncSeqId = maybeSyncSeqId;
         }
 
         final int transformHint = SurfaceControl.rotationToBufferTransform(
                 (mDisplayInstallOrientation + mDisplay.getRotation()) % 4);
 
+        final WindowConfiguration winConfig = getConfiguration().windowConfiguration;
         WindowLayout.computeSurfaceSize(mWindowAttributes, winConfig.getMaxBounds(), requestedWidth,
                 requestedHeight, mTmpFrames.frame, mPendingDragResizing, mSurfaceSize);
 
@@ -8150,10 +8099,23 @@
             destroySurface();
         }
 
+        mPendingAlwaysConsumeSystemBars =
+                (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0;
+
         if (restore) {
             params.restore();
         }
+
+        if (mTranslator != null) {
+            mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame);
+            mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame);
+            mTranslator.translateRectInScreenToAppWindow(mTmpFrames.attachedFrame);
+            mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
+            mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
+        }
         setFrame(mTmpFrames.frame);
+        mInsetsController.onStateChanged(mTempInsets);
+        mInsetsController.onControlsChanged(mTempControls);
         return relayoutResult;
     }
 
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 3a72f6e..94da274 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -338,21 +338,6 @@
     }
 
     @Override
-    public int updateVisibility(IWindow window, WindowManager.LayoutParams inAttrs,
-            int viewVisibility, MergedConfiguration outMergedConfiguration,
-            SurfaceControl outSurfaceControl, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
-        // TODO(b/161810301): Finish the implementation.
-        return 0;
-    }
-
-    @Override
-    public void updateLayout(IWindow window, WindowManager.LayoutParams inAttrs, int flags,
-            ClientWindowFrames clientWindowFrames, int requestedWidth, int requestedHeight) {
-        // TODO(b/161810301): Finish the implementation.
-    }
-
-    @Override
     public void prepareToReplaceWindows(android.os.IBinder appToken, boolean childrenOnly) {
     }
 
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index b2f0989..68a08513 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -157,7 +157,7 @@
     <string name="accessibility_bubble_dismissed">Bubble dismissed.</string>
 
     <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] -->
-    <string name="restart_button_description">Tap to restart this app and go full screen.</string>
+    <string name="restart_button_description">Tap to restart this app for a better view.</string>
 
     <!-- Description of the camera compat button for applying stretched issues treatment in the hint for
          compatibility control. [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index 0d309c1..19d3acb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -20,10 +20,8 @@
 import static android.graphics.Color.WHITE;
 import static android.graphics.Color.alpha;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
-import static android.view.ViewRootImpl.LOCAL_LAYOUT;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
-import static android.view.WindowLayout.UNSPECIFIED_LENGTH;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
 import static android.view.WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
@@ -53,7 +51,6 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.TaskDescription;
 import android.app.ActivityThread;
-import android.app.WindowConfiguration;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -80,7 +77,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
-import android.view.WindowLayout;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.window.ClientWindowFrames;
@@ -212,8 +208,6 @@
         final IWindowSession session = WindowManagerGlobal.getWindowSession();
         final SurfaceControl surfaceControl = new SurfaceControl();
         final ClientWindowFrames tmpFrames = new ClientWindowFrames();
-        final WindowLayout windowLayout = new WindowLayout();
-        final Rect displayCutoutSafe = new Rect();
 
         final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0];
         final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration();
@@ -251,25 +245,9 @@
         window.setOuter(snapshotSurface);
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout");
-            if (LOCAL_LAYOUT) {
-                if (!surfaceControl.isValid()) {
-                    session.updateVisibility(window, layoutParams, View.VISIBLE,
-                            tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls);
-                }
-                tmpInsetsState.getDisplayCutoutSafe(displayCutoutSafe);
-                final WindowConfiguration winConfig =
-                        tmpMergedConfiguration.getMergedConfiguration().windowConfiguration;
-                windowLayout.computeFrames(layoutParams, tmpInsetsState, displayCutoutSafe,
-                        winConfig.getBounds(), winConfig.getWindowingMode(), UNSPECIFIED_LENGTH,
-                        UNSPECIFIED_LENGTH, info.requestedVisibilities, 1f /* compatScale */,
-                        tmpFrames);
-                session.updateLayout(window, layoutParams, 0 /* flags */, tmpFrames,
-                        UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH);
-            } else {
-                session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0,
-                        tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState,
-                        tmpControls, new Bundle());
-            }
+            session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0,
+                    tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState,
+                    tmpControls, new Bundle());
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         } catch (RemoteException e) {
             snapshotSurface.clearWindowSynced();
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 77f1803..acf3e4d 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -102,12 +102,13 @@
          screen. -->
     <item name="half_opened_bouncer_height_ratio" type="dimen" format="float">0.0</item>
 
-    <!-- The actual amount of translation that is applied to the bouncer when it animates from one
-         side of the screen to the other in one-handed mode. Note that it will always translate from
-         the side of the screen to the other (it will "jump" closer to the destination while the
-         opacity is zero), but this controls how much motion will actually be applied to it while
-         animating. Larger values will cause it to move "faster" while fading out/in. -->
-    <dimen name="one_handed_bouncer_move_animation_translation">120dp</dimen>
+    <!-- The actual amount of translation that is applied to the security when it animates from one
+         side of the screen to the other in one-handed or user switcher mode. Note that it will
+         always translate from the side of the screen to the other (it will "jump" closer to the
+         destination while the opacity is zero), but this controls how much motion will actually be
+         applied to it while animating. Larger values will cause it to move "faster" while
+         fading out/in. -->
+    <dimen name="security_shift_animation_translation">120dp</dimen>
 
 
     <dimen name="bouncer_user_switcher_header_text_size">20sp</dimen>
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml
index 3f56baf..efbdd1a 100644
--- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml
@@ -20,5 +20,5 @@
     style="@style/clock_subtitle"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:format12Hour="EEE, MMM d"
-    android:format24Hour="EEE, MMM d"/>
+    android:format12Hour="@string/dream_date_complication_date_format"
+    android:format24Hour="@string/dream_date_complication_date_format"/>
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
index e066d38..7a57293 100644
--- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
@@ -22,8 +22,8 @@
     android:fontFamily="@font/clock"
     android:includeFontPadding="false"
     android:textColor="@android:color/white"
-    android:format12Hour="h:mm"
-    android:format24Hour="kk:mm"
+    android:format12Hour="@string/dream_time_complication_12_hr_time_format"
+    android:format24Hour="@string/dream_time_complication_24_hr_time_format"
     android:shadowColor="@color/keyguard_shadow_color"
     android:shadowRadius="?attr/shadowRadius"
     android:textSize="@dimen/dream_overlay_complication_clock_time_text_size"/>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d88428f..343ec4f6 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2581,4 +2581,12 @@
     <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is unknown -->
     <string name="bt_le_audio_broadcast_dialog_unknown_name">Unknown</string>
 
+    <!-- Date format for the Dream Date Complication [CHAR LIMIT=NONE] -->
+    <string name="dream_date_complication_date_format">EEE, MMM d</string>
+
+    <!-- Time format for the Dream Time Complication for 12-hour time format [CHAR LIMIT=NONE] -->
+    <string name="dream_time_complication_12_hr_time_format">h:mm</string>
+
+    <!-- Time format for the Dream Time Complication for 24-hour time format [CHAR LIMIT=NONE] -->
+    <string name="dream_time_complication_24_hr_time_format">kk:mm</string>
 </resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 11519bf..8fb622a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -94,6 +94,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Consumer;
 
 public class KeyguardSecurityContainer extends FrameLayout {
     static final int USER_TYPE_PRIMARY = 1;
@@ -128,12 +129,12 @@
 
     private static final long IME_DISAPPEAR_DURATION_MS = 125;
 
-    // The duration of the animation to switch bouncer sides.
-    private static final long BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS = 500;
+    // The duration of the animation to switch security sides.
+    private static final long SECURITY_SHIFT_ANIMATION_DURATION_MS = 500;
 
-    // How much of the switch sides animation should be dedicated to fading the bouncer out. The
+    // How much of the switch sides animation should be dedicated to fading the security out. The
     // remainder will fade it back in again.
-    private static final float BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION = 0.2f;
+    private static final float SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION = 0.2f;
 
     @VisibleForTesting
     KeyguardSecurityViewFlipper mSecurityViewFlipper;
@@ -796,12 +797,16 @@
      * screen devices
      */
     abstract static class SidedSecurityMode implements ViewMode {
+        @Nullable private ValueAnimator mRunningSecurityShiftAnimator;
+        private KeyguardSecurityViewFlipper mViewFlipper;
         private ViewGroup mView;
         private GlobalSettings mGlobalSettings;
         private int mDefaultSideSetting;
 
-        public void init(ViewGroup v, GlobalSettings globalSettings, boolean leftAlignedByDefault) {
+        public void init(ViewGroup v, KeyguardSecurityViewFlipper viewFlipper,
+                GlobalSettings globalSettings, boolean leftAlignedByDefault) {
             mView = v;
+            mViewFlipper = viewFlipper;
             mGlobalSettings = globalSettings;
             mDefaultSideSetting =
                     leftAlignedByDefault ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT
@@ -841,6 +846,127 @@
 
         protected abstract void updateSecurityViewLocation(boolean leftAlign, boolean animate);
 
+        protected void translateSecurityViewLocation(boolean leftAlign, boolean animate) {
+            translateSecurityViewLocation(leftAlign, animate, i -> {});
+        }
+
+        /**
+         * Moves the inner security view to the correct location with animation. This is triggered
+         * when the user double taps on the side of the screen that is not currently occupied by
+         * the security view.
+         */
+        protected void translateSecurityViewLocation(boolean leftAlign, boolean animate,
+                Consumer<Float> securityAlphaListener) {
+            if (mRunningSecurityShiftAnimator != null) {
+                mRunningSecurityShiftAnimator.cancel();
+                mRunningSecurityShiftAnimator = null;
+            }
+
+            int targetTranslation = leftAlign
+                    ? 0 : mView.getMeasuredWidth() - mViewFlipper.getWidth();
+
+            if (animate) {
+                // This animation is a bit fun to implement. The bouncer needs to move, and fade
+                // in/out at the same time. The issue is, the bouncer should only move a short
+                // amount (120dp or so), but obviously needs to go from one side of the screen to
+                // the other. This needs a pretty custom animation.
+                //
+                // This works as follows. It uses a ValueAnimation to simply drive the animation
+                // progress. This animator is responsible for both the translation of the bouncer,
+                // and the current fade. It will fade the bouncer out while also moving it along the
+                // 120dp path. Once the bouncer is fully faded out though, it will "snap" the
+                // bouncer closer to its destination, then fade it back in again. The effect is that
+                // the bouncer will move from 0 -> X while fading out, then
+                // (destination - X) -> destination while fading back in again.
+                // TODO(b/208250221): Make this animation properly abortable.
+                Interpolator positionInterpolator = AnimationUtils.loadInterpolator(
+                        mView.getContext(), android.R.interpolator.fast_out_extra_slow_in);
+                Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
+                Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+
+                mRunningSecurityShiftAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+                mRunningSecurityShiftAnimator.setDuration(SECURITY_SHIFT_ANIMATION_DURATION_MS);
+                mRunningSecurityShiftAnimator.setInterpolator(Interpolators.LINEAR);
+
+                int initialTranslation = (int) mViewFlipper.getTranslationX();
+                int totalTranslation = (int) mView.getResources().getDimension(
+                        R.dimen.security_shift_animation_translation);
+
+                final boolean shouldRestoreLayerType = mViewFlipper.hasOverlappingRendering()
+                        && mViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE;
+                if (shouldRestoreLayerType) {
+                    mViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null);
+                }
+
+                float initialAlpha = mViewFlipper.getAlpha();
+
+                mRunningSecurityShiftAnimator.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mRunningSecurityShiftAnimator = null;
+                    }
+                });
+                mRunningSecurityShiftAnimator.addUpdateListener(animation -> {
+                    float switchPoint = SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION;
+                    boolean isFadingOut = animation.getAnimatedFraction() < switchPoint;
+
+                    int currentTranslation = (int) (positionInterpolator.getInterpolation(
+                            animation.getAnimatedFraction()) * totalTranslation);
+                    int translationRemaining = totalTranslation - currentTranslation;
+
+                    // Flip the sign if we're going from right to left.
+                    if (leftAlign) {
+                        currentTranslation = -currentTranslation;
+                        translationRemaining = -translationRemaining;
+                    }
+
+                    float opacity;
+                    if (isFadingOut) {
+                        // The bouncer fades out over the first X%.
+                        float fadeOutFraction = MathUtils.constrainedMap(
+                                /* rangeMin= */1.0f,
+                                /* rangeMax= */0.0f,
+                                /* valueMin= */0.0f,
+                                /* valueMax= */switchPoint,
+                                animation.getAnimatedFraction());
+                        opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction);
+
+                        // When fading out, the alpha needs to start from the initial opacity of the
+                        // view flipper, otherwise we get a weird bit of jank as it ramps back to
+                        // 100%.
+                        mViewFlipper.setAlpha(opacity * initialAlpha);
+
+                        // Animate away from the source.
+                        mViewFlipper.setTranslationX(initialTranslation + currentTranslation);
+                    } else {
+                        // And in again over the remaining (100-X)%.
+                        float fadeInFraction = MathUtils.constrainedMap(
+                                /* rangeMin= */0.0f,
+                                /* rangeMax= */1.0f,
+                                /* valueMin= */switchPoint,
+                                /* valueMax= */1.0f,
+                                animation.getAnimatedFraction());
+
+                        opacity = fadeInInterpolator.getInterpolation(fadeInFraction);
+                        mViewFlipper.setAlpha(opacity);
+
+                        // Fading back in, animate towards the destination.
+                        mViewFlipper.setTranslationX(targetTranslation - translationRemaining);
+                    }
+                    securityAlphaListener.accept(opacity);
+
+                    if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) {
+                        mViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null);
+                    }
+                });
+
+                mRunningSecurityShiftAnimator.start();
+            } else {
+                mViewFlipper.setTranslationX(targetTranslation);
+            }
+        }
+
+
         boolean isLeftAligned() {
             return mGlobalSettings.getInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
                     mDefaultSideSetting)
@@ -899,12 +1025,15 @@
         private UserSwitcherController.UserSwitchCallback mUserSwitchCallback =
                 this::setupUserSwitcher;
 
+        private float mAnimationLastAlpha = 1f;
+        private boolean mAnimationWaitsToShift = true;
+
         @Override
         public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings,
                 @NonNull KeyguardSecurityViewFlipper viewFlipper,
                 @NonNull FalsingManager falsingManager,
                 @NonNull UserSwitcherController userSwitcherController) {
-            init(v, globalSettings, /* leftAlignedByDefault= */false);
+            init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */false);
             mView = v;
             mViewFlipper = viewFlipper;
             mFalsingManager = falsingManager;
@@ -918,9 +1047,7 @@
                         true);
                 mUserSwitcherViewGroup =  mView.findViewById(R.id.keyguard_bouncer_user_switcher);
             }
-
             updateSecurityViewLocation();
-
             mUserSwitcher = mView.findViewById(R.id.user_switcher_header);
             setupUserSwitcher();
             mUserSwitcherController.addUserSwitchCallback(mUserSwitchCallback);
@@ -1120,22 +1247,61 @@
         }
 
         public void updateSecurityViewLocation(boolean leftAlign, boolean animate) {
-            int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans);
+            setYTranslation();
+            setGravity();
+            setXTranslation(leftAlign, animate);
+        }
 
+        private void setXTranslation(boolean leftAlign, boolean animate) {
             if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
-                updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL);
-                updateViewGravity(mUserSwitcherViewGroup, Gravity.CENTER_HORIZONTAL);
+                mUserSwitcherViewGroup.setTranslationX(0);
+                mViewFlipper.setTranslationX(0);
+            } else {
+                int switcherTargetTranslation = leftAlign
+                        ? mView.getMeasuredWidth() - mViewFlipper.getWidth() : 0;
+                if (animate) {
+                    mAnimationWaitsToShift = true;
+                    mAnimationLastAlpha = 1f;
+                    translateSecurityViewLocation(leftAlign, animate, securityAlpha -> {
+                        // During the animation security view fades out - alpha goes from 1 to
+                        // (almost) 0 - and then fades in - alpha grows back to 1.
+                        // If new alpha is bigger than previous one it means we're at inflection
+                        // point and alpha is zero or almost zero. That's when we want to do
+                        // translation of user switcher, so that it's not visible to the user.
+                        boolean fullyFadeOut = securityAlpha == 0.0f
+                                || securityAlpha > mAnimationLastAlpha;
+                        if (fullyFadeOut && mAnimationWaitsToShift) {
+                            mUserSwitcherViewGroup.setTranslationX(switcherTargetTranslation);
+                            mAnimationWaitsToShift = false;
+                        }
+                        mUserSwitcherViewGroup.setAlpha(securityAlpha);
+                        mAnimationLastAlpha = securityAlpha;
+                    });
+                } else {
+                    translateSecurityViewLocation(leftAlign, animate);
+                    mUserSwitcherViewGroup.setTranslationX(switcherTargetTranslation);
+                }
+            }
 
+        }
+
+        private void setGravity() {
+            if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+                updateViewGravity(mUserSwitcherViewGroup, Gravity.CENTER_HORIZONTAL);
+                updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL);
+            } else {
+                // horizontal gravity is the same because we translate these views anyway
+                updateViewGravity(mViewFlipper, Gravity.LEFT | Gravity.BOTTOM);
+                updateViewGravity(mUserSwitcherViewGroup, Gravity.LEFT | Gravity.CENTER_VERTICAL);
+            }
+        }
+
+        private void setYTranslation() {
+            int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans);
+            if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
                 mUserSwitcherViewGroup.setTranslationY(yTrans);
                 mViewFlipper.setTranslationY(-yTrans);
             } else {
-                int securityHorizontalGravity = leftAlign ? Gravity.LEFT : Gravity.RIGHT;
-                int userSwitcherHorizontalGravity =
-                        securityHorizontalGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT;
-                updateViewGravity(mViewFlipper, securityHorizontalGravity | Gravity.BOTTOM);
-                updateViewGravity(mUserSwitcherViewGroup,
-                        userSwitcherHorizontalGravity | Gravity.CENTER_VERTICAL);
-
                 // Attempt to reposition a bit higher to make up for this frame being a bit lower
                 // on the device
                 mUserSwitcherViewGroup.setTranslationY(-yTrans);
@@ -1155,7 +1321,6 @@
      * between alternate sides of the display.
      */
     static class OneHandedViewMode extends SidedSecurityMode {
-        @Nullable private ValueAnimator mRunningOneHandedAnimator;
         private ViewGroup mView;
         private KeyguardSecurityViewFlipper mViewFlipper;
 
@@ -1164,7 +1329,7 @@
                 @NonNull KeyguardSecurityViewFlipper viewFlipper,
                 @NonNull FalsingManager falsingManager,
                 @NonNull UserSwitcherController userSwitcherController) {
-            init(v, globalSettings, /* leftAlignedByDefault= */true);
+            init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */true);
             mView = v;
             mViewFlipper = viewFlipper;
 
@@ -1205,117 +1370,8 @@
             updateSecurityViewLocation(isLeftAligned(), /* animate= */false);
         }
 
-        /**
-         * Moves the inner security view to the correct location (in one handed mode) with
-         * animation. This is triggered when the user double taps on the side of the screen that is
-         * not currently occupied by the security view.
-         */
         protected void updateSecurityViewLocation(boolean leftAlign, boolean animate) {
-            if (mRunningOneHandedAnimator != null) {
-                mRunningOneHandedAnimator.cancel();
-                mRunningOneHandedAnimator = null;
-            }
-
-            int targetTranslation = leftAlign
-                    ? 0 : (int) (mView.getMeasuredWidth() - mViewFlipper.getWidth());
-
-            if (animate) {
-                // This animation is a bit fun to implement. The bouncer needs to move, and fade
-                // in/out at the same time. The issue is, the bouncer should only move a short
-                // amount (120dp or so), but obviously needs to go from one side of the screen to
-                // the other. This needs a pretty custom animation.
-                //
-                // This works as follows. It uses a ValueAnimation to simply drive the animation
-                // progress. This animator is responsible for both the translation of the bouncer,
-                // and the current fade. It will fade the bouncer out while also moving it along the
-                // 120dp path. Once the bouncer is fully faded out though, it will "snap" the
-                // bouncer closer to its destination, then fade it back in again. The effect is that
-                // the bouncer will move from 0 -> X while fading out, then
-                // (destination - X) -> destination while fading back in again.
-                // TODO(b/208250221): Make this animation properly abortable.
-                Interpolator positionInterpolator = AnimationUtils.loadInterpolator(
-                        mView.getContext(), android.R.interpolator.fast_out_extra_slow_in);
-                Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
-                Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
-
-                mRunningOneHandedAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
-                mRunningOneHandedAnimator.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS);
-                mRunningOneHandedAnimator.setInterpolator(Interpolators.LINEAR);
-
-                int initialTranslation = (int) mViewFlipper.getTranslationX();
-                int totalTranslation = (int) mView.getResources().getDimension(
-                        R.dimen.one_handed_bouncer_move_animation_translation);
-
-                final boolean shouldRestoreLayerType = mViewFlipper.hasOverlappingRendering()
-                        && mViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE;
-                if (shouldRestoreLayerType) {
-                    mViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null);
-                }
-
-                float initialAlpha = mViewFlipper.getAlpha();
-
-                mRunningOneHandedAnimator.addListener(new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            mRunningOneHandedAnimator = null;
-                        }
-                    });
-                mRunningOneHandedAnimator.addUpdateListener(animation -> {
-                    float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION;
-                    boolean isFadingOut = animation.getAnimatedFraction() < switchPoint;
-
-                    int currentTranslation = (int) (positionInterpolator.getInterpolation(
-                            animation.getAnimatedFraction()) * totalTranslation);
-                    int translationRemaining = totalTranslation - currentTranslation;
-
-                    // Flip the sign if we're going from right to left.
-                    if (leftAlign) {
-                        currentTranslation = -currentTranslation;
-                        translationRemaining = -translationRemaining;
-                    }
-
-                    if (isFadingOut) {
-                        // The bouncer fades out over the first X%.
-                        float fadeOutFraction = MathUtils.constrainedMap(
-                                /* rangeMin= */1.0f,
-                                /* rangeMax= */0.0f,
-                                /* valueMin= */0.0f,
-                                /* valueMax= */switchPoint,
-                                animation.getAnimatedFraction());
-                        float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction);
-
-                        // When fading out, the alpha needs to start from the initial opacity of the
-                        // view flipper, otherwise we get a weird bit of jank as it ramps back to
-                        // 100%.
-                        mViewFlipper.setAlpha(opacity * initialAlpha);
-
-                        // Animate away from the source.
-                        mViewFlipper.setTranslationX(initialTranslation + currentTranslation);
-                    } else {
-                        // And in again over the remaining (100-X)%.
-                        float fadeInFraction = MathUtils.constrainedMap(
-                                /* rangeMin= */0.0f,
-                                /* rangeMax= */1.0f,
-                                /* valueMin= */switchPoint,
-                                /* valueMax= */1.0f,
-                                animation.getAnimatedFraction());
-
-                        float opacity = fadeInInterpolator.getInterpolation(fadeInFraction);
-                        mViewFlipper.setAlpha(opacity);
-
-                        // Fading back in, animate towards the destination.
-                        mViewFlipper.setTranslationX(targetTranslation - translationRemaining);
-                    }
-
-                    if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) {
-                        mViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null);
-                    }
-                });
-
-                mRunningOneHandedAnimator.start();
-            } else {
-                mViewFlipper.setTranslationX(targetTranslation);
-            }
+            translateSecurityViewLocation(leftAlign, animate);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
index 61b1b66..c595586 100644
--- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
@@ -27,12 +27,15 @@
 import android.graphics.Paint
 import android.graphics.Path
 import android.graphics.RectF
+import android.hardware.biometrics.BiometricSourceType
 import android.view.View
 import androidx.core.graphics.ColorUtils
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.settingslib.Utils
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import java.util.concurrent.Executor
 
 /**
  * When the face is enrolled, we use this view to show the face scanning animation and the camera
@@ -42,7 +45,8 @@
     context: Context,
     pos: Int,
     val statusBarStateController: StatusBarStateController,
-    val keyguardUpdateMonitor: KeyguardUpdateMonitor
+    val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    val mainExecutor: Executor
 ) : ScreenDecorations.DisplayCutoutView(context, pos) {
     private var showScanningAnim = false
     private val rimPaint = Paint()
@@ -54,11 +58,26 @@
             com.android.systemui.R.attr.wallpaperTextColorAccent)
     private var cameraProtectionAnimator: ValueAnimator? = null
     var hideOverlayRunnable: Runnable? = null
+    var faceAuthSucceeded = false
 
     init {
         visibility = View.INVISIBLE // only show this view when face scanning is happening
     }
 
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        mainExecutor.execute {
+            keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        }
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        mainExecutor.execute {
+            keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
+        }
+    }
+
     override fun setColor(color: Int) {
         cameraProtectionColor = color
         invalidate()
@@ -108,7 +127,6 @@
         if (showScanningAnimNow == showScanningAnim) {
             return
         }
-
         showScanningAnim = showScanningAnimNow
         updateProtectionBoundingPath()
         // Delay the relayout until the end of the animation when hiding,
@@ -120,13 +138,20 @@
 
         cameraProtectionAnimator?.cancel()
         cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress,
-                if (showScanningAnimNow) 1.0f else HIDDEN_CAMERA_PROTECTION_SCALE).apply {
-            startDelay = if (showScanningAnim) 0 else PULSE_DISAPPEAR_DURATION
-            duration = if (showScanningAnim) PULSE_APPEAR_DURATION else
-                CAMERA_PROTECTION_DISAPPEAR_DURATION
-            interpolator = if (showScanningAnim) Interpolators.STANDARD else
-                Interpolators.EMPHASIZED
-
+                if (showScanningAnimNow) SHOW_CAMERA_PROTECTION_SCALE
+                else HIDDEN_CAMERA_PROTECTION_SCALE).apply {
+            startDelay =
+                    if (showScanningAnim) 0
+                    else if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
+                    else PULSE_ERROR_DISAPPEAR_DURATION
+            duration =
+                    if (showScanningAnim) CAMERA_PROTECTION_APPEAR_DURATION
+                    else if (faceAuthSucceeded) CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION
+                    else CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION
+            interpolator =
+                    if (showScanningAnim) Interpolators.STANDARD_ACCELERATE
+                    else if (faceAuthSucceeded) Interpolators.STANDARD
+                    else Interpolators.STANDARD_DECELERATE
             addUpdateListener(ValueAnimator.AnimatorUpdateListener {
                 animation: ValueAnimator ->
                 cameraProtectionProgress = animation.animatedValue as Float
@@ -143,47 +168,73 @@
                     }
                 }
             })
-            start()
         }
 
         rimAnimator?.cancel()
         rimAnimator = AnimatorSet().apply {
-            val rimAppearOrDisappearAnimator = ValueAnimator.ofFloat(rimProgress,
-                    if (showScanningAnim) PULSE_RADIUS_OUT else (PULSE_RADIUS_IN * 1.15f)).apply {
-                duration = if (showScanningAnim) PULSE_APPEAR_DURATION else PULSE_DISAPPEAR_DURATION
-                interpolator = Interpolators.STANDARD
-                addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                    animation: ValueAnimator ->
-                    rimProgress = animation.animatedValue as Float
-                    invalidate()
-                })
-            }
             if (showScanningAnim) {
-                // appear and then pulse in/out
-                playSequentially(rimAppearOrDisappearAnimator,
+                val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE,
+                        PULSE_RADIUS_OUT).apply {
+                    duration = PULSE_APPEAR_DURATION
+                    interpolator = Interpolators.STANDARD_DECELERATE
+                    addUpdateListener(ValueAnimator.AnimatorUpdateListener {
+                        animation: ValueAnimator ->
+                        rimProgress = animation.animatedValue as Float
+                        invalidate()
+                    })
+                }
+
+                // animate in camera protection, rim, and then pulse in/out
+                playSequentially(cameraProtectionAnimator, rimAppearAnimator,
                         createPulseAnimator(), createPulseAnimator(),
                         createPulseAnimator(), createPulseAnimator(),
                         createPulseAnimator(), createPulseAnimator())
             } else {
-                val opacityAnimator = ValueAnimator.ofInt(255, 0).apply {
-                    duration = PULSE_DISAPPEAR_DURATION
-                    interpolator = Interpolators.LINEAR
+                val rimDisappearAnimator = ValueAnimator.ofFloat(
+                        rimProgress,
+                        if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS
+                        else SHOW_CAMERA_PROTECTION_SCALE
+                ).apply {
+                    duration =
+                            if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
+                            else PULSE_ERROR_DISAPPEAR_DURATION
+                    interpolator =
+                            if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE
+                            else Interpolators.STANDARD
                     addUpdateListener(ValueAnimator.AnimatorUpdateListener {
                         animation: ValueAnimator ->
-                        rimPaint.alpha = animation.animatedValue as Int
+                        rimProgress = animation.animatedValue as Float
                         invalidate()
                     })
+                    addListener(object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator) {
+                            rimProgress = HIDDEN_RIM_SCALE
+                            invalidate()
+                        }
+                    })
                 }
-                addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator) {
-                        rimProgress = HIDDEN_RIM_SCALE
-                        rimPaint.alpha = 255
-                        invalidate()
+                if (faceAuthSucceeded) {
+                    val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply {
+                        duration = PULSE_SUCCESS_DISAPPEAR_DURATION
+                        interpolator = Interpolators.LINEAR
+                        addUpdateListener(ValueAnimator.AnimatorUpdateListener {
+                            animation: ValueAnimator ->
+                            rimPaint.alpha = animation.animatedValue as Int
+                            invalidate()
+                        })
+                        addListener(object : AnimatorListenerAdapter() {
+                            override fun onAnimationEnd(animation: Animator) {
+                                rimPaint.alpha = 255
+                                invalidate()
+                            }
+                        })
                     }
-                })
-
-                // disappear
-                playTogether(rimAppearOrDisappearAnimator, opacityAnimator)
+                    val rimSuccessAnimator = AnimatorSet()
+                    rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator)
+                    playTogether(rimSuccessAnimator, cameraProtectionAnimator)
+                } else {
+                    playTogether(rimDisappearAnimator, cameraProtectionAnimator)
+                }
             }
 
             addListener(object : AnimatorListenerAdapter() {
@@ -253,15 +304,72 @@
         }
     }
 
+    private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
+        override fun onBiometricAuthenticated(
+            userId: Int,
+            biometricSourceType: BiometricSourceType?,
+            isStrongBiometric: Boolean
+        ) {
+            if (biometricSourceType == BiometricSourceType.FACE) {
+                post {
+                    faceAuthSucceeded = true
+                    enableShowProtection(true)
+                }
+            }
+        }
+
+        override fun onBiometricAcquired(
+            biometricSourceType: BiometricSourceType?,
+            acquireInfo: Int
+        ) {
+            if (biometricSourceType == BiometricSourceType.FACE) {
+                post {
+                    faceAuthSucceeded = false // reset
+                }
+            }
+        }
+
+        override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) {
+            if (biometricSourceType == BiometricSourceType.FACE) {
+                post {
+                    faceAuthSucceeded = false
+                    enableShowProtection(false)
+                }
+            }
+        }
+
+        override fun onBiometricError(
+            msgId: Int,
+            errString: String?,
+            biometricSourceType: BiometricSourceType?
+        ) {
+            if (biometricSourceType == BiometricSourceType.FACE) {
+                post {
+                    faceAuthSucceeded = false
+                    enableShowProtection(false)
+                }
+            }
+        }
+    }
+
     companion object {
         private const val HIDDEN_RIM_SCALE = HIDDEN_CAMERA_PROTECTION_SCALE
+        private const val SHOW_CAMERA_PROTECTION_SCALE = 1f
 
-        private const val PULSE_APPEAR_DURATION = 350L
+        private const val PULSE_RADIUS_IN = 1.1f
+        private const val PULSE_RADIUS_OUT = 1.125f
+        private const val PULSE_RADIUS_SUCCESS = 1.25f
+
+        private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L
+        private const val PULSE_APPEAR_DURATION = 250L // without start delay
+
         private const val PULSE_DURATION_INWARDS = 500L
         private const val PULSE_DURATION_OUTWARDS = 500L
-        private const val PULSE_DISAPPEAR_DURATION = 850L
-        private const val CAMERA_PROTECTION_DISAPPEAR_DURATION = 700L // excluding start delay
-        private const val PULSE_RADIUS_IN = 1.15f
-        private const val PULSE_RADIUS_OUT = 1.25f
+
+        private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L
+        private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay
+
+        private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L
+        private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index adc0096..81d3d6c 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -32,10 +32,12 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.FaceScanningOverlay
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import java.util.concurrent.Executor
 import javax.inject.Inject
 
 @SysUISingleton
@@ -44,6 +46,7 @@
     private val context: Context,
     private val statusBarStateController: StatusBarStateController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    @Main private val mainExecutor: Executor,
     private val featureFlags: FeatureFlags
 ) : DecorProviderFactory() {
     private val display = context.display
@@ -82,7 +85,9 @@
                                         bound.baseOnRotation0(displayInfo.rotation),
                                         authController,
                                         statusBarStateController,
-                                        keyguardUpdateMonitor)
+                                        keyguardUpdateMonitor,
+                                        mainExecutor
+                                )
                         )
                     }
                 }
@@ -102,7 +107,8 @@
     override val alignedBound: Int,
     private val authController: AuthController,
     private val statusBarStateController: StatusBarStateController,
-    private val keyguardUpdateMonitor: KeyguardUpdateMonitor
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val mainExecutor: Executor
 ) : BoundDecorProvider() {
     override val viewId: Int = com.android.systemui.R.id.face_scanning_anim
 
@@ -127,7 +133,9 @@
                 context,
                 alignedBound,
                 statusBarStateController,
-                keyguardUpdateMonitor)
+                keyguardUpdateMonitor,
+                mainExecutor
+        )
         view.id = viewId
         FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.MATCH_PARENT).let {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index ccc0a3d..bec6739 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -69,11 +69,13 @@
     View mHolderView;
     boolean mIsDragging;
     int mCurrentActivePosition;
+    private boolean mIsInitVolumeFirstTime;
 
     public MediaOutputBaseAdapter(MediaOutputController controller) {
         mController = controller;
         mIsDragging = false;
         mCurrentActivePosition = -1;
+        mIsInitVolumeFirstTime = true;
     }
 
     @Override
@@ -275,7 +277,7 @@
             mSeekBar.setMaxVolume(device.getMaxVolume());
             final int currentVolume = device.getCurrentVolume();
             if (mSeekBar.getVolume() != currentVolume) {
-                if (isCurrentSeekbarInvisible) {
+                if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) {
                     animateCornerAndVolume(mSeekBar.getProgress(),
                             MediaOutputSeekbar.scaleVolumeToProgress(currentVolume));
                 } else {
@@ -284,6 +286,9 @@
                     }
                 }
             }
+            if (mIsInitVolumeFirstTime) {
+                mIsInitVolumeFirstTime = false;
+            }
             mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                 @Override
                 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index add177d..38830c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -89,6 +89,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLoggerKt;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
@@ -110,6 +111,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Queue;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 import javax.inject.Inject;
@@ -157,6 +159,8 @@
     private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
     private final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>();
 
+    private Set<String> mNotificationsWithoutRankings = Collections.emptySet();
+
     private Queue<NotifEvent> mEventQueue = new ArrayDeque<>();
 
     private boolean mAttached = false;
@@ -560,6 +564,7 @@
     }
 
     private void applyRanking(@NonNull RankingMap rankingMap) {
+        ArrayMap<String, NotificationEntry> currentEntriesWithoutRankings = null;
         for (NotificationEntry entry : mNotificationSet.values()) {
             if (!isCanceled(entry)) {
 
@@ -581,10 +586,21 @@
                         }
                     }
                 } else {
-                    mLogger.logRankingMissing(entry, rankingMap);
+                    if (currentEntriesWithoutRankings == null) {
+                        currentEntriesWithoutRankings = new ArrayMap<>();
+                    }
+                    currentEntriesWithoutRankings.put(entry.getKey(), entry);
                 }
             }
         }
+        NotifCollectionLoggerKt.maybeLogInconsistentRankings(
+                mLogger,
+                mNotificationsWithoutRankings,
+                currentEntriesWithoutRankings,
+                rankingMap
+        );
+        mNotificationsWithoutRankings = currentEntriesWithoutRankings == null
+                ? Collections.emptySet() : currentEntriesWithoutRankings.keySet();
         mEventQueue.add(new RankingAppliedEvent());
     }
 
@@ -836,6 +852,11 @@
                         entries,
                         true,
                         "\t\t"));
+
+        pw.println("\n\tmNotificationsWithoutRankings: " + mNotificationsWithoutRankings.size());
+        for (String key : mNotificationsWithoutRankings) {
+            pw.println("\t * : " + key);
+        }
     }
 
     private final BatchableNotificationHandler mNotifHandler = new BatchableNotificationHandler() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java
index 05e8ec5..ef63be0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java
@@ -16,16 +16,16 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
-import com.android.keyguard.KeyguardUpdateMonitor;
+import androidx.annotation.NonNull;
+
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
-import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
-import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider;
+import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
 
 import javax.inject.Inject;
 
@@ -36,27 +36,21 @@
 @CoordinatorScope
 public class KeyguardCoordinator implements Coordinator {
     private static final String TAG = "KeyguardCoordinator";
-    private final StatusBarStateController mStatusBarStateController;
-    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final HighPriorityProvider mHighPriorityProvider;
-    private final SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider;
     private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
+    private final SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider;
     private final SharedCoordinatorLogger mLogger;
+    private final StatusBarStateController mStatusBarStateController;
 
     @Inject
     public KeyguardCoordinator(
-            StatusBarStateController statusBarStateController,
-            KeyguardUpdateMonitor keyguardUpdateMonitor,
-            HighPriorityProvider highPriorityProvider,
-            SectionHeaderVisibilityProvider sectionHeaderVisibilityProvider,
             KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
-            SharedCoordinatorLogger logger) {
-        mStatusBarStateController = statusBarStateController;
-        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mHighPriorityProvider = highPriorityProvider;
-        mSectionHeaderVisibilityProvider = sectionHeaderVisibilityProvider;
+            SectionHeaderVisibilityProvider sectionHeaderVisibilityProvider,
+            SharedCoordinatorLogger logger,
+            StatusBarStateController statusBarStateController) {
         mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
+        mSectionHeaderVisibilityProvider = sectionHeaderVisibilityProvider;
         mLogger = logger;
+        mStatusBarStateController = statusBarStateController;
     }
 
     @Override
@@ -72,7 +66,7 @@
 
     private final NotifFilter mNotifFilter = new NotifFilter(TAG) {
         @Override
-        public boolean shouldFilterOut(NotificationEntry entry, long now) {
+        public boolean shouldFilterOut(@NonNull NotificationEntry entry, long now) {
             return mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry);
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt
index 9e8b35a..1494574 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt
@@ -68,4 +68,4 @@
         // Show the "alerted" bell icon
         controller.setLastAudiblyAlertedMs(entry.lastAudiblyAlertedMs)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index 46e01c3..ebcac6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -196,16 +196,32 @@
         })
     }
 
-    fun logRankingMissing(entry: NotificationEntry, rankingMap: RankingMap) {
+    fun logMissingRankings(
+        newlyInconsistentEntries: List<NotificationEntry>,
+        totalInconsistent: Int,
+        rankingMap: RankingMap
+    ) {
         buffer.log(TAG, WARNING, {
-            str1 = entry.logKey
+            int1 = totalInconsistent
+            int2 = newlyInconsistentEntries.size
+            str1 = newlyInconsistentEntries.joinToString { it.logKey ?: "null" }
         }, {
-            "Ranking update is missing ranking for $str1"
+            "Ranking update is missing ranking for $int1 entries ($int2 new): $str1"
         })
-        buffer.log(TAG, DEBUG, {}, { "Ranking map contents:" })
-        for (entry in rankingMap.orderedKeys) {
-            buffer.log(TAG, DEBUG, { str1 = logKey(entry) }, { "  $str1" })
-        }
+        buffer.log(TAG, DEBUG, {
+            str1 = rankingMap.orderedKeys.map { logKey(it) ?: "null" }.toString()
+        }, {
+            "Ranking map contents: $str1"
+        })
+    }
+
+    fun logRecoveredRankings(newlyConsistentKeys: List<String>) {
+        buffer.log(TAG, INFO, {
+            int1 = newlyConsistentKeys.size
+            str1 = newlyConsistentKeys.joinToString { logKey(it) ?: "null" }
+        }, {
+            "Ranking update now contains rankings for $int1 previously inconsistent entries: $str1"
+        })
     }
 
     fun logRemoteExceptionOnNotificationClear(entry: NotificationEntry, e: RemoteException) {
@@ -346,4 +362,29 @@
     }
 }
 
+fun maybeLogInconsistentRankings(
+    logger: NotifCollectionLogger,
+    oldKeysWithoutRankings: Set<String>,
+    newEntriesWithoutRankings: Map<String, NotificationEntry>?,
+    rankingMap: RankingMap
+) {
+    if (oldKeysWithoutRankings.isEmpty() && newEntriesWithoutRankings == null) return
+    val newlyConsistent: List<String> = oldKeysWithoutRankings
+        .mapNotNull { key ->
+            key.takeIf { key !in (newEntriesWithoutRankings ?: emptyMap()) }
+                .takeIf { key in rankingMap.orderedKeys }
+        }.sorted()
+    if (newlyConsistent.isNotEmpty()) {
+        logger.logRecoveredRankings(newlyConsistent)
+    }
+    val newlyInconsistent: List<NotificationEntry> = newEntriesWithoutRankings
+        ?.mapNotNull { (key, entry) ->
+            entry.takeIf { key !in oldKeysWithoutRankings }
+        }?.sortedBy { it.key } ?: emptyList()
+    if (newlyInconsistent.isNotEmpty()) {
+        val totalInconsistent: Int = newEntriesWithoutRankings?.size ?: 0
+        logger.logMissingRankings(newlyInconsistent, totalInconsistent, rankingMap)
+    }
+}
+
 private const val TAG = "NotifCollection"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt
index 2148d3b..82c7aae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.statusbar.notification.collection.provider
 
 import android.content.Context
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt
index 50e7d1ce..7b94830 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt
@@ -43,4 +43,4 @@
     fun isMinimizedSection(section: NotifSection): Boolean {
         return lowPrioritySections.contains(section.sectioner)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
index 34d25cf..d234e54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
@@ -107,4 +107,4 @@
                 .apply { entry.children.forEach { children.add(buildNotifNode(this, it)) } }
         else -> throw RuntimeException("Unexpected entry: $entry")
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 1b9662c..f2ac0c7 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.keyguard;
 
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE;
 import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT;
 import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT;
@@ -46,7 +48,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
-import android.view.WindowInsetsController;
 import android.widget.FrameLayout;
 
 import androidx.test.filters.SmallTest;
@@ -85,8 +86,6 @@
     public MockitoRule mRule = MockitoJUnit.rule();
 
     @Mock
-    private WindowInsetsController mWindowInsetsController;
-    @Mock
     private KeyguardSecurityViewFlipper mSecurityViewFlipper;
     @Mock
     private GlobalSettings mGlobalSettings;
@@ -110,7 +109,6 @@
         mSecurityViewFlipperLayoutParams = new FrameLayout.LayoutParams(
                 MATCH_PARENT, MATCH_PARENT);
 
-        when(mSecurityViewFlipper.getWindowInsetsController()).thenReturn(mWindowInsetsController);
         when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams);
         mKeyguardSecurityContainer = new KeyguardSecurityContainer(getContext());
         mKeyguardSecurityContainer.mSecurityViewFlipper = mSecurityViewFlipper;
@@ -221,7 +219,7 @@
                 mKeyguardSecurityContainer.getWidth() - 1f);
 
         verify(mGlobalSettings).putInt(ONE_HANDED_KEYGUARD_SIDE, ONE_HANDED_KEYGUARD_SIDE_RIGHT);
-        verify(mSecurityViewFlipper).setTranslationX(
+        assertSecurityTranslationX(
                 mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth());
 
         mKeyguardSecurityContainer.updatePositionByTouchX(1f);
@@ -243,43 +241,43 @@
     }
 
     @Test
-    public void testUserSwitcherModeViewGravityLandscape() {
+    public void testUserSwitcherModeViewPositionLandscape() {
         // GIVEN one user has been setup and in landscape
         when(mUserSwitcherController.getUsers()).thenReturn(buildUserRecords(1));
-        Configuration config = new Configuration();
-        config.orientation = Configuration.ORIENTATION_LANDSCAPE;
-        when(getContext().getResources().getConfiguration()).thenReturn(config);
+        Configuration landscapeConfig = configuration(ORIENTATION_LANDSCAPE);
+        when(getContext().getResources().getConfiguration()).thenReturn(landscapeConfig);
 
         // WHEN UserSwitcherViewMode is initialized and config has changed
         setupUserSwitcher();
-        reset(mSecurityViewFlipper);
-        when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams);
-        mKeyguardSecurityContainer.onConfigurationChanged(config);
+        mKeyguardSecurityContainer.onConfigurationChanged(landscapeConfig);
 
         // THEN views are oriented side by side
-        verify(mSecurityViewFlipper).setLayoutParams(mLayoutCaptor.capture());
-        assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(Gravity.RIGHT | Gravity.BOTTOM);
+        assertSecurityGravity(Gravity.LEFT | Gravity.BOTTOM);
         assertUserSwitcherGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
+        assertSecurityTranslationX(
+                mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth());
+        assertUserSwitcherTranslationX(0f);
+
     }
 
     @Test
     public void testUserSwitcherModeViewGravityPortrait() {
         // GIVEN one user has been setup and in landscape
         when(mUserSwitcherController.getUsers()).thenReturn(buildUserRecords(1));
-        Configuration config = new Configuration();
-        config.orientation = Configuration.ORIENTATION_PORTRAIT;
-        when(getContext().getResources().getConfiguration()).thenReturn(config);
+        Configuration portraitConfig = configuration(ORIENTATION_PORTRAIT);
+        when(getContext().getResources().getConfiguration()).thenReturn(portraitConfig);
 
         // WHEN UserSwitcherViewMode is initialized and config has changed
         setupUserSwitcher();
         reset(mSecurityViewFlipper);
         when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams);
-        mKeyguardSecurityContainer.onConfigurationChanged(config);
+        mKeyguardSecurityContainer.onConfigurationChanged(portraitConfig);
 
         // THEN views are both centered horizontally
-        verify(mSecurityViewFlipper).setLayoutParams(mLayoutCaptor.capture());
-        assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(Gravity.CENTER_HORIZONTAL);
+        assertSecurityGravity(Gravity.CENTER_HORIZONTAL);
         assertUserSwitcherGravity(Gravity.CENTER_HORIZONTAL);
+        assertSecurityTranslationX(0);
+        assertUserSwitcherTranslationX(0);
     }
 
     @Test
@@ -315,14 +313,16 @@
         setupUserSwitcher();
         setViewWidth(VIEW_WIDTH);
 
+        // security is on the right side by default
         assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity(
                 touchEventLeftSide())).isTrue();
         assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity(
                 touchEventRightSide())).isFalse();
 
-        mKeyguardSecurityContainer.handleDoubleTap(touchEventLeftSide());
-        // settings should be updated, we need to keep the mock updated as well
+        // move security to the left side
         when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_LEFT);
+        mKeyguardSecurityContainer.onConfigurationChanged(new Configuration());
+
         assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity(
                 touchEventLeftSide())).isFalse();
         assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity(
@@ -331,23 +331,33 @@
 
     @Test
     public void testSecuritySwitchesSidesInLandscapeUserSwitcherMode() {
-        Configuration config = new Configuration();
-        config.orientation = Configuration.ORIENTATION_LANDSCAPE;
-        when(getContext().getResources().getConfiguration()).thenReturn(config);
+        when(getContext().getResources().getConfiguration())
+                .thenReturn(configuration(ORIENTATION_LANDSCAPE));
         setupUserSwitcher();
-        // check default position
-        assertSecurityGravity(Gravity.RIGHT | Gravity.BOTTOM);
-        assertUserSwitcherGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
 
-        reset(mSecurityViewFlipper);
-        when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams);
-        // change setting so configuration change triggers position change
+        // switch sides
         when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_LEFT);
         mKeyguardSecurityContainer.onConfigurationChanged(new Configuration());
 
-        // check position after switching
-        assertSecurityGravity(Gravity.LEFT | Gravity.BOTTOM);
-        assertUserSwitcherGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL);
+        assertSecurityTranslationX(0);
+        assertUserSwitcherTranslationX(
+                mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth());
+    }
+
+    private Configuration configuration(@Configuration.Orientation int orientation) {
+        Configuration config = new Configuration();
+        config.orientation = orientation;
+        return config;
+    }
+
+    private void assertSecurityTranslationX(float translation) {
+        verify(mSecurityViewFlipper).setTranslationX(translation);
+    }
+
+    private void assertUserSwitcherTranslationX(float translation) {
+        ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById(
+                R.id.keyguard_bouncer_user_switcher);
+        assertThat(userSwitcher.getTranslationX()).isEqualTo(translation);
     }
 
     private void assertUserSwitcherGravity(@Gravity.GravityFlags int gravity) {
@@ -391,6 +401,9 @@
         when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_RIGHT);
         mKeyguardSecurityContainer.initMode(KeyguardSecurityContainer.MODE_USER_SWITCHER,
                 mGlobalSettings, mFalsingManager, mUserSwitcherController);
+        // reset mSecurityViewFlipper so setup doesn't influence test verifications
+        reset(mSecurityViewFlipper);
+        when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams);
     }
 
     private ArrayList<UserRecord> buildUserRecords(int count) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index a35efa9..90609fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -210,7 +210,8 @@
                 BOUNDS_POSITION_TOP,
                 mAuthController,
                 mStatusBarStateController,
-                mKeyguardUpdateMonitor));
+                mKeyguardUpdateMonitor,
+                mExecutor));
 
         mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
                 mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 11326e7..59475cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -51,6 +52,8 @@
     private static final String TEST_DEVICE_ID_1 = "test_device_id_1";
     private static final String TEST_DEVICE_ID_2 = "test_device_id_2";
     private static final String TEST_SESSION_NAME = "test_session_name";
+    private static final int TEST_MAX_VOLUME = 20;
+    private static final int TEST_CURRENT_VOLUME = 10;
 
     // Mock
     private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
@@ -64,12 +67,14 @@
     private MediaOutputAdapter mMediaOutputAdapter;
     private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
     private List<MediaDevice> mMediaDevices = new ArrayList<>();
+    MediaOutputSeekbar mSpyMediaOutputSeekbar;
 
     @Before
     public void setUp() {
         mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController, mMediaOutputDialog);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
+        mSpyMediaOutputSeekbar = spy(mViewHolder.mSeekBar);
 
         when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
         when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
@@ -169,6 +174,16 @@
     }
 
     @Test
+    public void onBindViewHolder_initSeekbar_setsVolume() {
+        when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME);
+        when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_CURRENT_VOLUME);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_CURRENT_VOLUME);
+    }
+
+    @Test
     public void onBindViewHolder_bindNonActiveConnectedDevice_verifyView() {
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java
deleted file mode 100644
index 193d76d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2019 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.notification.collection.coordinator;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.os.Handler;
-import android.os.UserHandle;
-import android.testing.AndroidTestingRunner;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
-import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
-import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
-import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * TODO(b/224771204) Create test cases
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@Ignore
-public class KeyguardCoordinatorTest extends SysuiTestCase {
-    private static final int NOTIF_USER_ID = 0;
-    private static final int CURR_USER_ID = 1;
-
-    @Mock private Handler mMainHandler;
-    @Mock private KeyguardStateController mKeyguardStateController;
-    @Mock private BroadcastDispatcher mBroadcastDispatcher;
-    @Mock private StatusBarStateController mStatusBarStateController;
-    @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @Mock private HighPriorityProvider mHighPriorityProvider;
-    @Mock private SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider;
-    @Mock private NotifPipeline mNotifPipeline;
-    @Mock private KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
-
-    private NotificationEntry mEntry;
-    private NotifFilter mKeyguardFilter;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        KeyguardCoordinator keyguardCoordinator = new KeyguardCoordinator(
-                mStatusBarStateController,
-                mKeyguardUpdateMonitor, mHighPriorityProvider, mSectionHeaderVisibilityProvider,
-                mKeyguardNotificationVisibilityProvider, mock(SharedCoordinatorLogger.class));
-
-        mEntry = new NotificationEntryBuilder()
-                .setUser(new UserHandle(NOTIF_USER_ID))
-                .build();
-
-        ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class);
-        keyguardCoordinator.attach(mNotifPipeline);
-        verify(mNotifPipeline, times(1)).addFinalizeFilter(filterCaptor.capture());
-        mKeyguardFilter = filterCaptor.getValue();
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
new file mode 100644
index 0000000..8c506a6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.notification.collection.coordinator
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
+import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.withArgCaptor
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class KeyguardCoordinatorTest : SysuiTestCase() {
+    private val notifPipeline: NotifPipeline = mock()
+    private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock()
+    private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock()
+    private val sharedCoordinatorLogger: SharedCoordinatorLogger = mock()
+    private val statusBarStateController: StatusBarStateController = mock()
+
+    private lateinit var onStateChangeListener: Consumer<String>
+    private lateinit var keyguardFilter: NotifFilter
+
+    @Before
+    fun setup() {
+        val keyguardCoordinator = KeyguardCoordinator(
+            keyguardNotifVisibilityProvider,
+            sectionHeaderVisibilityProvider,
+            sharedCoordinatorLogger,
+            statusBarStateController
+        )
+        keyguardCoordinator.attach(notifPipeline)
+        onStateChangeListener = withArgCaptor {
+            verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
+        }
+        keyguardFilter = withArgCaptor {
+            verify(notifPipeline).addFinalizeFilter(capture())
+        }
+    }
+
+    @Test
+    fun testSetSectionHeadersVisibleInShade() {
+        clearInvocations(sectionHeaderVisibilityProvider)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+        onStateChangeListener.accept("state change")
+        verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(true)
+    }
+
+    @Test
+    fun testSetSectionHeadersNotVisibleOnKeyguard() {
+        clearInvocations(sectionHeaderVisibilityProvider)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        onStateChangeListener.accept("state change")
+        verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt
index 40859d0..3f3de00 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt
@@ -37,8 +37,8 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations.initMocks
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations.initMocks
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt
new file mode 100644
index 0000000..6c07174
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.notification.collection.notifcollection
+
+import android.service.notification.NotificationListenerService.RankingMap
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotifCollectionLoggerTest : SysuiTestCase() {
+    private val logger: NotifCollectionLogger = mock()
+    private val entry1: NotificationEntry = NotificationEntryBuilder().setId(1).build()
+    private val entry2: NotificationEntry = NotificationEntryBuilder().setId(2).build()
+
+    private fun mapOfEntries(vararg entries: NotificationEntry): Map<String, NotificationEntry> =
+        entries.associateBy { it.key }
+
+    private fun rankingMapOf(vararg entries: NotificationEntry): RankingMap =
+        RankingMap(entries.map { it.ranking }.toTypedArray())
+
+    @Test
+    fun testMaybeLogInconsistentRankings_logsNewlyInconsistentRanking() {
+        val rankingMap = rankingMapOf(entry1)
+        maybeLogInconsistentRankings(
+            logger = logger,
+            oldKeysWithoutRankings = emptySet(),
+            newEntriesWithoutRankings = mapOfEntries(entry2),
+            rankingMap = rankingMap
+        )
+        verify(logger).logMissingRankings(
+            newlyInconsistentEntries = eq(listOf(entry2)),
+            totalInconsistent = eq(1),
+            rankingMap = eq(rankingMap),
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun testMaybeLogInconsistentRankings_doesNotLogAlreadyInconsistentRanking() {
+        maybeLogInconsistentRankings(
+            logger = logger,
+            oldKeysWithoutRankings = setOf(entry2.key),
+            newEntriesWithoutRankings = mapOfEntries(entry2),
+            rankingMap = rankingMapOf(entry1)
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun testMaybeLogInconsistentRankings_logsWhenRankingIsAdded() {
+        maybeLogInconsistentRankings(
+            logger = logger,
+            oldKeysWithoutRankings = setOf(entry2.key),
+            newEntriesWithoutRankings = mapOfEntries(),
+            rankingMap = rankingMapOf(entry1, entry2)
+        )
+        verify(logger).logRecoveredRankings(
+            newlyConsistentKeys = eq(listOf(entry2.key)),
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun testMaybeLogInconsistentRankings_doesNotLogsWhenEntryIsRemoved() {
+        maybeLogInconsistentRankings(
+            logger = logger,
+            oldKeysWithoutRankings = setOf(entry2.key),
+            newEntriesWithoutRankings = mapOfEntries(),
+            rankingMap = rankingMapOf(entry1)
+        )
+        verifyNoMoreInteractions(logger)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt
index ff60193..ac254ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt
@@ -413,4 +413,4 @@
             return nodeController
         }
     }, index)
-}
\ No newline at end of file
+}
diff --git a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java
index 229799a..d5991d3 100644
--- a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java
+++ b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java
@@ -73,6 +73,9 @@
     private final Set<OnChangeListener> mListeners = new LinkedHashSet<>();
 
     void addAssociation(@NonNull AssociationInfo association) {
+        // Validity check first.
+        checkNotRevoked(association);
+
         final int id = association.getId();
 
         if (DEBUG) {
@@ -99,6 +102,9 @@
     }
 
     void updateAssociation(@NonNull AssociationInfo updated) {
+        // Validity check first.
+        checkNotRevoked(updated);
+
         final int id = updated.getId();
 
         if (DEBUG) {
@@ -292,6 +298,9 @@
     }
 
     void setAssociations(Collection<AssociationInfo> allAssociations) {
+        // Validity check first.
+        allAssociations.forEach(AssociationStoreImpl::checkNotRevoked);
+
         if (DEBUG) {
             Log.i(TAG, "setAssociations() n=" + allAssociations.size());
             final StringJoiner stringJoiner = new StringJoiner(", ");
@@ -324,4 +333,11 @@
         mAddressMap.clear();
         mCachedPerUser.clear();
     }
+
+    private static void checkNotRevoked(@NonNull AssociationInfo association) {
+        if (association.isRevoked()) {
+            throw new IllegalArgumentException(
+                    "Revoked (removed) associations MUST NOT appear in the AssociationStore");
+        }
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 3f7cba6..2aabac4 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -18,6 +18,7 @@
 package com.android.server.companion;
 
 import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
 import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Process.SYSTEM_UID;
@@ -48,6 +49,8 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningAppProcessInfo;
 import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
 import android.app.NotificationManager;
@@ -91,6 +94,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.content.PackageMonitor;
+import com.android.internal.infra.PerUser;
 import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ArrayUtils;
@@ -104,6 +108,7 @@
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -127,6 +132,9 @@
 
     private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90);
 
+    private final ActivityManager mActivityManager;
+    private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener;
+
     private PersistentDataStore mPersistentStore;
     private final PersistUserStateHandler mUserPersistenceHandler;
 
@@ -150,12 +158,40 @@
     @GuardedBy("mPreviouslyUsedIds")
     private final SparseArray<Map<String, Set<Integer>>> mPreviouslyUsedIds = new SparseArray<>();
 
+    /**
+     * A structure that consists of a set of revoked associations that pending for role holder
+     * removal per each user.
+     *
+     * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo)
+     * @see #addToPendingRoleHolderRemoval(AssociationInfo)
+     * @see #removeFromPendingRoleHolderRemoval(AssociationInfo)
+     * @see #getPendingRoleHolderRemovalAssociationsForUser(int)
+     */
+    @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval")
+    private final PerUserAssociationSet mRevokedAssociationsPendingRoleHolderRemoval =
+            new PerUserAssociationSet();
+    /**
+     * Contains uid-s of packages pending to be removed from the role holder list (after
+     * revocation of an association), which will happen one the package is no longer visible to the
+     * user.
+     * For quicker uid -> (userId, packageName) look-up this is not a {@code Set<Integer>} but
+     * a {@code Map<Integer, String>} which maps uid-s to packageName-s (userId-s can be derived
+     * from uid-s using {@link UserHandle#getUserId(int)}).
+     *
+     * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo)
+     * @see #addToPendingRoleHolderRemoval(AssociationInfo)
+     * @see #removeFromPendingRoleHolderRemoval(AssociationInfo)
+     */
+    @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval")
+    private final Map<Integer, String> mUidsPendingRoleHolderRemoval = new HashMap<>();
+
     private final RemoteCallbackList<IOnAssociationsChangedListener> mListeners =
             new RemoteCallbackList<>();
 
     public CompanionDeviceManagerService(Context context) {
         super(context);
 
+        mActivityManager = context.getSystemService(ActivityManager.class);
         mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class);
         mAppOpsManager = IAppOpsService.Stub.asInterface(
                 ServiceManager.getService(Context.APP_OPS_SERVICE));
@@ -165,6 +201,9 @@
 
         mUserPersistenceHandler = new PersistUserStateHandler();
         mAssociationStore = new AssociationStoreImpl();
+
+        mOnPackageVisibilityChangeListener =
+                new OnPackageVisibilityChangeListener(mActivityManager);
     }
 
     @Override
@@ -201,7 +240,33 @@
                     mUserManager.getAliveUsers(), allAssociations, mPreviouslyUsedIds);
         }
 
-        mAssociationStore.setAssociations(allAssociations);
+        final Set<AssociationInfo> activeAssociations =
+                new ArraySet<>(/* capacity */ allAssociations.size());
+        // A set contains the userIds that need to persist state after remove the app
+        // from the list of role holders.
+        final Set<Integer> usersToPersistStateFor = new ArraySet<>();
+
+        for (AssociationInfo association : allAssociations) {
+            if (!association.isRevoked()) {
+                activeAssociations.add(association);
+            } else if (maybeRemoveRoleHolderForAssociation(association)) {
+                // Nothing more to do here, but we'll need to persist all the associations to the
+                // disk afterwards.
+                usersToPersistStateFor.add(association.getUserId());
+            } else {
+                addToPendingRoleHolderRemoval(association);
+            }
+        }
+
+        mAssociationStore.setAssociations(activeAssociations);
+
+        // IMPORTANT: only do this AFTER mAssociationStore.setAssociations(), because
+        // persistStateForUser() queries AssociationStore.
+        // (If persistStateForUser() is invoked before mAssociationStore.setAssociations() it
+        // would effectively just clear-out all the persisted associations).
+        for (int userId : usersToPersistStateFor) {
+            persistStateForUser(userId);
+        }
     }
 
     @Override
@@ -351,10 +416,18 @@
     }
 
     private void persistStateForUser(@UserIdInt int userId) {
-        final List<AssociationInfo> updatedAssociations =
-                mAssociationStore.getAssociationsForUser(userId);
+        // We want to store both active associations and the revoked (removed) association that we
+        // are keeping around for the final clean-up (delayed role holder removal).
+        final List<AssociationInfo> allAssociations;
+        // Start with the active associations - these we can get from the AssociationStore.
+        allAssociations = new ArrayList<>(
+                mAssociationStore.getAssociationsForUser(userId));
+        // ... and add the revoked (removed) association, that are yet to be permanently removed.
+        allAssociations.addAll(getPendingRoleHolderRemovalAssociationsForUser(userId));
+
         final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId);
-        mPersistentStore.persistStateForUser(userId, updatedAssociations, usedIdsForUser);
+
+        mPersistentStore.persistStateForUser(userId, allAssociations, usedIdsForUser);
     }
 
     private void notifyListeners(
@@ -422,13 +495,17 @@
             removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT;
         }
 
-        for (AssociationInfo ai : mAssociationStore.getAssociations()) {
-            if (!ai.isSelfManaged()) continue;
-            final boolean isInactive =  currentTime - ai.getLastTimeConnectedMs() >= removalWindow;
-            if (isInactive) {
-                Slog.i(TAG, "Removing inactive self-managed association: " + ai.getId());
-                disassociateInternal(ai.getId());
-            }
+        for (AssociationInfo association : mAssociationStore.getAssociations()) {
+            if (!association.isSelfManaged()) continue;
+
+            final boolean isInactive =
+                    currentTime - association.getLastTimeConnectedMs() >= removalWindow;
+            if (!isInactive) continue;
+
+            final int id = association.getId();
+
+            Slog.i(TAG, "Removing inactive self-managed association id=" + id);
+            disassociateInternal(id);
         }
     }
 
@@ -668,7 +745,7 @@
             enforceCallerIsSystemOr(userId, packageName);
 
             AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress(
-                            userId, packageName, deviceAddress);
+                    userId, packageName, deviceAddress);
 
             if (association == null) {
                 throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
@@ -728,7 +805,7 @@
 
             enforceUsesCompanionDeviceFeature(getContext(), userId, callingPackage);
             checkState(!ArrayUtils.isEmpty(
-                    mAssociationStore.getAssociationsForPackage(userId, callingPackage)),
+                            mAssociationStore.getAssociationsForPackage(userId, callingPackage)),
                     "App must have an association before calling this API");
         }
 
@@ -788,8 +865,8 @@
         final long timestamp = System.currentTimeMillis();
 
         final AssociationInfo association = new AssociationInfo(id, userId, packageName,
-                macAddress, displayName, deviceProfile, selfManaged, false, timestamp,
-                Long.MAX_VALUE);
+                macAddress, displayName, deviceProfile, selfManaged,
+                /* notifyOnDeviceNearby */ false, /* revoked */ false, timestamp, Long.MAX_VALUE);
         Slog.i(TAG, "New CDM association created=" + association);
         mAssociationStore.addAssociation(association);
 
@@ -801,6 +878,11 @@
 
         updateSpecialAccessPermissionForAssociatedPackage(association);
         logCreateAssociation(deviceProfile);
+
+        // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since
+        // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case
+        // that there are other devices with the same profile, so the role holder won't be removed.
+
         return association;
     }
 
@@ -881,36 +963,184 @@
         final String packageName = association.getPackageName();
         final String deviceProfile = association.getDeviceProfile();
 
+        if (!maybeRemoveRoleHolderForAssociation(association)) {
+            // Need to remove the app from list of the role holders, but will have to do it later
+            // (the app is in foreground at the moment).
+            addToPendingRoleHolderRemoval(association);
+        }
+
+        // Need to check if device still present now because CompanionDevicePresenceMonitor will
+        // remove current connected device after mAssociationStore.removeAssociation
         final boolean wasPresent = mDevicePresenceMonitor.isDevicePresent(associationId);
 
         // Removing the association.
         mAssociationStore.removeAssociation(associationId);
+        // Do not need to persistUserState since CompanionDeviceManagerService will get callback
+        // from #onAssociationChanged, and it will handle the persistUserState which including
+        // active and revoked association.
         logRemoveAssociation(deviceProfile);
 
-        final List<AssociationInfo> otherAssociations =
-                mAssociationStore.getAssociationsForPackage(userId, packageName);
-
-        // Check if the package is associated with other devices with the same profile.
-        // If not: take away the role.
-        if (deviceProfile != null) {
-            final boolean shouldKeepTheRole = any(otherAssociations,
-                    it -> deviceProfile.equals(it.getDeviceProfile()));
-            if (!shouldKeepTheRole) {
-                Binder.withCleanCallingIdentity(() ->
-                        removeRoleHolderForAssociation(getContext(), association));
-            }
-        }
-
         if (!wasPresent || !association.isNotifyOnDeviceNearby()) return;
         // The device was connected and the app was notified: check if we need to unbind the app
         // now.
-        final boolean shouldStayBound = any(otherAssociations,
+        final boolean shouldStayBound = any(
+                mAssociationStore.getAssociationsForPackage(userId, packageName),
                 it -> it.isNotifyOnDeviceNearby()
                         && mDevicePresenceMonitor.isDevicePresent(it.getId()));
         if (shouldStayBound) return;
         mCompanionAppController.unbindCompanionApplication(userId, packageName);
     }
 
+    /**
+     * First, checks if the companion application should be removed from the list role holders when
+     * upon association's removal, i.e.: association's profile (matches the role) is not null,
+     * the application does not have other associations with the same profile, etc.
+     *
+     * <p>
+     * Then, if establishes that the application indeed has to be removed from the list of the role
+     * holders, checks if it could be done right now -
+     * {@link android.app.role.RoleManager#removeRoleHolderAsUser(String, String, int, UserHandle, java.util.concurrent.Executor, java.util.function.Consumer) RoleManager#removeRoleHolderAsUser()}
+     * will kill the application's process, which leads poor user experience if the application was
+     * in foreground when this happened, to avoid this CDMS delays invoking
+     * {@code RoleManager.removeRoleHolderAsUser()} until the app is no longer in foreground.
+     *
+     * @return {@code true} if the application does NOT need be removed from the list of the role
+     *         holders OR if the application was successfully removed from the list of role holders.
+     *         I.e.: from the role-management perspective the association is done with.
+     *         {@code false} if the application needs to be removed from the list of role the role
+     *         holders, BUT it CDMS would prefer to do it later.
+     *         I.e.: application is in the foreground at the moment, but invoking
+     *         {@code RoleManager.removeRoleHolderAsUser()} will kill the application's process,
+     *         which would lead to the poor UX, hence need to try later.
+     */
+
+    private boolean maybeRemoveRoleHolderForAssociation(@NonNull AssociationInfo association) {
+        if (DEBUG) Log.d(TAG, "maybeRemoveRoleHolderForAssociation() association=" + association);
+
+        final String deviceProfile = association.getDeviceProfile();
+        if (deviceProfile == null) {
+            // No role was granted to for this association, there is nothing else we need to here.
+            return true;
+        }
+
+        // Check if the applications is associated with another devices with the profile. If so,
+        // it should remain the role holder.
+        final int id = association.getId();
+        final int userId = association.getUserId();
+        final String packageName = association.getPackageName();
+        final boolean roleStillInUse = any(
+                mAssociationStore.getAssociationsForPackage(userId, packageName),
+                it -> deviceProfile.equals(it.getDeviceProfile()) && id != it.getId());
+        if (roleStillInUse) {
+            // Application should remain a role holder, there is nothing else we need to here.
+            return true;
+        }
+
+        final int packageProcessImportance = getPackageProcessImportance(userId, packageName);
+        if (packageProcessImportance <= IMPORTANCE_VISIBLE) {
+            // Need to remove the app from the list of role holders, but the process is visible to
+            // the user at the moment, so we'll need to it later: log and return false.
+            Slog.i(TAG, "Cannot remove role holder for the removed association id=" + id
+                    + " now - process is visible.");
+            return false;
+        }
+
+        removeRoleHolderForAssociation(getContext(), association);
+        return true;
+    }
+
+    private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) {
+        return Binder.withCleanCallingIdentity(() -> {
+            final int uid =
+                    mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
+            return mActivityManager.getUidImportance(uid);
+        });
+    }
+
+    /**
+     * Set revoked flag for active association and add the revoked association and the uid into
+     * the caches.
+     *
+     * @see #mRevokedAssociationsPendingRoleHolderRemoval
+     * @see #mUidsPendingRoleHolderRemoval
+     * @see OnPackageVisibilityChangeListener
+     */
+    private void addToPendingRoleHolderRemoval(@NonNull AssociationInfo association) {
+        // First: set revoked flag.
+        association = AssociationInfo.builder(association)
+                .setRevoked(true)
+                .build();
+
+        final String packageName = association.getPackageName();
+        final int userId = association.getUserId();
+        final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
+
+        // Second: add to the set.
+        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
+            mRevokedAssociationsPendingRoleHolderRemoval.forUser(association.getUserId())
+                    .add(association);
+            if (!mUidsPendingRoleHolderRemoval.containsKey(uid)) {
+                mUidsPendingRoleHolderRemoval.put(uid, packageName);
+
+                if (mUidsPendingRoleHolderRemoval.size() == 1) {
+                    // Just added first uid: start the listener
+                    mOnPackageVisibilityChangeListener.startListening();
+                }
+            }
+        }
+    }
+
+    /**
+     * Remove the revoked association form the cache and also remove the uid form the map if
+     * there are other associations with the same package still pending for role holder removal.
+     *
+     * @see #mRevokedAssociationsPendingRoleHolderRemoval
+     * @see #mUidsPendingRoleHolderRemoval
+     * @see OnPackageVisibilityChangeListener
+     */
+    private void removeFromPendingRoleHolderRemoval(@NonNull AssociationInfo association) {
+        final String packageName = association.getPackageName();
+        final int userId = association.getUserId();
+        final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
+
+        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
+            mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId)
+                    .remove(association);
+
+            final boolean shouldKeepUidForRemoval = any(
+                    getPendingRoleHolderRemovalAssociationsForUser(userId),
+                    ai -> packageName.equals(ai.getPackageName()));
+            // Do not remove the uid form the map since other associations with
+            // the same packageName still pending for role holder removal.
+            if (!shouldKeepUidForRemoval) {
+                mUidsPendingRoleHolderRemoval.remove(uid);
+            }
+
+            if (mUidsPendingRoleHolderRemoval.isEmpty()) {
+                // The set is empty now - can "turn off" the listener.
+                mOnPackageVisibilityChangeListener.stopListening();
+            }
+        }
+    }
+
+    /**
+     * @return a copy of the revoked associations set (safeguarding against
+     *         {@code ConcurrentModificationException}-s).
+     */
+    private @NonNull Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser(
+            @UserIdInt int userId) {
+        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
+            // Return a copy.
+            return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId));
+        }
+    }
+
+    private String getPackageNameByUid(int uid) {
+        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
+            return mUidsPendingRoleHolderRemoval.get(uid);
+        }
+    }
+
     private void updateSpecialAccessPermissionForAssociatedPackage(AssociationInfo association) {
         final PackageInfo packageInfo =
                 getPackageInfo(getContext(), association.getUserId(), association.getPackageName());
@@ -1128,4 +1358,80 @@
             persistStateForUser(userId);
         }
     }
+
+    /**
+     * An OnUidImportanceListener class which watches the importance of the packages.
+     * In this class, we ONLY interested in the importance of the running process is greater than
+     * {@link RunningAppProcessInfo.IMPORTANCE_VISIBLE} for the uids have been added into the
+     * {@link mUidsPendingRoleHolderRemoval}. Lastly remove the role holder for the revoked
+     * associations for the same packages.
+     *
+     * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo)
+     * @see #removeFromPendingRoleHolderRemoval(AssociationInfo)
+     * @see #getPendingRoleHolderRemovalAssociationsForUser(int)
+     */
+    private class OnPackageVisibilityChangeListener implements
+            ActivityManager.OnUidImportanceListener {
+        final @NonNull ActivityManager mAm;
+
+        OnPackageVisibilityChangeListener(@NonNull ActivityManager am) {
+            this.mAm = am;
+        }
+
+        void startListening() {
+            Binder.withCleanCallingIdentity(
+                    () -> mAm.addOnUidImportanceListener(
+                            /* listener */ OnPackageVisibilityChangeListener.this,
+                            RunningAppProcessInfo.IMPORTANCE_VISIBLE));
+        }
+
+        void stopListening() {
+            Binder.withCleanCallingIdentity(
+                    () -> mAm.removeOnUidImportanceListener(
+                            /* listener */ OnPackageVisibilityChangeListener.this));
+        }
+
+        @Override
+        public void onUidImportance(int uid, int importance) {
+            if (importance <= RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
+                // The lower the importance value the more "important" the process is.
+                // We are only interested when the process ceases to be visible.
+                return;
+            }
+
+            final String packageName = getPackageNameByUid(uid);
+            if (packageName == null) {
+                // Not interested in this uid.
+                return;
+            }
+
+            final int userId = UserHandle.getUserId(uid);
+
+            boolean needToPersistStateForUser = false;
+
+            for (AssociationInfo association :
+                    getPendingRoleHolderRemovalAssociationsForUser(userId)) {
+                if (!packageName.equals(association.getPackageName())) continue;
+
+                if (!maybeRemoveRoleHolderForAssociation(association)) {
+                    // Did not remove the role holder, will have to try again later.
+                    continue;
+                }
+
+                removeFromPendingRoleHolderRemoval(association);
+                needToPersistStateForUser = true;
+            }
+
+            if (needToPersistStateForUser) {
+                mUserPersistenceHandler.postPersistUserState(userId);
+            }
+        }
+    }
+
+    private static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> {
+        @Override
+        protected @NonNull Set<AssociationInfo> create(int userId) {
+            return new ArraySet<>();
+        }
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java
index 3639389..4b56c1b 100644
--- a/services/companion/java/com/android/server/companion/PersistentDataStore.java
+++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java
@@ -103,7 +103,7 @@
  * Since Android T the data is stored to "companion_device_manager.xml" file in
  * {@link Environment#getDataSystemDeDirectory(int) /data/system_de/}.
  *
- * See {@link #getBaseStorageFileForUser(int) getBaseStorageFileForUser()}
+ * See {@link DataStoreUtils#getBaseStorageFileForUser(int, String)}
  *
  * <p>
  * Since Android T the data is stored using the v1 schema.
@@ -120,7 +120,7 @@
  * <li> {@link #readPreviouslyUsedIdsV1(TypedXmlPullParser, Map) readPreviouslyUsedIdsV1()}
  * </ul>
  *
- * The following snippet is a sample of a file that is using v0 schema.
+ * The following snippet is a sample of a file that is using v1 schema.
  * <pre>{@code
  * <state persistence-version="1">
  *     <associations>
@@ -130,6 +130,8 @@
  *             mac_address="AA:BB:CC:DD:EE:00"
  *             self_managed="false"
  *             notify_device_nearby="false"
+ *             revoked="false"
+ *             last_time_connected="1634641160229"
  *             time_approved="1634389553216"/>
  *
  *         <association
@@ -139,6 +141,8 @@
  *             display_name="Jhon's Chromebook"
  *             self_managed="true"
  *             notify_device_nearby="false"
+ *             revoked="false"
+ *             last_time_connected="1634641160229"
  *             time_approved="1634641160229"/>
  *     </associations>
  *
@@ -178,6 +182,7 @@
     private static final String XML_ATTR_PROFILE = "profile";
     private static final String XML_ATTR_SELF_MANAGED = "self_managed";
     private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby";
+    private static final String XML_ATTR_REVOKED = "revoked";
     private static final String XML_ATTR_TIME_APPROVED = "time_approved";
     private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected";
 
@@ -415,7 +420,8 @@
 
         out.add(new AssociationInfo(associationId, userId, appPackage,
                 MacAddress.fromString(deviceAddress), null, profile,
-                /* managedByCompanionApp */false, notify, timeApproved, Long.MAX_VALUE));
+                /* managedByCompanionApp */ false, notify, /* revoked */ false, timeApproved,
+                Long.MAX_VALUE));
     }
 
     private static void readAssociationsV1(@NonNull TypedXmlPullParser parser,
@@ -444,13 +450,14 @@
         final String displayName = readStringAttribute(parser, XML_ATTR_DISPLAY_NAME);
         final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED);
         final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY);
+        final boolean revoked = readBooleanAttribute(parser, XML_ATTR_REVOKED, false);
         final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L);
         final long lastTimeConnected = readLongAttribute(
                 parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE);
 
         final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId,
-                appPackage, macAddress, displayName, profile, selfManaged, notify, timeApproved,
-                lastTimeConnected);
+                appPackage, macAddress, displayName, profile, selfManaged, notify, revoked,
+                timeApproved, lastTimeConnected);
         if (associationInfo != null) {
             out.add(associationInfo);
         }
@@ -503,6 +510,8 @@
         writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged());
         writeBooleanAttribute(
                 serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby());
+        writeBooleanAttribute(
+                serializer, XML_ATTR_REVOKED, a.isRevoked());
         writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs());
         writeLongAttribute(
                 serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs());
@@ -544,11 +553,12 @@
     private static AssociationInfo createAssociationInfoNoThrow(int associationId,
             @UserIdInt int userId, @NonNull String appPackage, @Nullable MacAddress macAddress,
             @Nullable CharSequence displayName, @Nullable String profile, boolean selfManaged,
-            boolean notify, long timeApproved, long lastTimeConnected) {
+            boolean notify, boolean revoked, long timeApproved, long lastTimeConnected) {
         AssociationInfo associationInfo = null;
         try {
             associationInfo = new AssociationInfo(associationId, userId, appPackage, macAddress,
-                    displayName, profile, selfManaged, notify, timeApproved, lastTimeConnected);
+                    displayName, profile, selfManaged, notify, revoked, timeApproved,
+                    lastTimeConnected);
         } catch (Exception e) {
             if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e);
         }
diff --git a/services/companion/java/com/android/server/companion/RolesUtils.java b/services/companion/java/com/android/server/companion/RolesUtils.java
index 35488a8..0fff3f4 100644
--- a/services/companion/java/com/android/server/companion/RolesUtils.java
+++ b/services/companion/java/com/android/server/companion/RolesUtils.java
@@ -85,6 +85,8 @@
         final int userId = associationInfo.getUserId();
         final UserHandle userHandle = UserHandle.of(userId);
 
+        Slog.i(TAG, "Removing CDM role holder, role=" + deviceProfile
+                + ", package=u" + userId + "\\" + packageName);
         roleManager.removeRoleHolderAsUser(deviceProfile, packageName,
                 MANAGE_HOLDERS_FLAG_DONT_KILL_APP, userHandle, context.getMainExecutor(),
                 success -> {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index d3af7d2..cff8b93 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1212,7 +1212,8 @@
                 break;
             default:
                 if (attrs.providedInsets != null) {
-                    for (InsetsFrameProvider provider : attrs.providedInsets) {
+                    for (int i = attrs.providedInsets.length - 1; i >= 0; i--) {
+                        final InsetsFrameProvider provider = attrs.providedInsets[i];
                         switch (provider.type) {
                             case ITYPE_STATUS_BAR:
                                 mStatusBarAlt = win;
@@ -1231,21 +1232,29 @@
                                 mExtraNavBarAltPosition = getAltBarPosition(attrs);
                                 break;
                         }
+                        // The index of the provider and corresponding insets types cannot change at
+                        // runtime as ensured in WMS. Make use of the index in the provider directly
+                        // to access the latest provided size at runtime.
+                        final int index = i;
                         final TriConsumer<DisplayFrames, WindowContainer, Rect> frameProvider =
                                 provider.insetsSize != null
                                         ? (displayFrames, windowContainer, inOutFrame) -> {
                                             inOutFrame.inset(win.mGivenContentInsets);
+                                            final InsetsFrameProvider ifp =
+                                                    win.mAttrs.forRotation(displayFrames.mRotation)
+                                                            .providedInsets[index];
                                             calculateInsetsFrame(displayFrames, windowContainer,
-                                                    inOutFrame, provider.source,
-                                                    provider.insetsSize);
+                                                    inOutFrame, ifp.source, ifp.insetsSize);
                                         } : null;
                         final TriConsumer<DisplayFrames, WindowContainer, Rect> imeFrameProvider =
                                 provider.imeInsetsSize != null
                                         ? (displayFrames, windowContainer, inOutFrame) -> {
                                             inOutFrame.inset(win.mGivenContentInsets);
+                                            final InsetsFrameProvider ifp =
+                                                    win.mAttrs.forRotation(displayFrames.mRotation)
+                                                            .providedInsets[index];
                                             calculateInsetsFrame(displayFrames, windowContainer,
-                                                    inOutFrame, provider.source,
-                                                    provider.imeInsetsSize);
+                                                    inOutFrame, ifp.source, ifp.imeInsetsSize);
                                         } : null;
                         mDisplayContent.setInsetProvider(provider.type, win, frameProvider,
                                 imeFrameProvider);
@@ -1256,14 +1265,14 @@
         }
     }
 
-    private void calculateInsetsFrame(DisplayFrames df, WindowContainer coutainer, Rect inOutFrame,
+    private void calculateInsetsFrame(DisplayFrames df, WindowContainer container, Rect inOutFrame,
             int source, Insets insetsSize) {
         if (source == InsetsFrameProvider.SOURCE_DISPLAY) {
             inOutFrame.set(df.mUnrestricted);
         } else if (source == InsetsFrameProvider.SOURCE_CONTAINER_BOUNDS) {
-            inOutFrame.set(coutainer.getBounds());
+            inOutFrame.set(container.getBounds());
         }
-        if (insetsSize == null || insetsSize.equals(Insets.NONE)) {
+        if (insetsSize == null) {
             return;
         }
         // Only one side of the provider shall be applied. Check in the order of left - top -
@@ -1276,6 +1285,8 @@
             inOutFrame.left = inOutFrame.right - insetsSize.right;
         } else if (insetsSize.bottom != 0) {
             inOutFrame.top = inOutFrame.bottom - insetsSize.bottom;
+        } else {
+            inOutFrame.setEmpty();
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index ff6a454..9b013da 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -115,8 +115,6 @@
     private float mLastReportedAnimatorScale;
     private String mPackageName;
     private String mRelayoutTag;
-    private String mUpdateViewVisibilityTag;
-    private String mUpdateWindowLayoutTag;
     private final InsetsVisibilities mDummyRequestedVisibilities = new InsetsVisibilities();
     private final InsetsSourceControl[] mDummyControls =  new InsetsSourceControl[0];
     final boolean mSetsUnrestrictedKeepClearAreas;
@@ -225,27 +223,6 @@
     }
 
     @Override
-    public int updateVisibility(IWindow client, WindowManager.LayoutParams attrs,
-            int viewVisibility, MergedConfiguration outMergedConfiguration,
-            SurfaceControl outSurfaceControl, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mUpdateViewVisibilityTag);
-        int res = mService.updateViewVisibility(this, client, attrs, viewVisibility,
-                outMergedConfiguration, outSurfaceControl, outInsetsState, outActiveControls);
-        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-        return res;
-    }
-
-    @Override
-    public void updateLayout(IWindow window, WindowManager.LayoutParams attrs, int flags,
-            ClientWindowFrames clientFrames, int requestedWidth, int requestedHeight) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mUpdateWindowLayoutTag);
-        mService.updateWindowLayout(this, window, attrs, flags, clientFrames, requestedWidth,
-                requestedHeight);
-        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-    }
-
-    @Override
     public void prepareToReplaceWindows(IBinder appToken, boolean childrenOnly) {
         mService.setWillReplaceWindows(appToken, childrenOnly);
     }
@@ -717,8 +694,6 @@
             if (wpc != null) {
                 mPackageName = wpc.mInfo.packageName;
                 mRelayoutTag = "relayoutWindow: " + mPackageName;
-                mUpdateViewVisibilityTag = "updateVisibility: " + mPackageName;
-                mUpdateWindowLayoutTag = "updateLayout: " + mPackageName;
             } else {
                 Slog.e(TAG_WM, "Unknown process pid=" + mPid);
             }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index ae61655..125d550 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -122,6 +122,7 @@
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
+import static com.android.server.wm.WindowContainer.SYNC_STATE_NONE;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_INPUT_METHOD;
@@ -2557,7 +2558,11 @@
 
                 win.mLastSeqIdSentToRelayout = win.mSyncSeqId;
                 outSyncIdBundle.putInt("seqid", win.mSyncSeqId);
-                win.mAlreadyRequestedSync = true;
+                // Only mark mAlreadyRequestedSync if there's an explicit sync request, and not if
+                // we're syncing due to mDrawHandlers
+                if (win.mSyncState != SYNC_STATE_NONE) {
+                    win.mAlreadyRequestedSync = true;
+                }
             } else {
                 outSyncIdBundle.putInt("seqid", -1);
             }
@@ -2670,29 +2675,6 @@
         return result;
     }
 
-    int updateViewVisibility(Session session, IWindow client, LayoutParams attrs,
-            int viewVisibility, MergedConfiguration outMergedConfiguration,
-            SurfaceControl outSurfaceControl, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
-        // TODO(b/161810301): Finish the implementation.
-        return 0;
-    }
-
-    void updateWindowLayout(Session session, IWindow client, LayoutParams attrs, int flags,
-            ClientWindowFrames clientWindowFrames, int requestedWidth, int requestedHeight) {
-        final long origId = Binder.clearCallingIdentity();
-        synchronized (mGlobalLock) {
-            final WindowState win = windowForClientLocked(session, client, false);
-            if (win == null) {
-                return;
-            }
-            win.setFrames(clientWindowFrames, requestedWidth, requestedHeight);
-
-            // TODO(b/161810301): Finish the implementation.
-        }
-        Binder.restoreCallingIdentity(origId);
-    }
-
     public boolean outOfMemoryWindow(Session session, IWindow client) {
         final long origId = Binder.clearCallingIdentity();
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index f242fda..c80547c 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -213,7 +213,7 @@
                 mContext.getSystemService(WindowManager.class), threadVerifier);
 
         mAssociationInfo = new AssociationInfo(1, 0, null,
-                MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0);
+                MacAddress.BROADCAST_ADDRESS, "", null, true, false, false, 0, 0);
 
         VirtualDeviceParams params = new VirtualDeviceParams
                 .Builder()