Merge "Add device logger for active device and action state change" into udc-qpr-dev
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 9a5247a..ced3554 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -2892,11 +2892,6 @@
                 }
             }
 
-            final Person person = extras.getParcelable(EXTRA_MESSAGING_PERSON, Person.class);
-            if (person != null) {
-                person.visitUris(visitor);
-            }
-
             final RemoteInputHistoryItem[] history = extras.getParcelableArray(
                     Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS,
                     RemoteInputHistoryItem.class);
@@ -2908,9 +2903,14 @@
                     }
                 }
             }
-        }
 
-        if (isStyle(MessagingStyle.class) && extras != null) {
+            // Extras for MessagingStyle. We visit them even if not isStyle(MessagingStyle), since
+            // Notification Listeners might use directly (without the isStyle check).
+            final Person person = extras.getParcelable(EXTRA_MESSAGING_PERSON, Person.class);
+            if (person != null) {
+                person.visitUris(visitor);
+            }
+
             final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES,
                     Parcelable.class);
             if (!ArrayUtils.isEmpty(messages)) {
@@ -2930,9 +2930,8 @@
             }
 
             visitIconUri(visitor, extras.getParcelable(EXTRA_CONVERSATION_ICON, Icon.class));
-        }
 
-        if (isStyle(CallStyle.class) & extras != null) {
+            // Extras for CallStyle (same reason for visiting without checking isStyle).
             Person callPerson = extras.getParcelable(EXTRA_CALL_PERSON, Person.class);
             if (callPerson != null) {
                 callPerson.visitUris(visitor);
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index b159321..85d669e 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -24,6 +24,7 @@
 import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
+import android.annotation.UiThread;
 import android.annotation.UserIdInt;
 import android.app.IServiceConnection;
 import android.app.PendingIntent;
@@ -39,6 +40,10 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.DisplayMetrics;
@@ -53,6 +58,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
 
 /**
  * Updates AppWidget state; gets information about installed AppWidget providers and other
@@ -475,6 +481,8 @@
 
     private static final String TAG = "AppWidgetManager";
 
+    private static Executor sUpdateExecutor;
+
     /**
      * An intent extra that contains multiple appWidgetIds.  These are id values as
      * they were provided to the application during a recent restore from backup.  It is
@@ -510,6 +518,8 @@
     private final IAppWidgetService mService;
     private final DisplayMetrics mDisplayMetrics;
 
+    private boolean mHasPostedLegacyLists = false;
+
     /**
      * Get the AppWidgetManager instance to use for the supplied {@link android.content.Context
      * Context} object.
@@ -552,6 +562,13 @@
         });
     }
 
+    private boolean isPostingTaskToBackground(@Nullable RemoteViews views) {
+        return Looper.myLooper() == Looper.getMainLooper()
+                && RemoteViews.isAdapterConversionEnabled()
+                && (mHasPostedLegacyLists = mHasPostedLegacyLists
+                        || (views != null && views.hasLegacyLists()));
+    }
+
     /**
      * Set the RemoteViews to use for the specified appWidgetIds.
      * <p>
@@ -575,6 +592,19 @@
         if (mService == null) {
             return;
         }
+
+        if (isPostingTaskToBackground(views)) {
+            createUpdateExecutorIfNull().execute(() -> {
+                try {
+                    mService.updateAppWidgetIds(mPackageName, appWidgetIds, views);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error updating app widget views in background", e);
+                }
+            });
+
+            return;
+        }
+
         try {
             mService.updateAppWidgetIds(mPackageName, appWidgetIds, views);
         } catch (RemoteException e) {
@@ -683,6 +713,19 @@
         if (mService == null) {
             return;
         }
+
+        if (isPostingTaskToBackground(views)) {
+            createUpdateExecutorIfNull().execute(() -> {
+                try {
+                    mService.partiallyUpdateAppWidgetIds(mPackageName, appWidgetIds, views);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error partially updating app widget views in background", e);
+                }
+            });
+
+            return;
+        }
+
         try {
             mService.partiallyUpdateAppWidgetIds(mPackageName, appWidgetIds, views);
         } catch (RemoteException e) {
@@ -738,6 +781,19 @@
         if (mService == null) {
             return;
         }
+
+        if (isPostingTaskToBackground(views)) {
+            createUpdateExecutorIfNull().execute(() -> {
+                try {
+                    mService.updateAppWidgetProvider(provider, views);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error updating app widget view using provider in background", e);
+                }
+            });
+
+            return;
+        }
+
         try {
             mService.updateAppWidgetProvider(provider, views);
         } catch (RemoteException e) {
@@ -786,28 +842,45 @@
         if (mService == null) {
             return;
         }
-        try {
-            if (RemoteViews.isAdapterConversionEnabled()) {
-                List<CompletableFuture<Void>> updateFutures = new ArrayList<>();
-                for (int i = 0; i < appWidgetIds.length; i++) {
-                    final int widgetId = appWidgetIds[i];
-                    updateFutures.add(CompletableFuture.runAsync(() -> {
-                        try {
-                            RemoteViews views = mService.getAppWidgetViews(mPackageName, widgetId);
-                            if (views.replaceRemoteCollections(viewId)) {
-                                updateAppWidget(widgetId, views);
-                            }
-                        } catch (Exception e) {
-                            Log.e(TAG, "Error notifying changes in RemoteViews", e);
-                        }
-                    }));
-                }
-                CompletableFuture.allOf(updateFutures.toArray(CompletableFuture[]::new)).join();
-            } else {
+
+        if (!RemoteViews.isAdapterConversionEnabled()) {
+            try {
                 mService.notifyAppWidgetViewDataChanged(mPackageName, appWidgetIds, viewId);
+            } catch (RemoteException re) {
+                throw re.rethrowFromSystemServer();
             }
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+
+            return;
+        }
+
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            mHasPostedLegacyLists = true;
+            createUpdateExecutorIfNull().execute(() -> notifyCollectionWidgetChange(appWidgetIds,
+                    viewId));
+        } else {
+            notifyCollectionWidgetChange(appWidgetIds, viewId);
+        }
+    }
+
+    private void notifyCollectionWidgetChange(int[] appWidgetIds, int viewId) {
+        try {
+            List<CompletableFuture<Void>> updateFutures = new ArrayList<>();
+            for (int i = 0; i < appWidgetIds.length; i++) {
+                final int widgetId = appWidgetIds[i];
+                updateFutures.add(CompletableFuture.runAsync(() -> {
+                    try {
+                        RemoteViews views = mService.getAppWidgetViews(mPackageName, widgetId);
+                        if (views.replaceRemoteCollections(viewId)) {
+                            updateAppWidget(widgetId, views);
+                        }
+                    } catch (Exception e) {
+                        Log.e(TAG, "Error notifying changes in RemoteViews", e);
+                    }
+                }));
+            }
+            CompletableFuture.allOf(updateFutures.toArray(CompletableFuture[]::new)).join();
+        } catch (Exception e) {
+            Log.e(TAG, "Error notifying changes for all widgets", e);
         }
     }
 
@@ -1338,4 +1411,20 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    @UiThread
+    private static @NonNull Executor createUpdateExecutorIfNull() {
+        if (sUpdateExecutor == null) {
+            sUpdateExecutor = new HandlerExecutor(createAndStartNewHandler(
+                    "widget_manager_update_helper_thread", Process.THREAD_PRIORITY_FOREGROUND));
+        }
+
+        return sUpdateExecutor;
+    }
+
+    private static @NonNull Handler createAndStartNewHandler(@NonNull String name, int priority) {
+        HandlerThread thread = new HandlerThread(name, priority);
+        thread.start();
+        return thread.getThreadHandler();
+    }
 }
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index af09a06..5b24dca 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -1294,32 +1294,30 @@
      * Round the given size of a storage device to a nice round power-of-two
      * value, such as 256MB or 32GB. This avoids showing weird values like
      * "29.5GB" in UI.
-     *
-     * Some storage devices are still using GiB (powers of 1024) over
-     * GB (powers of 1000) measurements and this method takes it into account.
-     *
      * Round ranges:
      * ...
-     * [256 GiB + 1; 512 GiB] -> 512 GB
-     * [512 GiB + 1; 1 TiB]   -> 1 TB
-     * [1 TiB + 1; 2 TiB]     -> 2 TB
+     * (128 GB; 256 GB]   -> 256 GB
+     * (256 GB; 512 GB]   -> 512 GB
+     * (512 GB; 1000 GB]  -> 1000 GB
+     * (1000 GB; 2000 GB] -> 2000 GB
+     * ...
      * etc
      *
      * @hide
      */
     public static long roundStorageSize(long size) {
         long val = 1;
-        long kiloPow = 1;
-        long kibiPow = 1;
-        while ((val * kibiPow) < size) {
+        long pow = 1;
+        while ((val * pow) < size) {
             val <<= 1;
             if (val > 512) {
                 val = 1;
-                kibiPow *= 1024;
-                kiloPow *= 1000;
+                pow *= 1000;
             }
         }
-        return val * kiloPow;
+
+        Log.d(TAG, String.format("Rounded bytes from %d to %d", size, val * pow));
+        return val * pow;
     }
 
     private static long toBytes(long value, String unit) {
diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl
index bc52744..369a193 100644
--- a/core/java/android/os/storage/IStorageManager.aidl
+++ b/core/java/android/os/storage/IStorageManager.aidl
@@ -174,4 +174,5 @@
     boolean isAppIoBlocked(in String volumeUuid, int uid, int tid, int reason) = 95;
     void setCloudMediaProvider(in String authority) = 96;
     String getCloudMediaProvider() = 97;
+    long getInternalStorageBlockDeviceSize() = 98;
 }
\ No newline at end of file
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 80dd488..ee387e7 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1359,6 +1359,15 @@
     }
 
     /** {@hide} */
+    public long getInternalStorageBlockDeviceSize() {
+        try {
+            return mStorageManager.getInternalStorageBlockDeviceSize();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
     public void mkdirs(File file) {
         BlockGuard.getVmPolicy().onPathAccess(file.getAbsolutePath());
         try {
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 9b34007..72861db 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -351,13 +351,13 @@
     }
 
     private static boolean shouldTriggerStylusHandwritingForView(@NonNull View view) {
-        if (!view.isAutoHandwritingEnabled()) {
+        if (!view.shouldInitiateHandwriting()) {
             return false;
         }
-        // The view may be a handwriting initiation delegate, in which case it is not the editor
+        // The view may be a handwriting initiation delegator, in which case it is not the editor
         // view for which handwriting would be started. However, in almost all cases, the return
-        // values of View#isStylusHandwritingAvailable will be the same for the delegate view and
-        // the delegator editor view. So the delegate view can be used to decide whether handwriting
+        // values of View#isStylusHandwritingAvailable will be the same for the delegator view and
+        // the delegate editor view. So the delegator view can be used to decide whether handwriting
         // should be triggered.
         return view.isStylusHandwritingAvailable();
     }
@@ -682,7 +682,7 @@
     /** The helper method to check if the given view is still active for handwriting. */
     private static boolean isViewActive(@Nullable View view) {
         return view != null && view.isAttachedToWindow() && view.isAggregatedVisible()
-                && view.isAutoHandwritingEnabled();
+                && view.shouldInitiateHandwriting();
     }
 
     /**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 2499be9..363e554 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -5488,7 +5488,6 @@
                 (TEXT_ALIGNMENT_DEFAULT << PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT) |
                 (PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT) |
                 (IMPORTANT_FOR_ACCESSIBILITY_DEFAULT << PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT);
-        mPrivateFlags4 = PFLAG4_AUTO_HANDWRITING_ENABLED;
 
         final ViewConfiguration configuration = ViewConfiguration.get(context);
         mTouchSlop = configuration.getScaledTouchSlop();
@@ -6213,7 +6212,7 @@
                     setPreferKeepClear(a.getBoolean(attr, false));
                     break;
                 case R.styleable.View_autoHandwritingEnabled:
-                    setAutoHandwritingEnabled(a.getBoolean(attr, true));
+                    setAutoHandwritingEnabled(a.getBoolean(attr, false));
                     break;
                 case R.styleable.View_handwritingBoundsOffsetLeft:
                     mHandwritingBoundsOffsetLeft = a.getDimension(attr, 0);
@@ -12078,7 +12077,7 @@
         if (getSystemGestureExclusionRects().isEmpty()
                 && collectPreferKeepClearRects().isEmpty()
                 && collectUnrestrictedPreferKeepClearRects().isEmpty()
-                && (info.mHandwritingArea == null || !isAutoHandwritingEnabled())) {
+                && (info.mHandwritingArea == null || !shouldInitiateHandwriting())) {
             if (info.mPositionUpdateListener != null) {
                 mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener);
                 info.mPositionUpdateListener = null;
@@ -12445,7 +12444,7 @@
 
     void updateHandwritingArea() {
         // If autoHandwritingArea is not enabled, do nothing.
-        if (!isAutoHandwritingEnabled()) return;
+        if (!shouldInitiateHandwriting()) return;
         final AttachInfo ai = mAttachInfo;
         if (ai != null) {
             ai.mViewRootImpl.getHandwritingInitiator().updateHandwritingAreasForView(this);
@@ -12453,6 +12452,16 @@
     }
 
     /**
+     * Returns true if a stylus {@link MotionEvent} within this view's bounds should initiate
+     * handwriting mode, either for this view ({@link #isAutoHandwritingEnabled()} is {@code true})
+     * or for a handwriting delegate view ({@link #getHandwritingDelegatorCallback()} is not {@code
+     * null}).
+     */
+    boolean shouldInitiateHandwriting() {
+        return isAutoHandwritingEnabled() || getHandwritingDelegatorCallback() != null;
+    }
+
+    /**
      * Sets a callback which should be called when a stylus {@link MotionEvent} occurs within this
      * view's bounds. The callback will be called from the UI thread.
      *
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 7903dd64..a740b65 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -801,6 +801,11 @@
                     mActions.set(i, new SetRemoteCollectionItemListAdapterAction(itemsAction.viewId,
                             itemsAction.mServiceIntent));
                     isActionReplaced = true;
+                } else if (action instanceof SetRemoteViewsAdapterIntent intentAction
+                        && intentAction.viewId == viewId) {
+                    mActions.set(i, new SetRemoteCollectionItemListAdapterAction(
+                            intentAction.viewId, intentAction.intent));
+                    isActionReplaced = true;
                 } else if (action instanceof ViewGroupActionAdd groupAction
                         && groupAction.mNestedViews != null) {
                     isActionReplaced |= groupAction.mNestedViews.replaceRemoteCollections(viewId);
@@ -822,6 +827,42 @@
         return isActionReplaced;
     }
 
+    /**
+     * @return True if has set remote adapter using service intent
+     * @hide
+     */
+    public boolean hasLegacyLists() {
+        if (mActions != null) {
+            for (int i = 0; i < mActions.size(); i++) {
+                Action action = mActions.get(i);
+                if ((action instanceof SetRemoteCollectionItemListAdapterAction itemsAction
+                        && itemsAction.mServiceIntent != null)
+                        || (action instanceof SetRemoteViewsAdapterIntent intentAction
+                                && intentAction.intent != null)
+                        || (action instanceof ViewGroupActionAdd groupAction
+                                && groupAction.mNestedViews != null
+                                && groupAction.mNestedViews.hasLegacyLists())) {
+                    return true;
+                }
+            }
+        }
+        if (mSizedRemoteViews != null) {
+            for (int i = 0; i < mSizedRemoteViews.size(); i++) {
+                if (mSizedRemoteViews.get(i).hasLegacyLists()) {
+                    return true;
+                }
+            }
+        }
+        if (mLandscape != null && mLandscape.hasLegacyLists()) {
+            return true;
+        }
+        if (mPortrait != null && mPortrait.hasLegacyLists()) {
+            return true;
+        }
+
+        return false;
+    }
+
     private static void visitIconUri(Icon icon, @NonNull Consumer<Uri> visitor) {
         if (icon != null && (icon.getType() == Icon.TYPE_URI
                 || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP)) {
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index b74c879..2ad3f74 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -1856,6 +1856,7 @@
         boolean clickable = canInputOrMove || isClickable();
         boolean longClickable = canInputOrMove || isLongClickable();
         int focusable = getFocusable();
+        boolean isAutoHandwritingEnabled = true;
 
         n = a.getIndexCount();
         for (int i = 0; i < n; i++) {
@@ -1878,6 +1879,10 @@
                 case com.android.internal.R.styleable.View_longClickable:
                     longClickable = a.getBoolean(attr, longClickable);
                     break;
+
+                case com.android.internal.R.styleable.View_autoHandwritingEnabled:
+                    isAutoHandwritingEnabled = a.getBoolean(attr, true);
+                    break;
             }
         }
         a.recycle();
@@ -1891,6 +1896,7 @@
         }
         setClickable(clickable);
         setLongClickable(longClickable);
+        setAutoHandwritingEnabled(isAutoHandwritingEnabled);
 
         if (mEditor != null) mEditor.prepareCursorControllers();
 
@@ -14994,7 +15000,9 @@
     }
 
     boolean canShare() {
-        if (!getContext().canStartActivityForResult() || !isDeviceProvisioned()) {
+        if (!getContext().canStartActivityForResult() || !isDeviceProvisioned()
+                || !getContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_textShareSupported)) {
             return false;
         }
         return canCopy();
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 75f1251..31d6f16 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5259,6 +5259,7 @@
         <item>1,1,1.0,0,1</item>
         <item>1,1,1.0,.4,1</item>
         <item>1,1,1.0,.15,15</item>
+        <item>0,0,0.7,0,1</item>
     </string-array>
 
     <!-- The integer index of the selected option in config_udfps_touch_detection_options -->
@@ -6553,4 +6554,8 @@
          environment to protect the user's privacy when the device is being repaired.
          Off by default, since OEMs may have had a similar feature on their devices. -->
     <bool name="config_repairModeSupported">false</bool>
+
+    <!-- Enables or disables the "Share" action item shown in the context menu that appears upon
+        long-pressing on selected text. Enabled by default. -->
+    <bool name="config_textShareSupported">true</bool>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 72b76c2..655892d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3046,6 +3046,7 @@
   <java-symbol type="id" name="addToDictionaryButton" />
   <java-symbol type="id" name="deleteButton" />
   <!-- TextView -->
+  <java-symbol type="bool" name="config_textShareSupported" />
   <java-symbol type="string" name="failed_to_copy_to_clipboard" />
 
   <java-symbol type="id" name="notification_material_reply_container" />
diff --git a/core/tests/coretests/src/android/os/FileUtilsTest.java b/core/tests/coretests/src/android/os/FileUtilsTest.java
index 394ff0a..a0d8183 100644
--- a/core/tests/coretests/src/android/os/FileUtilsTest.java
+++ b/core/tests/coretests/src/android/os/FileUtilsTest.java
@@ -505,45 +505,32 @@
 
     @Test
     public void testRoundStorageSize() throws Exception {
-        final long GB1 = DataUnit.GIGABYTES.toBytes(1);
-        final long GiB1 = DataUnit.GIBIBYTES.toBytes(1);
-        final long GB2 = DataUnit.GIGABYTES.toBytes(2);
-        final long GiB2 = DataUnit.GIBIBYTES.toBytes(2);
-        final long GiB128 = DataUnit.GIBIBYTES.toBytes(128);
-        final long GB256 = DataUnit.GIGABYTES.toBytes(256);
-        final long GiB256 = DataUnit.GIBIBYTES.toBytes(256);
-        final long GB512 = DataUnit.GIGABYTES.toBytes(512);
-        final long GiB512 = DataUnit.GIBIBYTES.toBytes(512);
-        final long TB1 = DataUnit.TERABYTES.toBytes(1);
-        final long TiB1 = DataUnit.TEBIBYTES.toBytes(1);
-        final long TB2 = DataUnit.TERABYTES.toBytes(2);
-        final long TiB2 = DataUnit.TEBIBYTES.toBytes(2);
-        final long TB4 = DataUnit.TERABYTES.toBytes(4);
-        final long TiB4 = DataUnit.TEBIBYTES.toBytes(4);
-        final long TB8 = DataUnit.TERABYTES.toBytes(8);
-        final long TiB8 = DataUnit.TEBIBYTES.toBytes(8);
+        final long M256 = DataUnit.MEGABYTES.toBytes(256);
+        final long M512 = DataUnit.MEGABYTES.toBytes(512);
+        final long G1 = DataUnit.GIGABYTES.toBytes(1);
+        final long G2 = DataUnit.GIGABYTES.toBytes(2);
+        final long G32 = DataUnit.GIGABYTES.toBytes(32);
+        final long G64 = DataUnit.GIGABYTES.toBytes(64);
+        final long G512 = DataUnit.GIGABYTES.toBytes(512);
+        final long G1000 = DataUnit.TERABYTES.toBytes(1);
+        final long G2000 = DataUnit.TERABYTES.toBytes(2);
 
-        assertEquals(GB1, roundStorageSize(GB1 - 1));
-        assertEquals(GB1, roundStorageSize(GB1));
-        assertEquals(GB1, roundStorageSize(GB1 + 1));
-        assertEquals(GB1, roundStorageSize(GiB1 - 1));
-        assertEquals(GB1, roundStorageSize(GiB1));
-        assertEquals(GB2, roundStorageSize(GiB1 + 1));
-        assertEquals(GB2, roundStorageSize(GiB2));
+        assertEquals(M256, roundStorageSize(M256 - 1));
+        assertEquals(M256, roundStorageSize(M256));
+        assertEquals(M512, roundStorageSize(M256 + 1));
+        assertEquals(M512, roundStorageSize(M512 - 1));
+        assertEquals(M512, roundStorageSize(M512));
+        assertEquals(G1, roundStorageSize(M512 + 1));
+        assertEquals(G1, roundStorageSize(G1));
+        assertEquals(G2, roundStorageSize(G1 + 1));
 
-        assertEquals(GB256, roundStorageSize(GiB128 + 1));
-        assertEquals(GB256, roundStorageSize(GiB256));
-        assertEquals(GB512, roundStorageSize(GiB256 + 1));
-        assertEquals(GB512, roundStorageSize(GiB512));
-        assertEquals(TB1, roundStorageSize(GiB512 + 1));
-        assertEquals(TB1, roundStorageSize(TiB1));
-        assertEquals(TB2, roundStorageSize(TiB1 + 1));
-        assertEquals(TB2, roundStorageSize(TiB2));
-        assertEquals(TB4, roundStorageSize(TiB2 + 1));
-        assertEquals(TB4, roundStorageSize(TiB4));
-        assertEquals(TB8, roundStorageSize(TiB4 + 1));
-        assertEquals(TB8, roundStorageSize(TiB8));
-        assertEquals(TB1, roundStorageSize(1013077688320L)); // b/268571529
+        assertEquals(G32, roundStorageSize(G32 - 1));
+        assertEquals(G32, roundStorageSize(G32));
+        assertEquals(G64, roundStorageSize(G32 + 1));
+
+        assertEquals(G512, roundStorageSize(G512 - 1));
+        assertEquals(G1000, roundStorageSize(G512 + 1));
+        assertEquals(G2000, roundStorageSize(G1000 + 1));
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index f1eef75..c46118d 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -245,7 +245,7 @@
 
     @Test
     public void onTouchEvent_tryAcceptDelegation_delegatorCallbackCreatesInputConnection() {
-        View delegateView = new View(mContext);
+        View delegateView = new EditText(mContext);
         delegateView.setIsHandwritingDelegate(true);
 
         mTestView1.setHandwritingDelegatorCallback(
@@ -266,7 +266,7 @@
 
     @Test
     public void onTouchEvent_tryAcceptDelegation_delegatorCallbackFocusesDelegate() {
-        View delegateView = new View(mContext);
+        View delegateView = new EditText(mContext);
         delegateView.setIsHandwritingDelegate(true);
         mHandwritingInitiator.onInputConnectionCreated(delegateView);
         reset(mHandwritingInitiator);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index bcbf728..3ad3045 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -261,10 +261,6 @@
             // Updates the Split
             final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
             final WindowContainerTransaction wct = transactionRecord.getTransaction();
-
-            mPresenter.setTaskFragmentIsolatedNavigation(wct,
-                    splitPinContainer.getSecondaryContainer().getTaskFragmentToken(),
-                    true /* isolatedNav */);
             mPresenter.updateSplitContainer(splitPinContainer, wct);
             transactionRecord.apply(false /* shouldApplyIndependently */);
             updateCallbackIfNecessary();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 5de6acf..896fe61 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -382,6 +382,19 @@
         }
         setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(),
                 secondaryContainer.getTaskFragmentToken(), splitRule, isStacked);
+
+        // Setting isolated navigation and clear non-sticky pinned container if needed.
+        final SplitPinRule splitPinRule =
+                splitRule instanceof SplitPinRule ? (SplitPinRule) splitRule : null;
+        if (splitPinRule == null) {
+            return;
+        }
+
+        setTaskFragmentIsolatedNavigation(wct, secondaryContainer.getTaskFragmentToken(),
+                !isStacked /* isolatedNav */);
+        if (isStacked && !splitPinRule.isSticky()) {
+            secondaryContainer.getTaskContainer().removeSplitPinContainer();
+        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml
new file mode 100644
index 0000000..a0a06f1
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<com.android.wm.shell.common.bubbles.BubblePopupView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_horizontal"
+    android:layout_marginHorizontal="@dimen/bubble_popup_margin_horizontal"
+    android:layout_marginTop="@dimen/bubble_popup_margin_top"
+    android:elevation="@dimen/bubble_manage_menu_elevation"
+    android:gravity="center_horizontal"
+    android:orientation="vertical">
+
+    <ImageView
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:tint="?android:attr/colorAccent"
+        android:contentDescription="@null"
+        android:src="@drawable/pip_ic_settings"/>
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:maxWidth="@dimen/bubble_popup_content_max_width"
+        android:maxLines="1"
+        android:ellipsize="end"
+        android:textAppearance="@android:style/TextAppearance.DeviceDefault.Headline"
+        android:textColor="?android:attr/textColorPrimary"
+        android:text="@string/bubble_bar_education_manage_title"/>
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:maxWidth="@dimen/bubble_popup_content_max_width"
+        android:textAppearance="@android:style/TextAppearance.DeviceDefault"
+        android:textColor="?android:attr/textColorSecondary"
+        android:textAlignment="center"
+        android:text="@string/bubble_bar_education_manage_text"/>
+
+</com.android.wm.shell.common.bubbles.BubblePopupView>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 0502a99..63a9723 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -226,6 +226,20 @@
     <dimen name="bubble_user_education_padding_end">58dp</dimen>
     <!-- Padding between the bubble and the user education text. -->
     <dimen name="bubble_user_education_stack_padding">16dp</dimen>
+    <!-- Max width for the bubble popup view. -->
+    <dimen name="bubble_popup_content_max_width">300dp</dimen>
+    <!-- Horizontal margin for the bubble popup view. -->
+    <dimen name="bubble_popup_margin_horizontal">32dp</dimen>
+    <!-- Top margin for the bubble popup view. -->
+    <dimen name="bubble_popup_margin_top">16dp</dimen>
+    <!-- Width for the bubble popup view arrow. -->
+    <dimen name="bubble_popup_arrow_width">12dp</dimen>
+    <!-- Height for the bubble popup view arrow. -->
+    <dimen name="bubble_popup_arrow_height">10dp</dimen>
+    <!-- Corner radius for the bubble popup view arrow. -->
+    <dimen name="bubble_popup_arrow_corner_radius">2dp</dimen>
+    <!-- Padding for the bubble popup view contents. -->
+    <dimen name="bubble_popup_padding">24dp</dimen>
     <!-- The size of the caption bar inset at the top of bubble bar expanded view. -->
     <dimen name="bubble_bar_expanded_view_caption_height">32dp</dimen>
     <!-- The height of the dots shown for the caption menu in the bubble bar expanded view.. -->
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 8cbc3d0..00c63d7 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -163,6 +163,11 @@
     <!-- [CHAR LIMIT=NONE] Empty overflow subtitle -->
     <string name="bubble_overflow_empty_subtitle">Recent bubbles and dismissed bubbles will appear here</string>
 
+    <!-- Title text for the bubble bar "manage" button tool tip highlighting where users can go to control bubble settings. [CHAR LIMIT=60]-->
+    <string name="bubble_bar_education_manage_title">Control bubbles anytime</string>
+    <!-- Descriptive text for the bubble bar "manage" button tool tip highlighting where users can go to control bubble settings. [CHAR LIMIT=80]-->
+    <string name="bubble_bar_education_manage_text">Tap here to manage which apps and conversations can bubble</string>
+
     <!-- [CHAR LIMIT=100] Notification Importance title -->
     <string name="notification_bubble_title">Bubble</string>
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 7e09c98..9a2b812 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -60,7 +60,6 @@
 /**
  * Encapsulates the data and UI elements of a bubble.
  */
-@VisibleForTesting
 public class Bubble implements BubbleViewProvider {
     private static final String TAG = "Bubble";
 
@@ -852,7 +851,10 @@
         return mAppIntent;
     }
 
-    boolean isAppBubble() {
+    /**
+     * Returns whether this bubble is from an app versus a notification.
+     */
+    public boolean isAppBubble() {
         return mIsAppBubble;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
index 250e010..76662c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDebugConfig.java
@@ -52,6 +52,11 @@
     private static final boolean FORCE_SHOW_USER_EDUCATION = false;
     private static final String FORCE_SHOW_USER_EDUCATION_SETTING =
             "force_show_bubbles_user_education";
+    /**
+     * When set to true, bubbles user education flow never shows up.
+     */
+    private static final String FORCE_HIDE_USER_EDUCATION_SETTING =
+            "force_hide_bubbles_user_education";
 
     /**
      * @return whether we should force show user education for bubbles. Used for debugging & demos.
@@ -62,6 +67,14 @@
         return FORCE_SHOW_USER_EDUCATION || forceShow;
     }
 
+    /**
+     * @return whether we should never show user education for bubbles. Used in tests.
+     */
+    static boolean neverShowUserEducation(Context context) {
+        return Settings.Secure.getInt(context.getContentResolver(),
+                FORCE_HIDE_USER_EDUCATION_SETTING, 0) != 0;
+    }
+
     static String formatBubblesString(List<Bubble> bubbles, BubbleViewProvider selected) {
         StringBuilder sb = new StringBuilder();
         for (Bubble bubble : bubbles) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt
new file mode 100644
index 0000000..e57f02c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles
+
+import android.content.Context
+import android.util.Log
+import androidx.core.content.edit
+import com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION
+import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES
+import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME
+
+/** Manages bubble education flags. Provides convenience methods to check the education state */
+class BubbleEducationController(private val context: Context) {
+    private val prefs = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
+
+    /** Whether the user has seen the stack education */
+    @get:JvmName(name = "hasSeenStackEducation")
+    var hasSeenStackEducation: Boolean
+        get() = prefs.getBoolean(PREF_STACK_EDUCATION, false)
+        set(value) = prefs.edit { putBoolean(PREF_STACK_EDUCATION, value) }
+
+    /** Whether the user has seen the expanded view "manage" menu education */
+    @get:JvmName(name = "hasSeenManageEducation")
+    var hasSeenManageEducation: Boolean
+        get() = prefs.getBoolean(PREF_MANAGED_EDUCATION, false)
+        set(value) = prefs.edit { putBoolean(PREF_MANAGED_EDUCATION, value) }
+
+    /** Whether education view should show for the collapsed stack. */
+    fun shouldShowStackEducation(bubble: BubbleViewProvider?): Boolean {
+        val shouldShow = bubble != null &&
+                bubble.isConversationBubble && // show education for conversation bubbles only
+                (!hasSeenStackEducation || BubbleDebugConfig.forceShowUserEducation(context))
+        logDebug("Show stack edu: $shouldShow")
+        return shouldShow
+    }
+
+    /** Whether the educational view should show for the expanded view "manage" menu. */
+    fun shouldShowManageEducation(bubble: BubbleViewProvider?): Boolean {
+        val shouldShow = bubble != null &&
+                bubble.isConversationBubble && // show education for conversation bubbles only
+                (!hasSeenManageEducation || BubbleDebugConfig.forceShowUserEducation(context))
+        logDebug("Show manage edu: $shouldShow")
+        return shouldShow
+    }
+
+    private fun logDebug(message: String) {
+        if (DEBUG_USER_EDUCATION) {
+            Log.d(TAG, message)
+        }
+    }
+
+    companion object {
+        private val TAG = if (TAG_WITH_CLASS_NAME) "BubbleEducationController" else TAG_BUBBLES
+        const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"
+        const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding"
+    }
+}
+
+/** Convenience extension method to check if the bubble is a conversation bubble */
+private val BubbleViewProvider.isConversationBubble: Boolean
+    get() = if (this is Bubble) isConversation else false
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt
new file mode 100644
index 0000000..bdb09e1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles
+
+import android.graphics.Color
+import com.android.wm.shell.R
+import com.android.wm.shell.common.bubbles.BubblePopupDrawable
+import com.android.wm.shell.common.bubbles.BubblePopupView
+
+/**
+ * A convenience method to setup the [BubblePopupView] with the correct config using local resources
+ */
+fun BubblePopupView.setup() {
+    val attrs =
+        context.obtainStyledAttributes(
+            intArrayOf(
+                com.android.internal.R.attr.materialColorSurface,
+                android.R.attr.dialogCornerRadius
+            )
+        )
+
+    val res = context.resources
+    val config =
+        BubblePopupDrawable.Config(
+            color = attrs.getColor(0, Color.WHITE),
+            cornerRadius = attrs.getDimension(1, 0f),
+            contentPadding = res.getDimensionPixelSize(R.dimen.bubble_popup_padding),
+            arrowWidth = res.getDimension(R.dimen.bubble_popup_arrow_width),
+            arrowHeight = res.getDimension(R.dimen.bubble_popup_arrow_height),
+            arrowRadius = res.getDimension(R.dimen.bubble_popup_arrow_corner_radius)
+        )
+    attrs.recycle()
+    setupBackground(config)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 2c10065..ea7053d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -653,14 +653,38 @@
     }
 
     /**
-     * @return the stack position to use if we don't have a saved location or if user education
-     * is being shown.
+     * Returns whether the {@link #getRestingPosition()} is equal to the default start position
+     * initialized for bubbles, if {@code true} this means the user hasn't moved the bubble
+     * from the initial start position (or they haven't received a bubble yet).
+     */
+    public boolean hasUserModifiedDefaultPosition() {
+        PointF defaultStart = getDefaultStartPosition();
+        return mRestingStackPosition != null
+                && !mRestingStackPosition.equals(defaultStart);
+    }
+
+    /**
+     * Returns the stack position to use if we don't have a saved location or if user education
+     * is being shown, for a normal bubble.
      */
     public PointF getDefaultStartPosition() {
-        // Start on the left if we're in LTR, right otherwise.
-        final boolean startOnLeft =
-                mContext.getResources().getConfiguration().getLayoutDirection()
-                        != LAYOUT_DIRECTION_RTL;
+        return getDefaultStartPosition(false /* isAppBubble */);
+    }
+
+    /**
+     * The stack position to use if we don't have a saved location or if user education
+     * is being shown.
+     *
+     * @param isAppBubble whether this start position is for an app bubble or not.
+     */
+    public PointF getDefaultStartPosition(boolean isAppBubble) {
+        final int layoutDirection = mContext.getResources().getConfiguration().getLayoutDirection();
+        // Normal bubbles start on the left if we're in LTR, right otherwise.
+        // TODO (b/294284894): update language around "app bubble" here
+        // App bubbles start on the right in RTL, left otherwise.
+        final boolean startOnLeft = isAppBubble
+                ? layoutDirection == LAYOUT_DIRECTION_RTL
+                : layoutDirection != LAYOUT_DIRECTION_RTL;
         final RectF allowableStackPositionRegion = getAllowableStackPositionRegion(
                 1 /* default starts with 1 bubble */);
         if (isLargeScreen()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 74f830e..52c9bf8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -131,7 +131,7 @@
 
     private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150;
 
-    private static final float SCRIM_ALPHA = 0.6f;
+    private static final float SCRIM_ALPHA = 0.32f;
 
     /** Minimum alpha value for scrim when alpha is being changed via drag */
     private static final float MIN_SCRIM_ALPHA_FOR_DRAG = 0.2f;
@@ -1284,6 +1284,12 @@
         if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
             Log.d(TAG, "Show manage edu: " + shouldShow);
         }
+        if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) {
+            if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
+                Log.d(TAG, "Want to show manage edu, but it is forced hidden");
+            }
+            return false;
+        }
         return shouldShow;
     }
 
@@ -1316,6 +1322,12 @@
         if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
             Log.d(TAG, "Show stack edu: " + shouldShow);
         }
+        if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) {
+            if (BubbleDebugConfig.DEBUG_USER_EDUCATION) {
+                Log.d(TAG, "Want to show stack edu, but it is forced hidden");
+            }
+            return false;
+        }
         return shouldShow;
     }
 
@@ -1763,13 +1775,26 @@
             return;
         }
 
+        if (firstBubble && bubble.isAppBubble() && !mPositioner.hasUserModifiedDefaultPosition()) {
+            // TODO (b/294284894): update language around "app bubble" here
+            // If it's an app bubble and we don't have a previous resting position, update the
+            // controllers to use the default position for the app bubble (it'd be different from
+            // the position initialized with the controllers originally).
+            PointF startPosition =  mPositioner.getDefaultStartPosition(true /* isAppBubble */);
+            mStackOnLeftOrWillBe = mPositioner.isStackOnLeft(startPosition);
+            mStackAnimationController.setStackPosition(startPosition);
+            mExpandedAnimationController.setCollapsePoint(startPosition);
+            // Set the translation x so that this bubble will animate in from the same side they
+            // expand / collapse on.
+            bubble.getIconView().setTranslationX(startPosition.x);
+        } else if (firstBubble) {
+            mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
+        }
+
         mBubbleContainer.addView(bubble.getIconView(), 0,
                 new FrameLayout.LayoutParams(mPositioner.getBubbleSize(),
                         mPositioner.getBubbleSize()));
 
-        if (firstBubble) {
-            mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
-        }
         // Set the dot position to the opposite of the side the stack is resting on, since the stack
         // resting slightly off-screen would result in the dot also being off-screen.
         bubble.getIconView().setDotBadgeOnLeft(!mStackOnLeftOrWillBe /* onLeft */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index c20733a..4d7042b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -131,6 +131,16 @@
 
     private BubbleStackView mBubbleStackView;
 
+    /**
+     * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
+     * the rest of the bubbles to animate to fill the gap.
+     */
+    private boolean mBubbleDraggedOutEnough = false;
+
+    /** End action to run when the lead bubble's expansion animation completes. */
+    @Nullable
+    private Runnable mLeadBubbleEndAction;
+
     public ExpandedAnimationController(BubblePositioner positioner,
             Runnable onBubbleAnimatedOutAction, BubbleStackView stackView) {
         mPositioner = positioner;
@@ -141,14 +151,12 @@
     }
 
     /**
-     * Whether the individual bubble has been dragged out of the row of bubbles far enough to cause
-     * the rest of the bubbles to animate to fill the gap.
+     * Overrides the collapse location without actually collapsing the stack.
+     * @param point the new collapse location.
      */
-    private boolean mBubbleDraggedOutEnough = false;
-
-    /** End action to run when the lead bubble's expansion animation completes. */
-    @Nullable
-    private Runnable mLeadBubbleEndAction;
+    public void setCollapsePoint(PointF point) {
+        mCollapsePoint = point;
+    }
 
     /**
      * Animates expanding the bubbles into a row along the top of the screen, optionally running an
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
index 4bb1ab4..aad2683 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
@@ -297,9 +297,6 @@
 
     /** Whether the stack is on the left side of the screen. */
     public boolean isStackOnLeftSide() {
-        if (mLayout == null || !isStackPositionSet()) {
-            return true; // Default to left, which is where it starts by default.
-        }
         return mPositioner.isStackOnLeft(mStackPosition);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 6b6d6ba..79f188a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -39,7 +39,6 @@
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.taskview.TaskView;
 
-import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 /**
@@ -48,6 +47,18 @@
  * {@link BubbleController#isShowingAsBubbleBar()}
  */
 public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewHelper.Listener {
+    /**
+     * The expanded view listener notifying the {@link BubbleBarLayerView} about the internal
+     * actions and events
+     */
+    public interface Listener {
+        /** Called when the task view task is first created. */
+        void onTaskCreated();
+        /** Called when expanded view needs to un-bubble the given conversation */
+        void onUnBubbleConversation(String bubbleKey);
+        /** Called when expanded view task view back button pressed */
+        void onBackPressed();
+    }
 
     private static final String TAG = BubbleBarExpandedView.class.getSimpleName();
     private static final int INVALID_TASK_ID = -1;
@@ -57,7 +68,7 @@
     private BubbleTaskViewHelper mBubbleTaskViewHelper;
     private BubbleBarMenuViewController mMenuViewController;
     private @Nullable Supplier<Rect> mLayerBoundsSupplier;
-    private @Nullable Consumer<String> mUnBubbleConversationCallback;
+    private @Nullable Listener mListener;
 
     private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext());
     private @Nullable TaskView mTaskView;
@@ -145,15 +156,13 @@
         mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() {
             @Override
             public void onMenuVisibilityChanged(boolean visible) {
-                if (mTaskView == null || mLayerBoundsSupplier == null) return;
-                // Updates the obscured touchable region for the task surface.
-                mTaskView.setObscuredTouchRect(visible ? mLayerBoundsSupplier.get() : null);
+                setObscured(visible);
             }
 
             @Override
             public void onUnBubbleConversation(Bubble bubble) {
-                if (mUnBubbleConversationCallback != null) {
-                    mUnBubbleConversationCallback.accept(bubble.getKey());
+                if (mListener != null) {
+                    mListener.onUnBubbleConversation(bubble.getKey());
                 }
             }
 
@@ -231,6 +240,9 @@
     public void onTaskCreated() {
         setContentVisibility(true);
         updateHandleColor(false /* animated */);
+        if (mListener != null) {
+            mListener.onTaskCreated();
+        }
     }
 
     @Override
@@ -240,7 +252,8 @@
 
     @Override
     public void onBackPressed() {
-        mController.collapseStack();
+        if (mListener == null) return;
+        mListener.onBackPressed();
     }
 
     /** Cleans up task view, should be called when the bubble is no longer active. */
@@ -254,6 +267,18 @@
         mMenuViewController.hideMenu(false /* animated */);
     }
 
+    /**
+     * Hides the current modal menu view or collapses the bubble stack.
+     * Called from {@link BubbleBarLayerView}
+     */
+    public void hideMenuOrCollapse() {
+        if (mMenuViewController.isMenuVisible()) {
+            mMenuViewController.hideMenu(/* animated = */ true);
+        } else {
+            mController.collapseStack();
+        }
+    }
+
     /** Updates the bubble shown in the expanded view. */
     public void update(Bubble bubble) {
         mBubbleTaskViewHelper.update(bubble);
@@ -270,10 +295,16 @@
         mLayerBoundsSupplier = supplier;
     }
 
-    /** Sets the function to call to un-bubble the given conversation. */
-    public void setUnBubbleConversationCallback(
-            @Nullable Consumer<String> unBubbleConversationCallback) {
-        mUnBubbleConversationCallback = unBubbleConversationCallback;
+    /** Sets expanded view listener */
+    void setListener(@Nullable Listener listener) {
+        mListener = listener;
+    }
+
+    /** Sets whether the view is obscured by some modal view */
+    void setObscured(boolean obscured) {
+        if (mTaskView == null || mLayerBoundsSupplier == null) return;
+        // Updates the obscured touchable region for the task surface.
+        mTaskView.setObscuredTouchRect(obscured ? mLayerBoundsSupplier.get() : null);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index bc04bfc..8f11253 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -52,6 +52,7 @@
     private final BubbleController mBubbleController;
     private final BubblePositioner mPositioner;
     private final BubbleBarAnimationHelper mAnimationHelper;
+    private final BubbleEducationViewController mEducationViewController;
     private final View mScrimView;
 
     @Nullable
@@ -80,6 +81,10 @@
 
         mAnimationHelper = new BubbleBarAnimationHelper(context,
                 this, mPositioner);
+        mEducationViewController = new BubbleEducationViewController(context, (boolean visible) -> {
+            if (mExpandedView == null) return;
+            mExpandedView.setObscured(visible);
+        });
 
         mScrimView = new View(getContext());
         mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
@@ -90,9 +95,7 @@
         mScrimView.setBackgroundDrawable(new ColorDrawable(
                 getResources().getColor(android.R.color.system_neutral1_1000)));
 
-        setOnClickListener(view -> {
-            mBubbleController.collapseStack();
-        });
+        setOnClickListener(view -> hideMenuOrCollapse());
     }
 
     @Override
@@ -108,6 +111,7 @@
         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
 
         if (mExpandedView != null) {
+            mEducationViewController.hideManageEducation(/* animated = */ false);
             removeView(mExpandedView);
             mExpandedView = null;
         }
@@ -162,14 +166,27 @@
             final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded);
             final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded);
             mExpandedView.setVisibility(GONE);
-            mExpandedView.setUnBubbleConversationCallback(mUnBubbleConversationCallback);
+            mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height);
             mExpandedView.setLayerBoundsSupplier(() -> new Rect(0, 0, getWidth(), getHeight()));
-            mExpandedView.setUnBubbleConversationCallback(bubbleKey -> {
-                if (mUnBubbleConversationCallback != null) {
-                    mUnBubbleConversationCallback.accept(bubbleKey);
+            mExpandedView.setListener(new BubbleBarExpandedView.Listener() {
+                @Override
+                public void onTaskCreated() {
+                    mEducationViewController.maybeShowManageEducation(b, mExpandedView);
+                }
+
+                @Override
+                public void onUnBubbleConversation(String bubbleKey) {
+                    if (mUnBubbleConversationCallback != null) {
+                        mUnBubbleConversationCallback.accept(bubbleKey);
+                    }
+                }
+
+                @Override
+                public void onBackPressed() {
+                    hideMenuOrCollapse();
                 }
             });
-            mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height);
+
             addView(mExpandedView, new FrameLayout.LayoutParams(width, height));
         }
 
@@ -193,6 +210,7 @@
     public void collapse() {
         mIsExpanded = false;
         final BubbleBarExpandedView viewToRemove = mExpandedView;
+        mEducationViewController.hideManageEducation(/* animated = */ true);
         mAnimationHelper.animateCollapse(() -> removeView(viewToRemove));
         mBubbleController.getSysuiProxy().onStackExpandChanged(false);
         mExpandedView = null;
@@ -206,6 +224,17 @@
         mUnBubbleConversationCallback = unBubbleConversationCallback;
     }
 
+    /** Hides the current modal education/menu view, expanded view or collapses the bubble stack */
+    private void hideMenuOrCollapse() {
+        if (mEducationViewController.isManageEducationVisible()) {
+            mEducationViewController.hideManageEducation(/* animated = */ true);
+        } else if (isExpanded() && mExpandedView != null) {
+            mExpandedView.hideMenuOrCollapse();
+        } else {
+            mBubbleController.collapseStack();
+        }
+    }
+
     /** Updates the expanded view size and position. */
     private void updateExpandedView() {
         if (mExpandedView == null) return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
index 8be140c..81e7582 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
@@ -56,6 +56,11 @@
                 SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
     }
 
+    /** Tells if the menu is visible or being animated */
+    boolean isMenuVisible() {
+        return mMenuView != null && mMenuView.getVisibility() == View.VISIBLE;
+    }
+
     /** Sets menu actions listener */
     void setListener(@Nullable Listener listener) {
         mListener = listener;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt
new file mode 100644
index 0000000..7b39c6f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles.bar
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.view.doOnLayout
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.wm.shell.R
+import com.android.wm.shell.animation.PhysicsAnimator
+import com.android.wm.shell.bubbles.BubbleEducationController
+import com.android.wm.shell.bubbles.BubbleViewProvider
+import com.android.wm.shell.bubbles.setup
+import com.android.wm.shell.common.bubbles.BubblePopupView
+
+/** Manages bubble education presentation and animation */
+class BubbleEducationViewController(private val context: Context, private val listener: Listener) {
+    interface Listener {
+        fun onManageEducationVisibilityChanged(isVisible: Boolean)
+    }
+
+    private var rootView: ViewGroup? = null
+    private var educationView: BubblePopupView? = null
+    private var animator: PhysicsAnimator<BubblePopupView>? = null
+
+    private val springConfig by lazy {
+        PhysicsAnimator.SpringConfig(
+            SpringForce.STIFFNESS_MEDIUM,
+            SpringForce.DAMPING_RATIO_LOW_BOUNCY
+        )
+    }
+
+    private val controller by lazy { BubbleEducationController(context) }
+
+    /** Whether the education view is visible or being animated */
+    val isManageEducationVisible: Boolean
+        get() = educationView != null && rootView != null
+
+    /**
+     * Show manage bubble education if hasn't been shown before
+     *
+     * @param bubble the bubble used for the manage education check
+     * @param root the view to show manage education in
+     */
+    fun maybeShowManageEducation(bubble: BubbleViewProvider, root: ViewGroup) {
+        if (!controller.shouldShowManageEducation(bubble)) return
+        showManageEducation(root)
+    }
+
+    /**
+     * Hide the manage education view if visible
+     *
+     * @param animated whether should hide with animation
+     */
+    fun hideManageEducation(animated: Boolean) {
+        rootView?.let {
+            fun cleanUp() {
+                it.removeView(educationView)
+                rootView = null
+                listener.onManageEducationVisibilityChanged(isVisible = false)
+            }
+
+            if (animated) {
+                animateTransition(show = false, ::cleanUp)
+            } else {
+                cleanUp()
+            }
+        }
+    }
+
+    /**
+     * Show manage education with animation
+     *
+     * @param root the view to show manage education in
+     */
+    private fun showManageEducation(root: ViewGroup) {
+        hideManageEducation(animated = false)
+        if (educationView == null) {
+            val eduView = createEducationView(root)
+            educationView = eduView
+            animator = createAnimation(eduView)
+        }
+        root.addView(educationView)
+        rootView = root
+        animateTransition(show = true) {
+            controller.hasSeenManageEducation = true
+            listener.onManageEducationVisibilityChanged(isVisible = true)
+        }
+    }
+
+    /**
+     * Animate show/hide transition for the education view
+     *
+     * @param show whether to show or hide the view
+     * @param endActions a closure to be called when the animation completes
+     */
+    private fun animateTransition(show: Boolean, endActions: () -> Unit) {
+        animator?.let { animator ->
+            animator
+                .spring(DynamicAnimation.ALPHA, if (show) 1f else 0f)
+                .spring(DynamicAnimation.SCALE_X, if (show) 1f else EDU_SCALE_HIDDEN)
+                .spring(DynamicAnimation.SCALE_Y, if (show) 1f else EDU_SCALE_HIDDEN)
+                .withEndActions(endActions)
+                .start()
+        } ?: endActions()
+    }
+
+    private fun createEducationView(root: ViewGroup): BubblePopupView {
+        val view =
+            LayoutInflater.from(context).inflate(R.layout.bubble_bar_manage_education, root, false)
+                as BubblePopupView
+
+        return view.apply {
+            setup()
+            alpha = 0f
+            pivotY = 0f
+            scaleX = EDU_SCALE_HIDDEN
+            scaleY = EDU_SCALE_HIDDEN
+            doOnLayout { it.pivotX = it.width / 2f }
+            setOnClickListener { hideManageEducation(animated = true) }
+        }
+    }
+
+    private fun createAnimation(view: BubblePopupView): PhysicsAnimator<BubblePopupView> {
+        val animator = PhysicsAnimator.getInstance(view)
+        animator.setDefaultSpringConfig(springConfig)
+        return animator
+    }
+
+    companion object {
+        private const val EDU_SCALE_HIDDEN = 0.5f
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt
new file mode 100644
index 0000000..1fd22d0a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.common.bubbles
+
+import android.annotation.ColorInt
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Matrix
+import android.graphics.Outline
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import kotlin.math.atan
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.properties.Delegates
+
+/** A drawable for the [BubblePopupView] that draws a popup background with a directional arrow */
+class BubblePopupDrawable(private val config: Config) : Drawable() {
+    /** The direction of the arrow in the popup drawable */
+    enum class ArrowDirection {
+        UP,
+        DOWN
+    }
+
+    /** The arrow position on the side of the popup bubble */
+    sealed class ArrowPosition {
+        object Start : ArrowPosition()
+        object Center : ArrowPosition()
+        object End : ArrowPosition()
+        class Custom(val value: Float) : ArrowPosition()
+    }
+
+    /** The configuration for drawable features */
+    data class Config(
+        @ColorInt val color: Int,
+        val cornerRadius: Float,
+        val contentPadding: Int,
+        val arrowWidth: Float,
+        val arrowHeight: Float,
+        val arrowRadius: Float
+    )
+
+    /**
+     * The direction of the arrow in the popup drawable. It affects the content padding and requires
+     * it to be updated in the view.
+     */
+    var arrowDirection: ArrowDirection by
+        Delegates.observable(ArrowDirection.UP) { _, _, _ -> requestPathUpdate() }
+
+    /**
+     * Arrow position along the X axis and its direction. The position is adjusted to the content
+     * corner radius when applied so it doesn't go into rounded corner area
+     */
+    var arrowPosition: ArrowPosition by
+        Delegates.observable(ArrowPosition.Center) { _, _, _ -> requestPathUpdate() }
+
+    private val path = Path()
+    private val paint = Paint()
+    private var shouldUpdatePath = true
+
+    init {
+        paint.color = config.color
+        paint.style = Paint.Style.FILL
+        paint.isAntiAlias = true
+    }
+
+    override fun draw(canvas: Canvas) {
+        updatePathIfNeeded()
+        canvas.drawPath(path, paint)
+    }
+
+    override fun onBoundsChange(bounds: Rect) {
+        requestPathUpdate()
+    }
+
+    /** Should be applied to the view padding if arrow direction changes */
+    override fun getPadding(padding: Rect): Boolean {
+        padding.set(
+            config.contentPadding,
+            config.contentPadding,
+            config.contentPadding,
+            config.contentPadding
+        )
+        when (arrowDirection) {
+            ArrowDirection.UP -> padding.top += config.arrowHeight.toInt()
+            ArrowDirection.DOWN -> padding.bottom += config.arrowHeight.toInt()
+        }
+        return true
+    }
+
+    override fun getOutline(outline: Outline) {
+        updatePathIfNeeded()
+        outline.setPath(path)
+    }
+
+    override fun getOpacity(): Int {
+        return paint.alpha
+    }
+
+    override fun setAlpha(alpha: Int) {
+        paint.alpha = alpha
+    }
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        paint.colorFilter = colorFilter
+    }
+
+    /** Schedules path update for the next redraw */
+    private fun requestPathUpdate() {
+        shouldUpdatePath = true
+    }
+
+    /** Updates the path if required, when bounds or arrow direction/position changes */
+    private fun updatePathIfNeeded() {
+        if (shouldUpdatePath) {
+            updatePath()
+            shouldUpdatePath = false
+        }
+    }
+
+    /** Updates the path value using the current bounds, config, arrow direction and position */
+    private fun updatePath() {
+        if (bounds.isEmpty) return
+        // Reset the path state
+        path.reset()
+        // The content rect where the filled rounded rect will be drawn
+        val contentRect = RectF(bounds)
+        when (arrowDirection) {
+            ArrowDirection.UP -> {
+                // Add rounded arrow pointing up to the path
+                addRoundedArrowPositioned(path, arrowPosition)
+                // Inset content rect by the arrow size from the top
+                contentRect.top += config.arrowHeight
+            }
+            ArrowDirection.DOWN -> {
+                val matrix = Matrix()
+                // Flip the path with the matrix to draw arrow pointing down
+                matrix.setScale(1f, -1f, bounds.width() / 2f, bounds.height() / 2f)
+                path.transform(matrix)
+                // Add rounded arrow with the flipped matrix applied, will point down
+                addRoundedArrowPositioned(path, arrowPosition)
+                // Restore the path matrix to the original state with inverted matrix
+                matrix.invert(matrix)
+                path.transform(matrix)
+                // Inset content rect by the arrow size from the bottom
+                contentRect.bottom -= config.arrowHeight
+            }
+        }
+        // Add the content area rounded rect
+        path.addRoundRect(contentRect, config.cornerRadius, config.cornerRadius, Path.Direction.CW)
+    }
+
+    /** Add a rounded arrow pointing up in the horizontal position on the canvas */
+    private fun addRoundedArrowPositioned(path: Path, position: ArrowPosition) {
+        val matrix = Matrix()
+        var translationX = positionValue(position) - config.arrowWidth / 2
+        // Offset to position between rounded corners of the content view
+        translationX = translationX.coerceIn(config.cornerRadius,
+                bounds.width() - config.cornerRadius - config.arrowWidth)
+        // Translate to add the arrow in the center horizontally
+        matrix.setTranslate(-translationX, 0f)
+        path.transform(matrix)
+        // Add rounded arrow
+        addRoundedArrow(path)
+        // Restore the path matrix to the original state with inverted matrix
+        matrix.invert(matrix)
+        path.transform(matrix)
+    }
+
+    /** Adds a rounded arrow pointing up to the path, can be flipped if needed */
+    private fun addRoundedArrow(path: Path) {
+        // Theta is half of the angle inside the triangle tip
+        val thetaTan = config.arrowWidth / (config.arrowHeight * 2f)
+        val theta = atan(thetaTan)
+        val thetaDeg = Math.toDegrees(theta.toDouble()).toFloat()
+        // The center Y value of the circle for the triangle tip
+        val tipCircleCenterY = config.arrowRadius / sin(theta)
+        // The length from triangle tip to intersection point with the circle
+        val tipIntersectionSideLength = config.arrowRadius / thetaTan
+        // The offset from the top to the point of intersection
+        val intersectionTopOffset = tipIntersectionSideLength * cos(theta)
+        // The offset from the center to the point of intersection
+        val intersectionCenterOffset = tipIntersectionSideLength * sin(theta)
+        // The center X of the triangle
+        val arrowCenterX = config.arrowWidth / 2f
+
+        // Set initial position in bottom left of the arrow
+        path.moveTo(0f, config.arrowHeight)
+        // Add the left side of the triangle
+        path.lineTo(arrowCenterX - intersectionCenterOffset, intersectionTopOffset)
+        // Add the arc from the left to the right side of the triangle
+        path.arcTo(
+            /* left = */ arrowCenterX - config.arrowRadius,
+            /* top = */ tipCircleCenterY - config.arrowRadius,
+            /* right = */ arrowCenterX + config.arrowRadius,
+            /* bottom = */ tipCircleCenterY + config.arrowRadius,
+            /* startAngle = */ 180 + thetaDeg,
+            /* sweepAngle = */ 180 - (2 * thetaDeg),
+            /* forceMoveTo = */ false
+        )
+        // Add the right side of the triangle
+        path.lineTo(config.arrowWidth, config.arrowHeight)
+        // Close the path
+        path.close()
+    }
+
+    /** The value of the arrow position provided the position and current bounds */
+    private fun positionValue(position: ArrowPosition): Float {
+        return when (position) {
+            is ArrowPosition.Start -> 0f
+            is ArrowPosition.Center -> bounds.width().toFloat() / 2f
+            is ArrowPosition.End -> bounds.width().toFloat()
+            is ArrowPosition.Custom -> position.value
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt
new file mode 100644
index 0000000..f8a4946
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.common.bubbles
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.widget.LinearLayout
+
+/** A popup container view that uses [BubblePopupDrawable] as a background */
+open class BubblePopupView
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
+    private var popupDrawable: BubblePopupDrawable? = null
+
+    /**
+     * Sets up the popup drawable with the config provided. Required to remove dependency on local
+     * resources
+     */
+    fun setupBackground(config: BubblePopupDrawable.Config) {
+        popupDrawable = BubblePopupDrawable(config)
+        background = popupDrawable
+        forceLayout()
+    }
+
+    /**
+     * Sets the arrow direction for the background drawable and updates the padding to fit the
+     * content inside of the popup drawable
+     */
+    fun setArrowDirection(direction: BubblePopupDrawable.ArrowDirection) {
+        popupDrawable?.let {
+            it.arrowDirection = direction
+            val padding = Rect()
+            if (it.getPadding(padding)) {
+                setPadding(padding.left, padding.top, padding.right, padding.bottom)
+            }
+        }
+    }
+
+    /** Sets the arrow position for the background drawable and triggers redraw */
+    fun setArrowPosition(position: BubblePopupDrawable.ArrowPosition) {
+        popupDrawable?.let {
+            it.arrowPosition = position
+            invalidate()
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt
index a141ff9..4abb35c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt
@@ -19,7 +19,6 @@
 import android.content.Context
 import android.content.pm.PackageManager
 import com.android.wm.shell.common.ShellExecutor
-import com.android.wm.shell.pip.PipUtils
 
 class PipAppOpsListener(
     private val mContext: Context,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMediaController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMediaController.kt
new file mode 100644
index 0000000..427a555
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMediaController.kt
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.common.pip
+
+import android.annotation.DrawableRes
+import android.annotation.StringRes
+import android.app.PendingIntent
+import android.app.RemoteAction
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.drawable.Icon
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.media.session.MediaSessionManager
+import android.media.session.PlaybackState
+import android.os.Handler
+import android.os.HandlerExecutor
+import android.os.UserHandle
+import com.android.wm.shell.R
+import java.util.function.Consumer
+
+/**
+ * Interfaces with the [MediaSessionManager] to compose the right set of actions to show (only
+ * if there are no actions from the PiP activity itself). The active media controller is only set
+ * when there is a media session from the top PiP activity.
+ */
+class PipMediaController(private val mContext: Context, private val mMainHandler: Handler) {
+    /**
+     * A listener interface to receive notification on changes to the media actions.
+     */
+    interface ActionListener {
+        /**
+         * Called when the media actions changed.
+         */
+        fun onMediaActionsChanged(actions: List<RemoteAction?>?)
+    }
+
+    /**
+     * A listener interface to receive notification on changes to the media metadata.
+     */
+    interface MetadataListener {
+        /**
+         * Called when the media metadata changed.
+         */
+        fun onMediaMetadataChanged(metadata: MediaMetadata?)
+    }
+
+    /**
+     * A listener interface to receive notification on changes to the media session token.
+     */
+    interface TokenListener {
+        /**
+         * Called when the media session token changed.
+         */
+        fun onMediaSessionTokenChanged(token: MediaSession.Token?)
+    }
+
+    private val mHandlerExecutor: HandlerExecutor = HandlerExecutor(mMainHandler)
+    private val mMediaSessionManager: MediaSessionManager?
+    private var mMediaController: MediaController? = null
+    private val mPauseAction: RemoteAction
+    private val mPlayAction: RemoteAction
+    private val mNextAction: RemoteAction
+    private val mPrevAction: RemoteAction
+    private val mMediaActionReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            if (mMediaController == null) {
+                // no active media session, bail early.
+                return
+            }
+            when (intent.action) {
+                ACTION_PLAY -> mMediaController!!.transportControls.play()
+                ACTION_PAUSE -> mMediaController!!.transportControls.pause()
+                ACTION_NEXT -> mMediaController!!.transportControls.skipToNext()
+                ACTION_PREV -> mMediaController!!.transportControls.skipToPrevious()
+            }
+        }
+    }
+    private val mPlaybackChangedListener: MediaController.Callback =
+        object : MediaController.Callback() {
+            override fun onPlaybackStateChanged(state: PlaybackState?) {
+                notifyActionsChanged()
+            }
+
+            override fun onMetadataChanged(metadata: MediaMetadata?) {
+                notifyMetadataChanged(metadata)
+            }
+        }
+    private val mSessionsChangedListener =
+        MediaSessionManager.OnActiveSessionsChangedListener { controllers: List<MediaController>? ->
+            resolveActiveMediaController(controllers)
+        }
+    private val mActionListeners = ArrayList<ActionListener>()
+    private val mMetadataListeners = ArrayList<MetadataListener>()
+    private val mTokenListeners = ArrayList<TokenListener>()
+
+    init {
+        val mediaControlFilter = IntentFilter()
+        mediaControlFilter.addAction(ACTION_PLAY)
+        mediaControlFilter.addAction(ACTION_PAUSE)
+        mediaControlFilter.addAction(ACTION_NEXT)
+        mediaControlFilter.addAction(ACTION_PREV)
+        mContext.registerReceiverForAllUsers(
+            mMediaActionReceiver, mediaControlFilter,
+            SYSTEMUI_PERMISSION, mMainHandler, Context.RECEIVER_EXPORTED
+        )
+
+        // Creates the standard media buttons that we may show.
+        mPauseAction = getDefaultRemoteAction(
+            R.string.pip_pause,
+            R.drawable.pip_ic_pause_white, ACTION_PAUSE
+        )
+        mPlayAction = getDefaultRemoteAction(
+            R.string.pip_play,
+            R.drawable.pip_ic_play_arrow_white, ACTION_PLAY
+        )
+        mNextAction = getDefaultRemoteAction(
+            R.string.pip_skip_to_next,
+            R.drawable.pip_ic_skip_next_white, ACTION_NEXT
+        )
+        mPrevAction = getDefaultRemoteAction(
+            R.string.pip_skip_to_prev,
+            R.drawable.pip_ic_skip_previous_white, ACTION_PREV
+        )
+        mMediaSessionManager = mContext.getSystemService(
+            MediaSessionManager::class.java
+        )
+    }
+
+    /**
+     * Handles when an activity is pinned.
+     */
+    fun onActivityPinned() {
+        // Once we enter PiP, try to find the active media controller for the top most activity
+        resolveActiveMediaController(
+            mMediaSessionManager!!.getActiveSessionsForUser(
+                null,
+                UserHandle.CURRENT
+            )
+        )
+    }
+
+    /**
+     * Adds a new media action listener.
+     */
+    fun addActionListener(listener: ActionListener) {
+        if (!mActionListeners.contains(listener)) {
+            mActionListeners.add(listener)
+            listener.onMediaActionsChanged(mediaActions)
+        }
+    }
+
+    /**
+     * Removes a media action listener.
+     */
+    fun removeActionListener(listener: ActionListener) {
+        listener.onMediaActionsChanged(emptyList<RemoteAction>())
+        mActionListeners.remove(listener)
+    }
+
+    /**
+     * Adds a new media metadata listener.
+     */
+    fun addMetadataListener(listener: MetadataListener) {
+        if (!mMetadataListeners.contains(listener)) {
+            mMetadataListeners.add(listener)
+            listener.onMediaMetadataChanged(mediaMetadata)
+        }
+    }
+
+    /**
+     * Removes a media metadata listener.
+     */
+    fun removeMetadataListener(listener: MetadataListener) {
+        listener.onMediaMetadataChanged(null)
+        mMetadataListeners.remove(listener)
+    }
+
+    /**
+     * Adds a new token listener.
+     */
+    fun addTokenListener(listener: TokenListener) {
+        if (!mTokenListeners.contains(listener)) {
+            mTokenListeners.add(listener)
+            listener.onMediaSessionTokenChanged(token)
+        }
+    }
+
+    /**
+     * Removes a token listener.
+     */
+    fun removeTokenListener(listener: TokenListener) {
+        listener.onMediaSessionTokenChanged(null)
+        mTokenListeners.remove(listener)
+    }
+
+    private val token: MediaSession.Token?
+        get() = if (mMediaController == null) {
+            null
+        } else mMediaController!!.sessionToken
+    private val mediaMetadata: MediaMetadata?
+        get() = if (mMediaController != null) mMediaController!!.metadata else null
+
+    private val mediaActions: List<RemoteAction?>
+        /**
+         * Gets the set of media actions currently available.
+         */
+        get() {
+            if (mMediaController == null) {
+                return emptyList<RemoteAction>()
+            }
+            // Cache the PlaybackState since it's a Binder call.
+            // Safe because mMediaController is guaranteed non-null here.
+            val playbackState: PlaybackState = mMediaController!!.playbackState
+                ?: return emptyList<RemoteAction>()
+            val mediaActions = ArrayList<RemoteAction?>()
+            val isPlaying = playbackState.isActive
+            val actions = playbackState.actions
+
+            // Prev action
+            mPrevAction.isEnabled =
+                actions and PlaybackState.ACTION_SKIP_TO_PREVIOUS != 0L
+            mediaActions.add(mPrevAction)
+
+            // Play/pause action
+            if (!isPlaying && actions and PlaybackState.ACTION_PLAY != 0L) {
+                mediaActions.add(mPlayAction)
+            } else if (isPlaying && actions and PlaybackState.ACTION_PAUSE != 0L) {
+                mediaActions.add(mPauseAction)
+            }
+
+            // Next action
+            mNextAction.isEnabled =
+                actions and PlaybackState.ACTION_SKIP_TO_NEXT != 0L
+            mediaActions.add(mNextAction)
+            return mediaActions
+        }
+
+    /** @return Default [RemoteAction] sends broadcast back to SysUI.
+     */
+    private fun getDefaultRemoteAction(
+        @StringRes titleAndDescription: Int,
+        @DrawableRes icon: Int,
+        action: String
+    ): RemoteAction {
+        val titleAndDescriptionStr = mContext.getString(titleAndDescription)
+        val intent = Intent(action)
+        intent.setPackage(mContext.packageName)
+        return RemoteAction(
+            Icon.createWithResource(mContext, icon),
+            titleAndDescriptionStr, titleAndDescriptionStr,
+            PendingIntent.getBroadcast(
+                mContext, 0 /* requestCode */, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+            )
+        )
+    }
+
+    /**
+     * Re-registers the session listener for the current user.
+     */
+    fun registerSessionListenerForCurrentUser() {
+        mMediaSessionManager!!.removeOnActiveSessionsChangedListener(mSessionsChangedListener)
+        mMediaSessionManager.addOnActiveSessionsChangedListener(
+            null, UserHandle.CURRENT,
+            mHandlerExecutor, mSessionsChangedListener
+        )
+    }
+
+    /**
+     * Tries to find and set the active media controller for the top PiP activity.
+     */
+    private fun resolveActiveMediaController(controllers: List<MediaController>?) {
+        if (controllers != null) {
+            val topActivity = PipUtils.getTopPipActivity(mContext).first
+            if (topActivity != null) {
+                for (i in controllers.indices) {
+                    val controller = controllers[i]
+                    if (controller.packageName == topActivity.packageName) {
+                        setActiveMediaController(controller)
+                        return
+                    }
+                }
+            }
+        }
+        setActiveMediaController(null)
+    }
+
+    /**
+     * Sets the active media controller for the top PiP activity.
+     */
+    private fun setActiveMediaController(controller: MediaController?) {
+        if (controller != mMediaController) {
+            if (mMediaController != null) {
+                mMediaController!!.unregisterCallback(mPlaybackChangedListener)
+            }
+            mMediaController = controller
+            controller?.registerCallback(mPlaybackChangedListener, mMainHandler)
+            notifyActionsChanged()
+            notifyMetadataChanged(mediaMetadata)
+            notifyTokenChanged(token)
+
+            // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
+        }
+    }
+
+    /**
+     * Notifies all listeners that the actions have changed.
+     */
+    private fun notifyActionsChanged() {
+        if (mActionListeners.isNotEmpty()) {
+            val actions = mediaActions
+            mActionListeners.forEach(
+                Consumer { l: ActionListener -> l.onMediaActionsChanged(actions) })
+        }
+    }
+
+    /**
+     * Notifies all listeners that the metadata have changed.
+     */
+    private fun notifyMetadataChanged(metadata: MediaMetadata?) {
+        if (mMetadataListeners.isNotEmpty()) {
+            mMetadataListeners.forEach(Consumer { l: MetadataListener ->
+                l.onMediaMetadataChanged(
+                    metadata
+                )
+            })
+        }
+    }
+
+    private fun notifyTokenChanged(token: MediaSession.Token?) {
+        if (mTokenListeners.isNotEmpty()) {
+            mTokenListeners.forEach(Consumer { l: TokenListener ->
+                l.onMediaSessionTokenChanged(
+                    token
+                )
+            })
+        }
+    }
+
+    companion object {
+        private const val SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"
+        private const val ACTION_PLAY = "com.android.wm.shell.pip.PLAY"
+        private const val ACTION_PAUSE = "com.android.wm.shell.pip.PAUSE"
+        private const val ACTION_NEXT = "com.android.wm.shell.pip.NEXT"
+        private const val ACTION_PREV = "com.android.wm.shell.pip.PREV"
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
new file mode 100644
index 0000000..84feb03
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.common.pip
+
+import android.app.ActivityTaskManager
+import android.app.RemoteAction
+import android.app.WindowConfiguration
+import android.content.ComponentName
+import android.content.Context
+import android.os.RemoteException
+import android.os.SystemProperties
+import android.util.DisplayMetrics
+import android.util.Log
+import android.util.Pair
+import android.util.TypedValue
+import android.window.TaskSnapshot
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import kotlin.math.abs
+
+/** A class that includes convenience methods.  */
+object PipUtils {
+    private const val TAG = "PipUtils"
+
+    // Minimum difference between two floats (e.g. aspect ratios) to consider them not equal.
+    private const val EPSILON = 1e-7
+    private const val ENABLE_PIP2_IMPLEMENTATION = "persist.wm.debug.enable_pip2_implementation"
+
+    /**
+     * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack.
+     * The component name may be null if no such activity exists.
+     */
+    @JvmStatic
+    fun getTopPipActivity(context: Context): Pair<ComponentName?, Int> {
+        try {
+            val sysUiPackageName = context.packageName
+            val pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo(
+                WindowConfiguration.WINDOWING_MODE_PINNED,
+                WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
+            )
+            if (pinnedTaskInfo?.childTaskIds != null && pinnedTaskInfo.childTaskIds.isNotEmpty()) {
+                for (i in pinnedTaskInfo.childTaskNames.indices.reversed()) {
+                    val cn = ComponentName.unflattenFromString(
+                        pinnedTaskInfo.childTaskNames[i]
+                    )
+                    if (cn != null && cn.packageName != sysUiPackageName) {
+                        return Pair(cn, pinnedTaskInfo.childTaskUserIds[i])
+                    }
+                }
+            }
+        } catch (e: RemoteException) {
+            ProtoLog.w(
+                ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: Unable to get pinned stack.", TAG
+            )
+        }
+        return Pair(null, 0)
+    }
+
+    /**
+     * @return the pixels for a given dp value.
+     */
+    @JvmStatic
+    fun dpToPx(dpValue: Float, dm: DisplayMetrics?): Int {
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, dm).toInt()
+    }
+
+    /**
+     * @return true if the aspect ratios differ
+     */
+    @JvmStatic
+    fun aspectRatioChanged(aspectRatio1: Float, aspectRatio2: Float): Boolean {
+        return abs(aspectRatio1 - aspectRatio2) > EPSILON
+    }
+
+    /**
+     * Checks whether title, description and intent match.
+     * Comparing icons would be good, but using equals causes false negatives
+     */
+    @JvmStatic
+    fun remoteActionsMatch(action1: RemoteAction?, action2: RemoteAction?): Boolean {
+        if (action1 === action2) return true
+        if (action1 == null || action2 == null) return false
+        return action1.isEnabled == action2.isEnabled &&
+                action1.shouldShowIcon() == action2.shouldShowIcon() &&
+                action1.title == action2.title &&
+                action1.contentDescription == action2.contentDescription &&
+                action1.actionIntent == action2.actionIntent
+    }
+
+    /**
+     * Returns true if the actions in the lists match each other according to
+     * [ ][PipUtils.remoteActionsMatch], including their position.
+     */
+    @JvmStatic
+    fun remoteActionsChanged(list1: List<RemoteAction?>?, list2: List<RemoteAction?>?): Boolean {
+        if (list1 == null && list2 == null) {
+            return false
+        }
+        if (list1 == null || list2 == null) {
+            return true
+        }
+        if (list1.size != list2.size) {
+            return true
+        }
+        for (i in list1.indices) {
+            if (!remoteActionsMatch(list1[i], list2[i])) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /** @return [TaskSnapshot] for a given task id.
+     */
+    @JvmStatic
+    fun getTaskSnapshot(taskId: Int, isLowResolution: Boolean): TaskSnapshot? {
+        return if (taskId <= 0) null else try {
+            ActivityTaskManager.getService().getTaskSnapshot(
+                taskId, isLowResolution, false /* takeSnapshotIfNeeded */
+            )
+        } catch (e: RemoteException) {
+            Log.e(TAG, "Failed to get task snapshot, taskId=$taskId", e)
+            null
+        }
+    }
+
+    @JvmStatic
+    val isPip2ExperimentEnabled: Boolean
+        get() = SystemProperties.getBoolean(ENABLE_PIP2_IMPLEMENTATION, false)
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 54f8984..7bf0893 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -230,8 +230,10 @@
             // The user aspect ratio button should not be handled when a new TaskInfo is
             // sent because of a double tap or when in multi-window mode.
             if (taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
-                mUserAspectRatioSettingsLayout.release();
-                mUserAspectRatioSettingsLayout = null;
+                if (mUserAspectRatioSettingsLayout != null) {
+                    mUserAspectRatioSettingsLayout.release();
+                    mUserAspectRatioSettingsLayout = null;
+                }
                 return;
             }
             if (!taskInfo.isFromLetterboxDoubleTap) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 8df89bc..aafd9fd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -57,6 +57,7 @@
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
+import com.android.wm.shell.common.pip.PipMediaController;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.compatui.CompatUIConfiguration;
 import com.android.wm.shell.compatui.CompatUIController;
@@ -332,6 +333,13 @@
         return new PipUiEventLogger(uiEventLogger, packageManager);
     }
 
+    @WMSingleton
+    @Provides
+    static PipMediaController providePipMediaController(Context context,
+            @ShellMainThread Handler mainHandler) {
+        return new PipMediaController(context, mainHandler);
+    }
+
 
     //
     // Bubbles (optional feature)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
index 5fd4f24..4e92ca1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
@@ -32,7 +32,9 @@
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
+import com.android.wm.shell.common.pip.PipMediaController;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.common.pip.SizeSpecSource;
 import com.android.wm.shell.dagger.WMShellBaseModule;
 import com.android.wm.shell.dagger.WMSingleton;
@@ -42,7 +44,6 @@
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipDisplayLayoutState;
-import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
@@ -50,7 +51,6 @@
 import com.android.wm.shell.pip.PipTransition;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
-import com.android.wm.shell.pip.PipUtils;
 import com.android.wm.shell.pip.phone.PhonePipKeepClearAlgorithm;
 import com.android.wm.shell.pip.phone.PhonePipMenuController;
 import com.android.wm.shell.pip.phone.PipController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java
index 55a810a..c4ca501 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1SharedModule.java
@@ -17,11 +17,8 @@
 package com.android.wm.shell.dagger.pip;
 
 import android.content.Context;
-import android.os.Handler;
 
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.dagger.WMSingleton;
-import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 
 import dagger.Module;
@@ -33,14 +30,6 @@
  */
 @Module
 public abstract class Pip1SharedModule {
-    // Needs handler for registering broadcast receivers
-    @WMSingleton
-    @Provides
-    static PipMediaController providePipMediaController(Context context,
-            @ShellMainThread Handler mainHandler) {
-        return new PipMediaController(context, mainHandler);
-    }
-
     @WMSingleton
     @Provides
     static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper(Context context) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java
index 04032bb1..9c9364e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java
@@ -18,9 +18,9 @@
 
 import android.annotation.Nullable;
 
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.dagger.WMSingleton;
 import com.android.wm.shell.pip.PipTransitionController;
-import com.android.wm.shell.pip.PipUtils;
 
 import dagger.Module;
 import dagger.Provides;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
index 4e332be..a6ff9ec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
@@ -30,6 +30,7 @@
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.LegacySizeSpecSource;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
+import com.android.wm.shell.common.pip.PipMediaController;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.common.pip.SizeSpecSource;
 import com.android.wm.shell.dagger.WMShellBaseModule;
@@ -37,7 +38,6 @@
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
 import com.android.wm.shell.pip.PipDisplayLayoutState;
-import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 633f627..b0f75c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -311,7 +311,7 @@
         )
         val wct = WindowContainerTransaction()
         wct.setWindowingMode(task.token, WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW)
-        wct.setBounds(task.token, null)
+        wct.setBounds(task.token, Rect())
         wct.setDensityDpi(task.token, getDefaultDensityDpi())
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
index ac711ea..4fef672 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
@@ -28,6 +28,7 @@
 import android.view.Gravity;
 
 import com.android.wm.shell.R;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.common.pip.SizeSpecSource;
 
 import java.io.PrintWriter;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java
index 456f85b..4aa260b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipDisplayLayoutState.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.pip;
 
-import static com.android.wm.shell.pip.PipUtils.dpToPx;
+import static com.android.wm.shell.common.pip.PipUtils.dpToPx;
 
 import android.content.Context;
 import android.content.res.Resources;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
deleted file mode 100644
index ddffb5b..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.pip;
-
-import static android.app.PendingIntent.FLAG_IMMUTABLE;
-import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
-
-import android.annotation.DrawableRes;
-import android.annotation.StringRes;
-import android.annotation.SuppressLint;
-import android.app.PendingIntent;
-import android.app.RemoteAction;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.drawable.Icon;
-import android.media.MediaMetadata;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.MediaSessionManager;
-import android.media.session.PlaybackState;
-import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.UserHandle;
-
-import androidx.annotation.Nullable;
-
-import com.android.wm.shell.R;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Interfaces with the {@link MediaSessionManager} to compose the right set of actions to show (only
- * if there are no actions from the PiP activity itself). The active media controller is only set
- * when there is a media session from the top PiP activity.
- */
-public class PipMediaController {
-    private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
-
-    private static final String ACTION_PLAY = "com.android.wm.shell.pip.PLAY";
-    private static final String ACTION_PAUSE = "com.android.wm.shell.pip.PAUSE";
-    private static final String ACTION_NEXT = "com.android.wm.shell.pip.NEXT";
-    private static final String ACTION_PREV = "com.android.wm.shell.pip.PREV";
-
-    /**
-     * A listener interface to receive notification on changes to the media actions.
-     */
-    public interface ActionListener {
-        /**
-         * Called when the media actions changed.
-         */
-        void onMediaActionsChanged(List<RemoteAction> actions);
-    }
-
-    /**
-     * A listener interface to receive notification on changes to the media metadata.
-     */
-    public interface MetadataListener {
-        /**
-         * Called when the media metadata changed.
-         */
-        void onMediaMetadataChanged(MediaMetadata metadata);
-    }
-
-    /**
-     * A listener interface to receive notification on changes to the media session token.
-     */
-    public interface TokenListener {
-        /**
-         * Called when the media session token changed.
-         */
-        void onMediaSessionTokenChanged(MediaSession.Token token);
-    }
-
-    private final Context mContext;
-    private final Handler mMainHandler;
-    private final HandlerExecutor mHandlerExecutor;
-
-    private final MediaSessionManager mMediaSessionManager;
-    private MediaController mMediaController;
-
-    private RemoteAction mPauseAction;
-    private RemoteAction mPlayAction;
-    private RemoteAction mNextAction;
-    private RemoteAction mPrevAction;
-
-    private final BroadcastReceiver mMediaActionReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (mMediaController == null || mMediaController.getTransportControls() == null) {
-                // no active media session, bail early.
-                return;
-            }
-            switch (intent.getAction()) {
-                case ACTION_PLAY:
-                    mMediaController.getTransportControls().play();
-                    break;
-                case ACTION_PAUSE:
-                    mMediaController.getTransportControls().pause();
-                    break;
-                case ACTION_NEXT:
-                    mMediaController.getTransportControls().skipToNext();
-                    break;
-                case ACTION_PREV:
-                    mMediaController.getTransportControls().skipToPrevious();
-                    break;
-            }
-        }
-    };
-
-    private final MediaController.Callback mPlaybackChangedListener =
-            new MediaController.Callback() {
-                @Override
-                public void onPlaybackStateChanged(PlaybackState state) {
-                    notifyActionsChanged();
-                }
-
-                @Override
-                public void onMetadataChanged(@Nullable MediaMetadata metadata) {
-                    notifyMetadataChanged(metadata);
-                }
-            };
-
-    private final MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener =
-            this::resolveActiveMediaController;
-
-    private final ArrayList<ActionListener> mActionListeners = new ArrayList<>();
-    private final ArrayList<MetadataListener> mMetadataListeners = new ArrayList<>();
-    private final ArrayList<TokenListener> mTokenListeners = new ArrayList<>();
-
-    public PipMediaController(Context context, Handler mainHandler) {
-        mContext = context;
-        mMainHandler = mainHandler;
-        mHandlerExecutor = new HandlerExecutor(mMainHandler);
-        if (!PipUtils.isPip2ExperimentEnabled()) {
-            IntentFilter mediaControlFilter = new IntentFilter();
-            mediaControlFilter.addAction(ACTION_PLAY);
-            mediaControlFilter.addAction(ACTION_PAUSE);
-            mediaControlFilter.addAction(ACTION_NEXT);
-            mediaControlFilter.addAction(ACTION_PREV);
-            mContext.registerReceiverForAllUsers(mMediaActionReceiver, mediaControlFilter,
-                    SYSTEMUI_PERMISSION, mainHandler, Context.RECEIVER_EXPORTED);
-        }
-
-        // Creates the standard media buttons that we may show.
-        mPauseAction = getDefaultRemoteAction(R.string.pip_pause,
-                R.drawable.pip_ic_pause_white, ACTION_PAUSE);
-        mPlayAction = getDefaultRemoteAction(R.string.pip_play,
-                R.drawable.pip_ic_play_arrow_white, ACTION_PLAY);
-        mNextAction = getDefaultRemoteAction(R.string.pip_skip_to_next,
-                R.drawable.pip_ic_skip_next_white, ACTION_NEXT);
-        mPrevAction = getDefaultRemoteAction(R.string.pip_skip_to_prev,
-                R.drawable.pip_ic_skip_previous_white, ACTION_PREV);
-
-        mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
-    }
-
-    /**
-     * Handles when an activity is pinned.
-     */
-    public void onActivityPinned() {
-        // Once we enter PiP, try to find the active media controller for the top most activity
-        resolveActiveMediaController(mMediaSessionManager.getActiveSessionsForUser(null,
-                UserHandle.CURRENT));
-    }
-
-    /**
-     * Adds a new media action listener.
-     */
-    public void addActionListener(ActionListener listener) {
-        if (!mActionListeners.contains(listener)) {
-            mActionListeners.add(listener);
-            listener.onMediaActionsChanged(getMediaActions());
-        }
-    }
-
-    /**
-     * Removes a media action listener.
-     */
-    public void removeActionListener(ActionListener listener) {
-        listener.onMediaActionsChanged(Collections.emptyList());
-        mActionListeners.remove(listener);
-    }
-
-    /**
-     * Adds a new media metadata listener.
-     */
-    public void addMetadataListener(MetadataListener listener) {
-        if (!mMetadataListeners.contains(listener)) {
-            mMetadataListeners.add(listener);
-            listener.onMediaMetadataChanged(getMediaMetadata());
-        }
-    }
-
-    /**
-     * Removes a media metadata listener.
-     */
-    public void removeMetadataListener(MetadataListener listener) {
-        listener.onMediaMetadataChanged(null);
-        mMetadataListeners.remove(listener);
-    }
-
-    /**
-     * Adds a new token listener.
-     */
-    public void addTokenListener(TokenListener listener) {
-        if (!mTokenListeners.contains(listener)) {
-            mTokenListeners.add(listener);
-            listener.onMediaSessionTokenChanged(getToken());
-        }
-    }
-
-    /**
-     * Removes a token listener.
-     */
-    public void removeTokenListener(TokenListener listener) {
-        listener.onMediaSessionTokenChanged(null);
-        mTokenListeners.remove(listener);
-    }
-
-    private MediaSession.Token getToken() {
-        if (mMediaController == null) {
-            return null;
-        }
-        return mMediaController.getSessionToken();
-    }
-
-    private MediaMetadata getMediaMetadata() {
-        return mMediaController != null ? mMediaController.getMetadata() : null;
-    }
-
-    /**
-     * Gets the set of media actions currently available.
-     */
-    // This is due to using PlaybackState#isActive, which is added in API 31.
-    // It can be removed when min_sdk of the app is set to 31 or greater.
-    @SuppressLint("NewApi")
-    private List<RemoteAction> getMediaActions() {
-        // Cache the PlaybackState since it's a Binder call.
-        final PlaybackState playbackState;
-        if (mMediaController == null
-                || (playbackState = mMediaController.getPlaybackState()) == null) {
-            return Collections.emptyList();
-        }
-
-        ArrayList<RemoteAction> mediaActions = new ArrayList<>();
-        boolean isPlaying = playbackState.isActive();
-        long actions = playbackState.getActions();
-
-        // Prev action
-        mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
-        mediaActions.add(mPrevAction);
-
-        // Play/pause action
-        if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
-            mediaActions.add(mPlayAction);
-        } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
-            mediaActions.add(mPauseAction);
-        }
-
-        // Next action
-        mNextAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0);
-        mediaActions.add(mNextAction);
-        return mediaActions;
-    }
-
-    /** @return Default {@link RemoteAction} sends broadcast back to SysUI. */
-    private RemoteAction getDefaultRemoteAction(@StringRes int titleAndDescription,
-            @DrawableRes int icon, String action) {
-        final String titleAndDescriptionStr = mContext.getString(titleAndDescription);
-        final Intent intent = new Intent(action);
-        intent.setPackage(mContext.getPackageName());
-        return new RemoteAction(Icon.createWithResource(mContext, icon),
-                titleAndDescriptionStr, titleAndDescriptionStr,
-                PendingIntent.getBroadcast(mContext, 0 /* requestCode */, intent,
-                        FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE));
-    }
-
-    /**
-     * Re-registers the session listener for the current user.
-     */
-    public void registerSessionListenerForCurrentUser() {
-        mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsChangedListener);
-        mMediaSessionManager.addOnActiveSessionsChangedListener(null, UserHandle.CURRENT,
-                mHandlerExecutor, mSessionsChangedListener);
-    }
-
-    /**
-     * Tries to find and set the active media controller for the top PiP activity.
-     */
-    private void resolveActiveMediaController(List<MediaController> controllers) {
-        if (controllers != null) {
-            final ComponentName topActivity = PipUtils.getTopPipActivity(mContext).first;
-            if (topActivity != null) {
-                for (int i = 0; i < controllers.size(); i++) {
-                    final MediaController controller = controllers.get(i);
-                    if (controller.getPackageName().equals(topActivity.getPackageName())) {
-                        setActiveMediaController(controller);
-                        return;
-                    }
-                }
-            }
-        }
-        setActiveMediaController(null);
-    }
-
-    /**
-     * Sets the active media controller for the top PiP activity.
-     */
-    private void setActiveMediaController(MediaController controller) {
-        if (controller != mMediaController) {
-            if (mMediaController != null) {
-                mMediaController.unregisterCallback(mPlaybackChangedListener);
-            }
-            mMediaController = controller;
-            if (controller != null) {
-                controller.registerCallback(mPlaybackChangedListener, mMainHandler);
-            }
-            notifyActionsChanged();
-            notifyMetadataChanged(getMediaMetadata());
-            notifyTokenChanged(getToken());
-
-            // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
-        }
-    }
-
-    /**
-     * Notifies all listeners that the actions have changed.
-     */
-    private void notifyActionsChanged() {
-        if (!mActionListeners.isEmpty()) {
-            List<RemoteAction> actions = getMediaActions();
-            mActionListeners.forEach(l -> l.onMediaActionsChanged(actions));
-        }
-    }
-
-    /**
-     * Notifies all listeners that the metadata have changed.
-     */
-    private void notifyMetadataChanged(MediaMetadata metadata) {
-        if (!mMetadataListeners.isEmpty()) {
-            mMetadataListeners.forEach(l -> l.onMediaMetadataChanged(metadata));
-        }
-    }
-
-    private void notifyTokenChanged(MediaSession.Token token) {
-        if (!mTokenListeners.isEmpty()) {
-            mTokenListeners.forEach(l -> l.onMediaSessionTokenChanged(token));
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 296857b..ed9ff1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -83,6 +83,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip.phone.PipMotionHelper;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index db7e2c0..83e03dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -64,6 +64,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellInit;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index 0f74f9e..64bba67 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -38,6 +38,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.common.split.SplitScreenUtils;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
deleted file mode 100644
index 3cd9848..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipUtils.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.pip;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.util.TypedValue.COMPLEX_UNIT_DIP;
-
-import android.annotation.Nullable;
-import android.app.ActivityTaskManager;
-import android.app.ActivityTaskManager.RootTaskInfo;
-import android.app.RemoteAction;
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.RemoteException;
-import android.os.SystemProperties;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.Pair;
-import android.util.TypedValue;
-import android.window.TaskSnapshot;
-
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.protolog.ShellProtoLogGroup;
-
-import java.util.List;
-import java.util.Objects;
-
-/** A class that includes convenience methods. */
-public class PipUtils {
-    private static final String TAG = "PipUtils";
-
-    // Minimum difference between two floats (e.g. aspect ratios) to consider them not equal.
-    private static final double EPSILON = 1e-7;
-
-    private static final String ENABLE_PIP2_IMPLEMENTATION =
-            "persist.wm.debug.enable_pip2_implementation";
-
-    /**
-     * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack.
-     * The component name may be null if no such activity exists.
-     */
-    public static Pair<ComponentName, Integer> getTopPipActivity(Context context) {
-        try {
-            final String sysUiPackageName = context.getPackageName();
-            final RootTaskInfo pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo(
-                    WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
-            if (pinnedTaskInfo != null && pinnedTaskInfo.childTaskIds != null
-                    && pinnedTaskInfo.childTaskIds.length > 0) {
-                for (int i = pinnedTaskInfo.childTaskNames.length - 1; i >= 0; i--) {
-                    ComponentName cn = ComponentName.unflattenFromString(
-                            pinnedTaskInfo.childTaskNames[i]);
-                    if (cn != null && !cn.getPackageName().equals(sysUiPackageName)) {
-                        return new Pair<>(cn, pinnedTaskInfo.childTaskUserIds[i]);
-                    }
-                }
-            }
-        } catch (RemoteException e) {
-            ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: Unable to get pinned stack.", TAG);
-        }
-        return new Pair<>(null, 0);
-    }
-
-    /**
-     * @return the pixels for a given dp value.
-     */
-    public static int dpToPx(float dpValue, DisplayMetrics dm) {
-        return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
-    }
-
-    /**
-     * @return true if the aspect ratios differ
-     */
-    public static boolean aspectRatioChanged(float aspectRatio1, float aspectRatio2) {
-        return Math.abs(aspectRatio1 - aspectRatio2) > EPSILON;
-    }
-
-    /**
-     * Checks whether title, description and intent match.
-     * Comparing icons would be good, but using equals causes false negatives
-     */
-    public static boolean remoteActionsMatch(RemoteAction action1, RemoteAction action2) {
-        if (action1 == action2) return true;
-        if (action1 == null || action2 == null) return false;
-        return action1.isEnabled() == action2.isEnabled()
-                && action1.shouldShowIcon() == action2.shouldShowIcon()
-                && Objects.equals(action1.getTitle(), action2.getTitle())
-                && Objects.equals(action1.getContentDescription(), action2.getContentDescription())
-                && Objects.equals(action1.getActionIntent(), action2.getActionIntent());
-    }
-
-    /**
-     * Returns true if the actions in the lists match each other according to {@link
-     * PipUtils#remoteActionsMatch(RemoteAction, RemoteAction)}, including their position.
-     */
-    public static boolean remoteActionsChanged(List<RemoteAction> list1, List<RemoteAction> list2) {
-        if (list1 == null && list2 == null) {
-            return false;
-        }
-        if (list1 == null || list2 == null) {
-            return true;
-        }
-        if (list1.size() != list2.size()) {
-            return true;
-        }
-        for (int i = 0; i < list1.size(); i++) {
-            if (!remoteActionsMatch(list1.get(i), list2.get(i))) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /** @return {@link TaskSnapshot} for a given task id. */
-    @Nullable
-    public static TaskSnapshot getTaskSnapshot(int taskId, boolean isLowResolution) {
-        if (taskId <= 0) return null;
-        try {
-            return ActivityTaskManager.getService().getTaskSnapshot(
-                    taskId, isLowResolution, false /* takeSnapshotIfNeeded */);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed to get task snapshot, taskId=" + taskId, e);
-            return null;
-        }
-    }
-
-    public static boolean isPip2ExperimentEnabled() {
-        return SystemProperties.getBoolean(ENABLE_PIP2_IMPLEMENTATION, false);
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index c93d850..cc182ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -38,10 +38,10 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SystemWindows;
+import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipMediaController.ActionListener;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipMediaController;
-import com.android.wm.shell.pip.PipMediaController.ActionListener;
 import com.android.wm.shell.pip.PipMenuController;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index f25110a..ddea574 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -76,6 +76,8 @@
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
+import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
 import com.android.wm.shell.pip.IPip;
@@ -87,13 +89,11 @@
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipDisplayLayoutState;
 import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
-import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
-import com.android.wm.shell.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
index 837426a..fc34772 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
@@ -67,7 +67,7 @@
 import com.android.wm.shell.animation.Interpolators;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
-import com.android.wm.shell.pip.PipUtils;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index bfba4b4..cb4e6c8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -51,13 +51,13 @@
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.common.pip.SizeSpecSource;
 import com.android.wm.shell.pip.PipAnimationController;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
-import com.android.wm.shell.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ShellInit;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java
index 3b44f10..4bba969 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java
@@ -35,8 +35,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
-import com.android.wm.shell.pip.PipMediaController;
-import com.android.wm.shell.pip.PipUtils;
+import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.util.ArrayList;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 5e583c2..0816dc7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -48,11 +48,11 @@
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
+import com.android.wm.shell.common.pip.PipMediaController;
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
 import com.android.wm.shell.pip.PipDisplayLayoutState;
-import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index 613791c..45e1cde 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -55,7 +55,7 @@
 import com.android.internal.widget.RecyclerView;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.TvWindowMenuActionButton;
-import com.android.wm.shell.pip.PipUtils;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
index f22ee59..1c94625 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
@@ -36,9 +36,9 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.ImageUtils;
 import com.android.wm.shell.R;
-import com.android.wm.shell.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
-import com.android.wm.shell.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
index dbec607..6720804 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
@@ -26,6 +26,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip.PipAnimationController;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
@@ -36,7 +37,6 @@
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
-import com.android.wm.shell.pip.PipUtils;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
 import java.util.Objects;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS
new file mode 100644
index 0000000..ec09827
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS
@@ -0,0 +1,3 @@
+# WM shell sub-module pip owner
+hwwang@google.com
+mateuszc@google.com
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java
index 139724f..58d9a64 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblePositionerTest.java
@@ -203,6 +203,60 @@
         assertThat(restingPosition.y).isEqualTo(getDefaultYPosition());
     }
 
+    /** Test that the default resting position on tablet is middle right. */
+    @Test
+    public void testGetDefaultPosition_appBubble_onTablet() {
+        new WindowManagerConfig().setLargeScreen().setUpConfig();
+        mPositioner.update();
+
+        RectF allowableStackRegion =
+                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
+        PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */);
+
+        assertThat(startPosition.x).isEqualTo(allowableStackRegion.right);
+        assertThat(startPosition.y).isEqualTo(getDefaultYPosition());
+    }
+
+    @Test
+    public void testGetRestingPosition_appBubble_onTablet_RTL() {
+        new WindowManagerConfig().setLargeScreen().setLayoutDirection(
+                LAYOUT_DIRECTION_RTL).setUpConfig();
+        mPositioner.update();
+
+        RectF allowableStackRegion =
+                mPositioner.getAllowableStackPositionRegion(1 /* bubbleCount */);
+        PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */);
+
+        assertThat(startPosition.x).isEqualTo(allowableStackRegion.left);
+        assertThat(startPosition.y).isEqualTo(getDefaultYPosition());
+    }
+
+    @Test
+    public void testHasUserModifiedDefaultPosition_false() {
+        new WindowManagerConfig().setLargeScreen().setLayoutDirection(
+                LAYOUT_DIRECTION_RTL).setUpConfig();
+        mPositioner.update();
+
+        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
+
+        mPositioner.setRestingPosition(mPositioner.getDefaultStartPosition());
+
+        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
+    }
+
+    @Test
+    public void testHasUserModifiedDefaultPosition_true() {
+        new WindowManagerConfig().setLargeScreen().setLayoutDirection(
+                LAYOUT_DIRECTION_RTL).setUpConfig();
+        mPositioner.update();
+
+        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isFalse();
+
+        mPositioner.setRestingPosition(new PointF(0, 100));
+
+        assertThat(mPositioner.hasUserModifiedDefaultPosition()).isTrue();
+    }
+
     /**
      * Calculates the Y position bubbles should be placed based on the config. Based on
      * the calculations in {@link BubblePositioner#getDefaultStartPosition()} and
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 05c6ba9..911f5e1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -56,12 +56,12 @@
 import com.android.wm.shell.common.TabletopModeController;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
+import com.android.wm.shell.common.pip.PipMediaController;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.pip.PipAnimationController;
 import com.android.wm.shell.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.pip.PipBoundsState;
 import com.android.wm.shell.pip.PipDisplayLayoutState;
-import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipTaskOrganizer;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java
index 02e6b8c..c40cd406 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java
@@ -37,7 +37,7 @@
 import android.util.Log;
 
 import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipMediaController;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index da48762..0a100ba 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -14,26 +14,20 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.keyguard.ui.composable
 
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
+import android.view.View
+import android.view.ViewGroup
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
 import com.android.compose.animation.scene.SceneScope
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.qualifiers.KeyguardRootView
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
@@ -42,6 +36,7 @@
 import com.android.systemui.scene.ui.composable.ComposableScene
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
@@ -54,6 +49,7 @@
 constructor(
     @Application private val applicationScope: CoroutineScope,
     private val viewModel: LockscreenSceneViewModel,
+    @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View,
 ) : ComposableScene {
     override val key = SceneKey.Lockscreen
 
@@ -72,6 +68,7 @@
     ) {
         LockscreenScene(
             viewModel = viewModel,
+            viewProvider = viewProvider,
             modifier = modifier,
         )
     }
@@ -89,25 +86,22 @@
 @Composable
 private fun LockscreenScene(
     viewModel: LockscreenSceneViewModel,
+    viewProvider: () -> View,
     modifier: Modifier = Modifier,
 ) {
-    // TODO(b/280879610): implement the real UI.
-
-    val lockButtonIcon: Icon by viewModel.lockButtonIcon.collectAsState()
-
-    Box(modifier = modifier) {
-        Column(
-            horizontalAlignment = Alignment.CenterHorizontally,
-            modifier = Modifier.align(Alignment.Center)
-        ) {
-            Text("Lockscreen", style = MaterialTheme.typography.headlineMedium)
-            Row(
-                horizontalArrangement = Arrangement.spacedBy(8.dp),
-            ) {
-                Button(onClick = { viewModel.onLockButtonClicked() }) { Icon(lockButtonIcon) }
-
-                Button(onClick = { viewModel.onContentClicked() }) { Text("Open some content") }
+    AndroidView(
+        factory = { _ ->
+            val keyguardRootView = viewProvider()
+            // Remove the KeyguardRootView from any parent it might already have in legacy code just
+            // in case (a view can't have two parents).
+            (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView)
+            keyguardRootView
+        },
+        update = { keyguardRootView ->
+            keyguardRootView.requireViewById<View>(R.id.lock_icon_view).setOnClickListener {
+                viewModel.onLockButtonClicked()
             }
-        }
-    }
+        },
+        modifier = modifier,
+    )
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index 0fa4ebd..dc93400 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -51,15 +51,18 @@
 private val KEY_TIMESTAMP = "appliedTimestamp"
 private val KNOWN_PLUGINS =
     mapOf<String, List<ClockMetadata>>(
-        "com.android.systemui.falcon.one" to listOf(ClockMetadata("ANALOG_CLOCK_BIGNUM")),
-        "com.android.systemui.falcon.two" to listOf(ClockMetadata("DIGITAL_CLOCK_CALLIGRAPHY")),
-        "com.android.systemui.falcon.three" to listOf(ClockMetadata("DIGITAL_CLOCK_FLEX")),
-        "com.android.systemui.falcon.four" to listOf(ClockMetadata("DIGITAL_CLOCK_GROWTH")),
-        "com.android.systemui.falcon.five" to listOf(ClockMetadata("DIGITAL_CLOCK_HANDWRITTEN")),
-        "com.android.systemui.falcon.six" to listOf(ClockMetadata("DIGITAL_CLOCK_INFLATE")),
-        "com.android.systemui.falcon.seven" to listOf(ClockMetadata("DIGITAL_CLOCK_METRO")),
-        "com.android.systemui.falcon.eight" to listOf(ClockMetadata("DIGITAL_CLOCK_NUMBEROVERLAP")),
-        "com.android.systemui.falcon.nine" to listOf(ClockMetadata("DIGITAL_CLOCK_WEATHER")),
+        "com.android.systemui.clocks.bignum" to listOf(ClockMetadata("ANALOG_CLOCK_BIGNUM")),
+        "com.android.systemui.clocks.calligraphy" to
+            listOf(ClockMetadata("DIGITAL_CLOCK_CALLIGRAPHY")),
+        "com.android.systemui.clocks.flex" to listOf(ClockMetadata("DIGITAL_CLOCK_FLEX")),
+        "com.android.systemui.clocks.growth" to listOf(ClockMetadata("DIGITAL_CLOCK_GROWTH")),
+        "com.android.systemui.clocks.handwritten" to
+            listOf(ClockMetadata("DIGITAL_CLOCK_HANDWRITTEN")),
+        "com.android.systemui.clocks.inflate" to listOf(ClockMetadata("DIGITAL_CLOCK_INFLATE")),
+        "com.android.systemui.clocks.metro" to listOf(ClockMetadata("DIGITAL_CLOCK_METRO")),
+        "com.android.systemui.clocks.numoverlap" to
+            listOf(ClockMetadata("DIGITAL_CLOCK_NUMBEROVERLAP")),
+        "com.android.systemui.clocks.weather" to listOf(ClockMetadata("DIGITAL_CLOCK_WEATHER")),
     )
 
 private fun <TKey, TVal> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut(
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index 006fc09..31df2ba 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -292,6 +292,7 @@
 -packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
+-packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
diff --git a/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml b/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml
new file mode 100644
index 0000000..a8abd79
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:color="?attr/onShadeActive" android:alpha="0.12" />
+    <item android:state_hovered="true" android:color="?attr/onShadeActive" android:alpha="0.09" />
+    <item android:color="@color/transparent" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
index a8c0349..47a2965 100644
--- a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
+++ b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
@@ -32,6 +32,12 @@
                 <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
             </shape>
         </item>
+        <item>
+            <shape android:shape="rectangle">
+                <solid android:color="@color/qs_footer_power_button_overlay_color"/>
+                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
+            </shape>
+        </item>
 
     </ripple>
 </inset>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/screen_share_dialog_spinner_item_text.xml b/packages/SystemUI/res/layout/screen_share_dialog_spinner_item_text.xml
index 66c2155..1e5b249 100644
--- a/packages/SystemUI/res/layout/screen_share_dialog_spinner_item_text.xml
+++ b/packages/SystemUI/res/layout/screen_share_dialog_spinner_item_text.xml
@@ -13,15 +13,32 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:id="@android:id/text1"
-    android:textAppearance="?android:attr/textAppearanceMedium"
-    android:textColor="?androidprv:attr/textColorOnAccent"
-    android:singleLine="true"
     android:layout_width="match_parent"
-    android:layout_height="@dimen/screenrecord_spinner_height"
-    android:gravity="center_vertical"
-    android:ellipsize="marquee"
+    android:layout_height="wrap_content"
+    android:minHeight="@dimen/screenrecord_spinner_height"
+    android:paddingEnd="@dimen/screenrecord_spinner_text_padding_end"
     android:paddingStart="@dimen/screenrecord_spinner_text_padding_start"
-    android:paddingEnd="@dimen/screenrecord_spinner_text_padding_end"/>
\ No newline at end of file
+    android:gravity="center_vertical"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@android:id/text1"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:ellipsize="marquee"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:textColor="?androidprv:attr/textColorOnAccent" />
+
+    <TextView
+        android:id="@android:id/text2"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:ellipsize="marquee"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?androidprv:attr/colorError" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index d2cb475..9957429 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -219,4 +219,10 @@
     <!-- Privacy dialog -->
     <item type="id" name="privacy_dialog_close_app_button" />
     <item type="id" name="privacy_dialog_manage_app_button" />
+
+    <!--
+    Used to tag views programmatically added to the smartspace area so they can be more easily
+    removed later.
+    -->
+    <item type="id" name="tag_smartspace_view" />
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index cddfda2..275e5d8 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1102,6 +1102,8 @@
     <string name="media_projection_entry_app_permission_dialog_warning_single_app">When you’re sharing, recording, or casting an app, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything shown or played on that app. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
     <!-- 1P/3P apps media projection permission button to continue with app selection or recording [CHAR LIMIT=60] -->
     <string name="media_projection_entry_app_permission_dialog_continue">Start</string>
+    <!-- 1P/3P apps disabled the single app projection option. [CHAR LIMIT=NONE] -->
+    <string name="media_projection_entry_app_permission_dialog_single_app_disabled"><xliff:g id="app_name" example="Meet">%1$s</xliff:g> has disabled this option</string>
 
     <!-- Casting that launched by SysUI (i.e. when there is no app name) -->
     <!-- System casting media projection permission dialog title. [CHAR LIMIT=100] -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index 4bc9491..33e453c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -76,26 +76,11 @@
     void onSystemBarAttributesChanged(int displayId, int behavior) = 20;
 
     /**
-     * Sent when screen turned on and ready to use (blocker scrim is hidden)
-     */
-    void onScreenTurnedOn() = 21;
-
-    /**
      * Sent when the desired dark intensity of the nav buttons has changed
      */
     void onNavButtonsDarkIntensityChanged(float darkIntensity) = 22;
 
      /**
-      * Sent when screen started turning on.
-      */
-     void onScreenTurningOn() = 23;
-
-     /**
-      * Sent when screen started turning off.
-      */
-     void onScreenTurningOff() = 24;
-
-     /**
       * Sent when split keyboard shortcut is triggered to enter stage split.
       */
      void enterStageSplitFromRunningApp(boolean leftOrTop) = 25;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
index cab54d0..8200e5c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
@@ -39,6 +39,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.os.SystemProperties;
 import android.provider.Settings;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
@@ -76,6 +77,8 @@
     private static final String TAG = "RotationButtonController";
     private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100;
     private static final int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000;
+    private static final boolean OEM_DISALLOW_ROTATION_IN_SUW =
+            SystemProperties.getBoolean("ro.setupwizard.rotation_locked", false);
     private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
 
     private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3;
@@ -375,6 +378,12 @@
     }
 
     public void onRotationProposal(int rotation, boolean isValid) {
+        boolean isUserSetupComplete = Settings.Secure.getInt(mContext.getContentResolver(),
+                Settings.Secure.USER_SETUP_COMPLETE, 0) != 0;
+        if (!isUserSetupComplete && OEM_DISALLOW_ROTATION_IN_SUW) {
+            return;
+        }
+
         int windowRotation = mWindowRotationProvider.get();
 
         if (!mRotationButton.acceptRotationProposal()) {
@@ -497,8 +506,7 @@
     boolean canShowRotationButton() {
         return mIsNavigationBarShowing
             || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT
-            || isGesturalMode(mNavBarMode)
-            || mTaskBarVisible;
+            || isGesturalMode(mNavBarMode);
     }
 
     @DrawableRes
diff --git a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
index 899cad89..006974c 100644
--- a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
@@ -66,7 +66,7 @@
         window.isNavigationBarContrastEnforced = false
         window.navigationBarColor = Color.TRANSPARENT
 
-        clock = findViewById(R.id.clock)
+        clock = requireViewById(R.id.clock)
         keyguardStatusViewController =
             keyguardStatusViewComponentFactory.build(clock).keyguardStatusViewController.apply {
                 setDisplayedOnSecondaryDisplay()
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 8108076..692b636 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -222,8 +222,10 @@
         mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view);
         mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large);
 
-        mDumpManager.unregisterDumpable(getClass().toString()); // unregister previous clocks
-        mDumpManager.registerDumpable(getClass().toString(), this);
+        if (!mOnlyClock) {
+            mDumpManager.unregisterDumpable(getClass().toString()); // unregister previous clocks
+            mDumpManager.registerDumpable(getClass().toString(), this);
+        }
 
         if (mFeatureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) {
             mStatusArea = mView.findViewById(R.id.keyguard_status_area);
@@ -290,7 +292,7 @@
             int viewIndex = mStatusArea.indexOfChild(ksv);
             ksv.setVisibility(View.GONE);
 
-            mSmartspaceController.removeViewsFromParent(mStatusArea);
+            removeViewsFromStatusArea();
             addSmartspaceView();
             // TODO(b/261757708): add content observer for the Settings toggle and add/remove
             //  weather according to the Settings.
@@ -322,7 +324,7 @@
 
     void onLocaleListChanged() {
         if (mSmartspaceController.isEnabled()) {
-            mSmartspaceController.removeViewsFromParent(mStatusArea);
+            removeViewsFromStatusArea();
             addSmartspaceView();
             if (mSmartspaceController.isDateWeatherDecoupled()) {
                 mDateWeatherView.removeView(mWeatherView);
@@ -616,4 +618,13 @@
         return ((mCurrentClockSize == LARGE) ? clock.getLargeClock() : clock.getSmallClock())
                 .getConfig().getHasCustomWeatherDataDisplay();
     }
+
+    private void removeViewsFromStatusArea() {
+        for  (int i = mStatusArea.getChildCount() - 1; i >= 0; i--) {
+            final View childView = mStatusArea.getChildAt(i);
+            if (childView.getTag(R.id.tag_smartspace_view) != null) {
+                mStatusArea.removeViewAt(i);
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 58c8000..802a550 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -73,9 +73,7 @@
 import com.android.systemui.biometrics.domain.model.BiometricModalities;
 import com.android.systemui.biometrics.ui.BiometricPromptLayout;
 import com.android.systemui.biometrics.ui.CredentialView;
-import com.android.systemui.biometrics.ui.binder.AuthBiometricFingerprintViewBinder;
 import com.android.systemui.biometrics.ui.binder.BiometricViewBinder;
-import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -142,8 +140,6 @@
 
     // TODO: these should be migrated out once ready
     private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor;
-    private final Provider<AuthBiometricFingerprintViewModel>
-            mAuthBiometricFingerprintViewModelProvider;
     private final @NonNull Provider<PromptSelectorInteractor> mPromptSelectorInteractorProvider;
     // TODO(b/251476085): these should be migrated out of the view
     private final Provider<CredentialViewModel> mCredentialViewModelProvider;
@@ -283,8 +279,6 @@
             @NonNull UserManager userManager,
             @NonNull LockPatternUtils lockPatternUtils,
             @NonNull InteractionJankMonitor jankMonitor,
-            @NonNull Provider<AuthBiometricFingerprintViewModel>
-                    authBiometricFingerprintViewModelProvider,
             @NonNull Provider<PromptCredentialInteractor> promptCredentialInteractor,
             @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor,
             @NonNull PromptViewModel promptViewModel,
@@ -293,9 +287,9 @@
             @NonNull VibratorHelper vibratorHelper) {
         this(config, featureFlags, applicationCoroutineScope, fpProps, faceProps,
                 wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
-                jankMonitor, authBiometricFingerprintViewModelProvider, promptSelectorInteractor,
-                promptCredentialInteractor, promptViewModel, credentialViewModelProvider,
-                new Handler(Looper.getMainLooper()), bgExecutor, vibratorHelper);
+                jankMonitor, promptSelectorInteractor, promptCredentialInteractor, promptViewModel,
+                credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor,
+                vibratorHelper);
     }
 
     @VisibleForTesting
@@ -309,8 +303,6 @@
             @NonNull UserManager userManager,
             @NonNull LockPatternUtils lockPatternUtils,
             @NonNull InteractionJankMonitor jankMonitor,
-            @NonNull Provider<AuthBiometricFingerprintViewModel>
-                    authBiometricFingerprintViewModelProvider,
             @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
             @NonNull Provider<PromptCredentialInteractor> credentialInteractor,
             @NonNull PromptViewModel promptViewModel,
@@ -359,7 +351,6 @@
         mBackgroundExecutor = bgExecutor;
         mInteractionJankMonitor = jankMonitor;
         mPromptCredentialInteractor = credentialInteractor;
-        mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider;
         mPromptSelectorInteractorProvider = promptSelectorInteractorProvider;
         mCredentialViewModelProvider = credentialViewModelProvider;
         mPromptViewModel = promptViewModel;
@@ -442,9 +433,6 @@
                 fingerprintAndFaceView.updateOverrideIconLayoutParamsSize();
                 fingerprintAndFaceView.setFaceClass3(
                         faceProperties.sensorStrength == STRENGTH_STRONG);
-                final AuthBiometricFingerprintViewModel fpAndFaceViewModel =
-                        mAuthBiometricFingerprintViewModelProvider.get();
-                AuthBiometricFingerprintViewBinder.bind(fingerprintAndFaceView, fpAndFaceViewModel);
                 mBiometricView = fingerprintAndFaceView;
             } else if (fpProperties != null) {
                 final AuthBiometricFingerprintView fpView =
@@ -453,9 +441,6 @@
                 fpView.setSensorProperties(fpProperties);
                 fpView.setScaleFactorProvider(config.mScaleProvider);
                 fpView.updateOverrideIconLayoutParamsSize();
-                final AuthBiometricFingerprintViewModel fpViewModel =
-                        mAuthBiometricFingerprintViewModelProvider.get();
-                AuthBiometricFingerprintViewBinder.bind(fpView, fpViewModel);
                 mBiometricView = fpView;
             } else if (faceProperties != null) {
                 mBiometricView = (AuthBiometricFaceView) layoutInflater.inflate(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 7b288a8..d5289a4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -73,7 +73,6 @@
 import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
 import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
-import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
 import com.android.systemui.dagger.SysUISingleton;
@@ -134,8 +133,6 @@
     private final CoroutineScope mApplicationCoroutineScope;
 
     // TODO: these should be migrated out once ready
-    @NonNull private final Provider<AuthBiometricFingerprintViewModel>
-            mAuthBiometricFingerprintViewModelProvider;
     @NonNull private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor;
     @NonNull private final Provider<PromptSelectorInteractor> mPromptSelectorInteractor;
     @NonNull private final Provider<CredentialViewModel> mCredentialViewModelProvider;
@@ -765,8 +762,6 @@
             @NonNull LockPatternUtils lockPatternUtils,
             @NonNull UdfpsLogger udfpsLogger,
             @NonNull LogContextInteractor logContextInteractor,
-            @NonNull Provider<AuthBiometricFingerprintViewModel>
-                    authBiometricFingerprintViewModelProvider,
             @NonNull Provider<PromptCredentialInteractor> promptCredentialInteractorProvider,
             @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
             @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
@@ -801,7 +796,6 @@
         mVibratorHelper = vibratorHelper;
 
         mLogContextInteractor = logContextInteractor;
-        mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider;
         mPromptSelectorInteractor = promptSelectorInteractorProvider;
         mPromptCredentialInteractor = promptCredentialInteractorProvider;
         mPromptViewModelProvider = promptViewModelProvider;
@@ -1344,9 +1338,8 @@
         config.mScaleProvider = this::getScaleFactor;
         return new AuthContainerView(config, mFeatureFlags, mApplicationCoroutineScope, mFpProps, mFaceProps,
                 wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
-                mInteractionJankMonitor, mAuthBiometricFingerprintViewModelProvider,
-                mPromptCredentialInteractor, mPromptSelectorInteractor, viewModel,
-                mCredentialViewModelProvider, bgExecutor, mVibratorHelper);
+                mInteractionJankMonitor, mPromptCredentialInteractor, mPromptSelectorInteractor,
+                viewModel, mCredentialViewModelProvider, bgExecutor, mVibratorHelper);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt
index 20c3e40..f7f9103 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/UdfpsModule.kt
@@ -56,10 +56,10 @@
                         )
                     )
                 } else {
-                    BoundingBoxOverlapDetector()
+                    BoundingBoxOverlapDetector(values[2])
                 }
             } else {
-                return BoundingBoxOverlapDetector()
+                return BoundingBoxOverlapDetector(1f)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
index bb87dca..5badcaf 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -21,15 +21,18 @@
 import com.android.systemui.biometrics.Utils
 import com.android.systemui.biometrics.Utils.getCredentialType
 import com.android.systemui.biometrics.Utils.isDeviceCredentialAllowed
+import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
 import com.android.systemui.biometrics.data.repository.PromptRepository
 import com.android.systemui.biometrics.domain.model.BiometricModalities
 import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
 import com.android.systemui.biometrics.shared.model.BiometricUserInfo
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.PromptKind
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
@@ -65,6 +68,9 @@
      */
     val isConfirmationRequired: Flow<Boolean>
 
+    /** Fingerprint sensor type */
+    val sensorType: StateFlow<FingerprintSensorType>
+
     /** Use biometrics for authentication. */
     fun useBiometricsForAuthentication(
         promptInfo: PromptInfo,
@@ -89,6 +95,7 @@
 class PromptSelectorInteractorImpl
 @Inject
 constructor(
+    private val fingerprintPropertyRepository: FingerprintPropertyRepository,
     private val promptRepository: PromptRepository,
     lockPatternUtils: LockPatternUtils,
 ) : PromptSelectorInteractor {
@@ -140,6 +147,9 @@
             }
         }
 
+    override val sensorType: StateFlow<FingerprintSensorType> =
+        fingerprintPropertyRepository.sensorType
+
     override fun useBiometricsForAuthentication(
         promptInfo: PromptInfo,
         userId: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt
index cf6044f..9b946db 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt
@@ -17,16 +17,30 @@
 package com.android.systemui.biometrics.udfps
 
 import android.graphics.Rect
+import android.os.Build
+import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 
 /** Returns whether the touch coordinates are within the sensor's bounding box. */
 @SysUISingleton
-class BoundingBoxOverlapDetector : OverlapDetector {
+class BoundingBoxOverlapDetector(private val targetSize: Float) : OverlapDetector {
+
+    private val TAG = "BoundingBoxOverlapDetector"
+
     override fun isGoodOverlap(
         touchData: NormalizedTouchData,
         nativeSensorBounds: Rect,
         nativeOverlayBounds: Rect,
-    ): Boolean =
-        touchData.isWithinBounds(nativeOverlayBounds) &&
-            touchData.isWithinBounds(nativeSensorBounds)
+    ): Boolean {
+        val scaledRadius = (nativeSensorBounds.width() / 2) * targetSize
+        val scaledSensorBounds =
+            Rect(
+                (nativeSensorBounds.centerX() - scaledRadius).toInt(),
+                (nativeSensorBounds.centerY() - scaledRadius).toInt(),
+                (nativeSensorBounds.centerX() + scaledRadius).toInt(),
+                (nativeSensorBounds.centerY() + scaledRadius).toInt(),
+            )
+
+        return touchData.isWithinBounds(scaledSensorBounds)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt
deleted file mode 100644
index 9c1bcec..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintViewBinder.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.biometrics.ui.binder
-
-import com.android.systemui.biometrics.AuthBiometricFingerprintView
-import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel
-
-object AuthBiometricFingerprintViewBinder {
-
-    /**
-     * Binds a [AuthBiometricFingerprintView.mIconView] to a [AuthBiometricFingerprintViewModel].
-     */
-    @JvmStatic
-    fun bind(view: AuthBiometricFingerprintView, viewModel: AuthBiometricFingerprintViewModel) {
-        if (view.isSfps) {
-            AuthBiometricFingerprintIconViewBinder.bind(view.getIconView(), viewModel)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index d054751..b1439fd 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -108,6 +108,9 @@
 
         val iconViewOverlay = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay)
         val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon)
+
+        PromptFingerprintIconViewBinder.bind(iconView, viewModel.fingerprintIconViewModel)
+
         val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator)
 
         // Negative-side (left) buttons
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintIconViewBinder.kt
rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt
index bd0907e..188c82b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/AuthBiometricFingerprintIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt
@@ -21,26 +21,29 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.airbnb.lottie.LottieAnimationView
-import com.android.systemui.biometrics.AuthBiometricFingerprintView
-import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel
+import com.android.systemui.biometrics.ui.viewmodel.PromptFingerprintIconViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import kotlinx.coroutines.launch
 
-/** Sub-binder for [AuthBiometricFingerprintView.mIconView]. */
-object AuthBiometricFingerprintIconViewBinder {
+/** Sub-binder for [BiometricPromptLayout.iconView]. */
+object PromptFingerprintIconViewBinder {
 
-    /**
-     * Binds a [AuthBiometricFingerprintView.mIconView] to a [AuthBiometricFingerprintViewModel].
-     */
+    /** Binds [BiometricPromptLayout.iconView] to [PromptFingerprintIconViewModel]. */
     @JvmStatic
-    fun bind(view: LottieAnimationView, viewModel: AuthBiometricFingerprintViewModel) {
+    fun bind(view: LottieAnimationView, viewModel: PromptFingerprintIconViewModel) {
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 val displayInfo = DisplayInfo()
                 view.context.display?.getDisplayInfo(displayInfo)
                 viewModel.setRotation(displayInfo.rotation)
                 viewModel.onConfigurationChanged(view.context.resources.configuration)
-                launch { viewModel.iconAsset.collect { iconAsset -> view.setAnimation(iconAsset) } }
+                launch {
+                    viewModel.iconAsset.collect { iconAsset ->
+                        if (iconAsset != -1) {
+                            view.setAnimation(iconAsset)
+                        }
+                    }
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt
index 617d80c..9b30acb 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt
@@ -22,23 +22,35 @@
 import android.view.Surface
 import com.android.systemui.R
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 
-/** Models UI of AuthBiometricFingerprintView to support rear display state changes. */
-class AuthBiometricFingerprintViewModel
+/** Models UI of [BiometricPromptLayout.iconView] */
+class PromptFingerprintIconViewModel
 @Inject
-constructor(private val interactor: DisplayStateInteractor) {
+constructor(
+    private val displayStateInteractor: DisplayStateInteractor,
+    private val promptSelectorInteractor: PromptSelectorInteractor,
+) {
     /** Current device rotation. */
     private var rotation: Int = Surface.ROTATION_0
 
-    /** Current AuthBiometricFingerprintView asset. */
+    /** Current BiometricPromptLayout.iconView asset. */
     val iconAsset: Flow<Int> =
-        combine(interactor.isFolded, interactor.isInRearDisplayMode) {
-            isFolded: Boolean,
-            isInRearDisplayMode: Boolean ->
-            getSideFpsAnimationAsset(isFolded, isInRearDisplayMode)
+        combine(
+            displayStateInteractor.isFolded,
+            displayStateInteractor.isInRearDisplayMode,
+            promptSelectorInteractor.sensorType,
+        ) { isFolded: Boolean, isInRearDisplayMode: Boolean, sensorType: FingerprintSensorType ->
+            when (sensorType) {
+                FingerprintSensorType.POWER_BUTTON ->
+                    getSideFpsAnimationAsset(isFolded, isInRearDisplayMode)
+                // Replace below when non-SFPS iconAsset logic is migrated to this ViewModel
+                else -> -1
+            }
         }
 
     @RawRes
@@ -75,7 +87,7 @@
 
     /** Called on configuration changes */
     fun onConfigurationChanged(newConfig: Configuration) {
-        interactor.onConfigurationChanged(newConfig)
+        displayStateInteractor.onConfigurationChanged(newConfig)
     }
 
     fun setRotation(newRotation: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index 89561a5..e8b8f54 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -20,6 +20,7 @@
 import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
 import com.android.systemui.biometrics.AuthBiometricView
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
 import com.android.systemui.biometrics.domain.model.BiometricModalities
 import com.android.systemui.biometrics.shared.model.BiometricModality
@@ -45,16 +46,23 @@
 class PromptViewModel
 @Inject
 constructor(
-    private val interactor: PromptSelectorInteractor,
+    private val displayStateInteractor: DisplayStateInteractor,
+    private val promptSelectorInteractor: PromptSelectorInteractor,
     private val vibrator: VibratorHelper,
     private val featureFlags: FeatureFlags,
 ) {
+    /** Models UI of [BiometricPromptLayout.iconView] */
+    val fingerprintIconViewModel: PromptFingerprintIconViewModel =
+        PromptFingerprintIconViewModel(displayStateInteractor, promptSelectorInteractor)
+
     /** The set of modalities available for this prompt */
     val modalities: Flow<BiometricModalities> =
-        interactor.prompt.map { it?.modalities ?: BiometricModalities() }.distinctUntilChanged()
+        promptSelectorInteractor.prompt
+            .map { it?.modalities ?: BiometricModalities() }
+            .distinctUntilChanged()
 
     // TODO(b/251476085): remove after icon controllers are migrated - do not keep this state
-    private var _legacyState = MutableStateFlow(AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN)
+    private var _legacyState = MutableStateFlow(AuthBiometricView.STATE_IDLE)
     val legacyState: StateFlow<Int> = _legacyState.asStateFlow()
 
     private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false)
@@ -75,17 +83,18 @@
      * successful authentication.
      */
     val isConfirmationRequired: Flow<Boolean> =
-        combine(_isOverlayTouched, interactor.isConfirmationRequired) {
+        combine(_isOverlayTouched, promptSelectorInteractor.isConfirmationRequired) {
             isOverlayTouched,
             isConfirmationRequired ->
             !isOverlayTouched && isConfirmationRequired
         }
 
     /** The kind of credential the user has. */
-    val credentialKind: Flow<PromptKind> = interactor.credentialKind
+    val credentialKind: Flow<PromptKind> = promptSelectorInteractor.credentialKind
 
     /** The label to use for the cancel button. */
-    val negativeButtonText: Flow<String> = interactor.prompt.map { it?.negativeButtonText ?: "" }
+    val negativeButtonText: Flow<String> =
+        promptSelectorInteractor.prompt.map { it?.negativeButtonText ?: "" }
 
     private val _message: MutableStateFlow<PromptMessage> = MutableStateFlow(PromptMessage.Empty)
 
@@ -113,7 +122,7 @@
                 _forceLargeSize,
                 _forceMediumSize,
                 modalities,
-                interactor.isConfirmationRequired,
+                promptSelectorInteractor.isConfirmationRequired,
                 fingerprintStartMode,
             ) { forceLarge, forceMedium, modalities, confirmationRequired, fpStartMode ->
                 when {
@@ -129,14 +138,16 @@
             .distinctUntilChanged()
 
     /** Title for the prompt. */
-    val title: Flow<String> = interactor.prompt.map { it?.title ?: "" }.distinctUntilChanged()
+    val title: Flow<String> =
+        promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged()
 
     /** Subtitle for the prompt. */
-    val subtitle: Flow<String> = interactor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged()
+    val subtitle: Flow<String> =
+        promptSelectorInteractor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged()
 
     /** Description for the prompt. */
     val description: Flow<String> =
-        interactor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
+        promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
 
     /** If the indicator (help, error) message should be shown. */
     val isIndicatorMessageVisible: Flow<Boolean> =
@@ -160,7 +171,9 @@
 
     /** If the icon can be used as a confirmation button. */
     val isIconConfirmButton: Flow<Boolean> =
-        combine(size, interactor.isConfirmationRequired) { size, isConfirmationRequired ->
+        combine(size, promptSelectorInteractor.isConfirmationRequired) {
+            size,
+            isConfirmationRequired ->
             size.isNotSmall && isConfirmationRequired
         }
 
@@ -169,7 +182,7 @@
         combine(
                 size,
                 isAuthenticated,
-                interactor.isCredentialAllowed,
+                promptSelectorInteractor.isCredentialAllowed,
             ) { size, authState, credentialAllowed ->
                 size.isNotSmall && authState.isNotAuthenticated && !credentialAllowed
             }
@@ -221,7 +234,7 @@
         combine(
                 size,
                 isAuthenticated,
-                interactor.isCredentialAllowed,
+                promptSelectorInteractor.isCredentialAllowed,
             ) { size, authState, credentialAllowed ->
                 size.isNotSmall && authState.isNotAuthenticated && credentialAllowed
             }
@@ -276,7 +289,7 @@
             if (authenticateAfterError) {
                 showAuthenticating(messageAfterError)
             } else {
-                showHelp(messageAfterError)
+                showInfo(messageAfterError)
             }
         }
     }
@@ -296,12 +309,15 @@
     private fun supportsRetry(failedModality: BiometricModality) =
         failedModality == BiometricModality.Face
 
+    suspend fun showHelp(message: String) = showHelp(message, clearIconError = false)
+    suspend fun showInfo(message: String) = showHelp(message, clearIconError = true)
+
     /**
      * Show a persistent help message.
      *
      * Will be show even if the user has already authenticated.
      */
-    suspend fun showHelp(message: String) {
+    private suspend fun showHelp(message: String, clearIconError: Boolean) {
         val alreadyAuthenticated = _isAuthenticated.value.isAuthenticated
         if (!alreadyAuthenticated) {
             _isAuthenticating.value = false
@@ -316,6 +332,8 @@
                 AuthBiometricView.STATE_PENDING_CONFIRMATION
             } else if (alreadyAuthenticated && !isConfirmationRequired.first()) {
                 AuthBiometricView.STATE_AUTHENTICATED
+            } else if (clearIconError) {
+                AuthBiometricView.STATE_IDLE
             } else {
                 AuthBiometricView.STATE_HELP
             }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
index 918e168..f2b4e09 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
@@ -58,7 +58,7 @@
      * ```
      */
     val panelExpansionAmount: StateFlow<Float>
-    val keyguardPosition: StateFlow<Float>
+    val keyguardPosition: StateFlow<Float?>
     val isBackButtonEnabled: StateFlow<Boolean?>
     /** Determines if user is already unlocked */
     val keyguardAuthenticated: StateFlow<Boolean?>
@@ -130,7 +130,7 @@
      */
     private val _panelExpansionAmount = MutableStateFlow(EXPANSION_HIDDEN)
     override val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
-    private val _keyguardPosition = MutableStateFlow(0f)
+    private val _keyguardPosition = MutableStateFlow<Float?>(null)
     override val keyguardPosition = _keyguardPosition.asStateFlow()
     private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
     override val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
@@ -244,6 +244,7 @@
             .logDiffsForTable(buffer, "", "PanelExpansionAmountMillis", -1)
             .launchIn(applicationScope)
         keyguardPosition
+            .filterNotNull()
             .map { it.toInt() }
             .logDiffsForTable(buffer, "", "KeyguardPosition", -1)
             .launchIn(applicationScope)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
index c486603..0e0f1f6 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
@@ -94,7 +94,7 @@
     val startingDisappearAnimation: Flow<Runnable> =
         repository.primaryBouncerStartingDisappearAnimation.filterNotNull()
     val resourceUpdateRequests: Flow<Boolean> = repository.resourceUpdateRequests.filter { it }
-    val keyguardPosition: Flow<Float> = repository.keyguardPosition
+    val keyguardPosition: Flow<Float> = repository.keyguardPosition.filterNotNull()
     val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount
     /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */
     val bouncerExpansion: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 484be9c..1b2a9eb 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -49,6 +49,7 @@
 import com.android.systemui.settings.dagger.MultiUserUtilsModule
 import com.android.systemui.shortcut.ShortcutKeyDispatcher
 import com.android.systemui.statusbar.ImmersiveModeConfirmation
+import com.android.systemui.statusbar.gesture.GesturePointerEventListener
 import com.android.systemui.statusbar.notification.InstantAppNotifier
 import com.android.systemui.statusbar.phone.KeyguardLiftController
 import com.android.systemui.statusbar.phone.LockscreenWallpaper
@@ -182,6 +183,12 @@
     @ClassKey(ScreenDecorations::class)
     abstract fun bindScreenDecorations(sysui: ScreenDecorations): CoreStartable
 
+    /** Inject into GesturePointerEventHandler. */
+    @Binds
+    @IntoMap
+    @ClassKey(GesturePointerEventListener::class)
+    abstract fun bindGesturePointerEventListener(sysui: GesturePointerEventListener): CoreStartable
+
     /** Inject into SessionTracker.  */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
index e501ece..635961b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
@@ -54,11 +54,7 @@
         if (event.handleAction()) {
             when (event.keyCode) {
                 KeyEvent.KEYCODE_MENU -> return dispatchMenuKeyEvent()
-                KeyEvent.KEYCODE_SPACE,
-                KeyEvent.KEYCODE_ENTER ->
-                    if (isDeviceInteractive()) {
-                        return collapseShadeLockedOrShowPrimaryBouncer()
-                    }
+                KeyEvent.KEYCODE_SPACE -> return dispatchSpaceEvent()
             }
         }
         return false
@@ -94,22 +90,16 @@
                 (statusBarStateController.state != StatusBarState.SHADE) &&
                 statusBarKeyguardViewManager.shouldDismissOnMenuPressed()
         if (shouldUnlockOnMenuPressed) {
-            return collapseShadeLockedOrShowPrimaryBouncer()
+            shadeController.animateCollapseShadeForced()
+            return true
         }
         return false
     }
 
-    private fun collapseShadeLockedOrShowPrimaryBouncer(): Boolean {
-        when (statusBarStateController.state) {
-            StatusBarState.SHADE -> return false
-            StatusBarState.SHADE_LOCKED -> {
-                shadeController.animateCollapseShadeForced()
-                return true
-            }
-            StatusBarState.KEYGUARD -> {
-                statusBarKeyguardViewManager.showPrimaryBouncer(true)
-                return true
-            }
+    private fun dispatchSpaceEvent(): Boolean {
+        if (isDeviceInteractive() && statusBarStateController.state != StatusBarState.SHADE) {
+            shadeController.animateCollapseShadeForced()
+            return true
         }
         return false
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/qualifiers/KeyguardRootView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/qualifiers/KeyguardRootView.kt
new file mode 100644
index 0000000..c2d2725
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/qualifiers/KeyguardRootView.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.qualifiers
+
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeyguardRootView
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
index fe2df21..692aa85 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplier.kt
@@ -87,7 +87,7 @@
             }
             addListener(
                 object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) {
+                    override fun onAnimationEnd(animation: Animator) {
                         updateIsAnimatingSurface()
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/LockscreenSceneModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/LockscreenSceneModule.kt
new file mode 100644
index 0000000..c88737e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/LockscreenSceneModule.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.view
+
+import android.view.View
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.KeyguardViewConfigurator
+import com.android.systemui.keyguard.qualifiers.KeyguardRootView
+import dagger.Module
+import dagger.Provides
+import javax.inject.Provider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@Module
+object LockscreenSceneModule {
+
+    @Provides
+    @SysUISingleton
+    @KeyguardRootView
+    fun viewProvider(
+        configurator: Provider<KeyguardViewConfigurator>,
+    ): () -> View {
+        return { configurator.get().getKeyguardRootView() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
index 6d3b7f1..93c4902 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
@@ -16,41 +16,22 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.R
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
-import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.shared.model.SceneKey
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
 
 /** Models UI state and handles user input for the lockscreen scene. */
 @SysUISingleton
 class LockscreenSceneViewModel
 @Inject
 constructor(
-    @Application applicationScope: CoroutineScope,
     authenticationInteractor: AuthenticationInteractor,
     private val bouncerInteractor: BouncerInteractor,
 ) {
-    /** The icon for the "lock" button on the lockscreen. */
-    val lockButtonIcon: StateFlow<Icon> =
-        authenticationInteractor.isUnlocked
-            .map { isUnlocked -> lockIcon(isUnlocked = isUnlocked) }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = lockIcon(isUnlocked = authenticationInteractor.isUnlocked.value),
-            )
-
     /** The key of the scene we should switch to when swiping up. */
     val upDestinationSceneKey: Flow<SceneKey> =
         authenticationInteractor.isUnlocked.map { isUnlocked ->
@@ -65,31 +46,4 @@
     fun onLockButtonClicked() {
         bouncerInteractor.showOrUnlockDevice()
     }
-
-    /** Notifies that some content on the lock screen was clicked. */
-    fun onContentClicked() {
-        bouncerInteractor.showOrUnlockDevice()
-    }
-
-    private fun upDestinationSceneKey(
-        canSwipeToDismiss: Boolean,
-    ): SceneKey {
-        return if (canSwipeToDismiss) SceneKey.Gone else SceneKey.Bouncer
-    }
-
-    private fun lockIcon(
-        isUnlocked: Boolean,
-    ): Icon {
-        return if (isUnlocked) {
-            Icon.Resource(
-                R.drawable.ic_device_lock_off,
-                ContentDescription.Resource(R.string.accessibility_unlock_button)
-            )
-        } else {
-            Icon.Resource(
-                R.drawable.ic_device_lock_on,
-                ContentDescription.Resource(R.string.accessibility_lock_icon)
-            )
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index 72352e3..a9d2b30 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -36,6 +36,7 @@
 import android.content.pm.PackageManager;
 import android.graphics.Typeface;
 import android.media.projection.IMediaProjection;
+import android.media.projection.MediaProjectionConfig;
 import android.media.projection.MediaProjectionManager;
 import android.media.projection.ReviewGrantedConsentResult;
 import android.os.Bundle;
@@ -54,6 +55,7 @@
 import com.android.systemui.flags.Flags;
 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
+import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.screenrecord.MediaProjectionPermissionDialog;
 import com.android.systemui.screenrecord.ScreenShareOption;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -71,6 +73,7 @@
 
     private final FeatureFlags mFeatureFlags;
     private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver;
+    private final ActivityStarter mActivityStarter;
 
     private String mPackageName;
     private int mUid;
@@ -86,8 +89,10 @@
 
     @Inject
     public MediaProjectionPermissionActivity(FeatureFlags featureFlags,
-            Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver) {
+            Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver,
+            ActivityStarter activityStarter) {
         mFeatureFlags = featureFlags;
+        mActivityStarter = activityStarter;
         mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver;
     }
 
@@ -208,11 +213,13 @@
         // the correct screen width when in split screen.
         Context dialogContext = getApplicationContext();
         if (isPartialScreenSharingEnabled()) {
-            mDialog = new MediaProjectionPermissionDialog(dialogContext, () -> {
-                ScreenShareOption selectedOption =
-                        ((MediaProjectionPermissionDialog) mDialog).getSelectedScreenShareOption();
-                grantMediaProjectionPermission(selectedOption.getMode());
-            }, () -> finish(RECORD_CANCEL, /* projection= */ null), appName);
+            mDialog = new MediaProjectionPermissionDialog(dialogContext, getMediaProjectionConfig(),
+                    () -> {
+                        MediaProjectionPermissionDialog dialog =
+                                (MediaProjectionPermissionDialog) mDialog;
+                        ScreenShareOption selectedOption = dialog.getSelectedScreenShareOption();
+                        grantMediaProjectionPermission(selectedOption.getMode());
+                    }, () -> finish(RECORD_CANCEL, /* projection= */ null), appName);
         } else {
             AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(dialogContext,
                     R.style.Theme_SystemUI_Dialog)
@@ -306,8 +313,16 @@
                 // Start activity from the current foreground user to avoid creating a separate
                 // SystemUI process without access to recent tasks because it won't have
                 // WM Shell running inside.
+                // It is also important to make sure the shade is dismissed, otherwise users won't
+                // see the app selector.
                 mUserSelectingTask = true;
-                startActivityAsUser(intent, UserHandle.of(ActivityManager.getCurrentUser()));
+                mActivityStarter.startActivity(
+                        intent,
+                        /* dismissShade= */ true,
+                        /* animationController= */ null,
+                        /* showOverLockscreenWhenLocked= */ false,
+                        UserHandle.of(ActivityManager.getCurrentUser())
+                );
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Error granting projection permission", e);
@@ -348,6 +363,16 @@
         }
     }
 
+    @Nullable
+    private MediaProjectionConfig getMediaProjectionConfig() {
+        Intent intent = getIntent();
+        if (intent == null) {
+            return null;
+        }
+        return intent.getParcelableExtra(
+                MediaProjectionManager.EXTRA_MEDIA_PROJECTION_CONFIG);
+    }
+
     private boolean isPartialScreenSharingEnabled() {
         return mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 9f45f66..1e82d44 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -87,7 +87,6 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
-import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationBar;
@@ -571,7 +570,6 @@
             SysUiState sysUiState,
             Provider<SceneInteractor> sceneInteractor,
             UserTracker userTracker,
-            ScreenLifecycle screenLifecycle,
             WakefulnessLifecycle wakefulnessLifecycle,
             UiEventLogger uiEventLogger,
             DisplayTracker displayTracker,
@@ -651,7 +649,6 @@
         // Listen for user setup
         mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
 
-        screenLifecycle.addObserver(mScreenLifecycleObserver);
         wakefulnessLifecycle.addObserver(mWakefulnessLifecycleObserver);
         // Connect to the service
         updateEnabledAndBinding();
@@ -923,60 +920,6 @@
         }
     }
 
-    private final ScreenLifecycle.Observer mScreenLifecycleObserver =
-            new ScreenLifecycle.Observer() {
-                /**
-                 * Notifies the Launcher that screen turned on and ready to use
-                 */
-                @Override
-                public void onScreenTurnedOn() {
-                    try {
-                        if (mOverviewProxy != null) {
-                            mOverviewProxy.onScreenTurnedOn();
-                        } else {
-                            Log.e(TAG_OPS,
-                                    "Failed to get overview proxy for screen turned on event.");
-                        }
-                    } catch (RemoteException e) {
-                        Log.e(TAG_OPS, "Failed to call onScreenTurnedOn()", e);
-                    }
-                }
-
-                /**
-                 * Notifies the Launcher that screen is starting to turn on.
-                 */
-                @Override
-                public void onScreenTurningOff() {
-                    try {
-                        if (mOverviewProxy != null) {
-                            mOverviewProxy.onScreenTurningOff();
-                        } else {
-                            Log.e(TAG_OPS,
-                                    "Failed to get overview proxy for screen turning off event.");
-                        }
-                    } catch (RemoteException e) {
-                        Log.e(TAG_OPS, "Failed to call onScreenTurningOff()", e);
-                    }
-                }
-
-                /**
-                 * Notifies the Launcher that screen is starting to turn on.
-                 */
-                @Override
-                public void onScreenTurningOn() {
-                    try {
-                        if (mOverviewProxy != null) {
-                            mOverviewProxy.onScreenTurningOn();
-                        } else {
-                            Log.e(TAG_OPS,
-                                    "Failed to get overview proxy for screen turning on event.");
-                        }
-                    } catch (RemoteException e) {
-                        Log.e(TAG_OPS, "Failed to call onScreenTurningOn()", e);
-                    }
-                }
-            };
-
     private final WakefulnessLifecycle.Observer mWakefulnessLifecycleObserver =
             new WakefulnessLifecycle.Observer() {
                 @Override
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 398e64b..7147951 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.scene
 
+import com.android.systemui.keyguard.ui.view.LockscreenSceneModule
 import com.android.systemui.scene.domain.startable.SceneContainerStartableModule
 import com.android.systemui.scene.shared.model.SceneContainerConfigModule
 import com.android.systemui.scene.ui.composable.SceneModule
@@ -24,6 +25,7 @@
 @Module(
     includes =
         [
+            LockscreenSceneModule::class,
             SceneContainerConfigModule::class,
             SceneContainerStartableModule::class,
             SceneModule::class,
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
index 23894a3..7859fa0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
@@ -18,7 +18,9 @@
 import android.content.Context
 import android.os.Bundle
 import android.view.Gravity
+import android.view.LayoutInflater
 import android.view.View
+import android.view.ViewGroup
 import android.view.ViewStub
 import android.view.WindowManager
 import android.widget.AdapterView
@@ -35,7 +37,7 @@
 
 /** Base permission dialog for screen share and recording */
 open class BaseScreenSharePermissionDialog(
-    context: Context?,
+    context: Context,
     private val screenShareOptions: List<ScreenShareOption>,
     private val appName: String?,
     @DrawableRes private val dialogIconDrawable: Int? = null,
@@ -82,14 +84,7 @@
         get() = context.getString(selectedScreenShareOption.warningText, appName)
 
     private fun initScreenShareSpinner() {
-        val options = screenShareOptions.map { context.getString(it.spinnerText) }.toTypedArray()
-        val adapter =
-            ArrayAdapter(
-                context.applicationContext,
-                R.layout.screen_share_dialog_spinner_text,
-                options
-            )
-        adapter.setDropDownViewResource(R.layout.screen_share_dialog_spinner_item_text)
+        val adapter = OptionsAdapter(context.applicationContext, screenShareOptions)
         screenShareModeSpinner = requireViewById(R.id.screen_share_mode_spinner)
         screenShareModeSpinner.adapter = adapter
         screenShareModeSpinner.onItemSelectedListener = this
@@ -131,3 +126,35 @@
         stub.inflate()
     }
 }
+
+private class OptionsAdapter(
+    context: Context,
+    private val options: List<ScreenShareOption>,
+) :
+    ArrayAdapter<String>(
+        context,
+        R.layout.screen_share_dialog_spinner_text,
+        options.map { context.getString(it.spinnerText) }
+    ) {
+
+    override fun isEnabled(position: Int): Boolean {
+        return options[position].spinnerDisabledText == null
+    }
+
+    override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
+        val inflater = LayoutInflater.from(parent.context)
+        val view = inflater.inflate(R.layout.screen_share_dialog_spinner_item_text, parent, false)
+        val titleTextView = view.findViewById<TextView>(android.R.id.text1)
+        val errorTextView = view.findViewById<TextView>(android.R.id.text2)
+        titleTextView.text = getItem(position)
+        errorTextView.text = options[position].spinnerDisabledText
+        if (isEnabled(position)) {
+            errorTextView.visibility = View.GONE
+            titleTextView.isEnabled = true
+        } else {
+            errorTextView.visibility = View.VISIBLE
+            titleTextView.isEnabled = false
+        }
+        return view
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt
index f4f5f66..8cbc4aab 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt
@@ -16,16 +16,23 @@
 package com.android.systemui.screenrecord
 
 import android.content.Context
+import android.media.projection.MediaProjectionConfig
 import android.os.Bundle
 import com.android.systemui.R
 
 /** Dialog to select screen recording options */
 class MediaProjectionPermissionDialog(
-    context: Context?,
+    context: Context,
+    mediaProjectionConfig: MediaProjectionConfig?,
     private val onStartRecordingClicked: Runnable,
     private val onCancelClicked: Runnable,
     private val appName: String?
-) : BaseScreenSharePermissionDialog(context, createOptionList(appName), appName) {
+) :
+    BaseScreenSharePermissionDialog(
+        context,
+        createOptionList(context, appName, mediaProjectionConfig),
+        appName
+    ) {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         // TODO(b/270018943): Handle the case of System sharing (not recording nor casting)
@@ -49,7 +56,11 @@
     }
 
     companion object {
-        private fun createOptionList(appName: String?): List<ScreenShareOption> {
+        private fun createOptionList(
+            context: Context,
+            appName: String?,
+            mediaProjectionConfig: MediaProjectionConfig?
+        ): List<ScreenShareOption> {
             val singleAppWarningText =
                 if (appName == null) {
                     R.string.media_projection_entry_cast_permission_dialog_warning_single_app
@@ -63,6 +74,19 @@
                     R.string.media_projection_entry_app_permission_dialog_warning_entire_screen
                 }
 
+            val singleAppDisabledText =
+                if (
+                    appName != null &&
+                        mediaProjectionConfig?.regionToCapture ==
+                            MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
+                ) {
+                    context.getString(
+                        R.string.media_projection_entry_app_permission_dialog_single_app_disabled,
+                        appName
+                    )
+                } else {
+                    null
+                }
             return listOf(
                 ScreenShareOption(
                     mode = ENTIRE_SCREEN,
@@ -72,7 +96,8 @@
                 ScreenShareOption(
                     mode = SINGLE_APP,
                     spinnerText = R.string.screen_share_permission_dialog_option_single_app,
-                    warningText = singleAppWarningText
+                    warningText = singleAppWarningText,
+                    spinnerDisabledText = singleAppDisabledText,
                 )
             )
         }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index fb99775..9c5da10 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -41,7 +41,7 @@
 
 /** Dialog to select screen recording options */
 class ScreenRecordPermissionDialog(
-    context: Context?,
+    context: Context,
     private val hostUserHandle: UserHandle,
     private val controller: RecordingController,
     private val activityStarter: ActivityStarter,
@@ -52,7 +52,7 @@
     BaseScreenSharePermissionDialog(
         context,
         createOptionList(),
-        null,
+        appName = null,
         R.drawable.ic_screenrecord,
         R.color.screenrecord_icon_color
     ) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenShareOption.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenShareOption.kt
index 3d39fd8..ebf0dd2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenShareOption.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenShareOption.kt
@@ -29,5 +29,6 @@
 class ScreenShareOption(
     @ScreenShareMode val mode: Int,
     @StringRes val spinnerText: Int,
-    @StringRes val warningText: Int
+    @StringRes val warningText: Int,
+    val spinnerDisabledText: String? = null,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt
new file mode 100644
index 0000000..b34c3ac
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.gesture
+
+import android.content.Context
+import android.view.InputEvent
+import android.view.MotionEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.settings.DisplayTracker
+import javax.inject.Inject
+
+/**
+ * A class to detect when a motion event happens. To be notified when the event is detected, add a
+ * callback via [addOnGestureDetectedCallback].
+ */
+@SysUISingleton
+class GesturePointerEventDetector @Inject constructor(
+        private val context: Context,
+        displayTracker: DisplayTracker
+) : GenericGestureDetector(
+        GesturePointerEventDetector::class.simpleName!!,
+        displayTracker.defaultDisplayId
+) {
+    override fun onInputEvent(ev: InputEvent) {
+        if (ev !is MotionEvent) {
+            return
+        }
+        // Pass all events to [gestureDetector], which will then notify [gestureListener] when a tap
+        // is detected.
+        onGestureDetected(ev)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt
new file mode 100644
index 0000000..8505c5f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.gesture
+
+import android.content.Context
+import android.graphics.Rect
+import android.graphics.Region
+import android.hardware.display.DisplayManagerGlobal
+import android.os.Handler
+import android.os.Looper
+import android.os.SystemClock
+import android.util.Log
+import android.view.DisplayCutout
+import android.view.DisplayInfo
+import android.view.GestureDetector
+import android.view.InputDevice
+import android.view.InputEvent
+import android.view.MotionEvent
+import android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT
+import android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
+import android.view.ViewRootImpl.CLIENT_TRANSIENT
+import android.widget.OverScroller
+import com.android.internal.R
+import com.android.systemui.CoreStartable
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Watches for gesture events that may trigger system bar related events and notify the registered
+ * callbacks. Add callback to this listener by calling {@link setCallbacks}.
+ */
+class GesturePointerEventListener
+@Inject
+constructor(context: Context, gestureDetector: GesturePointerEventDetector) : CoreStartable {
+    private val mContext: Context
+    private val mHandler = Handler(Looper.getMainLooper())
+    private var mGestureDetector: GesturePointerEventDetector
+    private var mFlingGestureDetector: GestureDetector? = null
+    private var mDisplayCutoutTouchableRegionSize = 0
+
+    // The thresholds for each edge of the display
+    private val mSwipeStartThreshold = Rect()
+    private var mSwipeDistanceThreshold = 0
+    private var mCallbacks: Callbacks? = null
+    private val mDownPointerId = IntArray(MAX_TRACKED_POINTERS)
+    private val mDownX = FloatArray(MAX_TRACKED_POINTERS)
+    private val mDownY = FloatArray(MAX_TRACKED_POINTERS)
+    private val mDownTime = LongArray(MAX_TRACKED_POINTERS)
+    var screenHeight = 0
+    var screenWidth = 0
+    private var mDownPointers = 0
+    private var mSwipeFireable = false
+    private var mDebugFireable = false
+    private var mMouseHoveringAtLeft = false
+    private var mMouseHoveringAtTop = false
+    private var mMouseHoveringAtRight = false
+    private var mMouseHoveringAtBottom = false
+    private var mLastFlingTime: Long = 0
+
+    init {
+        mContext = checkNull("context", context)
+        mGestureDetector = checkNull("gesture detector", gestureDetector)
+        onConfigurationChanged()
+    }
+
+    override fun start() {
+        if (!CLIENT_TRANSIENT) {
+            return
+        }
+        mGestureDetector.addOnGestureDetectedCallback(TAG) { ev -> onInputEvent(ev) }
+        mGestureDetector.startGestureListening()
+
+        mFlingGestureDetector =
+            object : GestureDetector(mContext, FlingGestureDetector(), mHandler) {}
+    }
+
+    fun onDisplayInfoChanged(info: DisplayInfo) {
+        screenWidth = info.logicalWidth
+        screenHeight = info.logicalHeight
+        onConfigurationChanged()
+    }
+
+    fun onConfigurationChanged() {
+        if (!CLIENT_TRANSIENT) {
+            return
+        }
+        val r = mContext.resources
+        val defaultThreshold = r.getDimensionPixelSize(R.dimen.system_gestures_start_threshold)
+        mSwipeStartThreshold[defaultThreshold, defaultThreshold, defaultThreshold] =
+            defaultThreshold
+        mSwipeDistanceThreshold = defaultThreshold
+        val display = DisplayManagerGlobal.getInstance().getRealDisplay(mContext.displayId)
+        val displayCutout = display.cutout
+        if (displayCutout != null) {
+            // Expand swipe start threshold such that we can catch touches that just start beyond
+            // the notch area
+            mDisplayCutoutTouchableRegionSize =
+                r.getDimensionPixelSize(R.dimen.display_cutout_touchable_region_size)
+            val bounds = displayCutout.boundingRectsAll
+            if (bounds[DisplayCutout.BOUNDS_POSITION_LEFT] != null) {
+                mSwipeStartThreshold.left =
+                    Math.max(
+                        mSwipeStartThreshold.left,
+                        bounds[DisplayCutout.BOUNDS_POSITION_LEFT]!!.width() +
+                            mDisplayCutoutTouchableRegionSize
+                    )
+            }
+            if (bounds[DisplayCutout.BOUNDS_POSITION_TOP] != null) {
+                mSwipeStartThreshold.top =
+                    Math.max(
+                        mSwipeStartThreshold.top,
+                        bounds[DisplayCutout.BOUNDS_POSITION_TOP]!!.height() +
+                            mDisplayCutoutTouchableRegionSize
+                    )
+            }
+            if (bounds[DisplayCutout.BOUNDS_POSITION_RIGHT] != null) {
+                mSwipeStartThreshold.right =
+                    Math.max(
+                        mSwipeStartThreshold.right,
+                        bounds[DisplayCutout.BOUNDS_POSITION_RIGHT]!!.width() +
+                            mDisplayCutoutTouchableRegionSize
+                    )
+            }
+            if (bounds[DisplayCutout.BOUNDS_POSITION_BOTTOM] != null) {
+                mSwipeStartThreshold.bottom =
+                    Math.max(
+                        mSwipeStartThreshold.bottom,
+                        bounds[DisplayCutout.BOUNDS_POSITION_BOTTOM]!!.height() +
+                            mDisplayCutoutTouchableRegionSize
+                    )
+            }
+        }
+        if (DEBUG)
+            Log.d(
+                TAG,
+                "mSwipeStartThreshold=$mSwipeStartThreshold" +
+                    " mSwipeDistanceThreshold=$mSwipeDistanceThreshold"
+            )
+    }
+
+    fun onInputEvent(ev: InputEvent) {
+        if (ev !is MotionEvent) {
+            return
+        }
+        if (DEBUG) Log.d(TAG, "Received motion event $ev")
+        if (ev.isTouchEvent) {
+            mFlingGestureDetector?.onTouchEvent(ev)
+        }
+        when (ev.actionMasked) {
+            MotionEvent.ACTION_DOWN -> {
+                mSwipeFireable = true
+                mDebugFireable = true
+                mDownPointers = 0
+                captureDown(ev, 0)
+                if (mMouseHoveringAtLeft) {
+                    mMouseHoveringAtLeft = false
+                    mCallbacks?.onMouseLeaveFromLeft()
+                }
+                if (mMouseHoveringAtTop) {
+                    mMouseHoveringAtTop = false
+                    mCallbacks?.onMouseLeaveFromTop()
+                }
+                if (mMouseHoveringAtRight) {
+                    mMouseHoveringAtRight = false
+                    mCallbacks?.onMouseLeaveFromRight()
+                }
+                if (mMouseHoveringAtBottom) {
+                    mMouseHoveringAtBottom = false
+                    mCallbacks?.onMouseLeaveFromBottom()
+                }
+                mCallbacks?.onDown()
+            }
+            MotionEvent.ACTION_POINTER_DOWN -> {
+                captureDown(ev, ev.actionIndex)
+                if (mDebugFireable) {
+                    mDebugFireable = ev.pointerCount < 5
+                    if (!mDebugFireable) {
+                        if (DEBUG) Log.d(TAG, "Firing debug")
+                        mCallbacks?.onDebug()
+                    }
+                }
+            }
+            MotionEvent.ACTION_MOVE ->
+                if (mSwipeFireable) {
+                    val trackpadSwipe = detectTrackpadThreeFingerSwipe(ev)
+                    mSwipeFireable = trackpadSwipe == TRACKPAD_SWIPE_NONE
+                    if (!mSwipeFireable) {
+                        if (trackpadSwipe == TRACKPAD_SWIPE_FROM_TOP) {
+                            if (DEBUG) Log.d(TAG, "Firing onSwipeFromTop from trackpad")
+                            mCallbacks?.onSwipeFromTop()
+                        } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_BOTTOM) {
+                            if (DEBUG) Log.d(TAG, "Firing onSwipeFromBottom from trackpad")
+                            mCallbacks?.onSwipeFromBottom()
+                        } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_RIGHT) {
+                            if (DEBUG) Log.d(TAG, "Firing onSwipeFromRight from trackpad")
+                            mCallbacks?.onSwipeFromRight()
+                        } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_LEFT) {
+                            if (DEBUG) Log.d(TAG, "Firing onSwipeFromLeft from trackpad")
+                            mCallbacks?.onSwipeFromLeft()
+                        }
+                    } else {
+                        val swipe = detectSwipe(ev)
+                        mSwipeFireable = swipe == SWIPE_NONE
+                        if (swipe == SWIPE_FROM_TOP) {
+                            if (DEBUG) Log.d(TAG, "Firing onSwipeFromTop")
+                            mCallbacks?.onSwipeFromTop()
+                        } else if (swipe == SWIPE_FROM_BOTTOM) {
+                            if (DEBUG) Log.d(TAG, "Firing onSwipeFromBottom")
+                            mCallbacks?.onSwipeFromBottom()
+                        } else if (swipe == SWIPE_FROM_RIGHT) {
+                            if (DEBUG) Log.d(TAG, "Firing onSwipeFromRight")
+                            mCallbacks?.onSwipeFromRight()
+                        } else if (swipe == SWIPE_FROM_LEFT) {
+                            if (DEBUG) Log.d(TAG, "Firing onSwipeFromLeft")
+                            mCallbacks?.onSwipeFromLeft()
+                        }
+                    }
+                }
+            MotionEvent.ACTION_HOVER_MOVE ->
+                if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
+                    val eventX = ev.x
+                    val eventY = ev.y
+                    if (!mMouseHoveringAtLeft && eventX == 0f) {
+                        mCallbacks?.onMouseHoverAtLeft()
+                        mMouseHoveringAtLeft = true
+                    } else if (mMouseHoveringAtLeft && eventX > 0) {
+                        mCallbacks?.onMouseLeaveFromLeft()
+                        mMouseHoveringAtLeft = false
+                    }
+                    if (!mMouseHoveringAtTop && eventY == 0f) {
+                        mCallbacks?.onMouseHoverAtTop()
+                        mMouseHoveringAtTop = true
+                    } else if (mMouseHoveringAtTop && eventY > 0) {
+                        mCallbacks?.onMouseLeaveFromTop()
+                        mMouseHoveringAtTop = false
+                    }
+                    if (!mMouseHoveringAtRight && eventX >= screenWidth - 1) {
+                        mCallbacks?.onMouseHoverAtRight()
+                        mMouseHoveringAtRight = true
+                    } else if (mMouseHoveringAtRight && eventX < screenWidth - 1) {
+                        mCallbacks?.onMouseLeaveFromRight()
+                        mMouseHoveringAtRight = false
+                    }
+                    if (!mMouseHoveringAtBottom && eventY >= screenHeight - 1) {
+                        mCallbacks?.onMouseHoverAtBottom()
+                        mMouseHoveringAtBottom = true
+                    } else if (mMouseHoveringAtBottom && eventY < screenHeight - 1) {
+                        mCallbacks?.onMouseLeaveFromBottom()
+                        mMouseHoveringAtBottom = false
+                    }
+                }
+            MotionEvent.ACTION_UP,
+            MotionEvent.ACTION_CANCEL -> {
+                mSwipeFireable = false
+                mDebugFireable = false
+                mCallbacks?.onUpOrCancel()
+            }
+            else -> if (DEBUG) Log.d(TAG, "Ignoring $ev")
+        }
+    }
+
+    fun setCallbacks(callbacks: Callbacks) {
+        mCallbacks = callbacks
+    }
+
+    private fun captureDown(event: MotionEvent, pointerIndex: Int) {
+        val pointerId = event.getPointerId(pointerIndex)
+        val i = findIndex(pointerId)
+        if (DEBUG) Log.d(TAG, "pointer $pointerId down pointerIndex=$pointerIndex trackingIndex=$i")
+        if (i != UNTRACKED_POINTER) {
+            mDownX[i] = event.getX(pointerIndex)
+            mDownY[i] = event.getY(pointerIndex)
+            mDownTime[i] = event.eventTime
+            if (DEBUG)
+                Log.d(TAG, "pointer " + pointerId + " down x=" + mDownX[i] + " y=" + mDownY[i])
+        }
+    }
+
+    protected fun currentGestureStartedInRegion(r: Region): Boolean {
+        return r.contains(mDownX[0].toInt(), mDownY[0].toInt())
+    }
+
+    private fun findIndex(pointerId: Int): Int {
+        for (i in 0 until mDownPointers) {
+            if (mDownPointerId[i] == pointerId) {
+                return i
+            }
+        }
+        if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
+            return UNTRACKED_POINTER
+        }
+        mDownPointerId[mDownPointers++] = pointerId
+        return mDownPointers - 1
+    }
+
+    private fun detectTrackpadThreeFingerSwipe(move: MotionEvent): Int {
+        if (!isTrackpadThreeFingerSwipe(move)) {
+            return TRACKPAD_SWIPE_NONE
+        }
+        val dx = move.x - mDownX[0]
+        val dy = move.y - mDownY[0]
+        if (Math.abs(dx) < Math.abs(dy)) {
+            if (Math.abs(dy) > mSwipeDistanceThreshold) {
+                return if (dy > 0) TRACKPAD_SWIPE_FROM_TOP else TRACKPAD_SWIPE_FROM_BOTTOM
+            }
+        } else {
+            if (Math.abs(dx) > mSwipeDistanceThreshold) {
+                return if (dx > 0) TRACKPAD_SWIPE_FROM_LEFT else TRACKPAD_SWIPE_FROM_RIGHT
+            }
+        }
+        return TRACKPAD_SWIPE_NONE
+    }
+
+    private fun isTrackpadThreeFingerSwipe(event: MotionEvent): Boolean {
+        return (event.classification == CLASSIFICATION_MULTI_FINGER_SWIPE &&
+            event.getAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3f)
+    }
+    private fun detectSwipe(move: MotionEvent): Int {
+        val historySize = move.historySize
+        val pointerCount = move.pointerCount
+        for (p in 0 until pointerCount) {
+            val pointerId = move.getPointerId(p)
+            val i = findIndex(pointerId)
+            if (i != UNTRACKED_POINTER) {
+                for (h in 0 until historySize) {
+                    val time = move.getHistoricalEventTime(h)
+                    val x = move.getHistoricalX(p, h)
+                    val y = move.getHistoricalY(p, h)
+                    val swipe = detectSwipe(i, time, x, y)
+                    if (swipe != SWIPE_NONE) {
+                        return swipe
+                    }
+                }
+                val swipe = detectSwipe(i, move.eventTime, move.getX(p), move.getY(p))
+                if (swipe != SWIPE_NONE) {
+                    return swipe
+                }
+            }
+        }
+        return SWIPE_NONE
+    }
+
+    private fun detectSwipe(i: Int, time: Long, x: Float, y: Float): Int {
+        val fromX = mDownX[i]
+        val fromY = mDownY[i]
+        val elapsed = time - mDownTime[i]
+        if (DEBUG)
+            Log.d(
+                TAG,
+                "pointer " +
+                    mDownPointerId[i] +
+                    " moved (" +
+                    fromX +
+                    "->" +
+                    x +
+                    "," +
+                    fromY +
+                    "->" +
+                    y +
+                    ") in " +
+                    elapsed
+            )
+        if (
+            fromY <= mSwipeStartThreshold.top &&
+                y > fromY + mSwipeDistanceThreshold &&
+                elapsed < SWIPE_TIMEOUT_MS
+        ) {
+            return SWIPE_FROM_TOP
+        }
+        if (
+            fromY >= screenHeight - mSwipeStartThreshold.bottom &&
+                y < fromY - mSwipeDistanceThreshold &&
+                elapsed < SWIPE_TIMEOUT_MS
+        ) {
+            return SWIPE_FROM_BOTTOM
+        }
+        if (
+            fromX >= screenWidth - mSwipeStartThreshold.right &&
+                x < fromX - mSwipeDistanceThreshold &&
+                elapsed < SWIPE_TIMEOUT_MS
+        ) {
+            return SWIPE_FROM_RIGHT
+        }
+        return if (
+            fromX <= mSwipeStartThreshold.left &&
+                x > fromX + mSwipeDistanceThreshold &&
+                elapsed < SWIPE_TIMEOUT_MS
+        ) {
+            SWIPE_FROM_LEFT
+        } else SWIPE_NONE
+    }
+
+    fun dump(pw: PrintWriter, prefix: String) {
+        val inner = "$prefix  "
+        pw.println(prefix + TAG + ":")
+        pw.print(inner)
+        pw.print("mDisplayCutoutTouchableRegionSize=")
+        pw.println(mDisplayCutoutTouchableRegionSize)
+        pw.print(inner)
+        pw.print("mSwipeStartThreshold=")
+        pw.println(mSwipeStartThreshold)
+        pw.print(inner)
+        pw.print("mSwipeDistanceThreshold=")
+        pw.println(mSwipeDistanceThreshold)
+    }
+
+    private inner class FlingGestureDetector internal constructor() :
+        GestureDetector.SimpleOnGestureListener() {
+        private val mOverscroller: OverScroller = OverScroller(mContext)
+
+        override fun onSingleTapUp(e: MotionEvent): Boolean {
+            if (!mOverscroller.isFinished) {
+                mOverscroller.forceFinished(true)
+            }
+            return true
+        }
+
+        override fun onFling(
+            down: MotionEvent?,
+            up: MotionEvent,
+            velocityX: Float,
+            velocityY: Float
+        ): Boolean {
+            mOverscroller.computeScrollOffset()
+            val now = SystemClock.uptimeMillis()
+            if (mLastFlingTime != 0L && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
+                mOverscroller.forceFinished(true)
+            }
+            mOverscroller.fling(
+                0,
+                0,
+                velocityX.toInt(),
+                velocityY.toInt(),
+                Int.MIN_VALUE,
+                Int.MAX_VALUE,
+                Int.MIN_VALUE,
+                Int.MAX_VALUE
+            )
+            var duration = mOverscroller.duration
+            if (duration > MAX_FLING_TIME_MILLIS) {
+                duration = MAX_FLING_TIME_MILLIS
+            }
+            mLastFlingTime = now
+            mCallbacks?.onFling(duration)
+            return true
+        }
+    }
+
+    interface Callbacks {
+        fun onSwipeFromTop()
+        fun onSwipeFromBottom()
+        fun onSwipeFromRight()
+        fun onSwipeFromLeft()
+        fun onFling(durationMs: Int)
+        fun onDown()
+        fun onUpOrCancel()
+        fun onMouseHoverAtLeft()
+        fun onMouseHoverAtTop()
+        fun onMouseHoverAtRight()
+        fun onMouseHoverAtBottom()
+        fun onMouseLeaveFromLeft()
+        fun onMouseLeaveFromTop()
+        fun onMouseLeaveFromRight()
+        fun onMouseLeaveFromBottom()
+        fun onDebug()
+    }
+
+    companion object {
+        private const val TAG = "GesturePointerEventHandler"
+        private const val DEBUG = false
+        private const val SWIPE_TIMEOUT_MS: Long = 500
+        private const val MAX_TRACKED_POINTERS = 32 // max per input system
+        private const val UNTRACKED_POINTER = -1
+        private const val MAX_FLING_TIME_MILLIS = 5000
+        private const val SWIPE_NONE = 0
+        private const val SWIPE_FROM_TOP = 1
+        private const val SWIPE_FROM_BOTTOM = 2
+        private const val SWIPE_FROM_RIGHT = 3
+        private const val SWIPE_FROM_LEFT = 4
+        private const val TRACKPAD_SWIPE_NONE = 0
+        private const val TRACKPAD_SWIPE_FROM_TOP = 1
+        private const val TRACKPAD_SWIPE_FROM_BOTTOM = 2
+        private const val TRACKPAD_SWIPE_FROM_RIGHT = 3
+        private const val TRACKPAD_SWIPE_FROM_LEFT = 4
+
+        private fun <T> checkNull(name: String, arg: T?): T {
+            requireNotNull(arg) { "$name must not be null" }
+            return arg
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index c5de165..092cbf1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -388,7 +388,10 @@
         })
         ssView.setFalsingManager(falsingManager)
         ssView.setKeyguardBypassEnabled(bypassController.bypassEnabled)
-        return (ssView as View).apply { addOnAttachStateChangeListener(stateChangeListener) }
+        return (ssView as View).apply {
+            setTag(R.id.tag_smartspace_view, Any())
+            addOnAttachStateChangeListener(stateChangeListener)
+        }
     }
 
     private fun connectSession() {
@@ -450,12 +453,6 @@
         session?.requestSmartspaceUpdate()
     }
 
-    fun removeViewsFromParent(viewGroup: ViewGroup) {
-        smartspaceViews.toList().forEach {
-            viewGroup.removeView(it as View)
-        }
-    }
-
     /**
      * Disconnects the smartspace view from the smartspace service and cleans up any resources.
      */
@@ -596,3 +593,4 @@
         }
     }
 }
+
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 62a0d13..5c2f9a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -39,7 +39,10 @@
 
     override fun attach(pipeline: NotifPipeline) {
         pipeline.addOnAfterRenderListListener(::onAfterRenderList)
-        groupExpansionManagerImpl.attach(pipeline)
+        // TODO(b/282865576): This has an issue where it makes changes to some groups without
+        // notifying listeners. To be fixed in QPR, but for now let's comment it out to avoid the
+        // group expansion bug.
+        // groupExpansionManagerImpl.attach(pipeline)
     }
 
     fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index 5d33804..46af03a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -67,29 +67,18 @@
      * Cleanup entries from mExpandedGroups that no longer exist in the pipeline.
      */
     private final OnBeforeRenderListListener mNotifTracker = (entries) -> {
-        if (mExpandedGroups.isEmpty()) {
-            return; // nothing to do
-        }
-
         final Set<NotificationEntry> renderingSummaries = new HashSet<>();
         for (ListEntry entry : entries) {
             if (entry instanceof GroupEntry) {
                 renderingSummaries.add(entry.getRepresentativeEntry());
             }
         }
-
-        // Create a copy of mExpandedGroups so we can modify it in a thread-safe way.
-        final var currentExpandedGroups = new HashSet<>(mExpandedGroups);
-        for (NotificationEntry entry : currentExpandedGroups) {
-            setExpanded(entry, renderingSummaries.contains(entry));
-        }
+        mExpandedGroups.removeIf(expandedGroup -> !renderingSummaries.contains(expandedGroup));
     };
 
     public void attach(NotifPipeline pipeline) {
-        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) {
-            mDumpManager.registerDumpable(this);
-            pipeline.addOnBeforeRenderListListener(mNotifTracker);
-        }
+        mDumpManager.registerDumpable(this);
+        pipeline.addOnBeforeRenderListListener(mNotifTracker);
     }
 
     @Override
@@ -105,24 +94,11 @@
     @Override
     public void setGroupExpanded(NotificationEntry entry, boolean expanded) {
         final NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
-        setExpanded(groupSummary, expanded);
-    }
-
-    /**
-     * Add or remove {@code entry} to/from {@code mExpandedGroups} and notify listeners if
-     * something changed. This assumes that {@code entry} is a group summary.
-     * <p>
-     * TODO(b/293434635): Currently, in spite of its docs,
-     * {@code mGroupMembershipManager.getGroupSummary(entry)} returns null if {@code entry} is
-     * already a summary. Instead of needing this helper method to bypass that, we probably want to
-     * move this code back to {@code setGroupExpanded} and use that everywhere.
-     */
-    private void setExpanded(NotificationEntry entry, boolean expanded) {
         boolean changed;
         if (expanded) {
-            changed = mExpandedGroups.add(entry);
+            changed = mExpandedGroups.add(groupSummary);
         } else {
-            changed = mExpandedGroups.remove(entry);
+            changed = mExpandedGroups.remove(groupSummary);
         }
 
         // Only notify listeners if something changed.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index ea57eb4..27b8406 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -92,6 +92,8 @@
 import com.android.systemui.unfold.FoldAodAnimationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 
+import dagger.Lazy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -101,7 +103,6 @@
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
 import kotlinx.coroutines.CoroutineDispatcher;
 
 /**
@@ -450,14 +451,6 @@
         }
     }
 
-    private KeyguardStateController.Callback mKeyguardStateControllerCallback =
-            new KeyguardStateController.Callback() {
-                @Override
-                public void onUnlockedChanged() {
-                    updateAlternateBouncerShowing(mAlternateBouncerInteractor.maybeHide());
-                }
-            };
-
     private void registerListeners() {
         mKeyguardUpdateManager.registerCallback(mUpdateMonitorCallback);
         mStatusBarStateController.addCallback(this);
@@ -471,7 +464,6 @@
             mDockManager.addListener(mDockEventListener);
             mIsDocked = mDockManager.isDocked();
         }
-        mKeyguardStateController.addCallback(mKeyguardStateControllerCallback);
 
         if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) {
             mShadeViewController.postToView(() ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index cd1afc7..37f032b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -226,7 +226,7 @@
         val builder = InteractionJankMonitor.Configuration.Builder
             .withView(
                     InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD,
-                    notifShadeWindowControllerLazy.get().windowRootView
+                    checkNotNull(notifShadeWindowControllerLazy.get().windowRootView)
             )
             .setTag(statusBarStateControllerImpl.getClockId())
 
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
index 091a54f..316b54e 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
@@ -109,7 +109,6 @@
         private WallpaperManager mWallpaperManager;
         private final WallpaperLocalColorExtractor mWallpaperLocalColorExtractor;
         private SurfaceHolder mSurfaceHolder;
-        private boolean mDrawn = false;
         @VisibleForTesting
         static final int MIN_SURFACE_WIDTH = 128;
         @VisibleForTesting
@@ -239,7 +238,6 @@
 
         private void drawFrameSynchronized() {
             synchronized (mLock) {
-                if (mDrawn) return;
                 drawFrameInternal();
             }
         }
@@ -277,7 +275,6 @@
                 Rect dest = mSurfaceHolder.getSurfaceFrame();
                 try {
                     canvas.drawBitmap(bitmap, null, dest, null);
-                    mDrawn = true;
                 } finally {
                     surface.unlockCanvasAndPost(canvas);
                 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
index 98d4d22..1be8746 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
@@ -23,7 +23,6 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -136,6 +135,10 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
+        mFakeDateView.setTag(R.id.tag_smartspace_view, new Object());
+        mFakeWeatherView.setTag(R.id.tag_smartspace_view, new Object());
+        mFakeSmartspaceView.setTag(R.id.tag_smartspace_view, new Object());
+
         when(mView.findViewById(R.id.left_aligned_notification_icon_container))
                 .thenReturn(mNotificationIcons);
         when(mNotificationIcons.getLayoutParams()).thenReturn(
@@ -158,12 +161,6 @@
         when(mSmartspaceController.buildAndConnectDateView(any())).thenReturn(mFakeDateView);
         when(mSmartspaceController.buildAndConnectWeatherView(any())).thenReturn(mFakeWeatherView);
         when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
-        doAnswer(invocation -> {
-            removeView(mFakeDateView);
-            removeView(mFakeWeatherView);
-            removeView(mFakeSmartspaceView);
-            return null;
-        }).when(mSmartspaceController).removeViewsFromParent(any());
         mExecutor = new FakeExecutor(new FakeSystemClock());
         mFakeFeatureFlags = new FakeFeatureFlags();
         mFakeFeatureFlags.set(FACE_AUTH_REFACTOR, false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 4e52e64..9584d88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -39,13 +39,13 @@
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.data.repository.FakePromptRepository
 import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
 import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
 import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
-import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
 import com.android.systemui.flags.FakeFeatureFlags
@@ -109,6 +109,7 @@
     private val testScope = TestScope(StandardTestDispatcher())
     private val fakeExecutor = FakeExecutor(FakeSystemClock())
     private val biometricPromptRepository = FakePromptRepository()
+    private val fingerprintRepository = FakeFingerprintPropertyRepository()
     private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
     private val credentialInteractor = FakeCredentialInteractor()
     private val bpCredentialInteractor = PromptCredentialInteractor(
@@ -118,10 +119,12 @@
     )
     private val promptSelectorInteractor by lazy {
         PromptSelectorInteractorImpl(
+            fingerprintRepository,
             biometricPromptRepository,
             lockPatternUtils,
         )
     }
+
     private val displayStateInteractor = DisplayStateInteractorImpl(
         testScope.backgroundScope,
         mContext,
@@ -129,9 +132,7 @@
         rearDisplayStateRepository
     )
 
-    private val authBiometricFingerprintViewModel = AuthBiometricFingerprintViewModel(
-        displayStateInteractor
-    )
+
     private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor)
 
     private var authContainer: TestAuthContainerView? = null
@@ -524,10 +525,14 @@
         userManager,
         lockPatternUtils,
         interactionJankMonitor,
-        { authBiometricFingerprintViewModel },
         { promptSelectorInteractor },
         { bpCredentialInteractor },
-        PromptViewModel(promptSelectorInteractor, vibrator, featureFlags),
+        PromptViewModel(
+            displayStateInteractor,
+            promptSelectorInteractor,
+            vibrator,
+            featureFlags
+        ),
         { credentialViewModel },
         Handler(TestableLooper.get(this).looper),
         fakeExecutor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 48e5131..6d71dd5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -92,7 +92,6 @@
 import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
 import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
-import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
 import com.android.systemui.flags.FakeFeatureFlags;
@@ -177,8 +176,6 @@
     @Mock
     private PromptSelectorInteractor mPromptSelectionInteractor;
     @Mock
-    private AuthBiometricFingerprintViewModel mAuthBiometricFingerprintViewModel;
-    @Mock
     private CredentialViewModel mCredentialViewModel;
     @Mock
     private PromptViewModel mPromptViewModel;
@@ -1095,11 +1092,10 @@
                     mFingerprintManager, mFaceManager, () -> mUdfpsController,
                     () -> mSideFpsController, mDisplayManager, mWakefulnessLifecycle,
                     mPanelInteractionDetector, mUserManager, mLockPatternUtils, mUdfpsLogger,
-                    mLogContextInteractor, () -> mAuthBiometricFingerprintViewModel,
-                    () -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor,
-                    () -> mCredentialViewModel, () -> mPromptViewModel,
-                    mInteractionJankMonitor, mHandler,
-                    mBackgroundExecutor, mUdfpsUtils, mVibratorHelper);
+                    mLogContextInteractor, () -> mBiometricPromptCredentialInteractor,
+                    () -> mPromptSelectionInteractor, () -> mCredentialViewModel,
+                    () -> mPromptViewModel, mInteractionJankMonitor, mHandler, mBackgroundExecutor,
+                    mUdfpsUtils, mVibratorHelper);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
index 81cbaea..4d5e1b7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
@@ -23,6 +23,7 @@
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.data.repository.FakePromptRepository
 import com.android.systemui.biometrics.domain.model.BiometricModalities
 import com.android.systemui.biometrics.faceSensorPropertiesInternal
@@ -61,13 +62,15 @@
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
 
     private val testScope = TestScope()
+    private val fingerprintRepository = FakeFingerprintPropertyRepository()
     private val promptRepository = FakePromptRepository()
 
     private lateinit var interactor: PromptSelectorInteractor
 
     @Before
     fun setup() {
-        interactor = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
+        interactor =
+            PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt
index da55d5a..95b72d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt
@@ -28,7 +28,7 @@
 @SmallTest
 @RunWith(Parameterized::class)
 class BoundingBoxOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() {
-    val underTest = BoundingBoxOverlapDetector()
+    val underTest = BoundingBoxOverlapDetector(1f)
 
     @Test
     fun isGoodOverlap() {
@@ -83,7 +83,7 @@
         GESTURE_START
     )
 
-private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 500 /* bottom */)
+private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 400 /* bottom */)
 private val OVERLAY = Rect(0 /* left */, 100 /* top */, 400 /* right */, 600 /* bottom */)
 
 private fun genTestCases(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModelTest.kt
deleted file mode 100644
index 0c210e5..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/AuthBiometricFingerprintViewModelTest.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.android.systemui.biometrics.ui.viewmodel
-
-import android.content.res.Configuration
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
-import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
-import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class AuthBiometricFingerprintViewModelTest : SysuiTestCase() {
-
-    private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
-    private val testScope = TestScope(StandardTestDispatcher())
-    private val fakeExecutor = FakeExecutor(FakeSystemClock())
-
-    private lateinit var interactor: DisplayStateInteractor
-    private lateinit var viewModel: AuthBiometricFingerprintViewModel
-
-    @Before
-    fun setup() {
-        interactor =
-            DisplayStateInteractorImpl(
-                testScope.backgroundScope,
-                mContext,
-                fakeExecutor,
-                rearDisplayStateRepository
-            )
-        viewModel = AuthBiometricFingerprintViewModel(interactor)
-    }
-
-    @Test
-    fun iconUpdates_onConfigurationChanged() {
-        testScope.runTest {
-            runCurrent()
-            val testConfig = Configuration()
-            val folded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP - 1
-            val unfolded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP + 1
-            val currentIcon = collectLastValue(viewModel.iconAsset)
-
-            testConfig.smallestScreenWidthDp = folded
-            viewModel.onConfigurationChanged(testConfig)
-            val foldedIcon = currentIcon()
-
-            testConfig.smallestScreenWidthDp = unfolded
-            viewModel.onConfigurationChanged(testConfig)
-            val unfoldedIcon = currentIcon()
-
-            assertThat(foldedIcon).isNotEqualTo(unfoldedIcon)
-        }
-    }
-}
-
-internal const val INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP = 600
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt
new file mode 100644
index 0000000..7697c09
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt
@@ -0,0 +1,94 @@
+package com.android.systemui.biometrics.ui.viewmodel
+
+import android.content.res.Configuration
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptFingerprintIconViewModelTest : SysuiTestCase() {
+
+    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var lockPatternUtils: LockPatternUtils
+
+    private val fingerprintRepository = FakeFingerprintPropertyRepository()
+    private val promptRepository = FakePromptRepository()
+    private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
+
+    private val testScope = TestScope(StandardTestDispatcher())
+    private val fakeExecutor = FakeExecutor(FakeSystemClock())
+
+    private lateinit var promptSelectorInteractor: PromptSelectorInteractor
+    private lateinit var displayStateInteractor: DisplayStateInteractor
+    private lateinit var viewModel: PromptFingerprintIconViewModel
+
+    @Before
+    fun setup() {
+        promptSelectorInteractor =
+            PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils)
+        displayStateInteractor =
+            DisplayStateInteractorImpl(
+                testScope.backgroundScope,
+                mContext,
+                fakeExecutor,
+                rearDisplayStateRepository
+            )
+        viewModel = PromptFingerprintIconViewModel(displayStateInteractor, promptSelectorInteractor)
+    }
+
+    @Test
+    fun sfpsIconUpdates_onConfigurationChanged() {
+        testScope.runTest {
+            runCurrent()
+            configureFingerprintPropertyRepository(FingerprintSensorType.POWER_BUTTON)
+            val testConfig = Configuration()
+            val folded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP - 1
+            val unfolded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP + 1
+            val currentIcon = collectLastValue(viewModel.iconAsset)
+
+            testConfig.smallestScreenWidthDp = folded
+            viewModel.onConfigurationChanged(testConfig)
+            val foldedIcon = currentIcon()
+
+            testConfig.smallestScreenWidthDp = unfolded
+            viewModel.onConfigurationChanged(testConfig)
+            val unfoldedIcon = currentIcon()
+
+            assertThat(foldedIcon).isNotEqualTo(unfoldedIcon)
+        }
+    }
+
+    private fun configureFingerprintPropertyRepository(sensorType: FingerprintSensorType) {
+        fingerprintRepository.setProperties(0, SensorStrength.STRONG, sensorType, mapOf())
+    }
+}
+
+internal const val INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP = 600
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 4d19543..47084c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -24,7 +24,10 @@
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthBiometricView
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
 import com.android.systemui.biometrics.domain.model.BiometricModalities
@@ -37,7 +40,9 @@
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
 import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
@@ -69,8 +74,19 @@
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
     @Mock private lateinit var vibrator: VibratorHelper
 
+    private val fakeExecutor = FakeExecutor(FakeSystemClock())
     private val testScope = TestScope()
+    private val fingerprintRepository = FakeFingerprintPropertyRepository()
     private val promptRepository = FakePromptRepository()
+    private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
+
+    private val displayStateInteractor =
+        DisplayStateInteractorImpl(
+            testScope.backgroundScope,
+            mContext,
+            fakeExecutor,
+            rearDisplayStateRepository
+        )
 
     private lateinit var selector: PromptSelectorInteractor
     private lateinit var viewModel: PromptViewModel
@@ -78,10 +94,11 @@
 
     @Before
     fun setup() {
-        selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
+        selector =
+            PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils)
         selector.resetPrompt()
 
-        viewModel = PromptViewModel(selector, vibrator, featureFlags)
+        viewModel = PromptViewModel(displayStateInteractor, selector, vibrator, featureFlags)
         featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
     }
 
@@ -105,7 +122,7 @@
             }
             assertThat(message).isEqualTo(PromptMessage.Empty)
             assertThat(size).isEqualTo(expectedSize)
-            assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN)
+            assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_IDLE)
 
             val startMessage = "here we go"
             viewModel.showAuthenticating(startMessage, isRetry = false)
@@ -295,10 +312,13 @@
             assertThat(message).isEqualTo(PromptMessage.Empty)
             assertThat(messageVisible).isFalse()
         }
+        val clearIconError = !restart
         assertThat(legacyState)
             .isEqualTo(
                 if (restart) {
                     AuthBiometricView.STATE_AUTHENTICATING
+                } else if (clearIconError) {
+                    AuthBiometricView.STATE_IDLE
                 } else {
                     AuthBiometricView.STATE_HELP
                 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
index 8236165..d4bba72 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
 import com.android.systemui.bouncer.ui.BouncerView
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.DismissCallbackRegistry
@@ -156,4 +158,24 @@
         assertThat(isShowing).isEqualTo(false)
         job.cancel()
     }
+
+    @Test
+    fun keyguardPosition_noValueSet_emptyByDefault() = runTest {
+        val positionValues by collectValues(underTest.keyguardPosition)
+
+        runCurrent()
+
+        assertThat(positionValues).isEmpty()
+    }
+
+    @Test
+    fun keyguardPosition_valueSet_returnsValue() = runTest {
+        val position by collectLastValue(underTest.keyguardPosition)
+        runCurrent()
+
+        repository.setKeyguardPosition(123f)
+        runCurrent()
+
+        assertThat(position).isEqualTo(123f)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
index e0ae0c3..a3f7fc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
@@ -131,12 +131,14 @@
     }
 
     @Test
-    fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_showsPrimaryBouncer() {
+    fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_collapsesShade() {
         keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_MENU)
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(shadeController).animateCollapseShadeForced()
     }
 
     @Test
@@ -145,48 +147,42 @@
         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_MENU)
+        // action down: does NOT collapse the shade
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+
+        // action up: collapses the shade
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(shadeController).animateCollapseShadeForced()
     }
 
     @Test
-    fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_doNothing() {
+    fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_neverCollapsesShade() {
         keyguardInteractorWithDependencies.repository.setWakefulnessModel(asleepWakefulnessMode)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        verifyActionsDoNothing(KeyEvent.KEYCODE_MENU)
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
     }
 
     @Test
-    fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_showsPrimaryBouncer() {
+    fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_collapsesShade() {
         keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
 
-        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_SPACE)
-    }
+        // action down: does NOT collapse the shade
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
 
-    @Test
-    fun dispatchKeyEvent_spaceActionUp_shadeLocked_collapsesShade() {
-        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
-        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
-
-        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_SPACE)
-    }
-
-    @Test
-    fun dispatchKeyEvent_enterActionUp_interactiveKeyguard_showsPrimaryBouncer() {
-        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
-        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
-
-        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_ENTER)
-    }
-
-    @Test
-    fun dispatchKeyEvent_enterActionUp_shadeLocked_collapsesShade() {
-        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
-        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
-
-        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_ENTER)
+        // action up: collapses the shade
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SPACE)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(shadeController).animateCollapseShadeForced()
     }
 
     @Test
@@ -256,42 +252,4 @@
             .isFalse()
         verify(statusBarKeyguardViewManager, never()).interceptMediaKey(any())
     }
-
-    private fun verifyActionUpCollapsesTheShade(keycode: Int) {
-        // action down: does NOT collapse the shade
-        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
-        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
-
-        // action up: collapses the shade
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(shadeController).animateCollapseShadeForced()
-    }
-
-    private fun verifyActionUpShowsPrimaryBouncer(keycode: Int) {
-        // action down: does NOT collapse the shade
-        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
-        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
-        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
-
-        // action up: collapses the shade
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(statusBarKeyguardViewManager).showPrimaryBouncer(eq(true))
-    }
-
-    private fun verifyActionsDoNothing(keycode: Int) {
-        // action down: does nothing
-        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
-        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
-        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
-
-        // action up: doesNothing
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
-        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index 23f243c..a9f288d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -17,10 +17,8 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import androidx.test.filters.SmallTest
-import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.model.AuthenticationMethodModel
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
@@ -48,7 +46,6 @@
 
     private val underTest =
         LockscreenSceneViewModel(
-            applicationScope = testScope.backgroundScope,
             authenticationInteractor = authenticationInteractor,
             bouncerInteractor =
                 utils.bouncerInteractor(
@@ -58,32 +55,6 @@
         )
 
     @Test
-    fun lockButtonIcon_whenLocked() =
-        testScope.runTest {
-            val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
-            utils.authenticationRepository.setUnlocked(false)
-
-            assertThat((lockButtonIcon as? Icon.Resource)?.res)
-                .isEqualTo(R.drawable.ic_device_lock_on)
-        }
-
-    @Test
-    fun lockButtonIcon_whenUnlocked() =
-        testScope.runTest {
-            val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
-            utils.authenticationRepository.setUnlocked(true)
-
-            assertThat((lockButtonIcon as? Icon.Resource)?.res)
-                .isEqualTo(R.drawable.ic_device_lock_off)
-        }
-
-    @Test
     fun upTransitionSceneKey_canSwipeToUnlock_gone() =
         testScope.runTest {
             val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
@@ -120,32 +91,6 @@
         }
 
     @Test
-    fun onContentClicked_deviceUnlocked_switchesToGone() =
-        testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(true)
-            runCurrent()
-
-            underTest.onContentClicked()
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
-        }
-
-    @Test
-    fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
-        testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(false)
-            runCurrent()
-
-            underTest.onContentClicked()
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-        }
-
-    @Test
     fun onLockButtonClicked_deviceUnlocked_switchesToGone() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 49ece66..ef07fab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -31,7 +31,6 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
-import com.android.systemui.keyguard.ScreenLifecycle
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.model.SysUiState
 import com.android.systemui.navigationbar.NavigationBarController
@@ -84,7 +83,6 @@
     private val fakeSystemClock = FakeSystemClock()
     private val sysUiState = SysUiState(displayTracker)
     private val featureFlags = FakeFeatureFlags()
-    private val screenLifecycle = ScreenLifecycle(dumpManager)
     private val wakefulnessLifecycle =
         WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager)
 
@@ -142,7 +140,6 @@
                 sysUiState,
                 mock(),
                 userTracker,
-                screenLifecycle,
                 wakefulnessLifecycle,
                 uiEventLogger,
                 displayTracker,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 53c04cc..46cbfac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -116,7 +116,6 @@
 
     private val lockscreenSceneViewModel =
         LockscreenSceneViewModel(
-            applicationScope = testScope.backgroundScope,
             authenticationInteractor = authenticationInteractor,
             bouncerInteractor = bouncerInteractor,
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index ecaf137..48665fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -340,9 +340,9 @@
     @Test
     fun knownPluginAttached_clockAndListChanged_notLoaded() {
         val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
-        whenever(mockPluginLifecycle1.getPackage()).thenReturn("com.android.systemui.falcon.one")
+        whenever(mockPluginLifecycle1.getPackage()).thenReturn("com.android.systemui.clocks.metro")
         val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
-        whenever(mockPluginLifecycle2.getPackage()).thenReturn("com.android.systemui.falcon.two")
+        whenever(mockPluginLifecycle2.getPackage()).thenReturn("com.android.systemui.clocks.bignum")
 
         var changeCallCount = 0
         var listChangeCallCount = 0
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt
index 9393a4f..ee3d870 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt
@@ -60,18 +60,4 @@
 
     assertThat(mController.canShowRotationButton()).isTrue()
   }
-
-  @Test
-  fun ifTaskbarVisible_showRotationSuggestion() {
-    mController.onNavigationBarWindowVisibilityChange( /* showing = */ false)
-    mController.onBehaviorChanged(Display.DEFAULT_DISPLAY,
-                                    WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE)
-    mController.onNavigationModeChanged(WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON)
-    mController.onTaskbarStateChange( /* visible = */ false, /* stashed = */ false)
-    assertThat(mController.canShowRotationButton()).isFalse()
-
-    mController.onTaskbarStateChange( /* visible = */ true, /* stashed = */ false)
-
-    assertThat(mController.canShowRotationButton()).isTrue()
-  }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
index 38a8f414..4a94dc8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -21,21 +21,11 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
-import com.android.systemui.statusbar.notification.collection.ListEntry
-import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
-import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener
-import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.withArgCaptor
 import org.junit.Assert
 import org.junit.Before
 import org.junit.Test
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
@@ -46,43 +36,13 @@
     private val groupMembershipManager: GroupMembershipManager = mock()
     private val featureFlags = FakeFeatureFlags()
 
-    private val pipeline: NotifPipeline = mock()
-    private lateinit var beforeRenderListListener: OnBeforeRenderListListener
-
-    private val summary1 = notificationEntry("foo", 1)
-    private val summary2 = notificationEntry("bar", 1)
-    private val entries =
-        listOf<ListEntry>(
-            GroupEntryBuilder()
-                .setSummary(summary1)
-                .setChildren(
-                    listOf(
-                        notificationEntry("foo", 2),
-                        notificationEntry("foo", 3),
-                        notificationEntry("foo", 4)
-                    )
-                )
-                .build(),
-            GroupEntryBuilder()
-                .setSummary(summary2)
-                .setChildren(
-                    listOf(
-                        notificationEntry("bar", 2),
-                        notificationEntry("bar", 3),
-                        notificationEntry("bar", 4)
-                    )
-                )
-                .build(),
-            notificationEntry("baz", 1)
-        )
-
-    private fun notificationEntry(pkg: String, id: Int) =
-        NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { row = mock() }
+    private val entry1 = NotificationEntryBuilder().build()
+    private val entry2 = NotificationEntryBuilder().build()
 
     @Before
     fun setUp() {
-        whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1)
-        whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2)
+        whenever(groupMembershipManager.getGroupSummary(entry1)).thenReturn(entry1)
+        whenever(groupMembershipManager.getGroupSummary(entry2)).thenReturn(entry2)
 
         gem = GroupExpansionManagerImpl(dumpManager, groupMembershipManager, featureFlags)
     }
@@ -94,15 +54,15 @@
         var listenerCalledCount = 0
         gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
 
-        gem.setGroupExpanded(summary1, false)
+        gem.setGroupExpanded(entry1, false)
         Assert.assertEquals(0, listenerCalledCount)
-        gem.setGroupExpanded(summary1, true)
+        gem.setGroupExpanded(entry1, true)
         Assert.assertEquals(1, listenerCalledCount)
-        gem.setGroupExpanded(summary2, true)
+        gem.setGroupExpanded(entry2, true)
         Assert.assertEquals(2, listenerCalledCount)
-        gem.setGroupExpanded(summary1, true)
+        gem.setGroupExpanded(entry1, true)
         Assert.assertEquals(2, listenerCalledCount)
-        gem.setGroupExpanded(summary2, false)
+        gem.setGroupExpanded(entry2, false)
         Assert.assertEquals(3, listenerCalledCount)
     }
 
@@ -113,39 +73,15 @@
         var listenerCalledCount = 0
         gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
 
-        gem.setGroupExpanded(summary1, false)
+        gem.setGroupExpanded(entry1, false)
         Assert.assertEquals(1, listenerCalledCount)
-        gem.setGroupExpanded(summary1, true)
+        gem.setGroupExpanded(entry1, true)
         Assert.assertEquals(2, listenerCalledCount)
-        gem.setGroupExpanded(summary2, true)
+        gem.setGroupExpanded(entry2, true)
         Assert.assertEquals(3, listenerCalledCount)
-        gem.setGroupExpanded(summary1, true)
+        gem.setGroupExpanded(entry1, true)
         Assert.assertEquals(4, listenerCalledCount)
-        gem.setGroupExpanded(summary2, false)
+        gem.setGroupExpanded(entry2, false)
         Assert.assertEquals(5, listenerCalledCount)
     }
-
-    @Test
-    fun testSyncWithPipeline() {
-        featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
-        gem.attach(pipeline)
-        beforeRenderListListener = withArgCaptor {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
-
-        val listener: OnGroupExpansionChangeListener = mock()
-        gem.registerGroupExpansionChangeListener(listener)
-
-        beforeRenderListListener.onBeforeRenderList(entries)
-        verify(listener, never()).onGroupExpansionChange(any(), any())
-
-        // Expand one of the groups.
-        gem.setGroupExpanded(summary1, true)
-        verify(listener).onGroupExpansionChange(summary1.row, true)
-
-        // Empty the pipeline list and verify that the group is no longer expanded.
-        beforeRenderListListener.onBeforeRenderList(emptyList())
-        verify(listener).onGroupExpansionChange(summary1.row, false)
-        verifyNoMoreInteractions(listener)
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
new file mode 100644
index 0000000..94ed608
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.wmshell
+
+import android.content.ContentResolver
+import android.content.Context
+import android.content.SharedPreferences
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.model.SysUiStateTest
+import com.android.wm.shell.bubbles.Bubble
+import com.android.wm.shell.bubbles.BubbleEducationController
+import com.android.wm.shell.bubbles.PREF_MANAGED_EDUCATION
+import com.android.wm.shell.bubbles.PREF_STACK_EDUCATION
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BubbleEducationControllerTest : SysUiStateTest() {
+    private val sharedPrefsEditor = Mockito.mock(SharedPreferences.Editor::class.java)
+    private val sharedPrefs = Mockito.mock(SharedPreferences::class.java)
+    private val context = Mockito.mock(Context::class.java)
+    private lateinit var sut: BubbleEducationController
+
+    @Before
+    fun setUp() {
+        Mockito.`when`(context.packageName).thenReturn("packageName")
+        Mockito.`when`(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs)
+        Mockito.`when`(context.contentResolver)
+            .thenReturn(Mockito.mock(ContentResolver::class.java))
+        Mockito.`when`(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
+        sut = BubbleEducationController(context)
+    }
+
+    @Test
+    fun testSeenStackEducation_read() {
+        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true)
+        assertEquals(sut.hasSeenStackEducation, true)
+        Mockito.verify(sharedPrefs).getBoolean(PREF_STACK_EDUCATION, false)
+    }
+
+    @Test
+    fun testSeenStackEducation_write() {
+        sut.hasSeenStackEducation = true
+        Mockito.verify(sharedPrefsEditor).putBoolean(PREF_STACK_EDUCATION, true)
+    }
+
+    @Test
+    fun testSeenManageEducation_read() {
+        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true)
+        assertEquals(sut.hasSeenManageEducation, true)
+        Mockito.verify(sharedPrefs).getBoolean(PREF_MANAGED_EDUCATION, false)
+    }
+
+    @Test
+    fun testSeenManageEducation_write() {
+        sut.hasSeenManageEducation = true
+        Mockito.verify(sharedPrefsEditor).putBoolean(PREF_MANAGED_EDUCATION, true)
+    }
+
+    @Test
+    fun testShouldShowStackEducation() {
+        val bubble = Mockito.mock(Bubble::class.java)
+        // When bubble is null
+        assertEquals(sut.shouldShowStackEducation(null), false)
+        // When bubble is not conversation
+        Mockito.`when`(bubble.isConversation).thenReturn(false)
+        assertEquals(sut.shouldShowStackEducation(bubble), false)
+        // When bubble is conversation and has seen stack edu
+        Mockito.`when`(bubble.isConversation).thenReturn(true)
+        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true)
+        assertEquals(sut.shouldShowStackEducation(bubble), false)
+        // When bubble is conversation and has not seen stack edu
+        Mockito.`when`(bubble.isConversation).thenReturn(true)
+        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(false)
+        assertEquals(sut.shouldShowStackEducation(bubble), true)
+    }
+
+    @Test
+    fun testShouldShowManageEducation() {
+        val bubble = Mockito.mock(Bubble::class.java)
+        // When bubble is null
+        assertEquals(sut.shouldShowManageEducation(null), false)
+        // When bubble is not conversation
+        Mockito.`when`(bubble.isConversation).thenReturn(false)
+        assertEquals(sut.shouldShowManageEducation(bubble), false)
+        // When bubble is conversation and has seen stack edu
+        Mockito.`when`(bubble.isConversation).thenReturn(true)
+        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true)
+        assertEquals(sut.shouldShowManageEducation(bubble), false)
+        // When bubble is conversation and has not seen stack edu
+        Mockito.`when`(bubble.isConversation).thenReturn(true)
+        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(false)
+        assertEquals(sut.shouldShowManageEducation(bubble), true)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
index 10529e6..0847c85 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
@@ -21,7 +21,7 @@
     override val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
     private val _panelExpansionAmount = MutableStateFlow(KeyguardBouncerConstants.EXPANSION_HIDDEN)
     override val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
-    private val _keyguardPosition = MutableStateFlow(0f)
+    private val _keyguardPosition = MutableStateFlow<Float?>(null)
     override val keyguardPosition = _keyguardPosition.asStateFlow()
     private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
     override val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index d5aee92..4c137bc 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -132,7 +132,8 @@
 import com.android.server.backup.transport.TransportNotAvailableException;
 import com.android.server.backup.transport.TransportNotRegisteredException;
 import com.android.server.backup.utils.BackupEligibilityRules;
-import com.android.server.backup.utils.BackupManagerMonitorUtils;
+import com.android.server.backup.utils.BackupManagerMonitorDumpsysUtils;
+import com.android.server.backup.utils.BackupManagerMonitorEventSender;
 import com.android.server.backup.utils.BackupObserverUtils;
 import com.android.server.backup.utils.SparseArrayUtils;
 
@@ -141,6 +142,7 @@
 import com.google.android.collect.Sets;
 
 import java.io.BufferedInputStream;
+import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -149,6 +151,7 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.RandomAccessFile;
@@ -1830,12 +1833,14 @@
      */
     public int requestBackup(String[] packages, IBackupObserver observer,
             IBackupManagerMonitor monitor, int flags) {
+        BackupManagerMonitorEventSender  mBackupManagerMonitorEventSender =
+                getBMMEventSender(monitor);
         mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "requestBackup");
 
         if (packages == null || packages.length < 1) {
             Slog.e(TAG, addUserIdToLogMessage(mUserId, "No packages named for backup request"));
             BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
-            monitor = BackupManagerMonitorUtils.monitorEvent(monitor,
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_NO_PACKAGES,
                     null, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null);
             throw new IllegalArgumentException("No packages are provided for backup");
@@ -1853,7 +1858,7 @@
             final int logTag = mSetupComplete
                     ? BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED
                     : BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
-            monitor = BackupManagerMonitorUtils.monitorEvent(monitor, logTag, null,
+            mBackupManagerMonitorEventSender.monitorEvent(logTag, null,
                     BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
             return BackupManager.ERROR_BACKUP_NOT_ALLOWED;
         }
@@ -1871,7 +1876,7 @@
         } catch (TransportNotRegisteredException | TransportNotAvailableException
                 | RemoteException e) {
             BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
-            monitor = BackupManagerMonitorUtils.monitorEvent(monitor,
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_IS_NULL,
                     null, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null);
             return BackupManager.ERROR_TRANSPORT_ABORTED;
@@ -3066,7 +3071,9 @@
                     /* caller */ "BMS.reportDelayedRestoreResult");
 
             IBackupManagerMonitor monitor = transportClient.getBackupManagerMonitor();
-            BackupManagerMonitorUtils.sendAgentLoggingResults(monitor, packageInfo, results,
+            BackupManagerMonitorEventSender  mBackupManagerMonitorEventSender =
+                    getBMMEventSender(monitor);
+            mBackupManagerMonitorEventSender.sendAgentLoggingResults(packageInfo, results,
                     BackupAnnotations.OperationType.RESTORE);
         } catch (NameNotFoundException | TransportNotAvailableException
                 | TransportNotRegisteredException | RemoteException e) {
@@ -3190,6 +3197,11 @@
         }
     }
 
+    @VisibleForTesting
+    BackupManagerMonitorEventSender getBMMEventSender(IBackupManagerMonitor monitor) {
+        return new BackupManagerMonitorEventSender(monitor);
+    }
+
     /** User-configurable enabling/disabling of backups. */
     public void setBackupEnabled(boolean enable) {
         setBackupEnabled(enable, /* persistToDisk */ true);
@@ -4148,6 +4160,7 @@
                 }
             }
             dumpInternal(pw);
+            dumpBMMEvents(pw);
         } finally {
             Binder.restoreCallingIdentity(identityToken);
         }
@@ -4165,6 +4178,23 @@
         }
     }
 
+    private void dumpBMMEvents(PrintWriter pw) {
+        BackupManagerMonitorDumpsysUtils bm =
+                new BackupManagerMonitorDumpsysUtils();
+        File events = bm.getBMMEventsFile();
+        pw.println("START OF BACKUP MANAGER MONITOR EVENTS");
+        try (BufferedReader reader = new BufferedReader(new FileReader(events))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                pw.println(line);
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "IO Exception when reading BMM events from file: " + e);
+            pw.println("IO Exception when reading BMM events from file");
+        }
+        pw.println("END OF BACKUP MANAGER MONITOR EVENTS");
+    }
+
     @NeverCompile // Avoid size overhead of debugging code.
     private void dumpInternal(PrintWriter pw) {
         // Add prefix for only non-system users so that system user dumpsys is the same as before
diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
index ad29422..1271206 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
@@ -23,13 +23,11 @@
 import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
 import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
 
-import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.ApplicationThreadConstants;
 import android.app.IBackupAgent;
 import android.app.backup.BackupTransport;
 import android.app.backup.FullBackupDataOutput;
-import android.app.backup.IBackupManagerMonitor;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -44,7 +42,7 @@
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.remote.RemoteCall;
 import com.android.server.backup.utils.BackupEligibilityRules;
-import com.android.server.backup.utils.BackupManagerMonitorUtils;
+import com.android.server.backup.utils.BackupManagerMonitorEventSender;
 import com.android.server.backup.utils.FullBackupUtils;
 
 import java.io.File;
@@ -69,7 +67,7 @@
     private final int mTransportFlags;
     private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
     private final BackupEligibilityRules mBackupEligibilityRules;
-    @Nullable private final IBackupManagerMonitor mMonitor;
+    private final BackupManagerMonitorEventSender mBackupManagerMonitorEventSender;
 
     class FullBackupRunner implements Runnable {
         private final @UserIdInt int mUserId;
@@ -198,7 +196,7 @@
             int opToken,
             int transportFlags,
             BackupEligibilityRules backupEligibilityRules,
-            IBackupManagerMonitor monitor) {
+            BackupManagerMonitorEventSender backupManagerMonitorEventSender) {
         this.backupManagerService = backupManagerService;
         mOutput = output;
         mPreflightHook = preflightHook;
@@ -213,7 +211,7 @@
                         backupManagerService.getAgentTimeoutParameters(),
                         "Timeout parameters cannot be null");
         mBackupEligibilityRules = backupEligibilityRules;
-        mMonitor = monitor;
+        mBackupManagerMonitorEventSender = backupManagerMonitorEventSender;
     }
 
     public int preflightCheck() throws RemoteException {
@@ -270,7 +268,7 @@
                     result = BackupTransport.TRANSPORT_OK;
                 }
 
-                BackupManagerMonitorUtils.monitorAgentLoggingResults(mMonitor, mPkg, mAgent);
+                mBackupManagerMonitorEventSender.monitorAgentLoggingResults(mPkg, mAgent);
             } catch (IOException e) {
                 Slog.e(TAG, "Error backing up " + mPkg.packageName + ": " + e.getMessage());
                 result = BackupTransport.AGENT_ERROR;
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
index cba1e29..dc67091 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
@@ -40,6 +40,7 @@
 import com.android.server.backup.OperationStorage;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.utils.BackupEligibilityRules;
+import com.android.server.backup.utils.BackupManagerMonitorEventSender;
 import com.android.server.backup.utils.PasswordUtils;
 
 import java.io.ByteArrayOutputStream;
@@ -421,7 +422,7 @@
                                 mCurrentOpToken,
                                 /*transportFlags=*/ 0,
                                 mBackupEligibilityRules,
-                                /* monitor= */ null);
+                                new BackupManagerMonitorEventSender(null));
                 sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName);
 
                 // Don't need to check preflight result as there is no preflight hook.
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index 162046a..6aed9aa 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -54,7 +54,7 @@
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.transport.TransportNotAvailableException;
 import com.android.server.backup.utils.BackupEligibilityRules;
-import com.android.server.backup.utils.BackupManagerMonitorUtils;
+import com.android.server.backup.utils.BackupManagerMonitorEventSender;
 import com.android.server.backup.utils.BackupObserverUtils;
 
 import com.google.android.collect.Sets;
@@ -153,7 +153,6 @@
     CountDownLatch mLatch;
     FullBackupJob mJob;             // if a scheduled job needs to be finished afterwards
     IBackupObserver mBackupObserver;
-    @Nullable private IBackupManagerMonitor mMonitor;
     boolean mUserInitiated;
     SinglePackageBackupRunner mBackupRunner;
     private final int mBackupRunnerOpToken;
@@ -167,6 +166,7 @@
     private final int mCurrentOpToken;
     private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
     private final BackupEligibilityRules mBackupEligibilityRules;
+    private BackupManagerMonitorEventSender mBackupManagerMonitorEventSender;
 
     public PerformFullTransportBackupTask(UserBackupManagerService backupManagerService,
             OperationStorage operationStorage,
@@ -185,11 +185,12 @@
         mJob = runningJob;
         mPackages = new ArrayList<>(whichPackages.length);
         mBackupObserver = backupObserver;
-        mMonitor = monitor;
         mListener = (listener != null) ? listener : OnTaskFinishedListener.NOP;
         mUserInitiated = userInitiated;
         mCurrentOpToken = backupManagerService.generateRandomIntegerToken();
         mBackupRunnerOpToken = backupManagerService.generateRandomIntegerToken();
+        mBackupManagerMonitorEventSender =
+                new BackupManagerMonitorEventSender(monitor);
         mAgentTimeoutParameters = Objects.requireNonNull(
                 backupManagerService.getAgentTimeoutParameters(),
                 "Timeout parameters cannot be null");
@@ -218,7 +219,7 @@
                     if (MORE_DEBUG) {
                         Slog.d(TAG, "Ignoring ineligible package " + pkg);
                     }
-                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    mBackupManagerMonitorEventSender.monitorEvent(
                             BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE,
                             mCurrentPackage,
                             BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -233,7 +234,7 @@
                         Slog.d(TAG, "Ignoring full-data backup of key/value participant "
                                 + pkg);
                     }
-                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    mBackupManagerMonitorEventSender.monitorEvent(
                             BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT,
                             mCurrentPackage,
                             BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -248,7 +249,7 @@
                     if (MORE_DEBUG) {
                         Slog.d(TAG, "Ignoring stopped package " + pkg);
                     }
-                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    mBackupManagerMonitorEventSender.monitorEvent(
                             BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED,
                             mCurrentPackage,
                             BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -260,7 +261,7 @@
                 mPackages.add(info);
             } catch (NameNotFoundException e) {
                 Slog.i(TAG, "Requested package " + pkg + " not found; ignoring");
-                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND,
                         mCurrentPackage,
                         BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -356,8 +357,8 @@
                 } else {
                     monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
                 }
-                mMonitor = BackupManagerMonitorUtils
-                        .monitorEvent(mMonitor, monitoringEvent, null,
+                mBackupManagerMonitorEventSender
+                        .monitorEvent(monitoringEvent, null,
                                 BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                                 null);
                 mUpdateSchedule = false;
@@ -369,7 +370,7 @@
             if (transport == null) {
                 Slog.w(TAG, "Transport not present; full data backup not performed");
                 backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
-                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT,
                         mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
                         null);
@@ -378,9 +379,10 @@
 
             // In some cases there may not be a monitor passed in when creating this task. So, if we
             // don't have one already we ask the transport for a monitor.
-            if (mMonitor == null) {
+            if (mBackupManagerMonitorEventSender.getMonitor() == null) {
                 try {
-                    mMonitor = transport.getBackupManagerMonitor();
+                    mBackupManagerMonitorEventSender
+                            .setMonitor(transport.getBackupManagerMonitor());
                 } catch (RemoteException e) {
                     Slog.i(TAG, "Failed to retrieve monitor from transport");
                 }
@@ -457,11 +459,11 @@
                                     + packageName + ": " + preflightResult
                                     + ", not running backup.");
                         }
-                        mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                        mBackupManagerMonitorEventSender.monitorEvent(
                                 BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT,
                                 mCurrentPackage,
                                 BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                                BackupManagerMonitorUtils.putMonitoringExtra(null,
+                                mBackupManagerMonitorEventSender.putMonitoringExtra(null,
                                         BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR,
                                         preflightResult));
                         backupPackageStatus = (int) preflightResult;
@@ -492,7 +494,7 @@
                         if (backupPackageStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
                             Slog.w(TAG, "Package hit quota limit in-flight " + packageName
                                     + ": " + totalRead + " of " + quota);
-                            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                            mBackupManagerMonitorEventSender.monitorEvent(
                                     BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT,
                                     mCurrentPackage,
                                     BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
@@ -647,11 +649,11 @@
         } catch (Exception e) {
             backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
             Slog.w(TAG, "Exception trying full transport backup", e);
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP,
                     mCurrentPackage,
                     BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                    BackupManagerMonitorUtils.putMonitoringExtra(null,
+                    mBackupManagerMonitorEventSender.putMonitoringExtra(null,
                             BackupManagerMonitor.EXTRA_LOG_EXCEPTION_FULL_BACKUP,
                             Log.getStackTraceString(e)));
 
@@ -885,7 +887,7 @@
                             mCurrentOpToken,
                             mTransportFlags,
                             mBackupEligibilityRules,
-                            mMonitor);
+                            mBackupManagerMonitorEventSender);
             try {
                 try {
                     if (!mIsCancelled) {
@@ -967,7 +969,7 @@
                 Slog.w(TAG, "Full backup cancel of " + mTarget.packageName);
             }
 
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL,
                     mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
             mIsCancelled = true;
diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java
index 4632cb0..20c8cf6 100644
--- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java
+++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java
@@ -32,7 +32,7 @@
 import com.android.server.backup.DataChangedJournal;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.remote.RemoteResult;
-import com.android.server.backup.utils.BackupManagerMonitorUtils;
+import com.android.server.backup.utils.BackupManagerMonitorEventSender;
 import com.android.server.backup.utils.BackupObserverUtils;
 
 import java.io.File;
@@ -65,21 +65,21 @@
 
     private final UserBackupManagerService mBackupManagerService;
     private final IBackupObserver mObserver;
-    @Nullable private IBackupManagerMonitor mMonitor;
+    private final BackupManagerMonitorEventSender mBackupManagerMonitorEventSender;
 
     KeyValueBackupReporter(
             UserBackupManagerService backupManagerService,
             IBackupObserver observer,
-            @Nullable IBackupManagerMonitor monitor) {
+            BackupManagerMonitorEventSender backupManagerMonitorEventSender) {
         mBackupManagerService = backupManagerService;
         mObserver = observer;
-        mMonitor = monitor;
+        mBackupManagerMonitorEventSender = backupManagerMonitorEventSender;
     }
 
     /** Returns the monitor or {@code null} if we lost connection to it. */
     @Nullable
     IBackupManagerMonitor getMonitor() {
-        return mMonitor;
+        return mBackupManagerMonitorEventSender.getMonitor();
     }
 
     IBackupObserver getObserver() {
@@ -208,13 +208,11 @@
     void onAgentIllegalKey(PackageInfo packageInfo, String key) {
         String packageName = packageInfo.packageName;
         EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName, "bad key");
-        mMonitor =
-                BackupManagerMonitorUtils.monitorEvent(
-                        mMonitor,
+        mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_ILLEGAL_KEY,
                         packageInfo,
                         BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                        BackupManagerMonitorUtils.putMonitoringExtra(
+                        mBackupManagerMonitorEventSender.putMonitoringExtra(
                                 null, BackupManagerMonitor.EXTRA_LOG_ILLEGAL_KEY, key));
         BackupObserverUtils.sendBackupOnPackageResult(
                 mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE);
@@ -254,13 +252,11 @@
         if (MORE_DEBUG) {
             Slog.i(TAG, "No backup data written, not calling transport");
         }
-        mMonitor =
-                BackupManagerMonitorUtils.monitorEvent(
-                        mMonitor,
-                        BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND,
-                        packageInfo,
-                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                        null);
+        mBackupManagerMonitorEventSender.monitorEvent(
+                BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND,
+                packageInfo,
+                BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                null);
     }
 
     void onPackageBackupComplete(String packageName, long size) {
@@ -291,8 +287,7 @@
 
     void onPackageBackupNonIncrementalRequired(PackageInfo packageInfo) {
         Slog.i(TAG, "Transport lost data, retrying package");
-        BackupManagerMonitorUtils.monitorEvent(
-                mMonitor,
+        mBackupManagerMonitorEventSender.monitorEvent(
                 BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED,
                 packageInfo,
                 BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
@@ -335,28 +330,24 @@
         EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName);
         // Time-out used to be implemented as cancel w/ cancelAll = false.
         // TODO: Change monitoring event to reflect time-out as an event itself.
-        mMonitor =
-                BackupManagerMonitorUtils.monitorEvent(
-                        mMonitor,
-                        BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL,
-                        packageInfo,
-                        BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
-                        BackupManagerMonitorUtils.putMonitoringExtra(
-                                null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, false));
+        mBackupManagerMonitorEventSender.monitorEvent(
+                BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL,
+                packageInfo,
+                BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+                mBackupManagerMonitorEventSender.putMonitoringExtra(
+                        null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, false));
     }
 
     void onAgentCancelled(@Nullable PackageInfo packageInfo) {
         String packageName = getPackageName(packageInfo);
         Slog.i(TAG, "Cancel backing up " + packageName);
         EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName);
-        mMonitor =
-                BackupManagerMonitorUtils.monitorEvent(
-                        mMonitor,
-                        BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL,
-                        packageInfo,
-                        BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
-                        BackupManagerMonitorUtils.putMonitoringExtra(
-                                null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, true));
+        mBackupManagerMonitorEventSender.monitorEvent(
+                BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL,
+                packageInfo,
+                BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+                mBackupManagerMonitorEventSender.putMonitoringExtra(
+                        null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL, true));
     }
 
     void onAgentResultError(@Nullable PackageInfo packageInfo) {
diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
index 41e8092..3a6e1ca 100644
--- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
+++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
@@ -68,7 +68,7 @@
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.transport.TransportNotAvailableException;
 import com.android.server.backup.utils.BackupEligibilityRules;
-import com.android.server.backup.utils.BackupManagerMonitorUtils;
+import com.android.server.backup.utils.BackupManagerMonitorEventSender;
 
 import libcore.io.IoUtils;
 
@@ -225,7 +225,8 @@
             boolean nonIncremental,
             BackupEligibilityRules backupEligibilityRules) {
         KeyValueBackupReporter reporter =
-                new KeyValueBackupReporter(backupManagerService, observer, monitor);
+                new KeyValueBackupReporter(backupManagerService, observer,
+                        new BackupManagerMonitorEventSender(monitor));
         KeyValueBackupTask task =
                 new KeyValueBackupTask(
                         backupManagerService,
@@ -698,8 +699,9 @@
 
         try {
             extractAgentData(mCurrentPackage);
-            BackupManagerMonitorUtils.monitorAgentLoggingResults(
-                    mReporter.getMonitor(), mCurrentPackage, mAgent);
+            BackupManagerMonitorEventSender mBackupManagerMonitorEventSender =
+                    new BackupManagerMonitorEventSender(mReporter.getMonitor());
+            mBackupManagerMonitorEventSender.monitorAgentLoggingResults(mCurrentPackage, mAgent);
             int status = sendDataToTransport(mCurrentPackage);
             cleanUpAgentForTransportStatus(status);
         } catch (AgentException | TaskException e) {
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 8cbb5dc..e04bf11 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -16,6 +16,8 @@
 
 package com.android.server.backup.restore;
 
+import static android.app.backup.BackupAnnotations.OperationType.RESTORE;
+
 import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
@@ -30,6 +32,7 @@
 import android.annotation.Nullable;
 import android.app.ApplicationThreadConstants;
 import android.app.IBackupAgent;
+import android.app.backup.BackupAnnotations;
 import android.app.backup.BackupDataInput;
 import android.app.backup.BackupDataOutput;
 import android.app.backup.BackupManagerMonitor;
@@ -70,7 +73,7 @@
 import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.utils.BackupEligibilityRules;
-import com.android.server.backup.utils.BackupManagerMonitorUtils;
+import com.android.server.backup.utils.BackupManagerMonitorEventSender;
 
 import libcore.io.IoUtils;
 
@@ -84,7 +87,6 @@
 import java.util.Set;
 
 public class PerformUnifiedRestoreTask implements BackupRestoreTask {
-
     private UserBackupManagerService backupManagerService;
     private final OperationStorage mOperationStorage;
     private final int mUserId;
@@ -98,8 +100,7 @@
     // Restore observer; may be null
     private IRestoreObserver mObserver;
 
-    // BackuoManagerMonitor; may be null
-    private IBackupManagerMonitor mMonitor;
+    private BackupManagerMonitorEventSender mBackupManagerMonitorEventSender;
 
     // Token identifying the dataset to the transport
     private long mToken;
@@ -181,6 +182,8 @@
         mUserId = 0;
         mBackupEligibilityRules = null;
         this.backupManagerService = backupManagerService;
+        mBackupManagerMonitorEventSender =
+                new BackupManagerMonitorEventSender(/*monitor*/null);
     }
 
     // This task can assume that the wakelock is properly held for it and doesn't have to worry
@@ -208,7 +211,8 @@
 
         mTransportConnection = transportConnection;
         mObserver = observer;
-        mMonitor = monitor;
+        mBackupManagerMonitorEventSender =
+                new BackupManagerMonitorEventSender(monitor);
         mToken = restoreSetToken;
         mPmToken = pmToken;
         mTargetPackage = targetPackage;
@@ -410,8 +414,8 @@
 
             // If the requester of the restore has not passed in a monitor, we ask the transport
             // for one.
-            if (mMonitor == null) {
-                mMonitor = transport.getBackupManagerMonitor();
+            if (mBackupManagerMonitorEventSender.getMonitor() == null) {
+                mBackupManagerMonitorEventSender.setMonitor(transport.getBackupManagerMonitor());
             }
 
             mStatus = transport.startRestore(mToken, packages);
@@ -425,10 +429,12 @@
             RestoreDescription desc = transport.nextRestorePackage();
             if (desc == null) {
                 Slog.e(TAG, "No restore metadata available; halting");
-                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null);
+                mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_NO_RESTORE_METADATA_AVAILABLE,
                         mCurrentPackage,
-                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        monitoringExtras);
                 mStatus = BackupTransport.TRANSPORT_ERROR;
                 executeNextState(UnifiedRestoreState.FINAL);
                 return;
@@ -437,10 +443,12 @@
                     desc.getPackageName())) {
                 Slog.e(TAG, "Required package metadata but got "
                         + desc.getPackageName());
-                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null);
+                mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_NO_PM_METADATA_RECEIVED,
                         mCurrentPackage,
-                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        monitoringExtras);
                 mStatus = BackupTransport.TRANSPORT_ERROR;
                 executeNextState(UnifiedRestoreState.FINAL);
                 return;
@@ -472,10 +480,12 @@
             // the restore operation.
             if (!mPmAgent.hasMetadata()) {
                 Slog.e(TAG, "PM agent has no metadata, so not restoring");
-                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null);
+                mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_PM_AGENT_HAS_NO_METADATA,
                         mCurrentPackage,
-                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+                        BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                        monitoringExtras);
                 EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
                         PACKAGE_MANAGER_SENTINEL,
                         "Package manager restore metadata missing");
@@ -492,10 +502,12 @@
         } catch (Exception e) {
             // If we lost the transport at any time, halt
             Slog.e(TAG, "Unable to contact transport for restore: " + e.getMessage());
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+            Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null);
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_LOST_TRANSPORT,
                     null,
-                    BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null);
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
+                    monitoringExtras);
             mStatus = BackupTransport.TRANSPORT_ERROR;
             backupManagerService.getBackupHandler().removeMessages(
                     MSG_BACKUP_RESTORE_STEP, this);
@@ -552,11 +564,12 @@
                 // Whoops, we thought we could restore this package but it
                 // turns out not to be present.  Skip it.
                 Slog.e(TAG, "Package not present: " + pkgName);
-                mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null);
+                mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_PRESENT,
                         mCurrentPackage,
                         BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                        null);
+                        monitoringExtras);
                 EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, pkgName,
                         "Package missing on device");
                 nextState = UnifiedRestoreState.RUNNING_QUEUE;
@@ -572,13 +585,15 @@
                     String message = "Source version " + metaInfo.versionCode
                             + " > installed version " + mCurrentPackage.getLongVersionCode();
                     Slog.w(TAG, "Package " + pkgName + ": " + message);
-                    Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
+                    Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
+                            null,
                             BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION,
                             metaInfo.versionCode);
-                    monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
+                    monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
                             monitoringExtras,
                             BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, false);
-                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    monitoringExtras = addRestoreOperationTypeToEvent(monitoringExtras);
+                    mBackupManagerMonitorEventSender.monitorEvent(
                             BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER,
                             mCurrentPackage,
                             BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -593,13 +608,15 @@
                                 + " > installed version " + mCurrentPackage.getLongVersionCode()
                                 + " but restoreAnyVersion");
                     }
-                    Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
+                    Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
+                            null,
                             BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION,
                             metaInfo.versionCode);
-                    monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
+                    monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
                             monitoringExtras,
                             BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, true);
-                    mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+                    monitoringExtras = addRestoreOperationTypeToEvent(monitoringExtras);
+                    mBackupManagerMonitorEventSender.monitorEvent(
                             BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER,
                             mCurrentPackage,
                             BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -652,9 +669,10 @@
                 Slog.i(TAG, "Data exists for package " + packageName
                         + " but app has no agent; skipping");
             }
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+            Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null);
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_APP_HAS_NO_AGENT, mCurrentPackage,
-                    BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, monitoringExtras);
             EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
                     "Package has no agent");
             executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
@@ -665,9 +683,11 @@
         PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
         if (!BackupUtils.signaturesMatch(metaInfo.sigHashes, mCurrentPackage, pmi)) {
             Slog.w(TAG, "Signature mismatch restoring " + packageName);
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+            Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null);
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_SIGNATURE_MISMATCH, mCurrentPackage,
-                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                    monitoringExtras);
             EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
                     "Signature mismatch");
             executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
@@ -681,9 +701,11 @@
                 mBackupEligibilityRules.getBackupDestination());
         if (mAgent == null) {
             Slog.w(TAG, "Can't find backup agent for " + packageName);
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+            Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null);
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_CANT_FIND_AGENT, mCurrentPackage,
-                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
+                    BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                    monitoringExtras);
             EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
                     "Restore agent missing");
             executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
@@ -941,8 +963,9 @@
             EventLog.writeEvent(EventLogTags.FULL_RESTORE_PACKAGE,
                     mCurrentPackage.packageName);
 
-            mEngine = new FullRestoreEngine(backupManagerService, mOperationStorage, this, null,
-                    mMonitor, mCurrentPackage, false, mEphemeralOpToken, false,
+            mEngine = new FullRestoreEngine(backupManagerService, mOperationStorage,
+                    this, null, mBackupManagerMonitorEventSender.getMonitor(),
+                    mCurrentPackage, false, mEphemeralOpToken, false,
                     mBackupEligibilityRules);
             mEngineThread = new FullRestoreEngineThread(mEngine, mEnginePipes[0]);
 
@@ -1095,10 +1118,11 @@
             if (DEBUG) {
                 Slog.w(TAG, "Full-data restore target timed out; shutting down");
             }
-
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+            Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null);
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_TIMEOUT,
-                    mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+                    mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+                    monitoringExtras);
             mEngineThread.handleTimeout();
 
             IoUtils.closeQuietly(mEnginePipes[1]);
@@ -1322,7 +1346,7 @@
 
                 // Ask the agent for logs after doRestoreFinished() has completed executing to allow
                 // it to finalize its logs.
-                BackupManagerMonitorUtils.monitorAgentLoggingResults(mMonitor, mCurrentPackage,
+                mBackupManagerMonitorEventSender.monitorAgentLoggingResults(mCurrentPackage,
                         mAgent);
 
                 // Just go back to running the restore queue
@@ -1358,9 +1382,10 @@
     public void handleCancel(boolean cancelAll) {
         mOperationStorage.removeOperation(mEphemeralOpToken);
         Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName);
-        mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
+        Bundle monitoringExtras = addRestoreOperationTypeToEvent(/*extras*/null);
+        mBackupManagerMonitorEventSender.monitorEvent(
                 BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_RESTORE_TIMEOUT,
-                mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+                mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, monitoringExtras);
         EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
                 mCurrentPackage.packageName, "restore timeout");
         // Handle like an agent that threw on invocation: wipe it and go on to the next
@@ -1433,4 +1458,10 @@
             }
         }
     }
+
+    private Bundle addRestoreOperationTypeToEvent (@Nullable Bundle extra) {
+        return mBackupManagerMonitorEventSender.putMonitoringExtra(
+                extra,
+                BackupManagerMonitor.EXTRA_LOG_OPERATION_TYPE, RESTORE);
+    }
 }
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
new file mode 100644
index 0000000..0b55ca2
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.utils;
+
+import android.app.backup.BackupAnnotations;
+import android.app.backup.BackupManagerMonitor;
+import android.app.backup.BackupRestoreEventLogger;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Slog;
+
+import com.android.internal.util.FastPrintWriter;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Map;
+
+
+/*
+ * Util class to parse a BMM event and write it to a text file, to be the printed in
+ * the backup dumpsys
+ *
+ * Note: this class is note thread safe
+ */
+public class BackupManagerMonitorDumpsysUtils {
+
+    private static final String TAG = "BackupManagerMonitorDumpsysUtils";
+    // Name of the subdirectory where the text file containing the BMM events will be stored.
+    // Same as {@link UserBackupManagerFiles}
+    private static final String BACKUP_PERSISTENT_DIR = "backup";
+
+    /**
+     * Parses the BackupManagerMonitor bundle for a RESTORE event in a series of strings that
+     * will be persisted in a text file and printed in the dumpsys.
+     *
+     * If the evenntBundle passed is not a RESTORE event, return early
+     *
+     * Key information related to the event:
+     * - Timestamp (HAS TO ALWAYS BE THE FIRST LINE OF EACH EVENT)
+     * - Event ID
+     * - Event Category
+     * - Operation type
+     * - Package name (can be null)
+     * - Agent logs (if available)
+     *
+     * Example of formatting:
+     * RESTORE Event: [2023-08-18 17:16:00.735] Agent - Agent logging results
+     *     Package name: com.android.wallpaperbackup
+     *     Agent Logs:
+     *         Data Type: wlp_img_system
+     *             Item restored: 0/1
+     *             Agent Error - Category: no_wallpaper, Count: 1
+     *         Data Type: wlp_img_lock
+     *             Item restored: 0/1
+     *             Agent Error - Category: no_wallpaper, Count: 1
+     */
+    public void parseBackupManagerMonitorRestoreEventForDumpsys(Bundle eventBundle) {
+        if (eventBundle == null) {
+            return;
+        }
+
+        if (!isOpTypeRestore(eventBundle)) {
+            //We only log Restore events
+            return;
+        }
+
+        if (!eventBundle.containsKey(BackupManagerMonitor.EXTRA_LOG_EVENT_ID)
+                || !eventBundle.containsKey(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY)) {
+            Slog.w(TAG, "Event id and category are not optional fields.");
+            return;
+        }
+        File bmmEvents = getBMMEventsFile();
+
+        try (FileOutputStream out = new FileOutputStream(bmmEvents, /*append*/ true);
+            PrintWriter pw = new FastPrintWriter(out);) {
+
+            int eventCategory = eventBundle.getInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY);
+            int eventId = eventBundle.getInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID);
+
+            if (eventId == BackupManagerMonitor.LOG_EVENT_ID_AGENT_LOGGING_RESULTS &&
+                    !hasAgentLogging(eventBundle)) {
+                // Do not record an empty agent logging event
+                return;
+            }
+
+            pw.println("RESTORE Event: [" + timestamp() + "] " +
+                    getCategory(eventCategory) + " - " +
+                    getId(eventId));
+
+            if (eventBundle.containsKey(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME)) {
+                pw.println("\tPackage name: "
+                        + eventBundle.getString(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME));
+            }
+
+            // TODO(b/296818666): add extras to the events
+            addAgentLogsIfAvailable(eventBundle, pw);
+        } catch (java.io.IOException e) {
+            Slog.e(TAG, "IO Exception when writing BMM events to file: " + e);
+        }
+
+    }
+
+    private boolean hasAgentLogging(Bundle eventBundle) {
+        if (eventBundle.containsKey(BackupManagerMonitor.EXTRA_LOG_AGENT_LOGGING_RESULTS)) {
+            ArrayList<BackupRestoreEventLogger.DataTypeResult> agentLogs =
+                    eventBundle.getParcelableArrayList(
+                            BackupManagerMonitor.EXTRA_LOG_AGENT_LOGGING_RESULTS);
+
+            return !agentLogs.isEmpty();
+        }
+        return false;
+    }
+
+    /**
+     * Extracts agent logs from the BackupManagerMonitor event. These logs detail:
+     * - the data type for the agent
+     * - the count of successfully restored items
+     * - the count of items that failed to restore
+     * - the metadata associated with this datatype
+     * - any errors
+     */
+    private void addAgentLogsIfAvailable(Bundle eventBundle, PrintWriter pw) {
+        if (hasAgentLogging(eventBundle)) {
+            pw.println("\tAgent Logs:");
+            ArrayList<BackupRestoreEventLogger.DataTypeResult> agentLogs =
+                    eventBundle.getParcelableArrayList(
+                            BackupManagerMonitor.EXTRA_LOG_AGENT_LOGGING_RESULTS);
+            for (BackupRestoreEventLogger.DataTypeResult result : agentLogs) {
+                int totalItems = result.getFailCount() + result.getSuccessCount();
+                pw.println("\t\tData Type: " + result.getDataType());
+                pw.println("\t\t\tItem restored: " + result.getSuccessCount() + "/" +
+                        totalItems);
+                for (Map.Entry<String, Integer> entry : result.getErrors().entrySet()) {
+                    pw.println("\t\t\tAgent Error - Category: " +
+                            entry.getKey() + ", Count: " + entry.getValue());
+                }
+            }
+        }
+    }
+
+    /*
+     * Get the path of the text files which stores the BMM events
+     */
+    public File getBMMEventsFile() {
+        File dataDir = new File(Environment.getDataDirectory(), BACKUP_PERSISTENT_DIR);
+        File fname = new File(dataDir, "bmmevents.txt");
+        return fname;
+    }
+
+    private String timestamp() {
+        long currentTime = System.currentTimeMillis();
+        Date date = new Date(currentTime);
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        return dateFormat.format(date);
+    }
+
+    private String getCategory(int code) {
+        String category = switch (code) {
+            case BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT -> "Transport";
+            case BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT -> "Agent";
+            case BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY ->
+                    "Backup Manager Policy";
+            default -> "Unknown category code: " + code;
+        };
+        return category;
+    }
+
+    private String getId(int code) {
+        String id = switch (code) {
+            case BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL -> "Full backup cancel";
+            case BackupManagerMonitor.LOG_EVENT_ID_ILLEGAL_KEY -> "Illegal key";
+            case BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND -> "No data to send";
+            case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE -> "Package ineligible";
+            case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT ->
+                    "Package key-value participant";
+            case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED -> "Package stopped";
+            case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND -> "Package not found";
+            case BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED -> "Backup disabled";
+            case BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED ->
+                    "Device not provisioned";
+            case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT ->
+                    "Package transport not present";
+            case BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT -> "Error preflight";
+            case BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT -> "Quota hit preflight";
+            case BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP -> "Exception full backup";
+            case BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL ->
+                    "Key-value backup cancel";
+            case BackupManagerMonitor.LOG_EVENT_ID_NO_RESTORE_METADATA_AVAILABLE ->
+                    "No restore metadata available";
+            case BackupManagerMonitor.LOG_EVENT_ID_NO_PM_METADATA_RECEIVED ->
+                    "No PM metadata received";
+            case BackupManagerMonitor.LOG_EVENT_ID_PM_AGENT_HAS_NO_METADATA ->
+                    "PM agent has no metadata";
+            case BackupManagerMonitor.LOG_EVENT_ID_LOST_TRANSPORT -> "Lost transport";
+            case BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_PRESENT -> "Package not present";
+            case BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER ->
+                    "Restore version higher";
+            case BackupManagerMonitor.LOG_EVENT_ID_APP_HAS_NO_AGENT -> "App has no agent";
+            case BackupManagerMonitor.LOG_EVENT_ID_SIGNATURE_MISMATCH -> "Signature mismatch";
+            case BackupManagerMonitor.LOG_EVENT_ID_CANT_FIND_AGENT -> "Can't find agent";
+            case BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_RESTORE_TIMEOUT ->
+                    "Key-value restore timeout";
+            case BackupManagerMonitor.LOG_EVENT_ID_RESTORE_ANY_VERSION -> "Restore any version";
+            case BackupManagerMonitor.LOG_EVENT_ID_VERSIONS_MATCH -> "Versions match";
+            case BackupManagerMonitor.LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER ->
+                    "Version of backup older";
+            case BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH ->
+                    "Full restore signature mismatch";
+            case BackupManagerMonitor.LOG_EVENT_ID_SYSTEM_APP_NO_AGENT -> "System app no agent";
+            case BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE ->
+                    "Full restore allow backup false";
+            case BackupManagerMonitor.LOG_EVENT_ID_APK_NOT_INSTALLED -> "APK not installed";
+            case BackupManagerMonitor.LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK ->
+                    "Cannot restore without APK";
+            case BackupManagerMonitor.LOG_EVENT_ID_MISSING_SIGNATURE -> "Missing signature";
+            case BackupManagerMonitor.LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE ->
+                    "Expected different package";
+            case BackupManagerMonitor.LOG_EVENT_ID_UNKNOWN_VERSION -> "Unknown version";
+            case BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_TIMEOUT -> "Full restore timeout";
+            case BackupManagerMonitor.LOG_EVENT_ID_CORRUPT_MANIFEST -> "Corrupt manifest";
+            case BackupManagerMonitor.LOG_EVENT_ID_WIDGET_METADATA_MISMATCH ->
+                    "Widget metadata mismatch";
+            case BackupManagerMonitor.LOG_EVENT_ID_WIDGET_UNKNOWN_VERSION ->
+                    "Widget unknown version";
+            case BackupManagerMonitor.LOG_EVENT_ID_NO_PACKAGES -> "No packages";
+            case BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_IS_NULL -> "Transport is null";
+            case BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED ->
+                    "Transport non-incremental backup required";
+            case BackupManagerMonitor.LOG_EVENT_ID_AGENT_LOGGING_RESULTS -> "Agent logging results";
+            default -> "Unknown log event ID: " + code;
+        };
+        return id;
+    }
+
+    private boolean isOpTypeRestore(Bundle eventBundle) {
+        return switch (eventBundle.getInt(
+                BackupManagerMonitor.EXTRA_LOG_OPERATION_TYPE, -1)) {
+            case BackupAnnotations.OperationType.RESTORE -> true;
+            default -> false;
+        };
+    }
+}
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
similarity index 69%
rename from services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java
rename to services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
index 439b836..92e3107 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
@@ -25,7 +25,6 @@
 import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.TAG;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.IBackupAgent;
 import android.app.backup.BackupAnnotations.OperationType;
@@ -37,6 +36,7 @@
 import android.os.RemoteException;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.infra.AndroidFuture;
 
 import java.util.List;
@@ -44,9 +44,9 @@
 import java.util.concurrent.TimeoutException;
 
 /**
- * Utility methods to communicate with BackupManagerMonitor.
+ * Utility methods to log BackupManagerMonitor events.
  */
-public class BackupManagerMonitorUtils {
+public class BackupManagerMonitorEventSender {
     /**
      * Timeout for how long we wait before we give up on getting logs from a {@link IBackupAgent}.
      * We expect this to be very fast since the agent immediately returns whatever logs have been
@@ -54,51 +54,77 @@
      * for non-essential logs.
      */
     private static final int AGENT_LOGGER_RESULTS_TIMEOUT_MILLIS = 500;
+    @Nullable private IBackupManagerMonitor mMonitor;
+    private final BackupManagerMonitorDumpsysUtils mBackupManagerMonitorDumpsysUtils;
+    public BackupManagerMonitorEventSender(@Nullable IBackupManagerMonitor monitor) {
+        mMonitor = monitor;
+        mBackupManagerMonitorDumpsysUtils = new BackupManagerMonitorDumpsysUtils();
+    }
+
+    @VisibleForTesting
+    BackupManagerMonitorEventSender(@Nullable IBackupManagerMonitor monitor,
+            BackupManagerMonitorDumpsysUtils backupManagerMonitorDumpsysUtils) {
+        mMonitor = monitor;
+        mBackupManagerMonitorDumpsysUtils = backupManagerMonitorDumpsysUtils;
+    }
+
+    public void setMonitor(IBackupManagerMonitor monitor) {
+        mMonitor = monitor;
+    }
+
+    public IBackupManagerMonitor getMonitor() {
+        return mMonitor;
+    }
 
     /**
      * Notifies monitor about the event.
      *
      * Calls {@link IBackupManagerMonitor#onEvent(Bundle)} with a bundle representing current event.
      *
-     * @param monitor - implementation of {@link IBackupManagerMonitor} to notify.
      * @param id - event id.
      * @param pkg - package event is related to.
      * @param category - event category.
      * @param extras - additional event data.
-     * @return <code>monitor</code> if call succeeded and <code>null</code> otherwise.
      */
-    @Nullable
-    public static IBackupManagerMonitor monitorEvent(
-            @Nullable IBackupManagerMonitor monitor,
+    public void monitorEvent(
             int id,
             PackageInfo pkg,
             int category,
             Bundle extras) {
-        if (monitor != null) {
-            try {
-                Bundle bundle = new Bundle();
-                bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id);
-                bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY, category);
-                if (pkg != null) {
-                    bundle.putString(EXTRA_LOG_EVENT_PACKAGE_NAME,
-                            pkg.packageName);
-                    bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_VERSION,
-                            pkg.versionCode);
-                    bundle.putLong(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION,
-                            pkg.getLongVersionCode());
-                }
-                if (extras != null) {
-                    bundle.putAll(extras);
-                }
-                monitor.onEvent(bundle);
-                return monitor;
-            } catch (RemoteException e) {
-                if (DEBUG) {
-                    Slog.w(TAG, "backup manager monitor went away");
+        try {
+            Bundle bundle = new Bundle();
+            bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id);
+            bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY, category);
+            if (pkg != null) {
+                bundle.putString(EXTRA_LOG_EVENT_PACKAGE_NAME,
+                        pkg.packageName);
+                bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_VERSION,
+                        pkg.versionCode);
+                bundle.putLong(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION,
+                        pkg.getLongVersionCode());
+            }
+            if (extras != null) {
+                bundle.putAll(extras);
+                if (extras.containsKey(EXTRA_LOG_OPERATION_TYPE) &&
+                        extras.getInt(EXTRA_LOG_OPERATION_TYPE) == OperationType.RESTORE){
+                    mBackupManagerMonitorDumpsysUtils
+                            .parseBackupManagerMonitorRestoreEventForDumpsys(bundle);
                 }
             }
+
+            if (mMonitor != null) {
+                mMonitor.onEvent(bundle);
+            } else {
+                if (DEBUG) {
+                    Slog.w(TAG, "backup manager monitor is null unable to send event");
+                }
+            }
+        } catch (RemoteException e) {
+            mMonitor = null;
+            if (DEBUG) {
+                Slog.w(TAG, "backup manager monitor went away");
+            }
         }
-        return null;
     }
 
     /**
@@ -108,17 +134,12 @@
      * <p>Note that this method does two separate binder calls (one to the agent and one to the
      * monitor).
      *
-     * @param monitor - implementation of {@link IBackupManagerMonitor} to notify.
      * @param pkg - package the {@code agent} belongs to.
      * @param agent - the {@link IBackupAgent} to retrieve logs from.
-     * @return {@code null} if the monitor is null. {@code monitor} if we fail to retrieve the logs
-     *     from the {@code agent}. Otherwise, the result of {@link
-     *     #monitorEvent(IBackupManagerMonitor, int, PackageInfo, int, Bundle)}.
      */
-    public static IBackupManagerMonitor monitorAgentLoggingResults(
-            @Nullable IBackupManagerMonitor monitor, PackageInfo pkg, IBackupAgent agent) {
-        if (monitor == null) {
-            return null;
+    public void monitorAgentLoggingResults(PackageInfo pkg, IBackupAgent agent) {
+        if (mMonitor == null) {
+            Slog.i(TAG, "backup manager monitor is null unable to send event"+pkg);
         }
 
         try {
@@ -127,7 +148,7 @@
             AndroidFuture<Integer> operationTypeFuture = new AndroidFuture<>();
             agent.getLoggerResults(resultsFuture);
             agent.getOperationType(operationTypeFuture);
-            return sendAgentLoggingResults(monitor, pkg,
+            sendAgentLoggingResults(pkg,
                     resultsFuture.get(AGENT_LOGGER_RESULTS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS),
                     operationTypeFuture.get(AGENT_LOGGER_RESULTS_TIMEOUT_MILLIS,
                             TimeUnit.MILLISECONDS));
@@ -136,18 +157,15 @@
         } catch (Exception e) {
             Slog.w(TAG, "Failed to retrieve logging results from agent", e);
         }
-        return monitor;
     }
 
-    public static IBackupManagerMonitor sendAgentLoggingResults(
-            @NonNull IBackupManagerMonitor monitor, PackageInfo pkg, List<DataTypeResult> results,
+    public void sendAgentLoggingResults(PackageInfo pkg, List<DataTypeResult> results,
             @OperationType int operationType) {
         Bundle loggerResultsBundle = new Bundle();
         loggerResultsBundle.putParcelableList(
                 EXTRA_LOG_AGENT_LOGGING_RESULTS, results);
         loggerResultsBundle.putInt(EXTRA_LOG_OPERATION_TYPE, operationType);
-        return monitorEvent(
-                monitor,
+        monitorEvent(
                 LOG_EVENT_ID_AGENT_LOGGING_RESULTS,
                 pkg,
                 LOG_EVENT_CATEGORY_AGENT,
diff --git a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
index 71ca8ca..78a9952 100644
--- a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
+++ b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
@@ -85,7 +85,8 @@
     private final InputStream mInputStream;
     private final BytesReadListener mBytesReadListener;
 
-    private IBackupManagerMonitor mMonitor;
+
+    private BackupManagerMonitorEventSender mBackupManagerMonitorEventSender;
 
     // Widget blob to be restored out-of-band.
     private byte[] mWidgetData = null;
@@ -94,7 +95,7 @@
             IBackupManagerMonitor monitor) {
         mInputStream = inputStream;
         mBytesReadListener = bytesReadListener;
-        mMonitor = monitor;
+        mBackupManagerMonitorEventSender = new BackupManagerMonitorEventSender(monitor);
     }
 
     /**
@@ -323,24 +324,22 @@
                         return sigs;
                     } else {
                         Slog.i(TAG, "Missing signature on backed-up package " + info.packageName);
-                        mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                                mMonitor,
+                        mBackupManagerMonitorEventSender.monitorEvent(
                                 LOG_EVENT_ID_MISSING_SIGNATURE,
                                 null,
                                 LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                                BackupManagerMonitorUtils.putMonitoringExtra(null,
+                                mBackupManagerMonitorEventSender.putMonitoringExtra(null,
                                         EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName));
                     }
                 } else {
                     Slog.i(TAG, "Expected package " + info.packageName
                             + " but restore manifest claims " + manifestPackage);
-                    Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
-                            EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
-                    monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
+                    Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
+                            null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
+                    monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
                             monitoringExtras,
                             EXTRA_LOG_MANIFEST_PACKAGE_NAME, manifestPackage);
-                    mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                            mMonitor,
+                    mBackupManagerMonitorEventSender.monitorEvent(
                             LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE,
                             null,
                             LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -349,12 +348,11 @@
             } else {
                 Slog.i(TAG, "Unknown restore manifest version " + version
                         + " for package " + info.packageName);
-                Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
-                        EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
-                monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras,
-                        EXTRA_LOG_EVENT_PACKAGE_VERSION, version);
-                mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                        mMonitor,
+                Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
+                        null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
+                monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
+                        monitoringExtras, EXTRA_LOG_EVENT_PACKAGE_VERSION, version);
+                mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_UNKNOWN_VERSION,
                         null,
                         LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -363,12 +361,12 @@
             }
         } catch (NumberFormatException e) {
             Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName);
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                    mMonitor,
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_CORRUPT_MANIFEST,
                     null,
                     LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                    BackupManagerMonitorUtils.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME,
+                    mBackupManagerMonitorEventSender.putMonitoringExtra(null,
+                            EXTRA_LOG_EVENT_PACKAGE_NAME,
                             info.packageName));
         } catch (IllegalArgumentException e) {
             Slog.w(TAG, e.getMessage());
@@ -436,8 +434,7 @@
                         if ((pkgInfo.applicationInfo.flags
                                 & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) != 0) {
                             Slog.i(TAG, "Package has restoreAnyVersion; taking data");
-                            mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                                    mMonitor,
+                            mBackupManagerMonitorEventSender.monitorEvent(
                                     LOG_EVENT_ID_RESTORE_ANY_VERSION,
                                     pkgInfo,
                                     LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -446,8 +443,7 @@
                         } else if (pkgInfo.getLongVersionCode() >= info.version) {
                             Slog.i(TAG, "Sig + version match; taking data");
                             policy = RestorePolicy.ACCEPT;
-                            mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                                    mMonitor,
+                            mBackupManagerMonitorEventSender.monitorEvent(
                                     LOG_EVENT_ID_VERSIONS_MATCH,
                                     pkgInfo,
                                     LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -466,12 +462,11 @@
                             } else {
                                 Slog.i(TAG, "Data requires newer version "
                                         + info.version + "; ignoring");
-                                mMonitor = BackupManagerMonitorUtils
-                                        .monitorEvent(mMonitor,
+                                mBackupManagerMonitorEventSender.monitorEvent(
                                                 LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER,
                                                 pkgInfo,
                                                 LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                                                BackupManagerMonitorUtils
+                                                mBackupManagerMonitorEventSender
                                                         .putMonitoringExtra(
                                                                 null,
                                                                 EXTRA_LOG_OLD_VERSION,
@@ -484,8 +479,7 @@
                         Slog.w(TAG, "Restore manifest signatures do not match "
                                 + "installed application for "
                                 + info.packageName);
-                        mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                                mMonitor,
+                        mBackupManagerMonitorEventSender.monitorEvent(
                                 LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH,
                                 pkgInfo,
                                 LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -494,8 +488,7 @@
                 } else {
                     Slog.w(TAG, "Package " + info.packageName
                             + " is system level with no agent");
-                    mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                            mMonitor,
+                    mBackupManagerMonitorEventSender.monitorEvent(
                             LOG_EVENT_ID_SYSTEM_APP_NO_AGENT,
                             pkgInfo,
                             LOG_EVENT_CATEGORY_AGENT,
@@ -506,8 +499,7 @@
                     Slog.i(TAG,
                             "Restore manifest from " + info.packageName + " but allowBackup=false");
                 }
-                mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                        mMonitor,
+                mBackupManagerMonitorEventSender.monitorEvent(
                         LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE,
                         pkgInfo,
                         LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -526,14 +518,13 @@
             } else {
                 policy = RestorePolicy.IGNORE;
             }
-            Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
+            Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
                     null,
                     EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
-            monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
+            monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
                     monitoringExtras,
                     EXTRA_LOG_POLICY_ALLOW_APKS, allowApks);
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                    mMonitor,
+            mBackupManagerMonitorEventSender.monitorEvent(
                     LOG_EVENT_ID_APK_NOT_INSTALLED,
                     null,
                     LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -543,12 +534,11 @@
         if (policy == RestorePolicy.ACCEPT_IF_APK && !info.hasApk) {
             Slog.i(TAG, "Cannot restore package " + info.packageName
                     + " without the matching .apk");
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                    mMonitor,
+            mBackupManagerMonitorEventSender.monitorEvent(
                     LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK,
                     null,
                     LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
-                    BackupManagerMonitorUtils.putMonitoringExtra(null,
+                    mBackupManagerMonitorEventSender.putMonitoringExtra(null,
                             EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName));
         }
 
@@ -632,12 +622,11 @@
                         "Metadata mismatch: package " + info.packageName + " but widget data for "
                                 + pkg);
 
-                Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
+                Bundle monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(null,
                         EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
-                monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras,
-                        BackupManagerMonitor.EXTRA_LOG_WIDGET_PACKAGE_NAME, pkg);
-                mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                        mMonitor,
+                monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(
+                        monitoringExtras, BackupManagerMonitor.EXTRA_LOG_WIDGET_PACKAGE_NAME, pkg);
+                mBackupManagerMonitorEventSender.monitorEvent(
                         BackupManagerMonitor.LOG_EVENT_ID_WIDGET_METADATA_MISMATCH,
                         null,
                         LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -646,13 +635,12 @@
         } else {
             Slog.w(TAG, "Unsupported metadata version " + version);
 
-            Bundle monitoringExtras = BackupManagerMonitorUtils
+            Bundle monitoringExtras = mBackupManagerMonitorEventSender
                     .putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME,
                             info.packageName);
-            monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras,
+            monitoringExtras = mBackupManagerMonitorEventSender.putMonitoringExtra(monitoringExtras,
                     EXTRA_LOG_EVENT_PACKAGE_VERSION, version);
-            mMonitor = BackupManagerMonitorUtils.monitorEvent(
-                    mMonitor,
+            mBackupManagerMonitorEventSender.monitorEvent(
                     BackupManagerMonitor.LOG_EVENT_ID_WIDGET_UNKNOWN_VERSION,
                     null,
                     LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
@@ -810,7 +798,7 @@
     }
 
     public IBackupManagerMonitor getMonitor() {
-        return mMonitor;
+        return mBackupManagerMonitorEventSender.getMonitor();
     }
 
     public byte[] getWidgetData() {
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
index f6e9415..5942145 100644
--- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
+++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
@@ -32,6 +32,7 @@
 import android.os.Message;
 import android.os.UserManager;
 import android.util.Log;
+import android.util.Slog;
 import android.util.SparseArray;
 
 import com.android.server.companion.AssociationStore;
@@ -226,53 +227,40 @@
 
     private void onDevicePresent(@NonNull Set<Integer> presentDevicesForSource,
             int newDeviceAssociationId, @NonNull String sourceLoggingTag) {
-        if (DEBUG) {
-            Log.i(TAG, "onDevice_Present() id=" + newDeviceAssociationId
-                    + ", source=" + sourceLoggingTag);
-            Log.d(TAG, "  > association="
-                    + mAssociationStore.getAssociationById(newDeviceAssociationId));
-        }
+        Slog.i(TAG, "onDevice_Present() id=" + newDeviceAssociationId
+                + ", source=" + sourceLoggingTag);
 
         final boolean alreadyPresent = isDevicePresent(newDeviceAssociationId);
         if (alreadyPresent) {
-            Log.i(TAG, "Device" + "id (" + newDeviceAssociationId + ") already present.");
+            Slog.i(TAG, "Device" + "id (" + newDeviceAssociationId + ") already present.");
         }
 
         final boolean added = presentDevicesForSource.add(newDeviceAssociationId);
         if (!added) {
-            Log.w(TAG, "Association with id "
+            Slog.i(TAG, "Association with id "
                     + newDeviceAssociationId + " is ALREADY reported as "
                     + "present by this source (" + sourceLoggingTag + ")");
         }
 
-        if (alreadyPresent) return;
-
         mCallback.onDeviceAppeared(newDeviceAssociationId);
     }
 
     private void onDeviceGone(@NonNull Set<Integer> presentDevicesForSource,
             int goneDeviceAssociationId, @NonNull String sourceLoggingTag) {
-        if (DEBUG) {
-            Log.i(TAG, "onDevice_Gone() id=" + goneDeviceAssociationId
-                    + ", source=" + sourceLoggingTag);
-            Log.d(TAG, "  > association="
-                    + mAssociationStore.getAssociationById(goneDeviceAssociationId));
-        }
+        Slog.i(TAG, "onDevice_Gone() id=" + goneDeviceAssociationId
+                + ", source=" + sourceLoggingTag);
 
         final boolean removed = presentDevicesForSource.remove(goneDeviceAssociationId);
         if (!removed) {
-            Log.w(TAG, "Association with id " + goneDeviceAssociationId + " was NOT reported "
+            Slog.w(TAG, "Association with id " + goneDeviceAssociationId + " was NOT reported "
                     + "as present by this source (" + sourceLoggingTag + ")");
-
             return;
         }
 
         final boolean stillPresent = isDevicePresent(goneDeviceAssociationId);
+
         if (stillPresent) {
-            if (DEBUG) {
-                Log.i(TAG, "  Device id (" + goneDeviceAssociationId + ") is still present.");
-            }
-            return;
+            Slog.w(TAG, "  Device id (" + goneDeviceAssociationId + ") is still present.");
         }
 
         mCallback.onDeviceDisappeared(goneDeviceAssociationId);
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index 8fc30e4..e11c028 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -80,9 +80,9 @@
 import android.util.apk.ApkSigningBlockUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.expresslog.Histogram;
 import com.android.internal.os.IBinaryTransparencyService;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.modules.expresslog.Histogram;
 import com.android.server.pm.ApexManager;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.AndroidPackageSplit;
@@ -1391,7 +1391,7 @@
         // Check the flag to determine whether biometric property verification is enabled. It's
         // disabled by default.
         if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BIOMETRICS,
-                KEY_ENABLE_BIOMETRIC_PROPERTY_VERIFICATION, false)) {
+                KEY_ENABLE_BIOMETRIC_PROPERTY_VERIFICATION, true)) {
             if (DEBUG) {
                 Slog.d(TAG, "Do not collect/verify biometric properties. Feature disabled by "
                         + "DeviceConfig");
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 73dbb86a..d47573d 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -73,8 +73,8 @@
 import android.content.pm.UserInfo;
 import android.content.res.ObbInfo;
 import android.database.ContentObserver;
-import android.media.MediaCodecList;
 import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
 import android.media.MediaFormat;
 import android.net.Uri;
 import android.os.BatteryManager;
@@ -219,6 +219,8 @@
     @GuardedBy("mLock")
     private final Set<Integer> mCeStoragePreparedUsers = new ArraySet<>();
 
+    private volatile long mInternalStorageSize = 0;
+
     public static class Lifecycle extends SystemService {
         private StorageManagerService mStorageManagerService;
 
@@ -3479,6 +3481,15 @@
         return authority;
     }
 
+    @Override
+    public long getInternalStorageBlockDeviceSize() throws RemoteException {
+        if (mInternalStorageSize == 0) {
+            mInternalStorageSize = mVold.getStorageSize();
+        }
+
+        return mInternalStorageSize;
+    }
+
     /**
      * Enforces that the caller is the {@link ExternalStorageService}
      *
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 533ecf1..b5cab17 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -2411,7 +2411,11 @@
     }
 
     @GuardedBy("mDeviceStateLock")
-    private boolean communnicationDeviceCompatOn() {
+    // LE Audio: For system server (Telecom) and APKs targeting S and above, we let the audio
+    // policy routing rules select the default communication device.
+    // For older APKs, we force LE Audio headset when connected as those APKs cannot select a LE
+    // Audiodevice explicitly.
+    private boolean communnicationDeviceLeAudioCompatOn() {
         return mAudioModeOwner.mMode == AudioSystem.MODE_IN_COMMUNICATION
                 && !(CompatChanges.isChangeEnabled(
                         USE_SET_COMMUNICATION_DEVICE, mAudioModeOwner.mUid)
@@ -2419,19 +2423,25 @@
     }
 
     @GuardedBy("mDeviceStateLock")
+    // Hearing Aid: For system server (Telecom) and IN_CALL mode we let the audio
+    // policy routing rules select the default communication device.
+    // For 3p apps and IN_COMMUNICATION mode we force Hearing aid when connected to maintain
+    // backwards compatibility
+    private boolean communnicationDeviceHaCompatOn() {
+        return mAudioModeOwner.mMode == AudioSystem.MODE_IN_COMMUNICATION
+                && !(mAudioModeOwner.mUid == android.os.Process.SYSTEM_UID);
+    }
+
+    @GuardedBy("mDeviceStateLock")
     AudioDeviceAttributes getDefaultCommunicationDevice() {
-        // For system server (Telecom) and APKs targeting S and above, we let the audio
-        // policy routing rules select the default communication device.
-        // For older APKs, we force Hearing Aid or LE Audio headset when connected as
-        // those APKs cannot select a LE Audio or Hearing Aid device explicitly.
         AudioDeviceAttributes device = null;
-        if (communnicationDeviceCompatOn()) {
-            // If both LE and Hearing Aid are active (thie should not happen),
-            // priority to Hearing Aid.
+        // If both LE and Hearing Aid are active (thie should not happen),
+        // priority to Hearing Aid.
+        if (communnicationDeviceHaCompatOn()) {
             device = mDeviceInventory.getDeviceOfType(AudioSystem.DEVICE_OUT_HEARING_AID);
-            if (device == null) {
-                device = mDeviceInventory.getDeviceOfType(AudioSystem.DEVICE_OUT_BLE_HEADSET);
-            }
+        }
+        if (device == null && communnicationDeviceLeAudioCompatOn()) {
+            device = mDeviceInventory.getDeviceOfType(AudioSystem.DEVICE_OUT_BLE_HEADSET);
         }
         return device;
     }
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
index 97e5c6f..356b301 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
@@ -26,6 +26,7 @@
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.UserHandle;
+import android.util.Slog;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -54,6 +55,7 @@
 
     private final float mThreshold;
     private final int mModality;
+    private boolean mPersisterInitialized = false;
 
     @NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap;
 
@@ -85,9 +87,15 @@
     }
 
     private void initializeUserAuthenticationStatsMap() {
-        mAuthenticationStatsPersister = new AuthenticationStatsPersister(mContext);
-        for (AuthenticationStats stats : mAuthenticationStatsPersister.getAllFrrStats(mModality)) {
-            mUserAuthenticationStatsMap.put(stats.getUserId(), stats);
+        try {
+            mAuthenticationStatsPersister = new AuthenticationStatsPersister(mContext);
+            for (AuthenticationStats stats :
+                    mAuthenticationStatsPersister.getAllFrrStats(mModality)) {
+                mUserAuthenticationStatsMap.put(stats.getUserId(), stats);
+            }
+            mPersisterInitialized = true;
+        } catch (IllegalStateException e) {
+            Slog.w(TAG, "Failed to initialize AuthenticationStatsPersister.", e);
         }
     }
 
@@ -108,7 +116,9 @@
 
         authenticationStats.authenticate(authenticated);
 
-        persistDataIfNeeded(userId);
+        if (mPersisterInitialized) {
+            persistDataIfNeeded(userId);
+        }
         sendNotificationIfNeeded(userId);
     }
 
@@ -166,11 +176,13 @@
     }
 
     private void onUserRemoved(final int userId) {
-        if (mAuthenticationStatsPersister == null) {
+        if (!mPersisterInitialized) {
             initializeUserAuthenticationStatsMap();
         }
-        mUserAuthenticationStatsMap.remove(userId);
-        mAuthenticationStatsPersister.removeFrrStats(userId);
+        if (mPersisterInitialized) {
+            mUserAuthenticationStatsMap.remove(userId);
+            mAuthenticationStatsPersister.removeFrrStats(userId);
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index f0e3895..d662aae 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -67,6 +67,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.DevicePolicyManagerInternal;
 import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
 import android.compat.annotation.EnabledSince;
 import android.content.ComponentName;
 import android.content.Context;
@@ -311,6 +312,19 @@
     private static final long SILENT_INSTALL_ALLOWED = 265131695L;
 
     /**
+     * The system supports pre-approval and update ownership features from
+     * {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE API 34}. The change id is used to make sure
+     * the system includes the fix of pre-approval with update ownership case. When checking the
+     * change id, if it is disabled, it means the build includes the fix. The more detail is on
+     * b/293644536.
+     * See {@link PackageInstaller.SessionParams#setRequestUpdateOwnership(boolean)} and
+     * {@link #requestUserPreapproval(PreapprovalDetails, IntentSender)} for more details.
+     */
+    @Disabled
+    @ChangeId
+    private static final long PRE_APPROVAL_WITH_UPDATE_OWNERSHIP_FIX = 293644536L;
+
+    /**
      * The default value of {@link #mValidatedTargetSdk} is {@link Integer#MAX_VALUE}. If {@link
      * #mValidatedTargetSdk} is compared with {@link Build.VERSION_CODES#S} before getting the
      * target sdk version from a validated apk in {@link #validateApkInstallLocked()}, the compared
@@ -893,16 +907,27 @@
             if (mPermissionsManuallyAccepted) {
                 return USER_ACTION_NOT_NEEDED;
             }
-            packageName = mPackageName;
+            // For pre-pappvoal case, the mPackageName would be null.
+            if (mPackageName != null) {
+                packageName = mPackageName;
+            } else if (mPreapprovalRequested.get() && mPreapprovalDetails != null) {
+                packageName = mPreapprovalDetails.getPackageName();
+            } else {
+                packageName = null;
+            }
             hasDeviceAdminReceiver = mHasDeviceAdminReceiver;
         }
 
-        final boolean forcePermissionPrompt =
+        // For the below cases, force user action prompt
+        // 1. installFlags includes INSTALL_FORCE_PERMISSION_PROMPT
+        // 2. params.requireUserAction is USER_ACTION_REQUIRED
+        final boolean forceUserActionPrompt =
                 (params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0
                         || params.requireUserAction == SessionParams.USER_ACTION_REQUIRED;
-        if (forcePermissionPrompt) {
-            return USER_ACTION_REQUIRED;
-        }
+        final int userActionNotTypicallyNeededResponse = forceUserActionPrompt
+                ? USER_ACTION_REQUIRED
+                : USER_ACTION_NOT_NEEDED;
+
         // It is safe to access mInstallerUid and mInstallSource without lock
         // because they are immutable after sealing.
         final Computer snapshot = mPm.snapshotComputer();
@@ -956,7 +981,7 @@
                 || isInstallerDeviceOwnerOrAffiliatedProfileOwner();
 
         if (noUserActionNecessary) {
-            return USER_ACTION_NOT_NEEDED;
+            return userActionNotTypicallyNeededResponse;
         }
 
         if (isUpdateOwnershipEnforcementEnabled
@@ -969,7 +994,7 @@
         }
 
         if (isPermissionGranted) {
-            return USER_ACTION_NOT_NEEDED;
+            return userActionNotTypicallyNeededResponse;
         }
 
         if (snapshot.isInstallDisabledForPackage(getInstallerPackageName(), mInstallerUid,
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index e5dc688..c583f17 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -5257,9 +5257,6 @@
         public int getUserMinAspectRatio(@NonNull String packageName, int userId) {
             final Computer snapshot = snapshotComputer();
             final int callingUid = Binder.getCallingUid();
-            snapshot.enforceCrossUserPermission(
-                    callingUid, userId, false /* requireFullPermission */,
-                    false /* checkShell */, "getUserMinAspectRatio");
             final PackageStateInternal packageState = snapshot
                     .getPackageStateForInstalledAndFiltered(packageName, callingUid, userId);
             return packageState == null ? USER_MIN_ASPECT_RATIO_UNSET
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d0f86c0..71502c6 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4375,7 +4375,6 @@
         // Reset the last saved PiP snap fraction on removal.
         mDisplayContent.mPinnedTaskController.onActivityHidden(mActivityComponent);
         mDisplayContent.onRunningActivityChanged();
-        mWmService.mEmbeddedWindowController.onActivityRemoved(this);
         mRemovingFromDisplay = false;
     }
 
diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java
index 2eceecc..0250475 100644
--- a/services/core/java/com/android/server/wm/AsyncRotationController.java
+++ b/services/core/java/com/android/server/wm/AsyncRotationController.java
@@ -172,10 +172,9 @@
                 if (recents != null && recents.isNavigationBarAttachedToApp()) {
                     return;
                 }
-            } else if (navigationBarCanMove || mTransitionOp == OP_CHANGE_MAY_SEAMLESS) {
+            } else if (navigationBarCanMove || mTransitionOp == OP_CHANGE_MAY_SEAMLESS
+                    || mDisplayContent.mTransitionController.mNavigationBarAttachedToApp) {
                 action = Operation.ACTION_SEAMLESS;
-            } else if (mDisplayContent.mTransitionController.mNavigationBarAttachedToApp) {
-                return;
             }
             mTargetWindowTokens.put(w.mToken, new Operation(action));
             return;
@@ -294,6 +293,11 @@
             finishOp(mTargetWindowTokens.keyAt(i));
         }
         mTargetWindowTokens.clear();
+        onAllCompleted();
+    }
+
+    private void onAllCompleted() {
+        if (DEBUG) Slog.d(TAG, "onAllCompleted");
         if (mTimeoutRunnable != null) {
             mService.mH.removeCallbacks(mTimeoutRunnable);
         }
@@ -333,7 +337,7 @@
             if (DEBUG) Slog.d(TAG, "Complete directly " + token.getTopChild());
             finishOp(token);
             if (mTargetWindowTokens.isEmpty()) {
-                if (mTimeoutRunnable != null) mService.mH.removeCallbacks(mTimeoutRunnable);
+                onAllCompleted();
                 return true;
             }
         }
@@ -411,14 +415,18 @@
         if (mDisplayContent.mInputMethodWindow == null) return;
         final WindowToken imeWindowToken = mDisplayContent.mInputMethodWindow.mToken;
         if (isTargetToken(imeWindowToken)) return;
+        hideImmediately(imeWindowToken, Operation.ACTION_TOGGLE_IME);
+        if (DEBUG) Slog.d(TAG, "hideImeImmediately " + imeWindowToken.getTopChild());
+    }
+
+    private void hideImmediately(WindowToken token, @Operation.Action int action) {
         final boolean original = mHideImmediately;
         mHideImmediately = true;
-        final Operation op = new Operation(Operation.ACTION_TOGGLE_IME);
-        mTargetWindowTokens.put(imeWindowToken, op);
-        fadeWindowToken(false /* show */, imeWindowToken, ANIMATION_TYPE_TOKEN_TRANSFORM);
-        op.mLeash = imeWindowToken.getAnimationLeash();
+        final Operation op = new Operation(action);
+        mTargetWindowTokens.put(token, op);
+        fadeWindowToken(false /* show */, token, ANIMATION_TYPE_TOKEN_TRANSFORM);
+        op.mLeash = token.getAnimationLeash();
         mHideImmediately = original;
-        if (DEBUG) Slog.d(TAG, "hideImeImmediately " + imeWindowToken.getTopChild());
     }
 
     /** Returns {@code true} if the window will rotate independently. */
@@ -428,11 +436,20 @@
                 || isTargetToken(w.mToken);
     }
 
-    /** Returns {@code true} if the controller will run fade animations on the window. */
+    /**
+     * Returns {@code true} if the rotation transition appearance of the window is currently
+     * managed by this controller.
+     */
     boolean isTargetToken(WindowToken token) {
         return mTargetWindowTokens.containsKey(token);
     }
 
+    /** Returns {@code true} if the controller will run fade animations on the window. */
+    boolean hasFadeOperation(WindowToken token) {
+        final Operation op = mTargetWindowTokens.get(token);
+        return op != null && op.mAction == Operation.ACTION_FADE;
+    }
+
     /**
      * Whether the insets animation leash should use previous position when running fade animation
      * or seamless transformation in a rotated display.
@@ -564,7 +581,18 @@
             return false;
         }
         final Operation op = mTargetWindowTokens.get(w.mToken);
-        if (op == null) return false;
+        if (op == null) {
+            // If a window becomes visible after the rotation transition is requested but before
+            // the transition is ready, hide it by an animation leash so it won't be flickering
+            // by drawing the rotated content before applying projection transaction of display.
+            // And it will fade in after the display transition is finished.
+            if (mTransitionOp == OP_APP_SWITCH && !mIsStartTransactionCommitted
+                    && canBeAsync(w.mToken)) {
+                hideImmediately(w.mToken, Operation.ACTION_FADE);
+                if (DEBUG) Slog.d(TAG, "Hide on finishDrawing " + w.mToken.getTopChild());
+            }
+            return false;
+        }
         if (DEBUG) Slog.d(TAG, "handleFinishDrawing " + w);
         if (postDrawTransaction == null || !mIsSyncDrawRequested
                 || canDrawBeforeStartTransaction(op)) {
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index 98027bb..c9bae12 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -135,19 +135,6 @@
         return mWindowsByWindowToken.get(windowToken);
     }
 
-    void onActivityRemoved(ActivityRecord activityRecord) {
-        for (int i = mWindows.size() - 1; i >= 0; i--) {
-            final EmbeddedWindow window = mWindows.valueAt(i);
-            if (window.mHostActivityRecord == activityRecord) {
-                final WindowProcessController processController =
-                        mAtmService.getProcessController(window.mOwnerPid, window.mOwnerUid);
-                if (processController != null) {
-                    processController.removeHostActivity(activityRecord);
-                }
-            }
-        }
-    }
-
     static class EmbeddedWindow implements InputTarget {
         final IWindow mClient;
         @Nullable final WindowState mHostWindowState;
@@ -230,6 +217,13 @@
                 mInputChannel.dispose();
                 mInputChannel = null;
             }
+            if (mHostActivityRecord != null) {
+                final WindowProcessController wpc =
+                        mWmService.mAtmService.getProcessController(mOwnerPid, mOwnerUid);
+                if (wpc != null) {
+                    wpc.removeHostActivity(mHostActivityRecord);
+                }
+            }
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java b/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java
index 2e5474e..79b26d2 100644
--- a/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java
+++ b/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java
@@ -86,7 +86,7 @@
                 ANIMATION_TYPE_TOKEN_TRANSFORM);
         if (controller == null) {
             fadeAnim.run();
-        } else if (!controller.isTargetToken(mNavigationBar.mToken)) {
+        } else if (!controller.hasFadeOperation(mNavigationBar.mToken)) {
             // If fade rotation animation is running and the nav bar is not controlled by it:
             // - For fade-in animation, defer the animation until fade rotation animation finishes.
             // - For fade-out animation, just play the animation.
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 05f95f81..d4fdc12 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2264,7 +2264,7 @@
     int finishTopCrashedActivities(WindowProcessController app, String reason) {
         Task focusedRootTask = getTopDisplayFocusedRootTask();
         final Task[] finishedTask = new Task[1];
-        forAllTasks(rootTask -> {
+        forAllRootTasks(rootTask -> {
             final Task t = rootTask.finishTopCrashedActivityLocked(app, reason);
             if (rootTask == focusedRootTask || finishedTask[0] == null) {
                 finishedTask[0] = t;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index f1fb17b..44632c9 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1930,11 +1930,6 @@
             break;
         }
 
-        final AsyncRotationController asyncRotationController = dc.getAsyncRotationController();
-        if (asyncRotationController != null) {
-            asyncRotationController.accept(navWindow);
-        }
-
         if (animate) {
             final NavBarFadeAnimationController controller =
                     new NavBarFadeAnimationController(dc);
diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java
index 14b4dc3..2db2438 100644
--- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 
 import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.utils.BackupManagerMonitorEventSender;
 import com.android.server.testing.shadows.ShadowEventLog;
 import com.android.server.testing.shadows.ShadowSlog;
 
@@ -46,10 +47,13 @@
     @Mock private IBackupManagerMonitor mMonitor;
 
     private KeyValueBackupReporter mReporter;
+    private BackupManagerMonitorEventSender mBackupManagerMonitorEventSender;
 
     @Before
     public void setUp() {
-        mReporter = new KeyValueBackupReporter(mBackupManagerService, mObserver, mMonitor);
+        mBackupManagerMonitorEventSender = new BackupManagerMonitorEventSender(mMonitor);
+        mReporter = new KeyValueBackupReporter(
+                mBackupManagerService, mObserver, mBackupManagerMonitorEventSender);
     }
 
     @Test
diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
index bfbc0f5..7349c14 100644
--- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
@@ -122,6 +122,7 @@
 import com.android.server.backup.testing.TransportTestUtils;
 import com.android.server.backup.testing.TransportTestUtils.TransportMock;
 import com.android.server.backup.utils.BackupEligibilityRules;
+import com.android.server.backup.utils.BackupManagerMonitorEventSender;
 import com.android.server.testing.shadows.FrameworkShadowLooper;
 import com.android.server.testing.shadows.ShadowApplicationPackageManager;
 import com.android.server.testing.shadows.ShadowBackupDataInput;
@@ -260,7 +261,8 @@
         mBackupHandler = mBackupManagerService.getBackupHandler();
         mShadowBackupLooper = shadowOf(mBackupHandler.getLooper());
         ShadowEventLog.setUp();
-        mReporter = spy(new KeyValueBackupReporter(mBackupManagerService, mObserver, mMonitor));
+        mReporter = spy(new KeyValueBackupReporter(mBackupManagerService, mObserver,
+                new BackupManagerMonitorEventSender(mMonitor)));
 
         when(mPackageManagerInternal.getApplicationEnabledState(any(), anyInt()))
                 .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
diff --git a/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java b/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java
index b7a0cf3..e33ca77 100644
--- a/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java
+++ b/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java
@@ -138,14 +138,6 @@
     }
 
     @Test
-    public void testGetUserMinAspectRatio_withCrossUserId() {
-        final int crossUserId = UserHandle.myUserId() + 1;
-        assertThrows(SecurityException.class,
-                () -> mIPackageManager.getUserMinAspectRatio(
-                        mInstrumentation.getContext().getPackageName(), crossUserId));
-    }
-
-    @Test
     public void testIsPackageSignedByKeySet_cannotDetectCrossUserPkg() throws Exception {
         final KeySet keySet = mIPackageManager.getSigningKeySet(mContext.getPackageName());
         assertThrows(IllegalArgumentException.class,
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
index dc1c6d5..c942cf4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -52,7 +52,7 @@
 import com.android.server.backup.transport.BackupTransportClient;
 import com.android.server.backup.transport.TransportConnection;
 import com.android.server.backup.utils.BackupEligibilityRules;
-import com.android.server.backup.utils.BackupManagerMonitorUtils;
+import com.android.server.backup.utils.BackupManagerMonitorEventSender;
 
 import com.google.common.collect.ImmutableSet;
 
@@ -86,6 +86,7 @@
     @Mock BackupTransportClient mBackupTransport;
     @Mock BackupEligibilityRules mBackupEligibilityRules;
     @Mock LifecycleOperationStorage mOperationStorage;
+    @Mock BackupManagerMonitorEventSender mBackupManagerMonitorEventSender;
 
     private MockitoSession mSession;
     private TestBackupService mService;
@@ -94,7 +95,7 @@
     public void setUp() throws Exception {
         mSession = mockitoSession()
                 .initMocks(this)
-                .mockStatic(BackupManagerMonitorUtils.class)
+                .mockStatic(BackupManagerMonitorEventSender.class)
                 .mockStatic(FeatureFlagUtils.class)
                 // TODO(b/263239775): Remove unnecessary stubbing.
                 .strictness(Strictness.LENIENT)
@@ -246,9 +247,9 @@
                 new DataTypeResult(/* dataType */ "type_2"));
         mService.reportDelayedRestoreResult(TEST_PACKAGE, results);
 
-        verify(() -> BackupManagerMonitorUtils.sendAgentLoggingResults(
-                eq(mBackupManagerMonitor), eq(packageInfo), eq(results), eq(
-                        BackupAnnotations.OperationType.RESTORE)));
+
+        verify(mBackupManagerMonitorEventSender).sendAgentLoggingResults(
+                eq(packageInfo), eq(results), eq(BackupAnnotations.OperationType.RESTORE));
     }
 
     private static PackageInfo getPackageInfo(String packageName) {
@@ -258,7 +259,7 @@
         return packageInfo;
     }
 
-    private static class TestBackupService extends UserBackupManagerService {
+    private class TestBackupService extends UserBackupManagerService {
         boolean isEnabledStatePersisted = false;
         boolean shouldUseNewBackupEligibilityRules = false;
 
@@ -293,6 +294,11 @@
             return mWorkerThread;
         }
 
+        @Override
+        BackupManagerMonitorEventSender getBMMEventSender(IBackupManagerMonitor monitor) {
+            return mBackupManagerMonitorEventSender;
+        }
+
         private void waitForAsyncOperation() {
             if (mWorkerThread == null) {
                 return;
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtilsTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtilsTest.java
new file mode 100644
index 0000000..8e17b3a
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtilsTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.utils;
+
+import static org.junit.Assert.assertTrue;
+
+import android.app.backup.BackupManagerMonitor;
+import android.os.Bundle;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+public class BackupManagerMonitorDumpsysUtilsTest {
+    private File mTempFile;
+    private TestBackupManagerMonitorDumpsysUtils mBackupManagerMonitorDumpsysUtils;
+    @Rule
+    public TemporaryFolder tmp = new TemporaryFolder();
+
+    @Before
+    public void setUp() throws Exception {
+        mTempFile = tmp.newFile("testbmmevents.txt");
+        mBackupManagerMonitorDumpsysUtils = new TestBackupManagerMonitorDumpsysUtils();
+    }
+
+
+    @Test
+    public void parseBackupManagerMonitorEventForDumpsys_bundleIsNull_noLogsWrittenToFile()
+            throws Exception {
+        mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(null);
+
+        assertTrue(mTempFile.length() == 0);
+
+    }
+
+    @Test
+    public void parseBackupManagerMonitorEventForDumpsys_missingID_noLogsWrittenToFile()
+            throws Exception {
+        Bundle event = new Bundle();
+        event.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY, 1);
+        mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event);
+
+        assertTrue(mTempFile.length() == 0);
+    }
+
+    @Test
+    public void parseBackupManagerMonitorEventForDumpsys_missingCategory_noLogsWrittenToFile()
+            throws Exception {
+        Bundle event = new Bundle();
+        event.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, 1);
+        mBackupManagerMonitorDumpsysUtils.parseBackupManagerMonitorRestoreEventForDumpsys(event);
+
+        assertTrue(mTempFile.length() == 0);
+    }
+
+    private class TestBackupManagerMonitorDumpsysUtils
+            extends BackupManagerMonitorDumpsysUtils {
+        TestBackupManagerMonitorDumpsysUtils() {
+            super();
+        }
+
+        @Override
+        public File getBMMEventsFile() {
+            return mTempFile;
+        }
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorEventSenderTest.java
similarity index 67%
rename from services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java
rename to services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorEventSenderTest.java
index 093ad3c..3af2932 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupManagerMonitorEventSenderTest.java
@@ -30,11 +30,11 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
 import android.app.IBackupAgent;
-import android.app.backup.BackupAnnotations;
 import android.app.backup.BackupAnnotations.OperationType;
 import android.app.backup.BackupManagerMonitor;
 import android.app.backup.BackupRestoreEventLogger;
@@ -62,39 +62,65 @@
 @SmallTest
 @Presubmit
 @RunWith(AndroidJUnit4.class)
-public class BackupManagerMonitorUtilsTest {
+public class BackupManagerMonitorEventSenderTest {
     @Mock private IBackupManagerMonitor mMonitorMock;
+    @Mock private BackupManagerMonitorDumpsysUtils mBackupManagerMonitorDumpsysUtilsMock;
+
+    private BackupManagerMonitorEventSender mBackupManagerMonitorEventSender;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        mBackupManagerMonitorEventSender = new BackupManagerMonitorEventSender(mMonitorMock,
+                mBackupManagerMonitorDumpsysUtilsMock);
     }
 
     @Test
-    public void monitorEvent_monitorIsNull_returnsNull() throws Exception {
-        IBackupManagerMonitor result = BackupManagerMonitorUtils.monitorEvent(null, 0, null, 0,
-                null);
+    public void monitorEvent_monitorIsNull_sendBundleToDumpsys() throws Exception {
+        Bundle extras = new Bundle();
+        extras.putInt(EXTRA_LOG_OPERATION_TYPE, OperationType.RESTORE);
+        mBackupManagerMonitorEventSender.setMonitor(null);
+        mBackupManagerMonitorEventSender.monitorEvent(0, null, 0, extras);
+        IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor();
 
-        assertThat(result).isNull();
+        verify(mBackupManagerMonitorDumpsysUtilsMock).parseBackupManagerMonitorRestoreEventForDumpsys(any(
+                Bundle.class));
     }
 
     @Test
-    public void monitorEvent_monitorOnEventThrows_returnsNull() throws Exception {
+    public void monitorEvent_monitorIsNull_doNotCallOnEvent() throws Exception {
+        mBackupManagerMonitorEventSender = new BackupManagerMonitorEventSender(null);
+        mBackupManagerMonitorEventSender.monitorEvent(0, null, 0, null);
+        IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor();
+
+        verify(mMonitorMock, never()).onEvent(any(Bundle.class));
+    }
+
+    @Test
+    public void monitorEvent_monitorOnEventThrows_setsMonitorToNull() throws Exception {
         doThrow(new RemoteException()).when(mMonitorMock).onEvent(any(Bundle.class));
 
-        IBackupManagerMonitor result = BackupManagerMonitorUtils.monitorEvent(mMonitorMock, 0, null,
-                0, null);
+        mBackupManagerMonitorEventSender.monitorEvent(0, null, 0, null);
+        IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor();
 
         verify(mMonitorMock).onEvent(any(Bundle.class));
-        assertThat(result).isNull();
+        assertThat(monitor).isNull();
+    }
+
+    @Test
+    public void monitorEvent_extrasAreNull_doNotSendBundleToDumpsys() throws Exception {
+        mBackupManagerMonitorEventSender.monitorEvent(1, null, 2, null);
+
+        verify(mBackupManagerMonitorDumpsysUtilsMock, never())
+                .parseBackupManagerMonitorRestoreEventForDumpsys(any(Bundle.class));
     }
 
     @Test
     public void monitorEvent_packageAndExtrasAreNull_fillsBundleCorrectly() throws Exception {
-        IBackupManagerMonitor result = BackupManagerMonitorUtils.monitorEvent(mMonitorMock, 1, null,
-                2, null);
+        mBackupManagerMonitorEventSender.monitorEvent(1, null, 2, null);
+        IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor();
 
-        assertThat(result).isEqualTo(mMonitorMock);
+        assertThat(monitor).isEqualTo(mMonitorMock);
         ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
         verify(mMonitorMock).onEvent(bundleCaptor.capture());
         Bundle eventBundle = bundleCaptor.getValue();
@@ -112,10 +138,10 @@
         extras.putInt("key1", 4);
         extras.putString("key2", "value2");
 
-        IBackupManagerMonitor result = BackupManagerMonitorUtils.monitorEvent(mMonitorMock, 1,
-                packageInfo, 2, extras);
+        mBackupManagerMonitorEventSender.monitorEvent(1, packageInfo, 2, extras);
+        IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor();
 
-        assertThat(result).isEqualTo(mMonitorMock);
+        assertThat(monitor).isEqualTo(mMonitorMock);
         ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
         verify(mMonitorMock).onEvent(bundleCaptor.capture());
         Bundle eventBundle = bundleCaptor.getValue();
@@ -130,7 +156,8 @@
     }
 
     @Test
-    public void monitorEvent_packageAndExtrasAreNotNull_fillsBundleCorrectlyLong() throws Exception {
+    public void monitorEvent_packageAndExtrasAreNotNull_fillsBundleCorrectlyLong()
+            throws Exception {
         PackageInfo packageInfo = new PackageInfo();
         packageInfo.packageName = "test.package";
         packageInfo.versionCode = 3;
@@ -139,10 +166,10 @@
         extras.putInt("key1", 4);
         extras.putString("key2", "value2");
 
-        IBackupManagerMonitor result = BackupManagerMonitorUtils.monitorEvent(mMonitorMock, 1,
-                packageInfo, 2, extras);
+        mBackupManagerMonitorEventSender.monitorEvent(1, packageInfo, 2, extras);
+        IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor();
 
-        assertThat(result).isEqualTo(mMonitorMock);
+        assertThat(monitor).isEqualTo(mMonitorMock);
         ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
         verify(mMonitorMock).onEvent(bundleCaptor.capture());
         Bundle eventBundle = bundleCaptor.getValue();
@@ -158,15 +185,45 @@
     }
 
     @Test
+    public void monitorEvent_eventOpTypeIsRestore_sendBundleToDumpsys() throws Exception {
+        Bundle extras = new Bundle();
+        extras.putInt(EXTRA_LOG_OPERATION_TYPE, OperationType.RESTORE);
+        mBackupManagerMonitorEventSender.monitorEvent(1, null, 2, extras);
+
+        verify(mBackupManagerMonitorDumpsysUtilsMock).parseBackupManagerMonitorRestoreEventForDumpsys(any(
+                Bundle.class));
+    }
+
+    @Test
+    public void monitorEvent_eventOpTypeIsBackup_doNotSendBundleToDumpsys() throws Exception {
+        Bundle extras = new Bundle();
+        extras.putInt(EXTRA_LOG_OPERATION_TYPE, OperationType.BACKUP);
+        mBackupManagerMonitorEventSender.monitorEvent(1, null, 2, extras);
+
+        verify(mBackupManagerMonitorDumpsysUtilsMock, never())
+                .parseBackupManagerMonitorRestoreEventForDumpsys(any(Bundle.class));
+    }
+
+    @Test
+    public void monitorEvent_eventOpTypeIsUnknown_doNotSendBundleToDumpsys() throws Exception {
+        Bundle extras = new Bundle();
+        extras.putInt(EXTRA_LOG_OPERATION_TYPE, OperationType.UNKNOWN);
+        mBackupManagerMonitorEventSender.monitorEvent(1, null, 2, extras);
+
+        verify(mBackupManagerMonitorDumpsysUtilsMock, never())
+                .parseBackupManagerMonitorRestoreEventForDumpsys(any(Bundle.class));
+    }
+
+    @Test
     public void monitorAgentLoggingResults_onBackup_fillsBundleCorrectly() throws Exception {
         PackageInfo packageInfo = new PackageInfo();
         packageInfo.packageName = "test.package";
         // Mock an agent that returns a logging result.
         IBackupAgent agent = setUpLoggingAgentForOperation(OperationType.BACKUP);
 
-        IBackupManagerMonitor monitor =
-                BackupManagerMonitorUtils.monitorAgentLoggingResults(
-                        mMonitorMock, packageInfo, agent);
+
+        mBackupManagerMonitorEventSender.monitorAgentLoggingResults(packageInfo, agent);
+        IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor();
 
         assertCorrectBundleSentToMonitor(monitor, OperationType.BACKUP);
     }
@@ -178,9 +235,8 @@
         // Mock an agent that returns a logging result.
         IBackupAgent agent = setUpLoggingAgentForOperation(OperationType.RESTORE);
 
-        IBackupManagerMonitor monitor =
-                BackupManagerMonitorUtils.monitorAgentLoggingResults(
-                        mMonitorMock, packageInfo, agent);
+        mBackupManagerMonitorEventSender.monitorAgentLoggingResults(packageInfo, agent);
+        IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor();
 
         assertCorrectBundleSentToMonitor(monitor, OperationType.RESTORE);
     }
@@ -217,9 +273,9 @@
         List<BackupRestoreEventLogger.DataTypeResult> loggingResults = new ArrayList<>();
         loggingResults.add(new BackupRestoreEventLogger.DataTypeResult("testLoggingResult"));
 
-        IBackupManagerMonitor monitor = BackupManagerMonitorUtils.sendAgentLoggingResults(
-                mMonitorMock, packageInfo, loggingResults, OperationType.BACKUP);
-
+        mBackupManagerMonitorEventSender.sendAgentLoggingResults(
+                packageInfo, loggingResults, OperationType.BACKUP);
+        IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor();
         assertCorrectBundleSentToMonitor(monitor, OperationType.BACKUP);
     }
 
@@ -230,8 +286,9 @@
         List<BackupRestoreEventLogger.DataTypeResult> loggingResults = new ArrayList<>();
         loggingResults.add(new BackupRestoreEventLogger.DataTypeResult("testLoggingResult"));
 
-        IBackupManagerMonitor monitor = BackupManagerMonitorUtils.sendAgentLoggingResults(
-                mMonitorMock, packageInfo, loggingResults, OperationType.RESTORE);
+        mBackupManagerMonitorEventSender.sendAgentLoggingResults(
+                packageInfo, loggingResults, OperationType.RESTORE);
+        IBackupManagerMonitor monitor = mBackupManagerMonitorEventSender.getMonitor();
 
         assertCorrectBundleSentToMonitor(monitor, OperationType.RESTORE);
     }
@@ -262,7 +319,7 @@
     public void putMonitoringExtraString_bundleExists_fillsBundleCorrectly() throws Exception {
         Bundle bundle = new Bundle();
 
-        Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(bundle, "key", "value");
+        Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(bundle, "key", "value");
 
         assertThat(result).isEqualTo(bundle);
         assertThat(result.size()).isEqualTo(1);
@@ -272,7 +329,7 @@
     @Test
     public void putMonitoringExtraString_bundleDoesNotExist_fillsBundleCorrectly()
             throws Exception {
-        Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(null, "key", "value");
+        Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(null, "key", "value");
 
         assertThat(result).isNotNull();
         assertThat(result.size()).isEqualTo(1);
@@ -284,7 +341,7 @@
     public void putMonitoringExtraLong_bundleExists_fillsBundleCorrectly() throws Exception {
         Bundle bundle = new Bundle();
 
-        Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(bundle, "key", 123);
+        Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(bundle, "key", 123);
 
         assertThat(result).isEqualTo(bundle);
         assertThat(result.size()).isEqualTo(1);
@@ -293,7 +350,7 @@
 
     @Test
     public void putMonitoringExtraLong_bundleDoesNotExist_fillsBundleCorrectly() throws Exception {
-        Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(null, "key", 123);
+        Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(null, "key", 123);
 
         assertThat(result).isNotNull();
         assertThat(result.size()).isEqualTo(1);
@@ -304,7 +361,7 @@
     public void putMonitoringExtraBoolean_bundleExists_fillsBundleCorrectly() throws Exception {
         Bundle bundle = new Bundle();
 
-        Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(bundle, "key", true);
+        Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(bundle, "key", true);
 
         assertThat(result).isEqualTo(bundle);
         assertThat(result.size()).isEqualTo(1);
@@ -314,10 +371,10 @@
     @Test
     public void putMonitoringExtraBoolean_bundleDoesNotExist_fillsBundleCorrectly()
             throws Exception {
-        Bundle result = BackupManagerMonitorUtils.putMonitoringExtra(null, "key", true);
+        Bundle result = mBackupManagerMonitorEventSender.putMonitoringExtra(null, "key", true);
 
         assertThat(result).isNotNull();
         assertThat(result.size()).isEqualTo(1);
         assertThat(result.getBoolean("key")).isTrue();
     }
-}
\ No newline at end of file
+}
diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
index 24029b1..fc27edc 100644
--- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
@@ -35,6 +35,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -68,6 +69,27 @@
         mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager();
     }
 
+    @After
+    public void tearDown() throws Exception {
+        // Changes to RoleManager persist between tests, so we need to clear out any funny
+        // business we did in previous tests.
+        RoleManager roleManager = mContext.getSystemService(RoleManager.class);
+        CallbackFuture future = new CallbackFuture();
+        runWithShellPermissionIdentity(
+                () -> {
+                    roleManager.setBypassingRoleQualification(false);
+                    roleManager.removeRoleHolderAsUser(
+                            "android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION",
+                            mContext.getPackageName(),
+                            /* flags= */ 0,
+                            Process.myUserHandle(),
+                            mContext.getMainExecutor(),
+                            future);
+                });
+
+        assertThat(future.get()).isEqualTo(true);
+    }
+
     @Test
     public void testBugreportFileManagerFileExists() {
         Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
@@ -131,14 +153,17 @@
                 new BugreportManagerServiceImpl.Injector(mContext, new ArraySet<>()));
         RoleManager roleManager = mContext.getSystemService(RoleManager.class);
         CallbackFuture future = new CallbackFuture();
-        runWithShellPermissionIdentity(() -> roleManager.setBypassingRoleQualification(true));
-        runWithShellPermissionIdentity(() -> roleManager.addRoleHolderAsUser(
-                "android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION",
-                mContext.getPackageName(),
-                /* flags= */ 0,
-                Process.myUserHandle(),
-                mContext.getMainExecutor(),
-                future));
+        runWithShellPermissionIdentity(
+                () -> {
+                    roleManager.setBypassingRoleQualification(true);
+                    roleManager.addRoleHolderAsUser(
+                            "android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION",
+                            mContext.getPackageName(),
+                            /* flags= */ 0,
+                            Process.myUserHandle(),
+                            mContext.getMainExecutor(),
+                            future);
+                });
 
         assertThat(future.get()).isEqualTo(true);
         mService.cancelBugreport(Binder.getCallingUid(), mContext.getPackageName());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 5c35b05..6a6fa3f 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -6043,6 +6043,49 @@
     }
 
     @Test
+    public void testVisitUris_styleExtrasWithoutStyle() {
+        Notification notification = new Notification.Builder(mContext, "a")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .build();
+
+        Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle(
+                personWithIcon("content://user"))
+                .addHistoricMessage(new Notification.MessagingStyle.Message("Heyhey!",
+                                System.currentTimeMillis(),
+                                personWithIcon("content://historicalMessenger")))
+                .addMessage(new Notification.MessagingStyle.Message("Are you there",
+                                System.currentTimeMillis(),
+                                personWithIcon("content://messenger")))
+                        .setShortcutIcon(
+                                Icon.createWithContentUri("content://conversationShortcut"));
+        messagingStyle.addExtras(notification.extras); // Instead of Builder.setStyle(style).
+
+        Notification.CallStyle callStyle = Notification.CallStyle.forOngoingCall(
+                        personWithIcon("content://caller"),
+                        PendingIntent.getActivity(mContext, 0, new Intent(),
+                                PendingIntent.FLAG_IMMUTABLE))
+                .setVerificationIcon(Icon.createWithContentUri("content://callVerification"));
+        callStyle.addExtras(notification.extras); // Same.
+
+        Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class);
+        notification.visitUris(visitor);
+
+        verify(visitor).accept(eq(Uri.parse("content://user")));
+        verify(visitor).accept(eq(Uri.parse("content://historicalMessenger")));
+        verify(visitor).accept(eq(Uri.parse("content://messenger")));
+        verify(visitor).accept(eq(Uri.parse("content://conversationShortcut")));
+        verify(visitor).accept(eq(Uri.parse("content://caller")));
+        verify(visitor).accept(eq(Uri.parse("content://callVerification")));
+    }
+
+    private static Person personWithIcon(String iconUri) {
+        return new Person.Builder()
+                .setName("Mr " + iconUri)
+                .setIcon(Icon.createWithContentUri(iconUri))
+                .build();
+    }
+
+    @Test
     public void testVisitUris_wearableExtender() {
         Icon actionIcon = Icon.createWithContentUri("content://media/action");
         Icon wearActionIcon = Icon.createWithContentUri("content://media/wearAction");
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index d7cd9f2..7544fda 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1187,6 +1187,7 @@
         final WindowState statusBar = createWindow(null, TYPE_STATUS_BAR, "statusBar");
         makeWindowVisible(statusBar);
         mDisplayContent.getDisplayPolicy().addWindowLw(statusBar, statusBar.mAttrs);
+        final WindowState navBar = createWindow(null, TYPE_NAVIGATION_BAR, "navBar");
         final ActivityRecord app = createActivityRecord(mDisplayContent);
         final Transition transition = app.mTransitionController.createTransition(TRANSIT_OPEN);
         app.mTransitionController.requestStartTransition(transition, app.getTask(),
@@ -1216,9 +1217,17 @@
         mDisplayContent.mTransitionController.dispatchLegacyAppTransitionFinished(app);
         assertTrue(mDisplayContent.hasTopFixedRotationLaunchingApp());
 
+        // The bar was invisible so it is not handled by the controller. But if it becomes visible
+        // and drawn before the transition starts,
+        assertFalse(asyncRotationController.isTargetToken(navBar.mToken));
+        navBar.finishDrawing(null /* postDrawTransaction */, Integer.MAX_VALUE);
+        assertTrue(asyncRotationController.isTargetToken(navBar.mToken));
+
         player.startTransition();
         // Non-app windows should not be collected.
         assertFalse(mDisplayContent.mTransitionController.isCollecting(statusBar.mToken));
+        // Avoid DeviceStateController disturbing the test by triggering another rotation change.
+        doReturn(false).when(mDisplayContent).updateRotationUnchecked();
 
         onRotationTransactionReady(player, mWm.mTransactionFactory.get()).onTransactionCommitted();
         assertEquals(ROTATION_ANIMATION_SEAMLESS, player.mLastReady.getChange(
diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java
index 0d88a0d..030615f 100644
--- a/services/usage/java/com/android/server/usage/StorageStatsService.java
+++ b/services/usage/java/com/android/server/usage/StorageStatsService.java
@@ -259,7 +259,24 @@
         // NOTE: No permissions required
 
         if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {
-            return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize());
+            // As a safety measure, use the original implementation for the devices
+            // with storage size <= 512GB to prevent any potential regressions
+            final long roundedUserspaceBytes = mStorage.getPrimaryStorageSize();
+            if (roundedUserspaceBytes <= DataUnit.GIGABYTES.toBytes(512)) {
+                return roundedUserspaceBytes;
+            }
+
+            // Since 1TB devices can actually have either 1000GB or 1024GB,
+            // get the block device size and do just a small rounding if any at all
+            final long totalBytes = mStorage.getInternalStorageBlockDeviceSize();
+            final long totalBytesRounded = FileUtils.roundStorageSize(totalBytes);
+            // If the storage size is 997GB-999GB, round it to a 1000GB to show
+            // 1TB in UI instead of 0.99TB. Same for 2TB, 4TB, 8TB etc.
+            if (totalBytesRounded - totalBytes <= DataUnit.GIGABYTES.toBytes(3)) {
+                return totalBytesRounded;
+            } else {
+                return totalBytes;
+            }
         } else {
             final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
             if (vol == null) {
@@ -286,15 +303,19 @@
             // Free space is usable bytes plus any cached data that we're
             // willing to automatically clear. To avoid user confusion, this
             // logic should be kept in sync with getAllocatableBytes().
+            long freeBytes;
             if (isQuotaSupported(volumeUuid, PLATFORM_PACKAGE_NAME)) {
                 final long cacheTotal = getCacheBytes(volumeUuid, PLATFORM_PACKAGE_NAME);
                 final long cacheReserved = mStorage.getStorageCacheBytes(path, 0);
                 final long cacheClearable = Math.max(0, cacheTotal - cacheReserved);
 
-                return path.getUsableSpace() + cacheClearable;
+                freeBytes = path.getUsableSpace() + cacheClearable;
             } else {
-                return path.getUsableSpace();
+                freeBytes = path.getUsableSpace();
             }
+
+            Slog.d(TAG, "getFreeBytes: " + freeBytes);
+            return freeBytes;
         } finally {
             Binder.restoreCallingIdentity(token);
         }