Merge "Fix unfold transition with RTL" into tm-qpr-dev
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 06c35b5..3d5c34c 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -962,7 +962,7 @@
          * also be bumped.
          */
         static final String[] USER_ACTIVITY_TYPES = {
-            "other", "button", "touch", "accessibility", "attention"
+            "other", "button", "touch", "accessibility", "attention", "faceDown", "deviceState"
         };
 
         public static final int NUM_USER_ACTIVITY_TYPES = USER_ACTIVITY_TYPES.length;
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 13ca2c3..a46868e 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -345,6 +345,39 @@
     public static final int USER_ACTIVITY_EVENT_DEVICE_STATE = 6;
 
     /**
+     * @hide
+     */
+    @IntDef(prefix = { "USER_ACTIVITY_EVENT_" }, value = {
+            USER_ACTIVITY_EVENT_OTHER,
+            USER_ACTIVITY_EVENT_BUTTON,
+            USER_ACTIVITY_EVENT_TOUCH,
+            USER_ACTIVITY_EVENT_ACCESSIBILITY,
+            USER_ACTIVITY_EVENT_ATTENTION,
+            USER_ACTIVITY_EVENT_FACE_DOWN,
+            USER_ACTIVITY_EVENT_DEVICE_STATE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface UserActivityEvent{}
+
+    /**
+     *
+     * Convert the user activity event to a string for debugging purposes.
+     * @hide
+     */
+    public static String userActivityEventToString(@UserActivityEvent int userActivityEvent) {
+        switch (userActivityEvent) {
+            case USER_ACTIVITY_EVENT_OTHER: return "other";
+            case USER_ACTIVITY_EVENT_BUTTON: return "button";
+            case USER_ACTIVITY_EVENT_TOUCH: return "touch";
+            case USER_ACTIVITY_EVENT_ACCESSIBILITY: return "accessibility";
+            case USER_ACTIVITY_EVENT_ATTENTION: return "attention";
+            case USER_ACTIVITY_EVENT_FACE_DOWN: return "faceDown";
+            case USER_ACTIVITY_EVENT_DEVICE_STATE: return "deviceState";
+            default: return Integer.toString(userActivityEvent);
+        }
+    }
+
+    /**
      * User activity flag: If already dimmed, extend the dim timeout
      * but do not brighten.  This flag is useful for keeping the screen on
      * a little longer without causing a visible change such as when
diff --git a/core/java/android/window/TaskFragmentParentInfo.java b/core/java/android/window/TaskFragmentParentInfo.java
index 64b2638..841354a 100644
--- a/core/java/android/window/TaskFragmentParentInfo.java
+++ b/core/java/android/window/TaskFragmentParentInfo.java
@@ -33,19 +33,19 @@
 
     private final int mDisplayId;
 
-    private final boolean mVisibleRequested;
+    private final boolean mVisible;
 
     public TaskFragmentParentInfo(@NonNull Configuration configuration, int displayId,
-            boolean visibleRequested) {
+            boolean visible) {
         mConfiguration.setTo(configuration);
         mDisplayId = displayId;
-        mVisibleRequested = visibleRequested;
+        mVisible = visible;
     }
 
     public TaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) {
         mConfiguration.setTo(info.getConfiguration());
         mDisplayId = info.mDisplayId;
-        mVisibleRequested = info.mVisibleRequested;
+        mVisible = info.mVisible;
     }
 
     /** The {@link Configuration} of the parent Task */
@@ -62,9 +62,9 @@
         return mDisplayId;
     }
 
-    /** Whether the parent Task is requested to be visible or not */
-    public boolean isVisibleRequested() {
-        return mVisibleRequested;
+    /** Whether the parent Task is visible or not */
+    public boolean isVisible() {
+        return mVisible;
     }
 
     /**
@@ -80,7 +80,7 @@
             return false;
         }
         return getWindowingMode() == that.getWindowingMode() && mDisplayId == that.mDisplayId
-                && mVisibleRequested == that.mVisibleRequested;
+                && mVisible == that.mVisible;
     }
 
     @WindowConfiguration.WindowingMode
@@ -93,7 +93,7 @@
         return TaskFragmentParentInfo.class.getSimpleName() + ":{"
                 + "config=" + mConfiguration
                 + ", displayId=" + mDisplayId
-                + ", visibleRequested=" + mVisibleRequested
+                + ", visible=" + mVisible
                 + "}";
     }
 
@@ -114,14 +114,14 @@
         final TaskFragmentParentInfo that = (TaskFragmentParentInfo) obj;
         return mConfiguration.equals(that.mConfiguration)
                 && mDisplayId == that.mDisplayId
-                && mVisibleRequested == that.mVisibleRequested;
+                && mVisible == that.mVisible;
     }
 
     @Override
     public int hashCode() {
         int result = mConfiguration.hashCode();
         result = 31 * result + mDisplayId;
-        result = 31 * result + (mVisibleRequested ? 1 : 0);
+        result = 31 * result + (mVisible ? 1 : 0);
         return result;
     }
 
@@ -129,13 +129,13 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         mConfiguration.writeToParcel(dest, flags);
         dest.writeInt(mDisplayId);
-        dest.writeBoolean(mVisibleRequested);
+        dest.writeBoolean(mVisible);
     }
 
     private TaskFragmentParentInfo(Parcel in) {
         mConfiguration.readFromParcel(in);
         mDisplayId = in.readInt();
-        mVisibleRequested = in.readBoolean();
+        mVisible = in.readBoolean();
     }
 
     public static final Creator<TaskFragmentParentInfo> CREATOR =
diff --git a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java
index 5f4a9cd..473134e 100644
--- a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java
+++ b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java
@@ -172,14 +172,14 @@
 
     @Override
     public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
-        prepareIntentForCrossProfileLaunch(mResolvedIntent, userId);
+        TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, userId);
         activity.startActivityAsCaller(mResolvedIntent, options, false, userId);
         return true;
     }
 
     @Override
     public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
-        prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier());
+        TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier());
         activity.startActivityAsUser(mResolvedIntent, options, user);
         return false;
     }
@@ -224,13 +224,6 @@
         }
     };
 
-    private static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) {
-        final int currentUserId = UserHandle.myUserId();
-        if (targetUserId != currentUserId) {
-            intent.fixUris(currentUserId);
-        }
-    }
-
     private DisplayResolveInfo(Parcel in) {
         mDisplayLabel = in.readCharSequence();
         mExtendedInfo = in.readCharSequence();
diff --git a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
index 264e4f7..4b9b7cb 100644
--- a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
+++ b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
@@ -232,6 +232,7 @@
         }
         intent.setComponent(mChooserTarget.getComponentName());
         intent.putExtras(mChooserTarget.getIntentExtras());
+        TargetInfo.prepareIntentForCrossProfileLaunch(intent, userId);
 
         // Important: we will ignore the target security checks in ActivityManager
         // if and only if the ChooserTarget's target package is the same package
diff --git a/core/java/com/android/internal/app/chooser/TargetInfo.java b/core/java/com/android/internal/app/chooser/TargetInfo.java
index f56ab17..7bb7ddc 100644
--- a/core/java/com/android/internal/app/chooser/TargetInfo.java
+++ b/core/java/com/android/internal/app/chooser/TargetInfo.java
@@ -130,4 +130,15 @@
      * @return true if this target should be pinned to the front by the request of the user
      */
     boolean isPinned();
+
+    /**
+     * Fix the URIs in {@code intent} if cross-profile sharing is required. This should be called
+     * before launching the intent as another user.
+     */
+    static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) {
+        final int currentUserId = UserHandle.myUserId();
+        if (targetUserId != currentUserId) {
+            intent.fixUris(currentUserId);
+        }
+    }
 }
diff --git a/core/java/com/android/internal/display/BrightnessSynchronizer.java b/core/java/com/android/internal/display/BrightnessSynchronizer.java
index 627631a..d503904 100644
--- a/core/java/com/android/internal/display/BrightnessSynchronizer.java
+++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java
@@ -115,7 +115,7 @@
             Slog.i(TAG, "Setting initial brightness to default value of: " + defaultBrightness);
         }
 
-        mBrightnessSyncObserver.startObserving();
+        mBrightnessSyncObserver.startObserving(mHandler);
         mHandler.sendEmptyMessageAtTime(MSG_RUN_UPDATE, mClock.uptimeMillis());
     }
 
@@ -482,30 +482,31 @@
             }
         };
 
-        private final ContentObserver mContentObserver = new ContentObserver(mHandler) {
-            @Override
-            public void onChange(boolean selfChange, Uri uri) {
-                if (selfChange) {
-                    return;
+        private ContentObserver createBrightnessContentObserver(Handler handler) {
+            return new ContentObserver(handler) {
+                @Override
+                public void onChange(boolean selfChange, Uri uri) {
+                    if (selfChange) {
+                        return;
+                    }
+                    if (BRIGHTNESS_URI.equals(uri)) {
+                        handleBrightnessChangeInt(getScreenBrightnessInt());
+                    }
                 }
-                if (BRIGHTNESS_URI.equals(uri)) {
-                    handleBrightnessChangeInt(getScreenBrightnessInt());
-                }
-            }
-        };
+            };
+        }
 
         boolean isObserving() {
             return mIsObserving;
         }
 
-        void startObserving() {
+        void startObserving(Handler handler) {
             final ContentResolver cr = mContext.getContentResolver();
-            cr.registerContentObserver(BRIGHTNESS_URI, false, mContentObserver,
-                    UserHandle.USER_ALL);
-            mDisplayManager.registerDisplayListener(mListener, mHandler,
+            cr.registerContentObserver(BRIGHTNESS_URI, false,
+                    createBrightnessContentObserver(handler), UserHandle.USER_ALL);
+            mDisplayManager.registerDisplayListener(mListener, handler,
                     DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS);
             mIsObserving = true;
         }
-
     }
 }
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index ec2bc7c..8b96597 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -167,7 +167,7 @@
     private static final int MAGIC = 0xBA757475; // 'BATSTATS'
 
     // Current on-disk Parcel version
-    static final int VERSION = 208;
+    static final int VERSION = 210;
 
     // The maximum number of names wakelocks we will keep track of
     // per uid; once the limit is reached, we batch the remaining wakelocks
@@ -6145,12 +6145,13 @@
 
     @UnsupportedAppUsage
     @GuardedBy("this")
-    public void noteUserActivityLocked(int uid, int event) {
+    public void noteUserActivityLocked(int uid, @PowerManager.UserActivityEvent int event) {
         noteUserActivityLocked(uid, event, mClock.elapsedRealtime(), mClock.uptimeMillis());
     }
 
     @GuardedBy("this")
-    public void noteUserActivityLocked(int uid, int event, long elapsedRealtimeMs, long uptimeMs) {
+    public void noteUserActivityLocked(int uid, @PowerManager.UserActivityEvent int event,
+            long elapsedRealtimeMs, long uptimeMs) {
         if (mOnBatteryInternal) {
             uid = mapUid(uid);
             getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs).noteUserActivityLocked(event);
@@ -9962,14 +9963,14 @@
         }
 
         @Override
-        public void noteUserActivityLocked(int type) {
+        public void noteUserActivityLocked(@PowerManager.UserActivityEvent int event) {
             if (mUserActivityCounters == null) {
                 initUserActivityLocked();
             }
-            if (type >= 0 && type < NUM_USER_ACTIVITY_TYPES) {
-                mUserActivityCounters[type].stepAtomic();
+            if (event >= 0 && event < NUM_USER_ACTIVITY_TYPES) {
+                mUserActivityCounters[event].stepAtomic();
             } else {
-                Slog.w(TAG, "Unknown user activity type " + type + " was specified.",
+                Slog.w(TAG, "Unknown user activity type " + event + " was specified.",
                         new Throwable());
             }
         }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index a4da8de4..1235b60 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -1522,8 +1522,7 @@
                         STRONG_AUTH_REQUIRED_AFTER_LOCKOUT,
                         STRONG_AUTH_REQUIRED_AFTER_TIMEOUT,
                         STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN,
-                        STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT,
-                        SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED})
+                        STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT})
         @Retention(RetentionPolicy.SOURCE)
         public @interface StrongAuthFlags {}
 
@@ -1576,12 +1575,6 @@
         public static final int STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT = 0x80;
 
         /**
-         * Some authentication is required because the trustagent either timed out or was disabled
-         * manually.
-         */
-        public static final int SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED = 0x100;
-
-        /**
          * Strong auth flags that do not prevent biometric methods from being accepted as auth.
          * If any other flags are set, biometric authentication is disabled.
          */
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 91573ff..00943f2d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -140,7 +140,7 @@
     void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) {
         mConfiguration.setTo(info.getConfiguration());
         mDisplayId = info.getDisplayId();
-        mIsVisible = info.isVisibleRequested();
+        mIsVisible = info.isVisible();
     }
 
     /**
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 7960dec..82573b2 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -44,6 +44,7 @@
     srcs: [
         "src/com/android/wm/shell/util/**/*.java",
         "src/com/android/wm/shell/common/split/SplitScreenConstants.java",
+        "src/com/android/wm/shell/sysui/ShellSharedConstants.java",
     ],
     path: "src",
 }
@@ -100,6 +101,21 @@
     out: ["wm_shell_protolog.json"],
 }
 
+genrule {
+    name: "protolog.json.gz",
+    srcs: [":generate-wm_shell_protolog.json"],
+    out: ["wmshell.protolog.json.gz"],
+    cmd: "$(location minigzip) -c < $(in) > $(out)",
+    tools: ["minigzip"],
+}
+
+prebuilt_etc {
+    name: "wmshell.protolog.json.gz",
+    system_ext_specific: true,
+    src: ":protolog.json.gz",
+    filename_from_src: true,
+}
+
 // End ProtoLog
 
 java_library {
@@ -123,9 +139,6 @@
     resource_dirs: [
         "res",
     ],
-    java_resources: [
-        ":generate-wm_shell_protolog.json",
-    ],
     static_libs: [
         "androidx.appcompat_appcompat",
         "androidx.arch.core_core-runtime",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
index 86f9d5b..8cbe44b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
@@ -29,13 +29,6 @@
 public interface BackAnimation {
 
     /**
-     * Returns a binder that can be passed to an external process to update back animations.
-     */
-    default IBackAnimation createExternalInterface() {
-        return null;
-    }
-
-    /**
      * Called when a {@link MotionEvent} is generated by a back gesture.
      *
      * @param touchX the X touch position of the {@link MotionEvent}.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 33ecdd8..938189f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -21,6 +21,7 @@
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -57,10 +58,12 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -105,6 +108,7 @@
     private final IActivityTaskManager mActivityTaskManager;
     private final Context mContext;
     private final ContentResolver mContentResolver;
+    private final ShellController mShellController;
     private final ShellExecutor mShellExecutor;
     private final Handler mBgHandler;
     @Nullable
@@ -231,21 +235,25 @@
 
     public BackAnimationController(
             @NonNull ShellInit shellInit,
+            @NonNull ShellController shellController,
             @NonNull @ShellMainThread ShellExecutor shellExecutor,
             @NonNull @ShellBackgroundThread Handler backgroundHandler,
             Context context) {
-        this(shellInit, shellExecutor, backgroundHandler, new SurfaceControl.Transaction(),
-                ActivityTaskManager.getService(), context, context.getContentResolver());
+        this(shellInit, shellController, shellExecutor, backgroundHandler,
+                new SurfaceControl.Transaction(), ActivityTaskManager.getService(),
+                context, context.getContentResolver());
     }
 
     @VisibleForTesting
     BackAnimationController(
             @NonNull ShellInit shellInit,
+            @NonNull ShellController shellController,
             @NonNull @ShellMainThread ShellExecutor shellExecutor,
             @NonNull @ShellBackgroundThread Handler bgHandler,
             @NonNull SurfaceControl.Transaction transaction,
             @NonNull IActivityTaskManager activityTaskManager,
             Context context, ContentResolver contentResolver) {
+        mShellController = shellController;
         mShellExecutor = shellExecutor;
         mTransaction = transaction;
         mActivityTaskManager = activityTaskManager;
@@ -257,6 +265,8 @@
 
     private void onInit() {
         setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler);
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION,
+                this::createExternalInterface, this);
     }
 
     private void setupAnimationDeveloperSettingsObserver(
@@ -289,7 +299,11 @@
         return mBackAnimation;
     }
 
-    private final BackAnimation mBackAnimation = new BackAnimationImpl();
+    private ExternalInterfaceBinder createExternalInterface() {
+        return new IBackAnimationImpl(this);
+    }
+
+    private final BackAnimationImpl mBackAnimation = new BackAnimationImpl();
 
     @Override
     public Context getContext() {
@@ -302,17 +316,6 @@
     }
 
     private class BackAnimationImpl implements BackAnimation {
-        private IBackAnimationImpl mBackAnimation;
-
-        @Override
-        public IBackAnimation createExternalInterface() {
-            if (mBackAnimation != null) {
-                mBackAnimation.invalidate();
-            }
-            mBackAnimation = new IBackAnimationImpl(BackAnimationController.this);
-            return mBackAnimation;
-        }
-
         @Override
         public void onBackMotion(
                 float touchX, float touchY, int keyAction, @BackEvent.SwipeEdge int swipeEdge) {
@@ -331,7 +334,8 @@
         }
     }
 
-    private static class IBackAnimationImpl extends IBackAnimation.Stub {
+    private static class IBackAnimationImpl extends IBackAnimation.Stub
+            implements ExternalInterfaceBinder {
         private BackAnimationController mController;
 
         IBackAnimationImpl(BackAnimationController controller) {
@@ -356,7 +360,8 @@
                     (controller) -> controller.onBackToLauncherAnimationFinished());
         }
 
-        void invalidate() {
+        @Override
+        public void invalidate() {
             mController = null;
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java
new file mode 100644
index 0000000..aa5b0cb
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common;
+
+import android.os.IBinder;
+
+/**
+ * An interface for binders which can be registered to be sent to other processes.
+ */
+public interface ExternalInterfaceBinder {
+    /**
+     * Invalidates this binder (detaches it from the controller it would call).
+     */
+    void invalidate();
+
+    /**
+     * Returns the IBinder to send.
+     */
+    IBinder asBinder();
+}
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 7d0c4bd..28a1959 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
@@ -261,13 +261,14 @@
     static Optional<BackAnimationController> provideBackAnimationController(
             Context context,
             ShellInit shellInit,
+            ShellController shellController,
             @ShellMainThread ShellExecutor shellExecutor,
             @ShellBackgroundThread Handler backgroundHandler
     ) {
         if (BackAnimationController.IS_ENABLED) {
             return Optional.of(
-                    new BackAnimationController(shellInit, shellExecutor, backgroundHandler,
-                            context));
+                    new BackAnimationController(shellInit, shellController, shellExecutor,
+                            backgroundHandler, context));
         }
         return Optional.empty();
     }
@@ -471,6 +472,7 @@
     static Optional<RecentTasksController> provideRecentTasksController(
             Context context,
             ShellInit shellInit,
+            ShellController shellController,
             ShellCommandHandler shellCommandHandler,
             TaskStackListenerImpl taskStackListener,
             ActivityTaskManager activityTaskManager,
@@ -478,9 +480,9 @@
             @ShellMainThread ShellExecutor mainExecutor
     ) {
         return Optional.ofNullable(
-                RecentTasksController.create(context, shellInit, shellCommandHandler,
-                        taskStackListener, activityTaskManager, desktopModeTaskRepository,
-                        mainExecutor));
+                RecentTasksController.create(context, shellInit, shellController,
+                        shellCommandHandler, taskStackListener, activityTaskManager,
+                        desktopModeTaskRepository, mainExecutor));
     }
 
     //
@@ -497,14 +499,15 @@
     @Provides
     static Transitions provideTransitions(Context context,
             ShellInit shellInit,
+            ShellController shellController,
             ShellTaskOrganizer organizer,
             TransactionPool pool,
             DisplayController displayController,
             @ShellMainThread ShellExecutor mainExecutor,
             @ShellMainThread Handler mainHandler,
             @ShellAnimationThread ShellExecutor animExecutor) {
-        return new Transitions(context, shellInit, organizer, pool, displayController, mainExecutor,
-                mainHandler, animExecutor);
+        return new Transitions(context, shellInit, shellController, organizer, pool,
+                displayController, mainExecutor, mainHandler, animExecutor);
     }
 
     @WMSingleton
@@ -621,13 +624,15 @@
 
     @WMSingleton
     @Provides
-    static StartingWindowController provideStartingWindowController(Context context,
+    static StartingWindowController provideStartingWindowController(
+            Context context,
             ShellInit shellInit,
+            ShellController shellController,
             ShellTaskOrganizer shellTaskOrganizer,
             @ShellSplashscreenThread ShellExecutor splashScreenExecutor,
             StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider,
             TransactionPool pool) {
-        return new StartingWindowController(context, shellInit, shellTaskOrganizer,
+        return new StartingWindowController(context, shellInit, shellController, shellTaskOrganizer,
                 splashScreenExecutor, startingWindowTypeAlgorithm, iconProvider, pool);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 461d7dc..1ec98d3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -598,7 +598,9 @@
     @WMSingleton
     @Provides
     @DynamicOverride
-    static DesktopModeController provideDesktopModeController(Context context, ShellInit shellInit,
+    static DesktopModeController provideDesktopModeController(Context context,
+            ShellInit shellInit,
+            ShellController shellController,
             ShellTaskOrganizer shellTaskOrganizer,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             Transitions transitions,
@@ -606,7 +608,7 @@
             @ShellMainThread Handler mainHandler,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
-        return new DesktopModeController(context, shellInit, shellTaskOrganizer,
+        return new DesktopModeController(context, shellInit, shellController, shellTaskOrganizer,
                 rootTaskDisplayAreaOrganizer, transitions, desktopModeTaskRepository, mainHandler,
                 mainExecutor);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index ff3be38..44a467f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -23,9 +23,4 @@
  */
 @ExternalThread
 public interface DesktopMode {
-
-    /** Returns a binder that can be passed to an external process to manipulate DesktopMode. */
-    default IDesktopMode createExternalInterface() {
-        return null;
-    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index 99739c4..b96facf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -19,9 +19,11 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OPEN;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
 
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.WindowConfiguration;
@@ -29,23 +31,30 @@
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.ArraySet;
+import android.view.SurfaceControl;
 import android.window.DisplayAreaInfo;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
 import android.window.WindowContainerTransaction;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
@@ -55,18 +64,22 @@
 /**
  * Handles windowing changes when desktop mode system setting changes
  */
-public class DesktopModeController implements RemoteCallable<DesktopModeController> {
+public class DesktopModeController implements RemoteCallable<DesktopModeController>,
+        Transitions.TransitionHandler {
 
     private final Context mContext;
+    private final ShellController mShellController;
     private final ShellTaskOrganizer mShellTaskOrganizer;
     private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
     private final Transitions mTransitions;
     private final DesktopModeTaskRepository mDesktopModeTaskRepository;
     private final ShellExecutor mMainExecutor;
-    private final DesktopMode mDesktopModeImpl = new DesktopModeImpl();
+    private final DesktopModeImpl mDesktopModeImpl = new DesktopModeImpl();
     private final SettingsObserver mSettingsObserver;
 
-    public DesktopModeController(Context context, ShellInit shellInit,
+    public DesktopModeController(Context context,
+            ShellInit shellInit,
+            ShellController shellController,
             ShellTaskOrganizer shellTaskOrganizer,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             Transitions transitions,
@@ -74,6 +87,7 @@
             @ShellMainThread Handler mainHandler,
             @ShellMainThread ShellExecutor mainExecutor) {
         mContext = context;
+        mShellController = shellController;
         mShellTaskOrganizer = shellTaskOrganizer;
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
         mTransitions = transitions;
@@ -85,10 +99,13 @@
 
     private void onInit() {
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController");
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_DESKTOP_MODE,
+                this::createExternalInterface, this);
         mSettingsObserver.observe();
         if (DesktopModeStatus.isActive(mContext)) {
             updateDesktopModeActive(true);
         }
+        mTransitions.addHandler(this);
     }
 
     @Override
@@ -108,6 +125,13 @@
         return mDesktopModeImpl;
     }
 
+    /**
+     * Creates a new instance of the external interface to pass to another process.
+     */
+    private ExternalInterfaceBinder createExternalInterface() {
+        return new IDesktopModeImpl(this);
+    }
+
     @VisibleForTesting
     void updateDesktopModeActive(boolean active) {
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active);
@@ -157,7 +181,7 @@
     /**
      * Show apps on desktop
      */
-    public void showDesktopApps() {
+    WindowContainerTransaction showDesktopApps() {
         ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
         ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>();
@@ -173,7 +197,12 @@
         for (RunningTaskInfo task : taskInfos) {
             wct.reorder(task.token, true);
         }
-        mShellTaskOrganizer.applyTransaction(wct);
+
+        if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mShellTaskOrganizer.applyTransaction(wct);
+        }
+
+        return wct;
     }
 
     /**
@@ -195,6 +224,35 @@
                 .configuration.windowConfiguration.getWindowingMode();
     }
 
+    @Override
+    public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        // This handler should never be the sole handler, so should not animate anything.
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+            @NonNull TransitionRequestInfo request) {
+
+        // Only do anything if we are in desktop mode and opening a task/app
+        if (!DesktopModeStatus.isActive(mContext) || request.getType() != TRANSIT_OPEN) {
+            return null;
+        }
+
+        WindowContainerTransaction wct = mTransitions.dispatchRequest(transition, request, this);
+        if (wct == null) {
+            wct = new WindowContainerTransaction();
+        }
+        wct.merge(showDesktopApps(), true /* transfer */);
+        wct.reorder(request.getTriggerTask().token, true /* onTop */);
+
+        return wct;
+    }
+
     /**
      * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE}
      */
@@ -235,24 +293,15 @@
      */
     @ExternalThread
     private final class DesktopModeImpl implements DesktopMode {
-
-        private IDesktopModeImpl mIDesktopMode;
-
-        @Override
-        public IDesktopMode createExternalInterface() {
-            if (mIDesktopMode != null) {
-                mIDesktopMode.invalidate();
-            }
-            mIDesktopMode = new IDesktopModeImpl(DesktopModeController.this);
-            return mIDesktopMode;
-        }
+        // Do nothing
     }
 
     /**
      * The interface for calls from outside the host process.
      */
     @BinderThread
-    private static class IDesktopModeImpl extends IDesktopMode.Stub {
+    private static class IDesktopModeImpl extends IDesktopMode.Stub
+            implements ExternalInterfaceBinder {
 
         private DesktopModeController mController;
 
@@ -263,7 +312,8 @@
         /**
          * Invalidates this instance, preventing future calls from updating the controller.
          */
-        void invalidate() {
+        @Override
+        public void invalidate() {
             mController = null;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
index 2aa933d..fbf326e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
@@ -29,19 +29,37 @@
 ### SysUI accessible components
 In addition to doing the above, you will also need to provide an interface for calling to SysUI
 from the Shell and vice versa.  The current pattern is to have a parallel `Optional<Component name>`
-interface that the `<Component name>Controller` implements and handles on the main Shell thread.
+interface that the `<Component name>Controller` implements and handles on the main Shell thread
+(see [SysUI/Shell threading](threading.md)).
 
 In addition, because components accessible to SysUI injection are explicitly listed, you'll have to
 add an appropriate method in `WMComponent` to get the interface and update the `Builder` in
 `SysUIComponent` to take the interface so it can be injected in SysUI code.  The binding between
 the two is done in `SystemUIFactory#init()` which will need to be updated as well.
 
+Specifically, to support calling into a controller from an external process (like Launcher):
+- Create an implementation of the external interface within the controller
+- Have all incoming calls post to the main shell thread (inject @ShellMainThread Executor into the
+  controller if needed)
+- Note that callbacks into SysUI should take an associated executor to call back on
+
 ### Launcher accessible components
 Because Launcher is not a part of SystemUI and is a separate process, exposing controllers to
 Launcher requires a new AIDL interface to be created and implemented by the controller.  The
 implementation of the stub interface in the controller otherwise behaves similar to the interface
 to SysUI where it posts the work to the main Shell thread.
 
+Specifically, to support calling into a controller from an external process (like Launcher):
+- Create an implementation of the interface binder's `Stub` class within the controller, have it
+  extend `ExternalInterfaceBinder` and implement `invalidate()` to ensure it doesn't hold long
+  references to the outer controller
+- Make the controller implement `RemoteCallable<T>`, and have all incoming calls use one of
+  the `ExecutorUtils.executeRemoteCallWithTaskPermission()` calls to verify the caller's identity
+  and ensure the call happens on the main shell thread and not the binder thread
+- Inject `ShellController` and add the instance of the implementation as external interface
+- In Launcher, update `TouchInteractionService` to pass the interface to `SystemUIProxy`, and then
+  call the SystemUIProxy method as needed in that code
+
 ### Component initialization
 To initialize the component:
 - On the Shell side, you potentially need to do two things to initialize the component:
@@ -64,8 +82,9 @@
 
 ### General Do's & Dont's
 Do:
-- Do add unit tests for all new components
-- Do keep controllers simple and break them down as needed
+- Add unit tests for all new components
+- Keep controllers simple and break them down as needed
+- Any SysUI callbacks should also take an associated executor to run the callback on
 
 Don't:
 - **Don't** do initialization in the constructor, only do initialization in the init callbacks.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java
index 9356660..f86d467 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java
@@ -33,9 +33,4 @@
      * - If there is a floating task for this intent, and it's not stashed, this stashes it.
      */
     void showOrSetStashed(Intent intent);
-
-    /** Returns a binder that can be passed to an external process to manipulate FloatingTasks. */
-    default IFloatingTasks createExternalInterface() {
-        return null;
-    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
index 6755299..b3c09d3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
@@ -21,6 +21,7 @@
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_FLOATING_TASKS;
 
 import android.annotation.Nullable;
 import android.content.Context;
@@ -40,6 +41,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.TaskViewTransitions;
 import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -136,11 +138,13 @@
         if (isFloatingTasksEnabled()) {
             shellInit.addInitCallback(this::onInit, this);
         }
-        mShellCommandHandler.addDumpCallback(this::dump, this);
     }
 
     protected void onInit() {
         mShellController.addConfigurationChangeListener(this);
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_FLOATING_TASKS,
+                this::createExternalInterface, this);
+        mShellCommandHandler.addDumpCallback(this::dump, this);
     }
 
     /** Only used for testing. */
@@ -168,6 +172,10 @@
         return FLOATING_TASKS_ENABLED || mFloatingTasksEnabledForTests;
     }
 
+    private ExternalInterfaceBinder createExternalInterface() {
+        return new IFloatingTasksImpl(this);
+    }
+
     @Override
     public void onThemeChanged() {
         if (mIsFloatingLayerAdded) {
@@ -412,28 +420,18 @@
      */
     @ExternalThread
     private class FloatingTaskImpl implements FloatingTasks {
-        private IFloatingTasksImpl mIFloatingTasks;
-
         @Override
         public void showOrSetStashed(Intent intent) {
             mMainExecutor.execute(() -> FloatingTasksController.this.showOrSetStashed(intent));
         }
-
-        @Override
-        public IFloatingTasks createExternalInterface() {
-            if (mIFloatingTasks != null) {
-                mIFloatingTasks.invalidate();
-            }
-            mIFloatingTasks = new IFloatingTasksImpl(FloatingTasksController.this);
-            return mIFloatingTasks;
-        }
     }
 
     /**
      * The interface for calls from outside the host process.
      */
     @BinderThread
-    private static class IFloatingTasksImpl extends IFloatingTasks.Stub {
+    private static class IFloatingTasksImpl extends IFloatingTasks.Stub
+            implements ExternalInterfaceBinder {
         private FloatingTasksController mController;
 
         IFloatingTasksImpl(FloatingTasksController controller) {
@@ -443,7 +441,8 @@
         /**
          * Invalidates this instance, preventing future calls from updating the controller.
          */
-        void invalidate() {
+        @Override
+        public void invalidate() {
             mController = null;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
index 7129165..2ee3348 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
@@ -30,13 +30,6 @@
             OneHandedController.SUPPORT_ONE_HANDED_MODE, false);
 
     /**
-     * Returns a binder that can be passed to an external process to manipulate OneHanded.
-     */
-    default IOneHanded createExternalInterface() {
-        return null;
-    }
-
-    /**
      * Enters one handed mode.
      */
     void startOneHanded();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
index e0c4fe8..679d4ca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -24,6 +24,7 @@
 import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING;
 import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING;
 import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED;
 
 import android.annotation.BinderThread;
 import android.content.ComponentName;
@@ -49,6 +50,7 @@
 import com.android.wm.shell.common.DisplayChangeController;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TaskStackListenerCallback;
@@ -296,12 +298,18 @@
         mShellController.addConfigurationChangeListener(this);
         mShellController.addKeyguardChangeListener(this);
         mShellController.addUserChangeListener(this);
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_ONE_HANDED,
+                this::createExternalInterface, this);
     }
 
     public OneHanded asOneHanded() {
         return mImpl;
     }
 
+    private ExternalInterfaceBinder createExternalInterface() {
+        return new IOneHandedImpl(this);
+    }
+
     @Override
     public Context getContext() {
         return mContext;
@@ -709,17 +717,6 @@
      */
     @ExternalThread
     private class OneHandedImpl implements OneHanded {
-        private IOneHandedImpl mIOneHanded;
-
-        @Override
-        public IOneHanded createExternalInterface() {
-            if (mIOneHanded != null) {
-                mIOneHanded.invalidate();
-            }
-            mIOneHanded = new IOneHandedImpl(OneHandedController.this);
-            return mIOneHanded;
-        }
-
         @Override
         public void startOneHanded() {
             mMainExecutor.execute(() -> {
@@ -767,7 +764,7 @@
      * The interface for calls from outside the host process.
      */
     @BinderThread
-    private static class IOneHandedImpl extends IOneHanded.Stub {
+    private static class IOneHandedImpl extends IOneHanded.Stub implements ExternalInterfaceBinder {
         private OneHandedController mController;
 
         IOneHandedImpl(OneHandedController controller) {
@@ -777,7 +774,8 @@
         /**
          * Invalidates this instance, preventing future calls from updating the controller.
          */
-        void invalidate() {
+        @Override
+        public void invalidate() {
             mController = null;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index 72b9dd3..f34d2a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -27,14 +27,6 @@
  */
 @ExternalThread
 public interface Pip {
-
-    /**
-     * Returns a binder that can be passed to an external process to manipulate PIP.
-     */
-    default IPip createExternalInterface() {
-        return null;
-    }
-
     /**
      * Expand PIP, it's possible that specific request to activate the window via Alt-tab.
      */
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 3345b1b..a918559 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
@@ -33,6 +33,7 @@
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE;
 import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP;
 
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
@@ -68,6 +69,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SingleInstanceRemoteListener;
@@ -632,6 +634,12 @@
         mShellController.addConfigurationChangeListener(this);
         mShellController.addKeyguardChangeListener(this);
         mShellController.addUserChangeListener(this);
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP,
+                this::createExternalInterface, this);
+    }
+
+    private ExternalInterfaceBinder createExternalInterface() {
+        return new IPipImpl(this);
     }
 
     @Override
@@ -1040,17 +1048,6 @@
      * The interface for calls from outside the Shell, within the host process.
      */
     private class PipImpl implements Pip {
-        private IPipImpl mIPip;
-
-        @Override
-        public IPip createExternalInterface() {
-            if (mIPip != null) {
-                mIPip.invalidate();
-            }
-            mIPip = new IPipImpl(PipController.this);
-            return mIPip;
-        }
-
         @Override
         public void expandPip() {
             mMainExecutor.execute(() -> {
@@ -1098,7 +1095,7 @@
      * The interface for calls from outside the host process.
      */
     @BinderThread
-    private static class IPipImpl extends IPip.Stub {
+    private static class IPipImpl extends IPip.Stub implements ExternalInterfaceBinder {
         private PipController mController;
         private final SingleInstanceRemoteListener<PipController,
                 IPipAnimationListener> mListener;
@@ -1129,7 +1126,8 @@
         /**
          * Invalidates this instance, preventing future calls from updating the controller.
          */
-        void invalidate() {
+        @Override
+        public void invalidate() {
             mController = null;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
index 552ebde..93ffb3d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
@@ -17,22 +17,14 @@
 package com.android.wm.shell.protolog;
 
 import android.annotation.Nullable;
-import android.content.Context;
-import android.util.Log;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.BaseProtoLogImpl;
 import com.android.internal.protolog.ProtoLogViewerConfigReader;
 import com.android.internal.protolog.common.IProtoLogGroup;
-import com.android.wm.shell.R;
 
 import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
 import java.io.PrintWriter;
 
-import org.json.JSONException;
-
 
 /**
  * A service for the ProtoLog logging system.
@@ -40,8 +32,9 @@
 public class ShellProtoLogImpl extends BaseProtoLogImpl {
     private static final String TAG = "ProtoLogImpl";
     private static final int BUFFER_CAPACITY = 1024 * 1024;
-    // TODO: Get the right path for the proto log file when we initialize the shell components
-    private static final String LOG_FILENAME = new File("wm_shell_log.pb").getAbsolutePath();
+    // TODO: find a proper location to save the protolog message file
+    private static final String LOG_FILENAME = "/data/misc/wmtrace/shell_log.winscope";
+    private static final String VIEWER_CONFIG_FILENAME = "/system_ext/etc/wmshell.protolog.json.gz";
 
     private static ShellProtoLogImpl sServiceInstance = null;
 
@@ -111,18 +104,8 @@
     }
 
     public int startTextLogging(String[] groups, PrintWriter pw) {
-        try (InputStream is =
-                     getClass().getClassLoader().getResourceAsStream("wm_shell_protolog.json")){
-            mViewerConfig.loadViewerConfig(is);
-            return setLogging(true /* setTextLogging */, true, pw, groups);
-        } catch (IOException e) {
-            Log.i(TAG, "Unable to load log definitions: IOException while reading "
-                    + "wm_shell_protolog. " + e);
-        } catch (JSONException e) {
-            Log.i(TAG, "Unable to load log definitions: JSON parsing exception while reading "
-                    + "wm_shell_protolog. " + e);
-        }
-        return -1;
+        mViewerConfig.loadViewerConfig(pw, VIEWER_CONFIG_FILENAME);
+        return setLogging(true /* setTextLogging */, true, pw, groups);
     }
 
     public int stopTextLogging(String[] groups, PrintWriter pw) {
@@ -130,7 +113,8 @@
     }
 
     private ShellProtoLogImpl() {
-        super(new File(LOG_FILENAME), null, BUFFER_CAPACITY, new ProtoLogViewerConfigReader());
+        super(new File(LOG_FILENAME), VIEWER_CONFIG_FILENAME, BUFFER_CAPACITY,
+                new ProtoLogViewerConfigReader());
     }
 }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
index 2a62552..069066e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
@@ -29,13 +29,6 @@
 @ExternalThread
 public interface RecentTasks {
     /**
-     * Returns a binder that can be passed to an external process to fetch recent tasks.
-     */
-    default IRecentTasks createExternalInterface() {
-        return null;
-    }
-
-    /**
      * Gets the set of recent tasks.
      */
     default void getRecentTasks(int maxNum, int flags, int userId, Executor callbackExecutor,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 02b5a35..08f3db6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -20,6 +20,7 @@
 import static android.content.pm.PackageManager.FEATURE_PC;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
 
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
@@ -37,6 +38,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SingleInstanceRemoteListener;
@@ -48,6 +50,7 @@
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.SplitBounds;
@@ -69,11 +72,12 @@
     private static final String TAG = RecentTasksController.class.getSimpleName();
 
     private final Context mContext;
+    private final ShellController mShellController;
     private final ShellCommandHandler mShellCommandHandler;
     private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository;
     private final ShellExecutor mMainExecutor;
     private final TaskStackListenerImpl mTaskStackListener;
-    private final RecentTasks mImpl = new RecentTasksImpl();
+    private final RecentTasksImpl mImpl = new RecentTasksImpl();
     private final ActivityTaskManager mActivityTaskManager;
     private IRecentTasksListener mListener;
     private final boolean mIsDesktopMode;
@@ -97,6 +101,7 @@
     public static RecentTasksController create(
             Context context,
             ShellInit shellInit,
+            ShellController shellController,
             ShellCommandHandler shellCommandHandler,
             TaskStackListenerImpl taskStackListener,
             ActivityTaskManager activityTaskManager,
@@ -106,18 +111,20 @@
         if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
             return null;
         }
-        return new RecentTasksController(context, shellInit, shellCommandHandler, taskStackListener,
-                activityTaskManager, desktopModeTaskRepository, mainExecutor);
+        return new RecentTasksController(context, shellInit, shellController, shellCommandHandler,
+                taskStackListener, activityTaskManager, desktopModeTaskRepository, mainExecutor);
     }
 
     RecentTasksController(Context context,
             ShellInit shellInit,
+            ShellController shellController,
             ShellCommandHandler shellCommandHandler,
             TaskStackListenerImpl taskStackListener,
             ActivityTaskManager activityTaskManager,
             Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
             ShellExecutor mainExecutor) {
         mContext = context;
+        mShellController = shellController;
         mShellCommandHandler = shellCommandHandler;
         mActivityTaskManager = activityTaskManager;
         mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
@@ -131,7 +138,13 @@
         return mImpl;
     }
 
+    private ExternalInterfaceBinder createExternalInterface() {
+        return new IRecentTasksImpl(this);
+    }
+
     private void onInit() {
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_RECENT_TASKS,
+                this::createExternalInterface, this);
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mTaskStackListener.addListener(this);
         mDesktopModeTaskRepository.ifPresent(it -> it.addListener(this));
@@ -366,17 +379,6 @@
      */
     @ExternalThread
     private class RecentTasksImpl implements RecentTasks {
-        private IRecentTasksImpl mIRecentTasks;
-
-        @Override
-        public IRecentTasks createExternalInterface() {
-            if (mIRecentTasks != null) {
-                mIRecentTasks.invalidate();
-            }
-            mIRecentTasks = new IRecentTasksImpl(RecentTasksController.this);
-            return mIRecentTasks;
-        }
-
         @Override
         public void getRecentTasks(int maxNum, int flags, int userId, Executor executor,
                 Consumer<List<GroupedRecentTaskInfo>> callback) {
@@ -393,7 +395,8 @@
      * The interface for calls from outside the host process.
      */
     @BinderThread
-    private static class IRecentTasksImpl extends IRecentTasks.Stub {
+    private static class IRecentTasksImpl extends IRecentTasks.Stub
+            implements ExternalInterfaceBinder {
         private RecentTasksController mController;
         private final SingleInstanceRemoteListener<RecentTasksController,
                 IRecentTasksListener> mListener;
@@ -424,7 +427,8 @@
         /**
          * Invalidates this instance, preventing future calls from updating the controller.
          */
-        void invalidate() {
+        @Override
+        public void invalidate() {
             mController = null;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
index ecdafa9..eb08d0e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
@@ -79,26 +79,47 @@
     /**
      * Starts tasks simultaneously in one transition.
      */
-    oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId,
-            in Bundle sideOptions, int sidePosition, float splitRatio,
-            in RemoteTransition remoteTransition, in InstanceId instanceId) = 10;
+    oneway void startTasks(int taskId1, in Bundle options1, int taskId2, in Bundle options2,
+            int splitPosition, float splitRatio, in RemoteTransition remoteTransition,
+            in InstanceId instanceId) = 10;
+
+    /**
+     * Starts a pair of intent and task in one transition.
+     */
+    oneway void startIntentAndTask(in PendingIntent pendingIntent, in Intent fillInIntent,
+            in Bundle options1, int taskId, in Bundle options2, int sidePosition, float splitRatio,
+            in RemoteTransition remoteTransition, in InstanceId instanceId) = 16;
+
+    /**
+     * Starts a pair of shortcut and task in one transition.
+     */
+    oneway void startShortcutAndTask(in ShortcutInfo shortcutInfo, in Bundle options1, int taskId,
+            in Bundle options2, int splitPosition, float splitRatio,
+             in RemoteTransition remoteTransition, in InstanceId instanceId) = 17;
 
     /**
      * Version of startTasks using legacy transition system.
      */
-    oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions,
-            int sideTaskId, in Bundle sideOptions, int sidePosition,
-            float splitRatio, in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 11;
+    oneway void startTasksWithLegacyTransition(int taskId1, in Bundle options1, int taskId2,
+            in Bundle options2, int splitPosition, float splitRatio,
+            in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 11;
 
     /**
      * Starts a pair of intent and task using legacy transition system.
      */
     oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent,
-            in Intent fillInIntent, int taskId, in Bundle mainOptions,in Bundle sideOptions,
-            int sidePosition, float splitRatio, in RemoteAnimationAdapter adapter,
+            in Intent fillInIntent, in Bundle options1, int taskId, in Bundle options2,
+            int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter,
             in InstanceId instanceId) = 12;
 
     /**
+     * Starts a pair of shortcut and task using legacy transition system.
+     */
+    oneway void startShortcutAndTaskWithLegacyTransition(in ShortcutInfo shortcutInfo,
+            in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio,
+            in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 15;
+
+    /**
      * Blocking call that notifies and gets additional split-screen targets when entering
      * recents (for example: the dividerBar).
      * @param appTargets apps that will be re-parented to display area
@@ -111,11 +132,5 @@
      * does not expect split to currently be running.
      */
     RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14;
-
-    /**
-     * Starts a pair of shortcut and task using legacy transition system.
-     */
-    oneway void startShortcutAndTaskWithLegacyTransition(in ShortcutInfo shortcutInfo, int taskId,
-            in Bundle mainOptions, in Bundle sideOptions, int sidePosition, float splitRatio,
-            in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 15;
 }
+// Last id = 17
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index e73b799..d86aadc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -70,13 +70,6 @@
     /** Unregisters listener that gets split screen callback. */
     void unregisterSplitScreenListener(@NonNull SplitScreenListener listener);
 
-    /**
-     * Returns a binder that can be passed to an external process to manipulate SplitScreen.
-     */
-    default ISplitScreen createExternalInterface() {
-        return null;
-    }
-
     /** Called when device waking up finished. */
     void onFinishedWakingUp();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 07a6895..c6a2b83 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -29,6 +29,7 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
 import android.app.ActivityManager;
@@ -71,6 +72,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SingleInstanceRemoteListener;
@@ -214,6 +216,10 @@
         return mImpl;
     }
 
+    private ExternalInterfaceBinder createExternalInterface() {
+        return new ISplitScreenImpl(this);
+    }
+
     /**
      * This will be called after ShellTaskOrganizer has initialized/registered because of the
      * dependency order.
@@ -224,6 +230,8 @@
         mShellCommandHandler.addCommandCallback("splitscreen", mSplitScreenShellCommandHandler,
                 this);
         mShellController.addKeyguardChangeListener(this);
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_SPLIT_SCREEN,
+                this::createExternalInterface, this);
         if (mStageCoordinator == null) {
             // TODO: Multi-display
             mStageCoordinator = createStageCoordinator();
@@ -658,7 +666,6 @@
      */
     @ExternalThread
     private class SplitScreenImpl implements SplitScreen {
-        private ISplitScreenImpl mISplitScreen;
         private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>();
         private final SplitScreen.SplitScreenListener mListener = new SplitScreenListener() {
             @Override
@@ -704,15 +711,6 @@
         };
 
         @Override
-        public ISplitScreen createExternalInterface() {
-            if (mISplitScreen != null) {
-                mISplitScreen.invalidate();
-            }
-            mISplitScreen = new ISplitScreenImpl(SplitScreenController.this);
-            return mISplitScreen;
-        }
-
-        @Override
         public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) {
             if (mExecutors.containsKey(listener)) return;
 
@@ -752,7 +750,8 @@
      * The interface for calls from outside the host process.
      */
     @BinderThread
-    private static class ISplitScreenImpl extends ISplitScreen.Stub {
+    private static class ISplitScreenImpl extends ISplitScreen.Stub
+            implements ExternalInterfaceBinder {
         private SplitScreenController mController;
         private final SingleInstanceRemoteListener<SplitScreenController,
                 ISplitScreenListener> mListener;
@@ -779,7 +778,8 @@
         /**
          * Invalidates this instance, preventing future calls from updating the controller.
          */
-        void invalidate() {
+        @Override
+        public void invalidate() {
             mController = null;
         }
 
@@ -828,47 +828,68 @@
         }
 
         @Override
-        public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
-                int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
+        public void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1,
+                int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition,
                 float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController, "startTasks",
                     (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition(
-                            mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition,
+                            taskId1, options1, taskId2, options2, splitPosition,
                             splitRatio, adapter, instanceId));
         }
 
         @Override
         public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
-                Intent fillInIntent, int taskId, Bundle mainOptions, Bundle sideOptions,
-                int sidePosition, float splitRatio, RemoteAnimationAdapter adapter,
+                Intent fillInIntent, Bundle options1, int taskId, Bundle options2,
+                int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
                 InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController,
                     "startIntentAndTaskWithLegacyTransition", (controller) ->
                             controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition(
-                                    pendingIntent, fillInIntent, taskId, mainOptions, sideOptions,
-                                    sidePosition, splitRatio, adapter, instanceId));
+                                    pendingIntent, fillInIntent, options1, taskId, options2,
+                                    splitPosition, splitRatio, adapter, instanceId));
         }
 
         @Override
         public void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
-                int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions,
-                @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter,
+                @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+                @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
                 InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController,
                     "startShortcutAndTaskWithLegacyTransition", (controller) ->
                             controller.mStageCoordinator.startShortcutAndTaskWithLegacyTransition(
-                                    shortcutInfo, taskId, mainOptions, sideOptions, sidePosition,
+                                    shortcutInfo, options1, taskId, options2, splitPosition,
                                     splitRatio, adapter, instanceId));
         }
 
         @Override
-        public void startTasks(int mainTaskId, @Nullable Bundle mainOptions,
-                int sideTaskId, @Nullable Bundle sideOptions,
-                @SplitPosition int sidePosition, float splitRatio,
+        public void startTasks(int taskId1, @Nullable Bundle options1, int taskId2,
+                @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
                 @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController, "startTasks",
-                    (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions,
-                            sideTaskId, sideOptions, sidePosition, splitRatio, remoteTransition,
+                    (controller) -> controller.mStageCoordinator.startTasks(taskId1, options1,
+                            taskId2, options2, splitPosition, splitRatio, remoteTransition,
+                            instanceId));
+        }
+
+        @Override
+        public void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent,
+                @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+                @SplitPosition int splitPosition, float splitRatio,
+                @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+            executeRemoteCallWithTaskPermission(mController, "startIntentAndTask",
+                    (controller) -> controller.mStageCoordinator.startIntentAndTask(pendingIntent,
+                            fillInIntent, options1, taskId, options2, splitPosition, splitRatio,
+                            remoteTransition, instanceId));
+        }
+
+        @Override
+        public void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1,
+                int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+                float splitRatio, @Nullable RemoteTransition remoteTransition,
+                InstanceId instanceId) {
+            executeRemoteCallWithTaskPermission(mController, "startShortcutAndTask",
+                    (controller) -> controller.mStageCoordinator.startShortcutAndTask(shortcutInfo,
+                            options1, taskId, options2, splitPosition, splitRatio, remoteTransition,
                             instanceId));
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index c17f822..9102bd3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -516,14 +516,55 @@
     }
 
     /** Starts 2 tasks in one transition. */
-    void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId,
-            @Nullable Bundle sideOptions, @SplitPosition int sidePosition, float splitRatio,
+    void startTasks(int taskId1, @Nullable Bundle options1, int taskId2,
+            @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
             @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        mainOptions = mainOptions != null ? mainOptions : new Bundle();
-        sideOptions = sideOptions != null ? sideOptions : new Bundle();
-        setSideStagePosition(sidePosition, wct);
+        setSideStagePosition(splitPosition, wct);
+        options1 = options1 != null ? options1 : new Bundle();
+        addActivityOptions(options1, mSideStage);
+        wct.startTask(taskId1, options1);
 
+        startWithTask(wct, taskId2, options2, splitRatio, remoteTransition, instanceId);
+    }
+
+    /** Start an intent and a task to a split pair in one transition. */
+    void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent,
+            @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+            @SplitPosition int splitPosition, float splitRatio,
+            @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        setSideStagePosition(splitPosition, wct);
+        options1 = options1 != null ? options1 : new Bundle();
+        addActivityOptions(options1, mSideStage);
+        wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
+
+        startWithTask(wct, taskId, options2, splitRatio, remoteTransition, instanceId);
+    }
+
+    /** Starts a shortcut and a task to a split pair in one transition. */
+    void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1,
+            int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+            float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        setSideStagePosition(splitPosition, wct);
+        options1 = options1 != null ? options1 : new Bundle();
+        addActivityOptions(options1, mSideStage);
+        wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
+
+        startWithTask(wct, taskId, options2, splitRatio, remoteTransition, instanceId);
+    }
+
+    /**
+     * Starts with the second task to a split pair in one transition.
+     *
+     * @param wct transaction to start the first task
+     * @param instanceId if {@code null}, will not log. Otherwise it will be used in
+     *      {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
+     */
+    private void startWithTask(WindowContainerTransaction wct, int mainTaskId,
+            @Nullable Bundle mainOptions, float splitRatio,
+            @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
         if (mMainStage.isActive()) {
             mMainStage.evictAllChildren(wct);
             mSideStage.evictAllChildren(wct);
@@ -538,60 +579,61 @@
         wct.setForceTranslucent(mRootTaskInfo.token, false);
 
         // Make sure the launch options will put tasks in the corresponding split roots
+        mainOptions = mainOptions != null ? mainOptions : new Bundle();
         addActivityOptions(mainOptions, mMainStage);
-        addActivityOptions(sideOptions, mSideStage);
 
         // Add task launch requests
         wct.startTask(mainTaskId, mainOptions);
-        wct.startTask(sideTaskId, sideOptions);
 
         mSplitTransitions.startEnterTransition(
                 TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null);
         setEnterInstanceId(instanceId);
     }
 
-    /** Starts 2 tasks in one legacy transition. */
-    void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
-            int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
+    /** Starts a pair of tasks using legacy transition. */
+    void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1,
+            int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition,
             float splitRatio, RemoteAnimationAdapter adapter,
             InstanceId instanceId) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        if (sideOptions == null) sideOptions = new Bundle();
-        addActivityOptions(sideOptions, mSideStage);
-        wct.startTask(sideTaskId, sideOptions);
+        if (options1 == null) options1 = new Bundle();
+        addActivityOptions(options1, mSideStage);
+        wct.startTask(taskId1, options1);
 
-        startWithLegacyTransition(wct, mainTaskId, mainOptions, sidePosition, splitRatio, adapter,
+        startWithLegacyTransition(wct, taskId2, options2, splitPosition, splitRatio, adapter,
                 instanceId);
     }
 
-    /** Start an intent and a task ordered by {@code intentFirst}. */
+    /** Starts a pair of intent and task using legacy transition. */
     void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent,
-            int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions,
-            @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter,
+            @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+            @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
             InstanceId instanceId) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        if (sideOptions == null) sideOptions = new Bundle();
-        addActivityOptions(sideOptions, mSideStage);
-        wct.sendPendingIntent(pendingIntent, fillInIntent, sideOptions);
+        if (options1 == null) options1 = new Bundle();
+        addActivityOptions(options1, mSideStage);
+        wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
 
-        startWithLegacyTransition(wct, taskId, mainOptions, sidePosition, splitRatio, adapter,
+        startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter,
                 instanceId);
     }
 
+    /** Starts a pair of shortcut and task using legacy transition. */
     void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
-            int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions,
-            @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter,
+            @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+            @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
             InstanceId instanceId) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        if (sideOptions == null) sideOptions = new Bundle();
-        addActivityOptions(sideOptions, mSideStage);
-        wct.startShortcut(mContext.getPackageName(), shortcutInfo, sideOptions);
+        if (options1 == null) options1 = new Bundle();
+        addActivityOptions(options1, mSideStage);
+        wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
 
-        startWithLegacyTransition(wct, taskId, mainOptions, sidePosition, splitRatio, adapter,
+        startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter,
                 instanceId);
     }
 
     /**
+     * @param wct transaction to start the first task
      * @param instanceId if {@code null}, will not log. Otherwise it will be used in
      *                   {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
      */
@@ -1727,6 +1769,7 @@
 
     @StageType
     private int getStageType(StageTaskListener stage) {
+        if (stage == null) return STAGE_TYPE_UNDEFINED;
         return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
     }
 
@@ -1981,8 +2024,8 @@
             }
         }
 
-        // TODO: fallback logic. Probably start a new transition to exit split before applying
-        //       anything here. Ideally consolidate with transition-merging.
+        // TODO(b/250853925): fallback logic. Probably start a new transition to exit split before
+        //       applying anything here. Ideally consolidate with transition-merging.
         if (info.getType() == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) {
             if (mainChild == null && sideChild == null) {
                 throw new IllegalStateException("Launched a task in split, but didn't receive any"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java
index 76105a3..538bbec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java
@@ -22,14 +22,6 @@
  * Interface to engage starting window feature.
  */
 public interface StartingSurface {
-
-    /**
-     * Returns a binder that can be passed to an external process to manipulate starting windows.
-     */
-    default IStartingWindow createExternalInterface() {
-        return null;
-    }
-
     /**
      * Returns the background color for a starting window if existing.
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
index 379af21..0c23f10 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
@@ -23,6 +23,7 @@
 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW;
 
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
@@ -43,10 +44,12 @@
 import com.android.internal.util.function.TriConsumer;
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SingleInstanceRemoteListener;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
 /**
@@ -76,6 +79,7 @@
     private TriConsumer<Integer, Integer, Integer> mTaskLaunchingCallback;
     private final StartingSurfaceImpl mImpl = new StartingSurfaceImpl();
     private final Context mContext;
+    private final ShellController mShellController;
     private final ShellTaskOrganizer mShellTaskOrganizer;
     private final ShellExecutor mSplashScreenExecutor;
     /**
@@ -86,12 +90,14 @@
 
     public StartingWindowController(Context context,
             ShellInit shellInit,
+            ShellController shellController,
             ShellTaskOrganizer shellTaskOrganizer,
             ShellExecutor splashScreenExecutor,
             StartingWindowTypeAlgorithm startingWindowTypeAlgorithm,
             IconProvider iconProvider,
             TransactionPool pool) {
         mContext = context;
+        mShellController = shellController;
         mShellTaskOrganizer = shellTaskOrganizer;
         mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor,
                 iconProvider, pool);
@@ -107,8 +113,14 @@
         return mImpl;
     }
 
+    private ExternalInterfaceBinder createExternalInterface() {
+        return new IStartingWindowImpl(this);
+    }
+
     private void onInit() {
         mShellTaskOrganizer.initStartingWindow(this);
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_STARTING_WINDOW,
+                this::createExternalInterface, this);
     }
 
     @Override
@@ -222,17 +234,6 @@
      * The interface for calls from outside the Shell, within the host process.
      */
     private class StartingSurfaceImpl implements StartingSurface {
-        private IStartingWindowImpl mIStartingWindow;
-
-        @Override
-        public IStartingWindowImpl createExternalInterface() {
-            if (mIStartingWindow != null) {
-                mIStartingWindow.invalidate();
-            }
-            mIStartingWindow = new IStartingWindowImpl(StartingWindowController.this);
-            return mIStartingWindow;
-        }
-
         @Override
         public int getBackgroundColor(TaskInfo taskInfo) {
             synchronized (mTaskBackgroundColors) {
@@ -256,7 +257,8 @@
      * The interface for calls from outside the host process.
      */
     @BinderThread
-    private static class IStartingWindowImpl extends IStartingWindow.Stub {
+    private static class IStartingWindowImpl extends IStartingWindow.Stub
+            implements ExternalInterfaceBinder {
         private StartingWindowController mController;
         private SingleInstanceRemoteListener<StartingWindowController,
                 IStartingWindowListener> mListener;
@@ -276,7 +278,8 @@
         /**
          * Invalidates this instance, preventing future calls from updating the controller.
          */
-        void invalidate() {
+        @Override
+        public void invalidate() {
             mController = null;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
index 5799394..fdf073f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
@@ -23,23 +23,28 @@
 import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
 import static android.content.pm.ActivityInfo.CONFIG_UI_MODE;
 
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SYSUI_EVENTS;
 
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.pm.UserInfo;
 import android.content.res.Configuration;
+import android.os.Bundle;
+import android.util.ArrayMap;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.annotations.ExternalThread;
 
 import java.io.PrintWriter;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Supplier;
 
 /**
  * Handles event callbacks from SysUI that can be used within the Shell.
@@ -59,6 +64,11 @@
     private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners =
             new CopyOnWriteArrayList<>();
 
+    private ArrayMap<String, Supplier<ExternalInterfaceBinder>> mExternalInterfaceSuppliers =
+            new ArrayMap<>();
+    // References to the existing interfaces, to be invalidated when they are recreated
+    private ArrayMap<String, ExternalInterfaceBinder> mExternalInterfaces = new ArrayMap<>();
+
     private Configuration mLastConfiguration;
 
 
@@ -67,6 +77,11 @@
         mShellInit = shellInit;
         mShellCommandHandler = shellCommandHandler;
         mMainExecutor = mainExecutor;
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        mShellCommandHandler.addDumpCallback(this::dump, this);
     }
 
     /**
@@ -124,6 +139,47 @@
         mUserChangeListeners.remove(listener);
     }
 
+    /**
+     * Adds an interface that can be called from a remote process. This method takes a supplier
+     * because each binder reference is valid for a single process, and in multi-user mode, SysUI
+     * will request new binder instances for each instance of Launcher that it provides binders
+     * to.
+     *
+     * @param extra the key for the interface, {@see ShellSharedConstants}
+     * @param binderSupplier the supplier of the binder to pass to the external process
+     * @param callerInstance the instance of the caller, purely for logging
+     */
+    public void addExternalInterface(String extra, Supplier<ExternalInterfaceBinder> binderSupplier,
+            Object callerInstance) {
+        ProtoLog.v(WM_SHELL_INIT, "Adding external interface from %s with key %s",
+                callerInstance.getClass().getSimpleName(), extra);
+        if (mExternalInterfaceSuppliers.containsKey(extra)) {
+            throw new IllegalArgumentException("Supplier with same key already exists: "
+                    + extra);
+        }
+        mExternalInterfaceSuppliers.put(extra, binderSupplier);
+    }
+
+    /**
+     * Updates the given bundle with the set of external interfaces, invalidating the old set of
+     * binders.
+     */
+    private void createExternalInterfaces(Bundle output) {
+        // Invalidate the old binders
+        for (int i = 0; i < mExternalInterfaces.size(); i++) {
+            mExternalInterfaces.valueAt(i).invalidate();
+        }
+        mExternalInterfaces.clear();
+
+        // Create new binders for each key
+        for (int i = 0; i < mExternalInterfaceSuppliers.size(); i++) {
+            final String key = mExternalInterfaceSuppliers.keyAt(i);
+            final ExternalInterfaceBinder b = mExternalInterfaceSuppliers.valueAt(i).get();
+            mExternalInterfaces.put(key, b);
+            output.putBinder(key, b.asBinder());
+        }
+    }
+
     @VisibleForTesting
     void onConfigurationChanged(Configuration newConfig) {
         // The initial config is send on startup and doesn't trigger listener callbacks
@@ -204,6 +260,14 @@
         pw.println(innerPrefix + "mLastConfiguration=" + mLastConfiguration);
         pw.println(innerPrefix + "mKeyguardChangeListeners=" + mKeyguardChangeListeners.size());
         pw.println(innerPrefix + "mUserChangeListeners=" + mUserChangeListeners.size());
+
+        if (!mExternalInterfaces.isEmpty()) {
+            pw.println(innerPrefix + "mExternalInterfaces={");
+            for (String key : mExternalInterfaces.keySet()) {
+                pw.println(innerPrefix + "\t" + key + ": " + mExternalInterfaces.get(key));
+            }
+            pw.println(innerPrefix + "}");
+        }
     }
 
     /**
@@ -211,7 +275,6 @@
      */
     @ExternalThread
     private class ShellInterfaceImpl implements ShellInterface {
-
         @Override
         public void onInit() {
             try {
@@ -222,28 +285,6 @@
         }
 
         @Override
-        public void dump(PrintWriter pw) {
-            try {
-                mMainExecutor.executeBlocking(() -> mShellCommandHandler.dump(pw));
-            } catch (InterruptedException e) {
-                throw new RuntimeException("Failed to dump the Shell in 2s", e);
-            }
-        }
-
-        @Override
-        public boolean handleCommand(String[] args, PrintWriter pw) {
-            try {
-                boolean[] result = new boolean[1];
-                mMainExecutor.executeBlocking(() -> {
-                    result[0] = mShellCommandHandler.handleCommand(args, pw);
-                });
-                return result[0];
-            } catch (InterruptedException e) {
-                throw new RuntimeException("Failed to handle Shell command in 2s", e);
-            }
-        }
-
-        @Override
         public void onConfigurationChanged(Configuration newConfiguration) {
             mMainExecutor.execute(() ->
                     ShellController.this.onConfigurationChanged(newConfiguration));
@@ -274,5 +315,38 @@
             mMainExecutor.execute(() ->
                     ShellController.this.onUserProfilesChanged(profiles));
         }
+
+        @Override
+        public boolean handleCommand(String[] args, PrintWriter pw) {
+            try {
+                boolean[] result = new boolean[1];
+                mMainExecutor.executeBlocking(() -> {
+                    result[0] = mShellCommandHandler.handleCommand(args, pw);
+                });
+                return result[0];
+            } catch (InterruptedException e) {
+                throw new RuntimeException("Failed to handle Shell command in 2s", e);
+            }
+        }
+
+        @Override
+        public void createExternalInterfaces(Bundle bundle) {
+            try {
+                mMainExecutor.executeBlocking(() -> {
+                    ShellController.this.createExternalInterfaces(bundle);
+                });
+            } catch (InterruptedException e) {
+                throw new RuntimeException("Failed to get Shell command in 2s", e);
+            }
+        }
+
+        @Override
+        public void dump(PrintWriter pw) {
+            try {
+                mMainExecutor.executeBlocking(() -> mShellCommandHandler.dump(pw));
+            } catch (InterruptedException e) {
+                throw new RuntimeException("Failed to dump the Shell in 2s", e);
+            }
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
index 2108c82..bc5dd11 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.content.res.Configuration;
+import android.os.Bundle;
 
 import androidx.annotation.NonNull;
 
@@ -37,18 +38,6 @@
     default void onInit() {}
 
     /**
-     * Dumps the shell state.
-     */
-    default void dump(PrintWriter pw) {}
-
-    /**
-     * Handles a shell command.
-     */
-    default boolean handleCommand(final String[] args, PrintWriter pw) {
-        return false;
-    }
-
-    /**
      * Notifies the Shell that the configuration has changed.
      */
     default void onConfigurationChanged(Configuration newConfiguration) {}
@@ -74,4 +63,21 @@
      * Notifies the Shell when a profile belonging to the user changes.
      */
     default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
+
+    /**
+     * Handles a shell command.
+     */
+    default boolean handleCommand(final String[] args, PrintWriter pw) {
+        return false;
+    }
+
+    /**
+     * Updates the given {@param bundle} with the set of exposed interfaces.
+     */
+    default void createExternalInterfaces(Bundle bundle) {}
+
+    /**
+     * Dumps the shell state.
+     */
+    default void dump(PrintWriter pw) {}
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java
new file mode 100644
index 0000000..bdda6a8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.sysui;
+
+/**
+ * General shell-related constants that are shared with users of the library.
+ */
+public class ShellSharedConstants {
+    // See IPip.aidl
+    public static final String KEY_EXTRA_SHELL_PIP = "extra_shell_pip";
+    // See ISplitScreen.aidl
+    public static final String KEY_EXTRA_SHELL_SPLIT_SCREEN = "extra_shell_split_screen";
+    // See IOneHanded.aidl
+    public static final String KEY_EXTRA_SHELL_ONE_HANDED = "extra_shell_one_handed";
+    // See IShellTransitions.aidl
+    public static final String KEY_EXTRA_SHELL_SHELL_TRANSITIONS =
+            "extra_shell_shell_transitions";
+    // See IStartingWindow.aidl
+    public static final String KEY_EXTRA_SHELL_STARTING_WINDOW =
+            "extra_shell_starting_window";
+    // See IRecentTasks.aidl
+    public static final String KEY_EXTRA_SHELL_RECENT_TASKS = "extra_shell_recent_tasks";
+    // See IBackAnimation.aidl
+    public static final String KEY_EXTRA_SHELL_BACK_ANIMATION = "extra_shell_back_animation";
+    // See IFloatingTasks.aidl
+    public static final String KEY_EXTRA_SHELL_FLOATING_TASKS = "extra_shell_floating_tasks";
+    // See IDesktopMode.aidl
+    public static final String KEY_EXTRA_SHELL_DESKTOP_MODE = "extra_shell_desktop_mode";
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
index b34049d..da39017 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
@@ -27,14 +27,6 @@
  */
 @ExternalThread
 public interface ShellTransitions {
-
-    /**
-     * Returns a binder that can be passed to an external process to manipulate remote transitions.
-     */
-    default IShellTransitions createExternalInterface() {
-        return null;
-    }
-
     /**
      * Registers a remote transition.
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 7fdd391..d1bc738 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -30,6 +30,7 @@
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -62,11 +63,13 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.ArrayList;
@@ -116,6 +119,7 @@
     private final DefaultTransitionHandler mDefaultTransitionHandler;
     private final RemoteTransitionHandler mRemoteTransitionHandler;
     private final DisplayController mDisplayController;
+    private final ShellController mShellController;
     private final ShellTransitionImpl mImpl = new ShellTransitionImpl();
 
     /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */
@@ -143,6 +147,7 @@
 
     public Transitions(@NonNull Context context,
             @NonNull ShellInit shellInit,
+            @NonNull ShellController shellController,
             @NonNull WindowOrganizer organizer,
             @NonNull TransactionPool pool,
             @NonNull DisplayController displayController,
@@ -157,10 +162,14 @@
         mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit,
                 displayController, pool, mainExecutor, mainHandler, animExecutor);
         mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor);
+        mShellController = shellController;
         shellInit.addInitCallback(this::onInit, this);
     }
 
     private void onInit() {
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS,
+                this::createExternalInterface, this);
+
         // The very last handler (0 in the list) should be the default one.
         mHandlers.add(mDefaultTransitionHandler);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default");
@@ -194,6 +203,10 @@
         return mImpl;
     }
 
+    private ExternalInterfaceBinder createExternalInterface() {
+        return new IShellTransitionsImpl(this);
+    }
+
     @Override
     public Context getContext() {
         return mContext;
@@ -546,6 +559,22 @@
                 "This shouldn't happen, maybe the default handler is broken.");
     }
 
+    /**
+     * Gives every handler (in order) a chance to handle request until one consumes the transition.
+     * @return the WindowContainerTransaction given by the handler which consumed the transition.
+     */
+    public WindowContainerTransaction dispatchRequest(@NonNull IBinder transition,
+            @NonNull TransitionRequestInfo request, @Nullable TransitionHandler skip) {
+        for (int i = mHandlers.size() - 1; i >= 0; --i) {
+            if (mHandlers.get(i) == skip) continue;
+            WindowContainerTransaction wct = mHandlers.get(i).handleRequest(transition, request);
+            if (wct != null) {
+                return wct;
+            }
+        }
+        return null;
+    }
+
     /** Special version of finish just for dealing with no-op/invalid transitions. */
     private void onAbort(IBinder transition) {
         onFinish(transition, null /* wct */, null /* wctCB */, true /* abort */);
@@ -901,17 +930,6 @@
      */
     @ExternalThread
     private class ShellTransitionImpl implements ShellTransitions {
-        private IShellTransitionsImpl mIShellTransitions;
-
-        @Override
-        public IShellTransitions createExternalInterface() {
-            if (mIShellTransitions != null) {
-                mIShellTransitions.invalidate();
-            }
-            mIShellTransitions = new IShellTransitionsImpl(Transitions.this);
-            return mIShellTransitions;
-        }
-
         @Override
         public void registerRemote(@NonNull TransitionFilter filter,
                 @NonNull RemoteTransition remoteTransition) {
@@ -932,7 +950,8 @@
      * The interface for calls from outside the host process.
      */
     @BinderThread
-    private static class IShellTransitionsImpl extends IShellTransitions.Stub {
+    private static class IShellTransitionsImpl extends IShellTransitions.Stub
+            implements ExternalInterfaceBinder {
         private Transitions mTransitions;
 
         IShellTransitionsImpl(Transitions transitions) {
@@ -942,7 +961,8 @@
         /**
          * Invalidates this instance, preventing future calls from updating the controller.
          */
-        void invalidate() {
+        @Override
+        public void invalidate() {
             mTransitions = null;
         }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 90a3773..077e9ca 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -63,7 +63,9 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.ShellSharedConstants;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -102,6 +104,9 @@
     @Mock
     private IBackNaviAnimationController mIBackNaviAnimationController;
 
+    @Mock
+    private ShellController mShellController;
+
     private BackAnimationController mController;
 
     private int mEventTime = 0;
@@ -118,7 +123,7 @@
                 ANIMATION_ENABLED);
         mTestableLooper = TestableLooper.get(this);
         mShellInit = spy(new ShellInit(mShellExecutor));
-        mController = new BackAnimationController(mShellInit,
+        mController = new BackAnimationController(mShellInit, mShellController,
                 mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
                 mActivityTaskManager, mContext,
                 mContentResolver);
@@ -175,6 +180,12 @@
     }
 
     @Test
+    public void instantiateController_addExternalInterface() {
+        verify(mShellController, times(1)).addExternalInterface(
+                eq(ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION), any(), any());
+    }
+
+    @Test
     @Ignore("b/207481538")
     public void crossActivity_screenshotAttachedAndVisible() {
         SurfaceControl screenshotSurface = new SurfaceControl();
@@ -250,7 +261,7 @@
         // Toggle the setting off
         Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0");
         ShellInit shellInit = new ShellInit(mShellExecutor);
-        mController = new BackAnimationController(shellInit,
+        mController = new BackAnimationController(shellInit, mShellController,
                 mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
                 mActivityTaskManager, mContext,
                 mContentResolver);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index dd23d97..c850a3b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -52,6 +52,7 @@
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
@@ -68,6 +69,8 @@
 public class DesktopModeControllerTest extends ShellTestCase {
 
     @Mock
+    private ShellController mShellController;
+    @Mock
     private ShellTaskOrganizer mShellTaskOrganizer;
     @Mock
     private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
@@ -94,8 +97,8 @@
 
         mDesktopModeTaskRepository = new DesktopModeTaskRepository();
 
-        mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer,
-                mRootTaskDisplayAreaOrganizer, mMockTransitions,
+        mController = new DesktopModeController(mContext, mShellInit, mShellController,
+                mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mMockTransitions,
                 mDesktopModeTaskRepository, mMockHandler, mExecutor);
 
         when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(anyInt())).thenReturn(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
index a88c837..d378a17 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
@@ -52,6 +52,7 @@
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.ShellSharedConstants;
 
 import org.junit.After;
 import org.junit.Before;
@@ -168,6 +169,18 @@
         }
     }
 
+    @Test
+    public void onInit_addExternalInterface() {
+        if (FLOATING_TASKS_ACTUALLY_ENABLED) {
+            createController();
+            setUpTabletConfig();
+            mController.onInit();
+
+            verify(mShellController, times(1)).addExternalInterface(
+                    ShellSharedConstants.KEY_EXTRA_SHELL_FLOATING_TASKS, any(), any());
+        }
+    }
+
     //
     // Tests for floating layer, which is only available for tablets.
     //
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
index cf8297e..8ad3d2a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
@@ -51,6 +51,7 @@
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.ShellSharedConstants;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -176,6 +177,12 @@
     }
 
     @Test
+    public void testControllerRegisteresExternalInterface() {
+        verify(mMockShellController, times(1)).addExternalInterface(
+                eq(ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED), any(), any());
+    }
+
+    @Test
     public void testDefaultShouldNotInOneHanded() {
         // Assert default transition state is STATE_NONE
         assertThat(mSpiedTransitionState.getState()).isEqualTo(STATE_NONE);
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 1e08f1e..d06fb55 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
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -61,6 +62,7 @@
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.ShellSharedConstants;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -152,6 +154,12 @@
     }
 
     @Test
+    public void instantiatePipController_registerExternalInterface() {
+        verify(mShellController, times(1)).addExternalInterface(
+                eq(ShellSharedConstants.KEY_EXTRA_SHELL_PIP), any(), any());
+    }
+
+    @Test
     public void instantiatePipController_registerUserChangeListener() {
         verify(mShellController, times(1)).addUserChangeListener(any());
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index b8aaaa7..f6ac3ee 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -28,6 +28,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -57,7 +58,9 @@
 import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.ShellSharedConstants;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.SplitBounds;
 
@@ -84,6 +87,8 @@
     @Mock
     private TaskStackListenerImpl mTaskStackListener;
     @Mock
+    private ShellController mShellController;
+    @Mock
     private ShellCommandHandler mShellCommandHandler;
     @Mock
     private DesktopModeTaskRepository mDesktopModeTaskRepository;
@@ -101,7 +106,7 @@
         when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
         mShellInit = spy(new ShellInit(mMainExecutor));
         mRecentTasksController = spy(new RecentTasksController(mContext, mShellInit,
-                mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
+                mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
                 Optional.of(mDesktopModeTaskRepository), mMainExecutor));
         mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
                 null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController),
@@ -121,6 +126,12 @@
     }
 
     @Test
+    public void instantiateController_addExternalInterface() {
+        verify(mShellController, times(1)).addExternalInterface(
+                eq(ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS), any(), any());
+    }
+
+    @Test
     public void testAddRemoveSplitNotifyChange() {
         ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
         ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 5a68361..55883ab 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -58,6 +58,7 @@
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.ShellSharedConstants;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -133,6 +134,15 @@
     }
 
     @Test
+    public void instantiateController_addExternalInterface() {
+        doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
+        when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
+        mSplitScreenController.onInit();
+        verify(mShellController, times(1)).addExternalInterface(
+                eq(ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN), any(), any());
+    }
+
+    @Test
     public void testShouldAddMultipleTaskFlag_notInSplitScreen() {
         doReturn(false).when(mSplitScreenController).isSplitScreenVisible();
         doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
index 35515e3..90165d1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -36,7 +37,9 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.ShellSharedConstants;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -56,25 +59,34 @@
 
     private @Mock Context mContext;
     private @Mock DisplayManager mDisplayManager;
-    private @Mock ShellInit mShellInit;
+    private @Mock ShellController mShellController;
     private @Mock ShellTaskOrganizer mTaskOrganizer;
     private @Mock ShellExecutor mMainExecutor;
     private @Mock StartingWindowTypeAlgorithm mTypeAlgorithm;
     private @Mock IconProvider mIconProvider;
     private @Mock TransactionPool mTransactionPool;
     private StartingWindowController mController;
+    private ShellInit mShellInit;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         doReturn(mock(Display.class)).when(mDisplayManager).getDisplay(anyInt());
         doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class));
-        mController = new StartingWindowController(mContext, mShellInit, mTaskOrganizer,
-                mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool);
+        mShellInit = spy(new ShellInit(mMainExecutor));
+        mController = new StartingWindowController(mContext, mShellInit, mShellController,
+                mTaskOrganizer, mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool);
+        mShellInit.init();
     }
 
     @Test
-    public void instantiate_addInitCallback() {
+    public void instantiateController_addInitCallback() {
         verify(mShellInit, times(1)).addInitCallback(any(), any());
     }
+
+    @Test
+    public void instantiateController_addExternalInterface() {
+        verify(mShellController, times(1)).addExternalInterface(
+                eq(ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW), any(), any());
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
index d6ddba9..fbc50c6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
@@ -16,12 +16,16 @@
 
 package com.android.wm.shell.sysui;
 
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.content.res.Configuration;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
@@ -30,6 +34,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.ShellExecutor;
 
 import org.junit.After;
@@ -49,6 +54,7 @@
 public class ShellControllerTest extends ShellTestCase {
 
     private static final int TEST_USER_ID = 100;
+    private static final String EXTRA_TEST_BINDER = "test_binder";
 
     @Mock
     private ShellInit mShellInit;
@@ -81,6 +87,47 @@
     }
 
     @Test
+    public void testAddExternalInterface_ensureCallback() {
+        Binder callback = new Binder();
+        ExternalInterfaceBinder wrapper = new ExternalInterfaceBinder() {
+            @Override
+            public void invalidate() {
+                // Do nothing
+            }
+
+            @Override
+            public IBinder asBinder() {
+                return callback;
+            }
+        };
+        mController.addExternalInterface(EXTRA_TEST_BINDER, () -> wrapper, this);
+
+        Bundle b = new Bundle();
+        mController.asShell().createExternalInterfaces(b);
+        assertTrue(b.getIBinder(EXTRA_TEST_BINDER) == callback);
+    }
+
+    @Test
+    public void testAddExternalInterface_disallowDuplicateKeys() {
+        Binder callback = new Binder();
+        ExternalInterfaceBinder wrapper = new ExternalInterfaceBinder() {
+            @Override
+            public void invalidate() {
+                // Do nothing
+            }
+
+            @Override
+            public IBinder asBinder() {
+                return callback;
+            }
+        };
+        mController.addExternalInterface(EXTRA_TEST_BINDER, () -> wrapper, this);
+        assertThrows(IllegalArgumentException.class, () -> {
+            mController.addExternalInterface(EXTRA_TEST_BINDER, () -> wrapper, this);
+        });
+    }
+
+    @Test
     public void testAddUserChangeListener_ensureCallback() {
         mController.addUserChangeListener(mUserChangeListener);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index db9136d..c764741 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -87,7 +87,9 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.ShellSharedConstants;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -124,12 +126,25 @@
     @Test
     public void instantiate_addInitCallback() {
         ShellInit shellInit = mock(ShellInit.class);
-        final Transitions t = new Transitions(mContext, shellInit, mOrganizer, mTransactionPool,
-                createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor);
+        final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
+                mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
+                mMainHandler, mAnimExecutor);
         verify(shellInit, times(1)).addInitCallback(any(), eq(t));
     }
 
     @Test
+    public void instantiateController_addExternalInterface() {
+        ShellInit shellInit = new ShellInit(mMainExecutor);
+        ShellController shellController = mock(ShellController.class);
+        final Transitions t = new Transitions(mContext, shellInit, shellController,
+                mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
+                mMainHandler, mAnimExecutor);
+        shellInit.init();
+        verify(shellController, times(1)).addExternalInterface(
+                eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any());
+    }
+
+    @Test
     public void testBasicTransitionFlow() {
         Transitions transitions = createTestTransitions();
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
@@ -1063,8 +1078,9 @@
 
     private Transitions createTestTransitions() {
         ShellInit shellInit = new ShellInit(mMainExecutor);
-        final Transitions t = new Transitions(mContext, shellInit, mOrganizer, mTransactionPool,
-                createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor);
+        final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
+                mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
+                mMainHandler, mAnimExecutor);
         shellInit.init();
         return t;
     }
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index df6f08d..2f5b5f4 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -294,5 +294,6 @@
     dxflags: ["--multi-dex"],
     required: [
         "privapp_whitelist_com.android.systemui",
+        "wmshell.protolog.json.gz",
     ],
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
index dc2c6356..1b7e26b 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -361,13 +361,17 @@
          *
          * The end state of the animation is controlled by [destination]. This value can be any of
          * the four corners, any of the four edges, or the center of the view.
+         *
+         * @param onAnimationEnd an optional runnable that will be run once the animation finishes
+         *    successfully. Will not be run if the animation is cancelled.
          */
         @JvmOverloads
         fun animateRemoval(
             rootView: View,
             destination: Hotspot = Hotspot.CENTER,
             interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR,
-            duration: Long = DEFAULT_DURATION
+            duration: Long = DEFAULT_DURATION,
+            onAnimationEnd: Runnable? = null,
         ): Boolean {
             if (
                 !occupiesSpace(
@@ -391,13 +395,28 @@
                 addListener(child, listener, recursive = false)
             }
 
-            // Remove the view so that a layout update is triggered for the siblings and they
-            // animate to their next position while the view's removal is also animating.
-            parent.removeView(rootView)
-            // By adding the view to the overlay, we can animate it while it isn't part of the view
-            // hierarchy. It is correctly positioned because we have its previous bounds, and we set
-            // them manually during the animation.
-            parent.overlay.add(rootView)
+            val viewHasSiblings = parent.childCount > 1
+            if (viewHasSiblings) {
+                // Remove the view so that a layout update is triggered for the siblings and they
+                // animate to their next position while the view's removal is also animating.
+                parent.removeView(rootView)
+                // By adding the view to the overlay, we can animate it while it isn't part of the
+                // view hierarchy. It is correctly positioned because we have its previous bounds,
+                // and we set them manually during the animation.
+                parent.overlay.add(rootView)
+            }
+            // If this view has no siblings, the parent view may shrink to (0,0) size and mess
+            // up the animation if we immediately remove the view. So instead, we just leave the
+            // view in the real hierarchy until the animation finishes.
+
+            val endRunnable = Runnable {
+                if (viewHasSiblings) {
+                    parent.overlay.remove(rootView)
+                } else {
+                    parent.removeView(rootView)
+                }
+                onAnimationEnd?.run()
+            }
 
             val startValues =
                 mapOf(
@@ -430,7 +449,8 @@
                 endValues,
                 interpolator,
                 duration,
-                ephemeral = true
+                ephemeral = true,
+                endRunnable,
             )
 
             if (rootView is ViewGroup) {
@@ -463,7 +483,6 @@
                                 .alpha(0f)
                                 .setInterpolator(Interpolators.ALPHA_OUT)
                                 .setDuration(duration / 2)
-                                .withEndAction { parent.overlay.remove(rootView) }
                                 .start()
                         }
                     }
@@ -477,7 +496,6 @@
                     .setInterpolator(Interpolators.ALPHA_OUT)
                     .setDuration(duration / 2)
                     .setStartDelay(duration / 2)
-                    .withEndAction { parent.overlay.remove(rootView) }
                     .start()
             }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt
index 3175dcf..4d94bab 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt
@@ -17,8 +17,6 @@
 
 package com.android.systemui.user.ui.compose
 
-import android.graphics.Bitmap
-import android.graphics.Canvas
 import android.graphics.drawable.Drawable
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.compose.foundation.Image
@@ -50,10 +48,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.asImageBitmap
 import androidx.compose.ui.graphics.painter.ColorPainter
-import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
@@ -62,6 +58,7 @@
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.core.graphics.drawable.toBitmap
 import com.android.systemui.common.ui.compose.load
 import com.android.systemui.compose.SysUiOutlinedButton
 import com.android.systemui.compose.SysUiTextButton
@@ -356,10 +353,11 @@
         remember(viewModel.iconResourceId) {
             val drawable =
                 checkNotNull(AppCompatResources.getDrawable(context, viewModel.iconResourceId))
+            val size = with(density) { 20.dp.toPx() }.toInt()
             drawable
                 .toBitmap(
-                    size = with(density) { 20.dp.toPx() }.toInt(),
-                    tintColor = Color.White,
+                    width = size,
+                    height = size,
                 )
                 .asImageBitmap()
         }
@@ -392,32 +390,3 @@
                 ),
     )
 }
-
-/**
- * Converts the [Drawable] to a [Bitmap].
- *
- * Note that this is a relatively memory-heavy operation as it allocates a whole bitmap and draws
- * the `Drawable` onto it. Use sparingly and with care.
- */
-private fun Drawable.toBitmap(
-    size: Int? = null,
-    tintColor: Color? = null,
-): Bitmap {
-    val bitmap =
-        if (intrinsicWidth <= 0 || intrinsicHeight <= 0) {
-            Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
-        } else {
-            Bitmap.createBitmap(
-                size ?: intrinsicWidth,
-                size ?: intrinsicHeight,
-                Bitmap.Config.ARGB_8888
-            )
-        }
-    val canvas = Canvas(bitmap)
-    setBounds(0, 0, canvas.width, canvas.height)
-    if (tintColor != null) {
-        setTint(tintColor.toArgb())
-    }
-    draw(canvas)
-    return bitmap
-}
diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml
new file mode 100644
index 0000000..29832a0
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2022, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:systemui="http://schemas.android.com/apk/res-auto" >
+
+    <com.android.keyguard.AlphaOptimizedLinearLayout
+        android:id="@+id/mobile_group"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:orientation="horizontal" >
+
+        <FrameLayout
+            android:id="@+id/inout_container"
+            android:layout_height="17dp"
+            android:layout_width="wrap_content"
+            android:layout_gravity="center_vertical">
+            <ImageView
+                android:id="@+id/mobile_in"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:src="@drawable/ic_activity_down"
+                android:visibility="gone"
+                android:paddingEnd="2dp"
+                />
+            <ImageView
+                android:id="@+id/mobile_out"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:src="@drawable/ic_activity_up"
+                android:paddingEnd="2dp"
+                android:visibility="gone"
+                />
+        </FrameLayout>
+        <ImageView
+            android:id="@+id/mobile_type"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:paddingStart="2.5dp"
+            android:paddingEnd="1dp"
+            android:visibility="gone" />
+        <Space
+            android:id="@+id/mobile_roaming_space"
+            android:layout_height="match_parent"
+            android:layout_width="@dimen/roaming_icon_start_padding"
+            android:visibility="gone"
+            />
+        <FrameLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical">
+            <com.android.systemui.statusbar.AnimatedImageView
+                android:id="@+id/mobile_signal"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                systemui:hasOverlappingRendering="false"
+                />
+            <ImageView
+                android:id="@+id/mobile_roaming"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/stat_sys_roaming"
+                android:contentDescription="@string/data_connection_roaming"
+                android:visibility="gone" />
+        </FrameLayout>
+        <ImageView
+            android:id="@+id/mobile_roaming_large"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/stat_sys_roaming_large"
+            android:contentDescription="@string/data_connection_roaming"
+            android:visibility="gone" />
+    </com.android.keyguard.AlphaOptimizedLinearLayout>
+</merge>
diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml
new file mode 100644
index 0000000..1b38fd2
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2022, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/mobile_combo"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:gravity="center_vertical" >
+
+    <include layout="@layout/status_bar_mobile_signal_group_inner" />
+
+</com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView>
+
diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml
index a1d1266..b86929e 100644
--- a/packages/SystemUI/res-keyguard/values/styles.xml
+++ b/packages/SystemUI/res-keyguard/values/styles.xml
@@ -25,7 +25,7 @@
     </style>
     <style name="Keyguard.TextView.EmergencyButton" parent="Theme.SystemUI">
         <item name="android:textColor">?androidprv:attr/textColorOnAccent</item>
-        <item name="android:textSize">14dp</item>
+        <item name="android:textSize">14sp</item>
         <item name="android:background">@drawable/kg_emergency_button_background</item>
         <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:paddingLeft">12dp</item>
diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
index e079fd3..21d12c2 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
@@ -29,6 +29,7 @@
 
     <com.android.internal.widget.CachingIconView
         android:id="@+id/app_icon"
+        android:background="@drawable/media_ttt_chip_background_receiver"
         android:layout_width="@dimen/media_ttt_icon_size_receiver"
         android:layout_height="@dimen/media_ttt_icon_size_receiver"
         android:layout_gravity="center|bottom"
diff --git a/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml b/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml
index 10d49b3..d6c63eb 100644
--- a/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml
+++ b/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml
@@ -18,80 +18,12 @@
 -->
 <com.android.systemui.statusbar.StatusBarMobileView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:systemui="http://schemas.android.com/apk/res-auto"
     android:id="@+id/mobile_combo"
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
     android:gravity="center_vertical" >
 
-    <com.android.keyguard.AlphaOptimizedLinearLayout
-        android:id="@+id/mobile_group"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="center_vertical"
-        android:orientation="horizontal" >
+    <include layout="@layout/status_bar_mobile_signal_group_inner" />
 
-        <FrameLayout
-            android:id="@+id/inout_container"
-            android:layout_height="17dp"
-            android:layout_width="wrap_content"
-            android:layout_gravity="center_vertical">
-            <ImageView
-                android:id="@+id/mobile_in"
-                android:layout_height="wrap_content"
-                android:layout_width="wrap_content"
-                android:src="@drawable/ic_activity_down"
-                android:visibility="gone"
-                android:paddingEnd="2dp"
-            />
-            <ImageView
-                android:id="@+id/mobile_out"
-                android:layout_height="wrap_content"
-                android:layout_width="wrap_content"
-                android:src="@drawable/ic_activity_up"
-                android:paddingEnd="2dp"
-                android:visibility="gone"
-            />
-        </FrameLayout>
-        <ImageView
-            android:id="@+id/mobile_type"
-            android:layout_height="wrap_content"
-            android:layout_width="wrap_content"
-            android:layout_gravity="center_vertical"
-            android:paddingStart="2.5dp"
-            android:paddingEnd="1dp"
-            android:visibility="gone" />
-        <Space
-            android:id="@+id/mobile_roaming_space"
-            android:layout_height="match_parent"
-            android:layout_width="@dimen/roaming_icon_start_padding"
-            android:visibility="gone"
-        />
-        <FrameLayout
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_vertical">
-            <com.android.systemui.statusbar.AnimatedImageView
-                android:id="@+id/mobile_signal"
-                android:layout_height="wrap_content"
-                android:layout_width="wrap_content"
-                systemui:hasOverlappingRendering="false"
-            />
-            <ImageView
-                android:id="@+id/mobile_roaming"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:src="@drawable/stat_sys_roaming"
-                android:contentDescription="@string/data_connection_roaming"
-                android:visibility="gone" />
-        </FrameLayout>
-        <ImageView
-            android:id="@+id/mobile_roaming_large"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/stat_sys_roaming_large"
-            android:contentDescription="@string/data_connection_roaming"
-            android:visibility="gone" />
-    </com.android.keyguard.AlphaOptimizedLinearLayout>
 </com.android.systemui.statusbar.StatusBarMobileView>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f7019dc..8fdfc89 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1056,9 +1056,8 @@
     <!-- Media tap-to-transfer chip for receiver device -->
     <dimen name="media_ttt_chip_size_receiver">100dp</dimen>
     <dimen name="media_ttt_icon_size_receiver">95dp</dimen>
-    <!-- Since the generic icon isn't circular, we need to scale it down so it still fits within
-         the circular chip. -->
-    <dimen name="media_ttt_generic_icon_size_receiver">70dp</dimen>
+    <!-- Add some padding for the generic icon so it doesn't go all the way to the border. -->
+    <dimen name="media_ttt_generic_icon_padding">12dp</dimen>
     <dimen name="media_ttt_receiver_vert_translation">20dp</dimen>
 
     <!-- Window magnification -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index 85278dd..f2742b7 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -43,27 +43,8 @@
     public static final String KEY_EXTRA_SYSUI_PROXY = "extra_sysui_proxy";
     public static final String KEY_EXTRA_WINDOW_CORNER_RADIUS = "extra_window_corner_radius";
     public static final String KEY_EXTRA_SUPPORTS_WINDOW_CORNERS = "extra_supports_window_corners";
-    // See IPip.aidl
-    public static final String KEY_EXTRA_SHELL_PIP = "extra_shell_pip";
-    // See ISplitScreen.aidl
-    public static final String KEY_EXTRA_SHELL_SPLIT_SCREEN = "extra_shell_split_screen";
-    // See IFloatingTasks.aidl
-    public static final String KEY_EXTRA_SHELL_FLOATING_TASKS = "extra_shell_floating_tasks";
-    // See IOneHanded.aidl
-    public static final String KEY_EXTRA_SHELL_ONE_HANDED = "extra_shell_one_handed";
-    // See IShellTransitions.aidl
-    public static final String KEY_EXTRA_SHELL_SHELL_TRANSITIONS =
-            "extra_shell_shell_transitions";
-    // See IStartingWindow.aidl
-    public static final String KEY_EXTRA_SHELL_STARTING_WINDOW =
-            "extra_shell_starting_window";
     // See ISysuiUnlockAnimationController.aidl
     public static final String KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER = "unlock_animation";
-    // See IRecentTasks.aidl
-    public static final String KEY_EXTRA_RECENT_TASKS = "recent_tasks";
-    public static final String KEY_EXTRA_SHELL_BACK_ANIMATION = "extra_shell_back_animation";
-    // See IDesktopMode.aidl
-    public static final String KEY_EXTRA_SHELL_DESKTOP_MODE = "extra_shell_desktop_mode";
 
     public static final String NAV_BAR_MODE_3BUTTON_OVERLAY =
             WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY;
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index b444f4c..fd38661 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -69,7 +69,7 @@
 
     private var isCharging = false
     private var dozeAmount = 0f
-    private var isKeyguardShowing = false
+    private var isKeyguardVisible = false
 
     private val regionSamplingEnabled =
             featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING)
@@ -145,7 +145,7 @@
 
     private val batteryCallback = object : BatteryStateChangeCallback {
         override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
-            if (isKeyguardShowing && !isCharging && charging) {
+            if (isKeyguardVisible && !isCharging && charging) {
                 clock?.animations?.charge()
             }
             isCharging = charging
@@ -168,9 +168,9 @@
     }
 
     private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
-        override fun onKeyguardVisibilityChanged(showing: Boolean) {
-            isKeyguardShowing = showing
-            if (!isKeyguardShowing) {
+        override fun onKeyguardVisibilityChanged(visible: Boolean) {
+            isKeyguardVisible = visible
+            if (!isKeyguardVisible) {
                 clock?.animations?.doze(if (isDozing) 1f else 0f)
             }
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index 1e5c53d..2cc5ccdc 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -24,7 +24,6 @@
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT;
-import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
 
 import android.animation.Animator;
@@ -107,8 +106,6 @@
                 return R.string.kg_prompt_reason_timeout_password;
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 return R.string.kg_prompt_reason_timeout_password;
-            case PROMPT_REASON_TRUSTAGENT_EXPIRED:
-                return R.string.kg_prompt_reason_timeout_password;
             case PROMPT_REASON_NONE:
                 return 0;
             default:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 5b22324..9871645 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -330,9 +330,6 @@
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern);
                 break;
-            case PROMPT_REASON_TRUSTAGENT_EXPIRED:
-                mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern);
-                break;
             case PROMPT_REASON_NONE:
                 break;
             default:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
index 0a91150..c46e33d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
@@ -22,7 +22,6 @@
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT;
-import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
 
 import android.animation.Animator;
@@ -124,8 +123,6 @@
                 return R.string.kg_prompt_reason_timeout_pin;
             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                 return R.string.kg_prompt_reason_timeout_pin;
-            case PROMPT_REASON_TRUSTAGENT_EXPIRED:
-                return R.string.kg_prompt_reason_timeout_pin;
             case PROMPT_REASON_NONE:
                 return 0;
             default:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
index 9d0a8ac..ac00e94 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
@@ -61,12 +61,6 @@
     int PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT = 7;
 
     /**
-     * Some auth is required because the trustagent expired either from timeout or manually by
-     * the user
-     */
-    int PROMPT_REASON_TRUSTAGENT_EXPIRED = 8;
-
-    /**
      * Reset the view and prepare to take input. This should do things like clearing the
      * password or pattern and clear error messages.
      */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index c715a4e..e9f06ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -212,9 +212,9 @@
         }
 
         @Override
-        public void onKeyguardVisibilityChanged(boolean showing) {
-            if (showing) {
-                if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing);
+        public void onKeyguardVisibilityChanged(boolean visible) {
+            if (visible) {
+                if (DEBUG) Slog.v(TAG, "refresh statusview visible:true");
                 refreshTime();
             }
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index d14666d..cd9089e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -308,7 +308,8 @@
     HashMap<Integer, ServiceState> mServiceStates = new HashMap<>();
 
     private int mPhoneState;
-    private boolean mKeyguardIsVisible;
+    private boolean mKeyguardShowing;
+    private boolean mKeyguardOccluded;
     private boolean mCredentialAttempted;
     private boolean mKeyguardGoingAway;
     private boolean mGoingToSleep;
@@ -318,7 +319,6 @@
     private boolean mAuthInterruptActive;
     private boolean mNeedsSlowUnlockTransition;
     private boolean mAssistantVisible;
-    private boolean mKeyguardOccluded;
     private boolean mOccludingAppRequestingFp;
     private boolean mOccludingAppRequestingFace;
     private boolean mSecureCameraLaunched;
@@ -681,14 +681,42 @@
     }
 
     /**
-     * Updates KeyguardUpdateMonitor's internal state to know if keyguard is occluded
+     * Updates KeyguardUpdateMonitor's internal state to know if keyguard is showing and if
+     * its occluded. The keyguard is considered visible if its showing and NOT occluded.
      */
-    public void setKeyguardOccluded(boolean occluded) {
-        mKeyguardOccluded = occluded;
-        updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
-                FACE_AUTH_UPDATED_KEYGUARD_OCCLUSION_CHANGED);
-    }
+    public void setKeyguardShowing(boolean showing, boolean occluded) {
+        final boolean occlusionChanged = mKeyguardOccluded != occluded;
+        final boolean showingChanged = mKeyguardShowing != showing;
+        if (!occlusionChanged && !showingChanged) {
+            return;
+        }
 
+        final boolean wasKeyguardVisible = isKeyguardVisible();
+        mKeyguardShowing = showing;
+        mKeyguardOccluded = occluded;
+        final boolean isKeyguardVisible = isKeyguardVisible();
+        mLogger.logKeyguardShowingChanged(showing, occluded, isKeyguardVisible);
+
+        if (isKeyguardVisible != wasKeyguardVisible) {
+            if (isKeyguardVisible) {
+                mSecureCameraLaunched = false;
+            }
+            for (int i = 0; i < mCallbacks.size(); i++) {
+                KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+                if (cb != null) {
+                    cb.onKeyguardVisibilityChanged(isKeyguardVisible);
+                }
+            }
+        }
+
+        if (occlusionChanged) {
+            updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
+                    FACE_AUTH_UPDATED_KEYGUARD_OCCLUSION_CHANGED);
+        } else if (showingChanged) {
+            updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
+                    FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED);
+        }
+    }
 
     /**
      * Request to listen for face authentication when an app is occluding keyguard.
@@ -2442,7 +2470,7 @@
         // Triggers:
         final boolean triggerActiveUnlockForAssistant = shouldTriggerActiveUnlockForAssistant();
         final boolean awakeKeyguard = mBouncerFullyShown || mUdfpsBouncerShowing
-                || (mKeyguardIsVisible && !mGoingToSleep
+                || (isKeyguardVisible() && !mGoingToSleep
                 && mStatusBarState != StatusBarState.SHADE_LOCKED);
 
         // Gates:
@@ -2518,7 +2546,7 @@
         final boolean userDoesNotHaveTrust = !getUserHasTrust(user);
         final boolean shouldListenForFingerprintAssistant = shouldListenForFingerprintAssistant();
         final boolean shouldListenKeyguardState =
-                mKeyguardIsVisible
+                isKeyguardVisible()
                         || !mDeviceInteractive
                         || (mBouncerIsOrWillBeShowing && !mKeyguardGoingAway)
                         || mGoingToSleep
@@ -2567,7 +2595,7 @@
                     mFingerprintLockedOut,
                     mGoingToSleep,
                     mKeyguardGoingAway,
-                    mKeyguardIsVisible,
+                    isKeyguardVisible(),
                     mKeyguardOccluded,
                     mOccludingAppRequestingFp,
                     mIsPrimaryUser,
@@ -2589,7 +2617,7 @@
         }
 
         final boolean statusBarShadeLocked = mStatusBarState == StatusBarState.SHADE_LOCKED;
-        final boolean awakeKeyguard = mKeyguardIsVisible && mDeviceInteractive
+        final boolean awakeKeyguard = isKeyguardVisible() && mDeviceInteractive
                 && !statusBarShadeLocked;
         final int user = getCurrentUser();
         final int strongAuth = mStrongAuthTracker.getStrongAuthForUser(user);
@@ -3146,32 +3174,18 @@
         callbacksRefreshCarrierInfo();
     }
 
+    /**
+     * Whether the keyguard is showing and not occluded.
+     */
     public boolean isKeyguardVisible() {
-        return mKeyguardIsVisible;
+        return isKeyguardShowing() && !mKeyguardOccluded;
     }
 
     /**
-     * Notifies that the visibility state of Keyguard has changed.
-     *
-     * <p>Needs to be called from the main thread.
+     * Whether the keyguard is showing. It may still be occluded and not visible.
      */
-    public void onKeyguardVisibilityChanged(boolean showing) {
-        Assert.isMainThread();
-        mLogger.logKeyguardVisibilityChanged(showing);
-        mKeyguardIsVisible = showing;
-
-        if (showing) {
-            mSecureCameraLaunched = false;
-        }
-
-        for (int i = 0; i < mCallbacks.size(); i++) {
-            KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
-            if (cb != null) {
-                cb.onKeyguardVisibilityChangedRaw(showing);
-            }
-        }
-        updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
-                FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED);
+    public boolean isKeyguardShowing() {
+        return mKeyguardShowing;
     }
 
     /**
@@ -3380,7 +3394,7 @@
         callback.onTimeChanged();
         callback.onPhoneStateChanged(mPhoneState);
         callback.onRefreshCarrierInfo();
-        callback.onKeyguardVisibilityChangedRaw(mKeyguardIsVisible);
+        callback.onKeyguardVisibilityChanged(isKeyguardVisible());
         callback.onTelephonyCapable(mTelephonyCapable);
 
         for (Entry<Integer, SimData> data : mSimDatas.entrySet()) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index 7a42803..bc5ab88 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -16,7 +16,6 @@
 package com.android.keyguard;
 
 import android.hardware.biometrics.BiometricSourceType;
-import android.os.SystemClock;
 import android.telephony.TelephonyManager;
 import android.view.WindowManagerPolicyConstants;
 
@@ -32,10 +31,6 @@
  */
 public class KeyguardUpdateMonitorCallback {
 
-    private static final long VISIBILITY_CHANGED_COLLAPSE_MS = 1000;
-    private long mVisibilityChangedCalled;
-    private boolean mShowing;
-
     /**
      * Called when the battery status changes, e.g. when plugged in or unplugged, charge
      * level, etc. changes.
@@ -75,21 +70,6 @@
     public void onPhoneStateChanged(int phoneState) { }
 
     /**
-     * Called when the visibility of the keyguard changes.
-     * @param showing Indicates if the keyguard is now visible.
-     */
-    public void onKeyguardVisibilityChanged(boolean showing) { }
-
-    public void onKeyguardVisibilityChangedRaw(boolean showing) {
-        final long now = SystemClock.elapsedRealtime();
-        if (showing == mShowing
-                && (now - mVisibilityChangedCalled) < VISIBILITY_CHANGED_COLLAPSE_MS) return;
-        onKeyguardVisibilityChanged(showing);
-        mVisibilityChangedCalled = now;
-        mShowing = showing;
-    }
-
-    /**
      * Called when the keyguard enters or leaves bouncer mode.
      * @param bouncerIsOrWillBeShowing if true, keyguard is showing the bouncer or transitioning
      *                                 from/to bouncer mode.
@@ -97,6 +77,12 @@
     public void onKeyguardBouncerStateChanged(boolean bouncerIsOrWillBeShowing) { }
 
     /**
+     * Called when the keyguard visibility changes.
+     * @param visible whether the keyguard is showing and is NOT occluded
+     */
+    public void onKeyguardVisibilityChanged(boolean visible) { }
+
+    /**
      * Called when the keyguard fully transitions to the bouncer or is no longer the bouncer
      * @param bouncerIsFullyShowing if true, keyguard is fully showing the bouncer
      */
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 2a36676..c41b752 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -447,14 +447,6 @@
     private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
             new KeyguardUpdateMonitorCallback() {
                 @Override
-                public void onKeyguardVisibilityChanged(boolean showing) {
-                    // reset mIsBouncerShowing state in case it was preemptively set
-                    // onLongPress
-                    mIsBouncerShowing = mKeyguardViewController.isBouncerShowing();
-                    updateVisibility();
-                }
-
-                @Override
                 public void onKeyguardBouncerStateChanged(boolean bouncer) {
                     mIsBouncerShowing = bouncer;
                     updateVisibility();
@@ -507,6 +499,11 @@
             // If biometrics were removed, local vars mCanDismissLockScreen and
             // mUserUnlockedWithBiometric may not be updated.
             mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
+
+            // reset mIsBouncerShowing state in case it was preemptively set
+            // onLongPress
+            mIsBouncerShowing = mKeyguardViewController.isBouncerShowing();
+
             updateKeyguardShowing();
             if (mIsKeyguardShowing) {
                 mUserUnlockedWithBiometric =
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index bf9f4c8..54cec71 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -170,8 +170,14 @@
         logBuffer.log(TAG, VERBOSE, { str1 = "$model" }, { str1!! })
     }
 
-    fun logKeyguardVisibilityChanged(showing: Boolean) {
-        logBuffer.log(TAG, DEBUG, { bool1 = showing }, { "onKeyguardVisibilityChanged($bool1)" })
+    fun logKeyguardShowingChanged(showing: Boolean, occluded: Boolean, visible: Boolean) {
+        logBuffer.log(TAG, DEBUG, {
+            bool1 = showing
+            bool2 = occluded
+            bool3 = visible
+        }, {
+            "keyguardShowingChanged(showing=$bool1 occluded=$bool2 visible=$bool3)"
+        })
     }
 
     fun logMissingSupervisorAppError(userId: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index 11353f6..403941f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -61,8 +61,8 @@
         }
 
         @Override
-        public void onKeyguardVisibilityChanged(boolean showing) {
-            mIsKeyguardVisible = showing;
+        public void onKeyguardVisibilityChanged(boolean visible) {
+            mIsKeyguardVisible = visible;
             handleFloatingMenuVisibility(mIsKeyguardVisible, mBtnMode, mBtnTargets);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index 4fee083..4363b88 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -117,7 +117,7 @@
     }
 
     fun showUnlockRipple(biometricSourceType: BiometricSourceType?) {
-        if (!(keyguardUpdateMonitor.isKeyguardVisible || keyguardUpdateMonitor.isDreaming) ||
+        if (!keyguardStateController.isShowing ||
             keyguardUpdateMonitor.userNeedsStrongAuth()) {
             return
         }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 8ae305b..2e51b51 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -431,8 +431,8 @@
         }
 
         @Override
-        public void onKeyguardVisibilityChanged(boolean showing) {
-            traceKeyguard(showing);
+        public void onKeyguardVisibilityChanged(boolean visible) {
+            traceKeyguard(visible);
         }
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index 21a2c3b..cc57662 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -155,11 +155,11 @@
         })
     }
 
-    fun logKeyguardVisibilityChange(isShowing: Boolean) {
+    fun logKeyguardVisibilityChange(isVisible: Boolean) {
         buffer.log(TAG, INFO, {
-            bool1 = isShowing
+            bool1 = isVisible
         }, {
-            "Keyguard visibility change, isShowing=$bool1"
+            "Keyguard visibility change, isVisible=$bool1"
         })
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index ef4ad9b..e8c1262 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -68,6 +68,9 @@
 
     public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true);
 
+    public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112,
+            false);
+
     // next id: 112
 
     /***************************************/
@@ -225,7 +228,8 @@
             new ReleasedFlag(1000);
     public static final ReleasedFlag DOCK_SETUP_ENABLED = new ReleasedFlag(1001);
 
-    public static final UnreleasedFlag ROUNDED_BOX_RIPPLE = new UnreleasedFlag(1002, false);
+    public static final UnreleasedFlag ROUNDED_BOX_RIPPLE =
+            new UnreleasedFlag(1002, /* teamfood= */ true);
 
     // 1100 - windowing
     @Keep
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 38b98eb..da0b910 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -25,7 +25,6 @@
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_UNLOCK_ANIMATION;
-import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
@@ -512,9 +511,9 @@
     KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() {
 
         @Override
-        public void onKeyguardVisibilityChanged(boolean showing) {
+        public void onKeyguardVisibilityChanged(boolean visible) {
             synchronized (KeyguardViewMediator.this) {
-                if (!showing && mPendingPinLock) {
+                if (!visible && mPendingPinLock) {
                     Log.i(TAG, "PIN lock requested, starting keyguard");
 
                     // Bring the keyguard back in order to show the PIN lock
@@ -804,9 +803,6 @@
             } else if (trustAgentsEnabled
                     && (strongAuth & SOME_AUTH_REQUIRED_AFTER_USER_REQUEST) != 0) {
                 return KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
-            } else if (trustAgentsEnabled
-                    && (strongAuth & SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED) != 0) {
-                return KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
             } else if (any && ((strongAuth & STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) != 0
                     || mUpdateMonitor.isFingerprintLockedOut())) {
                 return KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT;
@@ -987,9 +983,11 @@
 
                 @Override
                 public void onAnimationCancelled(boolean isKeyguardOccluded) {
-                    if (mUnoccludeAnimator != null) {
-                        mUnoccludeAnimator.cancel();
-                    }
+                    mContext.getMainExecutor().execute(() -> {
+                        if (mUnoccludeAnimator != null) {
+                            mUnoccludeAnimator.cancel();
+                        }
+                    });
 
                     setOccluded(isKeyguardOccluded /* isOccluded */, false /* animate */);
                     Log.d(TAG, "Unocclude animation cancelled. Occluded state is now: "
@@ -1808,7 +1806,6 @@
 
             if (mOccluded != isOccluded) {
                 mOccluded = isOccluded;
-                mUpdateMonitor.setKeyguardOccluded(isOccluded);
                 mKeyguardViewControllerLazy.get().setOccluded(isOccluded, animate
                         && mDeviceInteractive);
                 adjustStatusBarLocked();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 4c4b588..45b668e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -157,7 +157,11 @@
                         }
                     }
                 dozeHost.addCallback(callback)
-                trySendWithFailureLogging(false, TAG, "initial isDozing: false")
+                trySendWithFailureLogging(
+                    statusBarStateController.isDozing,
+                    TAG,
+                    "initial isDozing",
+                )
 
                 awaitClose { dozeHost.removeCallback(callback) }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index c6bd777..ea57806 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.media
 
 import android.app.ActivityOptions
+import android.app.IActivityTaskManager
 import android.content.Intent
 import android.media.projection.IMediaProjection
 import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
@@ -46,6 +47,7 @@
 
 class MediaProjectionAppSelectorActivity(
     private val activityLauncher: AsyncActivityLauncher,
+    private val activityTaskManager: IActivityTaskManager,
     private val controller: MediaProjectionAppSelectorController,
     private val recentTasksAdapterFactory: RecentTasksAdapter.Factory,
     /** This is used to override the dependency in a screenshot test */
@@ -56,9 +58,10 @@
     @Inject
     constructor(
         activityLauncher: AsyncActivityLauncher,
+        activityTaskManager: IActivityTaskManager,
         controller: MediaProjectionAppSelectorController,
         recentTasksAdapterFactory: RecentTasksAdapter.Factory,
-    ) : this(activityLauncher, controller, recentTasksAdapterFactory, null)
+    ) : this(activityLauncher, activityTaskManager, controller, recentTasksAdapterFactory, null)
 
     private var recentsRoot: ViewGroup? = null
     private var recentsProgress: View? = null
@@ -176,8 +179,14 @@
         recycler.adapter = recentTasksAdapterFactory.create(recentTasks, this)
     }
 
-    override fun onRecentClicked(task: RecentTask, view: View) {
-        // TODO(b/240924732) Handle clicking on a recent task
+    override fun onRecentAppClicked(task: RecentTask, view: View) {
+        val launchCookie = Binder()
+        val activityOptions = ActivityOptions.makeScaleUpAnimation(view, /* startX= */ 0,
+                /* startY= */ 0, view.width, view.height)
+        activityOptions.launchCookie = launchCookie
+
+        activityTaskManager.startActivityFromRecents(task.taskId, activityOptions.toBundle())
+        onTargetActivityLaunched(launchCookie)
     }
 
     private fun onTargetActivityLaunched(launchToken: IBinder) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index 792ae7c..c3de94f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
@@ -19,7 +19,6 @@
 import android.content.Context
 import android.content.pm.PackageManager
 import android.graphics.drawable.Drawable
-import com.android.internal.widget.CachingIconView
 import com.android.settingslib.Utils
 import com.android.systemui.R
 
@@ -76,29 +75,6 @@
                 isAppIcon = false
             )
         }
-
-        /**
-         * Sets an icon to be displayed by the given view.
-         *
-         * @param iconSize the size in pixels that the icon should be. If null, the size of
-         * [appIconView] will not be adjusted.
-         */
-        fun setIcon(
-            appIconView: CachingIconView,
-            icon: Drawable,
-            iconContentDescription: CharSequence,
-            iconSize: Int? = null,
-        ) {
-            iconSize?.let { size ->
-                val lp = appIconView.layoutParams
-                lp.width = size
-                lp.height = size
-                appIconView.layoutParams = lp
-            }
-
-            appIconView.contentDescription = iconContentDescription
-            appIconView.setImageDrawable(icon)
-        }
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index dfd9e22..8fc5519 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -30,6 +30,7 @@
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
+import com.android.internal.widget.CachingIconView
 import com.android.settingslib.Utils
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
@@ -146,20 +147,17 @@
         )
         val iconDrawable = newInfo.appIconDrawableOverride ?: iconInfo.drawable
         val iconContentDescription = newInfo.appNameOverride ?: iconInfo.contentDescription
-        val iconSize = context.resources.getDimensionPixelSize(
+        val iconPadding =
             if (iconInfo.isAppIcon) {
-                R.dimen.media_ttt_icon_size_receiver
+                0
             } else {
-                R.dimen.media_ttt_generic_icon_size_receiver
+                context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding)
             }
-        )
 
-        MediaTttUtils.setIcon(
-            currentView.requireViewById(R.id.app_icon),
-            iconDrawable,
-            iconContentDescription,
-            iconSize,
-        )
+        val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon)
+        iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding)
+        iconView.setImageDrawable(iconDrawable)
+        iconView.contentDescription = iconContentDescription
     }
 
     override fun animateViewIn(view: ViewGroup) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index 007eb8f..11c5528 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -29,6 +29,7 @@
 import android.view.accessibility.AccessibilityManager
 import android.widget.TextView
 import com.android.internal.statusbar.IUndoMediaTransferCallback
+import com.android.internal.widget.CachingIconView
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
@@ -53,7 +54,7 @@
  * chip is shown when a user is transferring media to/from this device and a receiver device.
  */
 @SysUISingleton
-class MediaTttChipControllerSender @Inject constructor(
+open class MediaTttChipControllerSender @Inject constructor(
         commandQueue: CommandQueue,
         context: Context,
         @MediaTttSenderLogger logger: MediaTttLogger,
@@ -145,11 +146,9 @@
         val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
             context, newInfo.routeInfo.clientPackageName, logger
         )
-        MediaTttUtils.setIcon(
-            currentView.requireViewById(R.id.app_icon),
-            iconInfo.drawable,
-            iconInfo.contentDescription
-        )
+        val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon)
+        iconView.setImageDrawable(iconInfo.drawable)
+        iconView.contentDescription = iconInfo.contentDescription
 
         // Text
         val otherDeviceName = newInfo.routeInfo.name.toString()
@@ -196,7 +195,19 @@
         )
     }
 
-    override fun removeView(removalReason: String) {
+    override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+        ViewHierarchyAnimator.animateRemoval(
+            view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner),
+            ViewHierarchyAnimator.Hotspot.TOP,
+            Interpolators.EMPHASIZED_ACCELERATE,
+            ANIMATION_DURATION,
+            onAnimationEnd,
+        )
+        // TODO(b/203800644): Add includeMargins as an option to ViewHierarchyAnimator so that the
+        //   animateChipOut matches the animateChipIn.
+    }
+
+    override fun shouldIgnoreViewRemoval(removalReason: String): Boolean {
         // Don't remove the chip if we're in progress or succeeded, since the user should still be
         // able to see the status of the transfer. (But do remove it if it's finally timed out.)
         val transferStatus = info?.state?.transferStatus
@@ -208,9 +219,9 @@
             logger.logRemovalBypass(
                 removalReason, bypassReason = "transferStatus=${transferStatus.name}"
             )
-            return
+            return true
         }
-        super.removeView(removalReason)
+        return false
     }
 
     private fun Boolean.visibleIfTrue(): Int {
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt
index ec9cfa8..51b4012 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt
@@ -43,7 +43,7 @@
     override fun onBindViewHolder(holder: RecentTaskViewHolder, position: Int) {
         val task = items[position]
         holder.bind(task, onClick = {
-            listener.onRecentClicked(task, holder.itemView)
+            listener.onRecentAppClicked(task, holder.itemView)
         })
     }
 
@@ -54,7 +54,7 @@
     }
 
     interface RecentTaskClickListener {
-        fun onRecentClicked(task: RecentTask, view: View)
+        fun onRecentAppClicked(task: RecentTask, view: View)
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index ded466a..2727c83 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -23,8 +23,8 @@
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.res.Configuration;
+import android.content.res.Configuration.Orientation;
 import android.metrics.LogMaker;
-import android.util.Log;
 import android.view.View;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -75,6 +75,7 @@
 
     @Nullable
     private Consumer<Boolean> mMediaVisibilityChangedListener;
+    @Orientation
     private int mLastOrientation;
     private String mCachedSpecs = "";
     @Nullable
@@ -88,21 +89,16 @@
             new QSPanel.OnConfigurationChangedListener() {
                 @Override
                 public void onConfigurationChange(Configuration newConfig) {
+                    mQSLogger.logOnConfigurationChanged(
+                        /* lastOrientation= */ mLastOrientation,
+                        /* newOrientation= */ newConfig.orientation,
+                        /* containerName= */ mView.getDumpableTag());
+
                     mShouldUseSplitNotificationShade =
-                            LargeScreenUtils.shouldUseSplitNotificationShade(getResources());
-                    // Logging to aid the investigation of b/216244185.
-                    Log.d(TAG,
-                            "onConfigurationChange: "
-                                    + "mShouldUseSplitNotificationShade="
-                                    + mShouldUseSplitNotificationShade + ", "
-                                    + "newConfig.windowConfiguration="
-                                    + newConfig.windowConfiguration);
-                    mQSLogger.logOnConfigurationChanged(mLastOrientation, newConfig.orientation,
-                            mView.getDumpableTag());
-                    if (newConfig.orientation != mLastOrientation) {
-                        mLastOrientation = newConfig.orientation;
-                        switchTileLayout(false);
-                    }
+                        LargeScreenUtils.shouldUseSplitNotificationShade(getResources());
+                    mLastOrientation = newConfig.orientation;
+
+                    switchTileLayoutIfNeeded();
                     onConfigurationChanged();
                 }
             };
@@ -334,6 +330,10 @@
         }
     }
 
+    private void switchTileLayoutIfNeeded() {
+        switchTileLayout(/* force= */ false);
+    }
+
     boolean switchTileLayout(boolean force) {
         /* Whether or not the panel currently contains a media player. */
         boolean horizontal = shouldUseHorizontalLayout();
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 899e57d..66be00d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -25,15 +25,6 @@
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
 
 import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_RECENT_TASKS;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_BACK_ANIMATION;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_DESKTOP_MODE;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_FLOATING_TASKS;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_ONE_HANDED;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_PIP;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SPLIT_SCREEN;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_STARTING_WINDOW;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SUPPORTS_WINDOW_CORNERS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER;
@@ -110,16 +101,7 @@
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
 import com.android.systemui.statusbar.policy.CallbackController;
-import com.android.wm.shell.back.BackAnimation;
-import com.android.wm.shell.desktopmode.DesktopMode;
-import com.android.wm.shell.floating.FloatingTasks;
-import com.android.wm.shell.onehanded.OneHanded;
-import com.android.wm.shell.pip.Pip;
-import com.android.wm.shell.pip.PipAnimationController;
-import com.android.wm.shell.recents.RecentTasks;
-import com.android.wm.shell.splitscreen.SplitScreen;
-import com.android.wm.shell.startingsurface.StartingSurface;
-import com.android.wm.shell.transition.ShellTransitions;
+import com.android.wm.shell.sysui.ShellInterface;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -151,10 +133,8 @@
     private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
 
     private final Context mContext;
-    private final Optional<Pip> mPipOptional;
+    private final ShellInterface mShellInterface;
     private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
-    private final Optional<SplitScreen> mSplitScreenOptional;
-    private final Optional<FloatingTasks> mFloatingTasksOptional;
     private SysUiState mSysUiState;
     private final Handler mHandler;
     private final Lazy<NavigationBarController> mNavBarControllerLazy;
@@ -164,14 +144,8 @@
     private final List<OverviewProxyListener> mConnectionCallbacks = new ArrayList<>();
     private final Intent mQuickStepIntent;
     private final ScreenshotHelper mScreenshotHelper;
-    private final Optional<OneHanded> mOneHandedOptional;
     private final CommandQueue mCommandQueue;
-    private final ShellTransitions mShellTransitions;
-    private final Optional<StartingSurface> mStartingSurface;
     private final KeyguardUnlockAnimationController mSysuiUnlockAnimationController;
-    private final Optional<RecentTasks> mRecentTasks;
-    private final Optional<BackAnimation> mBackAnimation;
-    private final Optional<DesktopMode> mDesktopModeOptional;
     private final UiEventLogger mUiEventLogger;
 
     private Region mActiveNavBarRegion;
@@ -456,36 +430,10 @@
             params.putBinder(KEY_EXTRA_SYSUI_PROXY, mSysUiProxy.asBinder());
             params.putFloat(KEY_EXTRA_WINDOW_CORNER_RADIUS, mWindowCornerRadius);
             params.putBoolean(KEY_EXTRA_SUPPORTS_WINDOW_CORNERS, mSupportsRoundedCornersOnWindows);
-
-            mPipOptional.ifPresent((pip) -> params.putBinder(
-                    KEY_EXTRA_SHELL_PIP,
-                    pip.createExternalInterface().asBinder()));
-            mSplitScreenOptional.ifPresent((splitscreen) -> params.putBinder(
-                    KEY_EXTRA_SHELL_SPLIT_SCREEN,
-                    splitscreen.createExternalInterface().asBinder()));
-            mFloatingTasksOptional.ifPresent(floatingTasks -> params.putBinder(
-                    KEY_EXTRA_SHELL_FLOATING_TASKS,
-                    floatingTasks.createExternalInterface().asBinder()));
-            mOneHandedOptional.ifPresent((onehanded) -> params.putBinder(
-                    KEY_EXTRA_SHELL_ONE_HANDED,
-                    onehanded.createExternalInterface().asBinder()));
-            params.putBinder(KEY_EXTRA_SHELL_SHELL_TRANSITIONS,
-                    mShellTransitions.createExternalInterface().asBinder());
-            mStartingSurface.ifPresent((startingwindow) -> params.putBinder(
-                    KEY_EXTRA_SHELL_STARTING_WINDOW,
-                    startingwindow.createExternalInterface().asBinder()));
-            params.putBinder(
-                    KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER,
+            params.putBinder(KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER,
                     mSysuiUnlockAnimationController.asBinder());
-            mRecentTasks.ifPresent(recentTasks -> params.putBinder(
-                    KEY_EXTRA_RECENT_TASKS,
-                    recentTasks.createExternalInterface().asBinder()));
-            mBackAnimation.ifPresent((backAnimation) -> params.putBinder(
-                    KEY_EXTRA_SHELL_BACK_ANIMATION,
-                    backAnimation.createExternalInterface().asBinder()));
-            mDesktopModeOptional.ifPresent((desktopMode -> params.putBinder(
-                    KEY_EXTRA_SHELL_DESKTOP_MODE,
-                    desktopMode.createExternalInterface().asBinder())));
+            // Add all the interfaces exposed by the shell
+            mShellInterface.createExternalInterfaces(params);
 
             try {
                 Log.d(TAG_OPS, "OverviewProxyService connected, initializing overview proxy");
@@ -559,21 +507,14 @@
 
     @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
     @Inject
-    public OverviewProxyService(Context context, CommandQueue commandQueue,
+    public OverviewProxyService(Context context,
+            CommandQueue commandQueue,
+            ShellInterface shellInterface,
             Lazy<NavigationBarController> navBarControllerLazy,
             Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
             NavigationModeController navModeController,
             NotificationShadeWindowController statusBarWinController, SysUiState sysUiState,
-            Optional<Pip> pipOptional,
-            Optional<SplitScreen> splitScreenOptional,
-            Optional<FloatingTasks> floatingTasksOptional,
-            Optional<OneHanded> oneHandedOptional,
-            Optional<RecentTasks> recentTasks,
-            Optional<BackAnimation> backAnimation,
-            Optional<StartingSurface> startingSurface,
-            Optional<DesktopMode> desktopModeOptional,
             BroadcastDispatcher broadcastDispatcher,
-            ShellTransitions shellTransitions,
             ScreenLifecycle screenLifecycle,
             UiEventLogger uiEventLogger,
             KeyguardUnlockAnimationController sysuiUnlockAnimationController,
@@ -587,7 +528,7 @@
         }
 
         mContext = context;
-        mPipOptional = pipOptional;
+        mShellInterface = shellInterface;
         mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
         mHandler = new Handler();
         mNavBarControllerLazy = navBarControllerLazy;
@@ -602,11 +543,6 @@
                 .supportsRoundedCornersOnWindows(mContext.getResources());
         mSysUiState = sysUiState;
         mSysUiState.addCallback(this::notifySystemUiStateFlags);
-        mOneHandedOptional = oneHandedOptional;
-        mShellTransitions = shellTransitions;
-        mRecentTasks = recentTasks;
-        mBackAnimation = backAnimation;
-        mDesktopModeOptional = desktopModeOptional;
         mUiEventLogger = uiEventLogger;
 
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
@@ -636,9 +572,6 @@
         });
         mCommandQueue = commandQueue;
 
-        mSplitScreenOptional = splitScreenOptional;
-        mFloatingTasksOptional = floatingTasksOptional;
-
         // Listen for user setup
         startTracking();
 
@@ -647,7 +580,6 @@
         // Connect to the service
         updateEnabledState();
         startConnectionToCurrentUser();
-        mStartingSurface = startingSurface;
         mSysuiUnlockAnimationController = sysuiUnlockAnimationController;
 
         // Listen for assistant changes
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt
similarity index 80%
rename from packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt
index 4d53064..ce730ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt
@@ -20,14 +20,16 @@
 import android.widget.FrameLayout
 
 /**
- * A temporary base class that's shared between our old status bar wifi view implementation
- * ([StatusBarWifiView]) and our new status bar wifi view implementation
- * ([ModernStatusBarWifiView]).
+ * A temporary base class that's shared between our old status bar connectivity view implementations
+ * ([StatusBarWifiView], [StatusBarMobileView]) and our new status bar implementations (
+ * [ModernStatusBarWifiView], [ModernStatusBarMobileView]).
  *
  * Once our refactor is over, we should be able to delete this go-between class and the old view
  * class.
  */
-abstract class BaseStatusBarWifiView @JvmOverloads constructor(
+abstract class BaseStatusBarFrameLayout
+@JvmOverloads
+constructor(
     context: Context,
     attrs: AttributeSet? = null,
     defStyleAttrs: Int = 0,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 8699441..c290ce2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -42,7 +42,6 @@
 
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.internal.widget.LockPatternUtils;
-import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
@@ -97,6 +96,7 @@
     private final List<UserChangedListener> mListeners = new ArrayList<>();
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final NotificationClickNotifier mClickNotifier;
+    private final Lazy<OverviewProxyService> mOverviewProxyServiceLazy;
 
     private boolean mShowLockscreenNotifications;
     private boolean mAllowLockscreenRemoteInput;
@@ -157,7 +157,7 @@
                     break;
                 case Intent.ACTION_USER_UNLOCKED:
                     // Start the overview connection to the launcher service
-                    Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser();
+                    mOverviewProxyServiceLazy.get().startConnectionToCurrentUser();
                     break;
                 case NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION:
                     final IntentSender intentSender = intent.getParcelableExtra(
@@ -199,6 +199,7 @@
             Lazy<NotificationVisibilityProvider> visibilityProviderLazy,
             Lazy<CommonNotifCollection> commonNotifCollectionLazy,
             NotificationClickNotifier clickNotifier,
+            Lazy<OverviewProxyService> overviewProxyServiceLazy,
             KeyguardManager keyguardManager,
             StatusBarStateController statusBarStateController,
             @Main Handler mainHandler,
@@ -214,6 +215,7 @@
         mVisibilityProviderLazy = visibilityProviderLazy;
         mCommonNotifCollectionLazy = commonNotifCollectionLazy;
         mClickNotifier = clickNotifier;
+        mOverviewProxyServiceLazy = overviewProxyServiceLazy;
         statusBarStateController.addCallback(this);
         mLockPatternUtils = new LockPatternUtils(context);
         mKeyguardManager = keyguardManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index c900c5a..4be5a1a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -43,7 +43,6 @@
 import android.view.View;
 import android.widget.ImageView;
 
-import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -89,11 +88,9 @@
     private static final String TAG = "NotificationMediaManager";
     public static final boolean DEBUG_MEDIA = false;
 
-    private final StatusBarStateController mStatusBarStateController
-            = Dependency.get(StatusBarStateController.class);
-    private final SysuiColorExtractor mColorExtractor = Dependency.get(SysuiColorExtractor.class);
-    private final KeyguardStateController mKeyguardStateController = Dependency.get(
-            KeyguardStateController.class);
+    private final StatusBarStateController mStatusBarStateController;
+    private final SysuiColorExtractor mColorExtractor;
+    private final KeyguardStateController mKeyguardStateController;
     private final KeyguardBypassController mKeyguardBypassController;
     private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>();
     private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>();
@@ -179,6 +176,9 @@
             NotifCollection notifCollection,
             @Main DelayableExecutor mainExecutor,
             MediaDataManager mediaDataManager,
+            StatusBarStateController statusBarStateController,
+            SysuiColorExtractor colorExtractor,
+            KeyguardStateController keyguardStateController,
             DumpManager dumpManager) {
         mContext = context;
         mMediaArtworkProcessor = mediaArtworkProcessor;
@@ -192,6 +192,9 @@
         mMediaDataManager = mediaDataManager;
         mNotifPipeline = notifPipeline;
         mNotifCollection = notifCollection;
+        mStatusBarStateController = statusBarStateController;
+        mColorExtractor = colorExtractor;
+        mKeyguardStateController = keyguardStateController;
 
         setupNotifPipeline();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
index 48c6e27..fdad101 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
@@ -29,7 +29,6 @@
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
@@ -43,7 +42,10 @@
 
 import java.util.ArrayList;
 
-public class StatusBarMobileView extends FrameLayout implements DarkReceiver,
+/**
+ * View group for the mobile icon in the status bar
+ */
+public class StatusBarMobileView extends BaseStatusBarFrameLayout implements DarkReceiver,
         StatusIconDisplayable {
     private static final String TAG = "StatusBarMobileView";
 
@@ -101,11 +103,6 @@
         super(context, attrs, defStyleAttr);
     }
 
-    public StatusBarMobileView(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-    }
-
     @Override
     public void getDrawingRect(Rect outRect) {
         super.getDrawingRect(outRect);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
index f3e74d9..decc70d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
@@ -40,7 +40,7 @@
 /**
  * Start small: StatusBarWifiView will be able to layout from a WifiIconState
  */
-public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkReceiver {
+public class StatusBarWifiView extends BaseStatusBarFrameLayout implements DarkReceiver {
     private static final String TAG = "StatusBarWifiView";
 
     /// Used to show etc dots
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 7cd79ca..11e3d17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -26,6 +26,7 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
@@ -130,6 +131,9 @@
             NotifCollection notifCollection,
             @Main DelayableExecutor mainExecutor,
             MediaDataManager mediaDataManager,
+            StatusBarStateController statusBarStateController,
+            SysuiColorExtractor colorExtractor,
+            KeyguardStateController keyguardStateController,
             DumpManager dumpManager) {
         return new NotificationMediaManager(
                 context,
@@ -142,6 +146,9 @@
                 notifCollection,
                 mainExecutor,
                 mediaDataManager,
+                statusBarStateController,
+                colorExtractor,
+                keyguardStateController,
                 dumpManager);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 1aa0295..8e646a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -19,6 +19,8 @@
 import android.service.notification.StatusBarNotification
 import com.android.systemui.ForegroundServiceNotificationListener
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
 import com.android.systemui.statusbar.NotificationListener
@@ -36,6 +38,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
+import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor
 import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
 import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -68,6 +71,8 @@
     private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
     private val bubblesOptional: Optional<Bubbles>,
     private val fgsNotifListener: ForegroundServiceNotificationListener,
+    private val memoryMonitor: Lazy<NotificationMemoryMonitor>,
+    private val featureFlags: FeatureFlags
 ) : NotificationsController {
 
     override fun initialize(
@@ -107,6 +112,9 @@
 
         peopleSpaceWidgetManager.attach(notificationListener)
         fgsNotifListener.init()
+        if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_MONITOR_ENABLED)) {
+            memoryMonitor.get().init()
+        }
     }
 
     // TODO: Convert all functions below this line into listeners instead of public methods
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
new file mode 100644
index 0000000..832a739
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
@@ -0,0 +1,41 @@
+/*
+ *
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.logging
+
+/** Describes usage of a notification. */
+data class NotificationMemoryUsage(
+    val packageName: String,
+    val notificationId: String,
+    val objectUsage: NotificationObjectUsage,
+)
+
+/**
+ * Describes current memory usage of a [android.app.Notification] object.
+ *
+ * The values are in bytes.
+ */
+data class NotificationObjectUsage(
+    val smallIcon: Int,
+    val largeIcon: Int,
+    val extras: Int,
+    val style: String?,
+    val styleIcon: Int,
+    val bigPicture: Int,
+    val extender: Int,
+    val hasCustomView: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
new file mode 100644
index 0000000..958978e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
@@ -0,0 +1,243 @@
+/*
+ *
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.Person
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.Log
+import androidx.annotation.WorkerThread
+import androidx.core.util.contains
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/** This class monitors and logs current Notification memory use. */
+@SysUISingleton
+class NotificationMemoryMonitor
+@Inject
+constructor(
+    val notificationPipeline: NotifPipeline,
+    val dumpManager: DumpManager,
+) : Dumpable {
+
+    companion object {
+        private const val TAG = "NotificationMemMonitor"
+        private const val CAR_EXTENSIONS = "android.car.EXTENSIONS"
+        private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon"
+        private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
+        private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
+        private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
+    }
+
+    fun init() {
+        Log.d(TAG, "NotificationMemoryMonitor initialized.")
+        dumpManager.registerDumpable(javaClass.simpleName, this)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) }
+    }
+
+    @WorkerThread
+    fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> {
+        return notificationMemoryUse(notificationPipeline.allNotifs)
+    }
+
+    /** Returns a list of memory use entries for currently shown notifications. */
+    @WorkerThread
+    fun notificationMemoryUse(
+        notifications: Collection<NotificationEntry>
+    ): List<NotificationMemoryUsage> {
+        return notifications
+            .asSequence()
+            .map { entry ->
+                val packageName = entry.sbn.packageName
+                val notificationObjectUsage =
+                    computeNotificationObjectUse(entry.sbn.notification, hashSetOf())
+                NotificationMemoryUsage(
+                    packageName,
+                    NotificationUtils.logKey(entry.sbn.key),
+                    notificationObjectUsage
+                )
+            }
+            .toList()
+    }
+
+    /**
+     * Computes the estimated memory usage of a given [Notification] object. It'll attempt to
+     * inspect Bitmaps in the object and provide summary of memory usage.
+     */
+    private fun computeNotificationObjectUse(
+        notification: Notification,
+        seenBitmaps: HashSet<Int>
+    ): NotificationObjectUsage {
+        val extras = notification.extras
+        val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps)
+        val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps)
+
+        // Collect memory usage of extra styles
+
+        // Big Picture
+        val bigPictureIconUse =
+            computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) +
+                computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps)
+        val bigPictureUse =
+            computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) +
+                computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps)
+
+        // People
+        val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST)
+        val peopleUse =
+            peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0
+
+        // Calling
+        val callingPersonUse =
+            computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps)
+        val verificationIconUse =
+            computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps)
+
+        // Messages
+        val messages =
+            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                extras.getParcelableArray(Notification.EXTRA_MESSAGES)
+            )
+        val messagesUse =
+            messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+        val historicMessages =
+            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
+            )
+        val historyicMessagesUse =
+            historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+
+        // Extenders
+        val carExtender = extras.getBundle(CAR_EXTENSIONS)
+        val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0
+        val carExtenderIcon =
+            computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps)
+
+        val tvExtender = extras.getBundle(TV_EXTENSIONS)
+        val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0
+
+        val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS)
+        val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0
+        val wearExtenderBackground =
+            computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
+
+        val style = notification.notificationStyle
+        val hasCustomView = notification.contentView != null || notification.bigContentView != null
+        val extrasSize = computeBundleSize(extras)
+
+        return NotificationObjectUsage(
+            smallIconUse,
+            largeIconUse,
+            extrasSize,
+            style?.simpleName,
+            bigPictureIconUse +
+                peopleUse +
+                callingPersonUse +
+                verificationIconUse +
+                messagesUse +
+                historyicMessagesUse,
+            bigPictureUse,
+            carExtenderSize +
+                carExtenderIcon +
+                tvExtenderSize +
+                wearExtenderSize +
+                wearExtenderBackground,
+            hasCustomView
+        )
+    }
+
+    /**
+     * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
+     * bitmaps). Can be slow.
+     */
+    private fun computeBundleSize(extras: Bundle): Int {
+        val parcel = Parcel.obtain()
+        try {
+            extras.writeToParcel(parcel, 0)
+            return parcel.dataSize()
+        } finally {
+            parcel.recycle()
+        }
+    }
+
+    /**
+     * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0
+     * if the key does not exist in extras.
+     */
+    private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int {
+        return when (val parcelable = extras?.getParcelable<Parcelable>(key)) {
+            is Bitmap -> computeBitmapUse(parcelable, seenBitmaps)
+            is Icon -> computeIconUse(parcelable, seenBitmaps)
+            is Person -> computeIconUse(parcelable.icon, seenBitmaps)
+            else -> 0
+        }
+    }
+
+    /**
+     * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is
+     * defined via Uri or a resource.
+     *
+     * @return memory usage in bytes or 0 if the icon is Uri/Resource based
+     */
+    private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) =
+        when (icon?.type) {
+            Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+            Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+            Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps)
+            else -> 0
+        }
+
+    /**
+     * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of
+     * seenBitmaps set, this method returns 0 to avoid double counting.
+     *
+     * @return memory usage of the bitmap in bytes
+     */
+    private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int {
+        val refId = System.identityHashCode(bitmap)
+        if (seenBitmaps?.contains(refId) == true) {
+            return 0
+        }
+
+        seenBitmaps?.add(refId)
+        return bitmap.allocationByteCount
+    }
+
+    private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int {
+        val refId = System.identityHashCode(icon.dataBytes)
+        if (seenBitmaps.contains(refId)) {
+            return 0
+        }
+
+        seenBitmaps.add(refId)
+        return icon.dataLength
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index df81c0e..8de0365 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1986,6 +1986,25 @@
     public void setRemoteInputVisible(boolean remoteInputVisible) {
         mRemoteInputVisible = remoteInputVisible;
         setClipChildren(!remoteInputVisible);
+        setActionsImportanceForAccessibility(
+                remoteInputVisible ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                        : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+    }
+
+    private void setActionsImportanceForAccessibility(int mode) {
+        if (mExpandedChild != null) {
+            setActionsImportanceForAccessibility(mode, mExpandedChild);
+        }
+        if (mHeadsUpChild != null) {
+            setActionsImportanceForAccessibility(mode, mHeadsUpChild);
+        }
+    }
+
+    private void setActionsImportanceForAccessibility(int mode, View child) {
+        View actionsCandidate = child.findViewById(com.android.internal.R.id.actions);
+        if (actionsCandidate != null) {
+            actionsCandidate.setImportantForAccessibility(mode);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index 54d39fd..de7b152 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -93,13 +93,13 @@
     private boolean mControlScreenOffAnimation;
     private boolean mIsQuickPickupEnabled;
 
-    private boolean mKeyguardShowing;
+    private boolean mKeyguardVisible;
     @VisibleForTesting
     final KeyguardUpdateMonitorCallback mKeyguardVisibilityCallback =
             new KeyguardUpdateMonitorCallback() {
                 @Override
-                public void onKeyguardVisibilityChanged(boolean showing) {
-                    mKeyguardShowing = showing;
+                public void onKeyguardVisibilityChanged(boolean visible) {
+                    mKeyguardVisible = visible;
                     updateControlScreenOff();
                 }
 
@@ -293,7 +293,7 @@
     public void updateControlScreenOff() {
         if (!getDisplayNeedsBlanking()) {
             final boolean controlScreenOff =
-                    getAlwaysOn() && (mKeyguardShowing || shouldControlUnlockedScreenOff());
+                    getAlwaysOn() && (mKeyguardVisible || shouldControlUnlockedScreenOff());
             setControlScreenOffAnimation(controlScreenOff);
         }
     }
@@ -348,7 +348,7 @@
     }
 
     private boolean willAnimateFromLockScreenToAod() {
-        return getAlwaysOn() && mKeyguardShowing;
+        return getAlwaysOn() && mKeyguardVisible;
     }
 
     private boolean getBoolean(String propName, int resId) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
index b58dbe2..e3e8572 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -88,7 +88,7 @@
             updateListeningState()
         }
 
-        override fun onKeyguardVisibilityChanged(showing: Boolean) {
+        override fun onKeyguardVisibilityChanged(visible: Boolean) {
             updateListeningState()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 054bd28..14cebf4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -187,8 +187,8 @@
                 }
 
                 @Override
-                public void onKeyguardVisibilityChanged(boolean showing) {
-                    if (showing) {
+                public void onKeyguardVisibilityChanged(boolean visible) {
+                    if (visible) {
                         updateUserSwitcher();
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 4d1c361..9f93223 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -1534,7 +1534,7 @@
     private class KeyguardVisibilityCallback extends KeyguardUpdateMonitorCallback {
 
         @Override
-        public void onKeyguardVisibilityChanged(boolean showing) {
+        public void onKeyguardVisibilityChanged(boolean visible) {
             mNeedsDrawableColorUpdate = true;
             scheduleUpdate();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index d6d021f..ece7ee0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -16,6 +16,7 @@
 
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE;
+import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI;
 
 import android.annotation.Nullable;
@@ -38,7 +39,7 @@
 import com.android.systemui.demomode.DemoModeCommandReceiver;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.statusbar.BaseStatusBarWifiView;
+import com.android.systemui.statusbar.BaseStatusBarFrameLayout;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusBarMobileView;
 import com.android.systemui.statusbar.StatusBarWifiView;
@@ -48,6 +49,10 @@
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
+import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
 import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
 import com.android.systemui.util.Assert;
@@ -84,6 +89,12 @@
     void setMobileIcons(String slot, List<MobileIconState> states);
 
     /**
+     * This method completely replaces {@link #setMobileIcons} with the information from the new
+     * mobile data pipeline. Icons will automatically keep their state up to date, so we don't have
+     * to worry about funneling MobileIconState objects through anymore.
+     */
+    void setNewMobileIconSubIds(List<Integer> subIds);
+    /**
      * Display the no calling & SMS icons.
      */
     void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states);
@@ -141,12 +152,14 @@
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
                 WifiViewModel wifiViewModel,
+                MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider mobileContextProvider,
                 DarkIconDispatcher darkIconDispatcher) {
             super(linearLayout,
                     location,
                     statusBarPipelineFlags,
                     wifiViewModel,
+                    mobileUiAdapter,
                     mobileContextProvider);
             mIconHPadding = mContext.getResources().getDimensionPixelSize(
                     R.dimen.status_bar_icon_padding);
@@ -207,6 +220,7 @@
             private final StatusBarPipelineFlags mStatusBarPipelineFlags;
             private final WifiViewModel mWifiViewModel;
             private final MobileContextProvider mMobileContextProvider;
+            private final MobileUiAdapter mMobileUiAdapter;
             private final DarkIconDispatcher mDarkIconDispatcher;
 
             @Inject
@@ -214,10 +228,12 @@
                     StatusBarPipelineFlags statusBarPipelineFlags,
                     WifiViewModel wifiViewModel,
                     MobileContextProvider mobileContextProvider,
+                    MobileUiAdapter mobileUiAdapter,
                     DarkIconDispatcher darkIconDispatcher) {
                 mStatusBarPipelineFlags = statusBarPipelineFlags;
                 mWifiViewModel = wifiViewModel;
                 mMobileContextProvider = mobileContextProvider;
+                mMobileUiAdapter = mobileUiAdapter;
                 mDarkIconDispatcher = darkIconDispatcher;
             }
 
@@ -227,6 +243,7 @@
                         location,
                         mStatusBarPipelineFlags,
                         mWifiViewModel,
+                        mMobileUiAdapter,
                         mMobileContextProvider,
                         mDarkIconDispatcher);
             }
@@ -244,11 +261,14 @@
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
                 WifiViewModel wifiViewModel,
-                MobileContextProvider mobileContextProvider) {
+                MobileUiAdapter mobileUiAdapter,
+                MobileContextProvider mobileContextProvider
+        ) {
             super(group,
                     location,
                     statusBarPipelineFlags,
                     wifiViewModel,
+                    mobileUiAdapter,
                     mobileContextProvider);
         }
 
@@ -284,14 +304,18 @@
             private final StatusBarPipelineFlags mStatusBarPipelineFlags;
             private final WifiViewModel mWifiViewModel;
             private final MobileContextProvider mMobileContextProvider;
+            private final MobileUiAdapter mMobileUiAdapter;
 
             @Inject
             public Factory(
                     StatusBarPipelineFlags statusBarPipelineFlags,
                     WifiViewModel wifiViewModel,
-                    MobileContextProvider mobileContextProvider) {
+                    MobileUiAdapter mobileUiAdapter,
+                    MobileContextProvider mobileContextProvider
+            ) {
                 mStatusBarPipelineFlags = statusBarPipelineFlags;
                 mWifiViewModel = wifiViewModel;
+                mMobileUiAdapter = mobileUiAdapter;
                 mMobileContextProvider = mobileContextProvider;
             }
 
@@ -301,6 +325,7 @@
                         location,
                         mStatusBarPipelineFlags,
                         mWifiViewModel,
+                        mMobileUiAdapter,
                         mMobileContextProvider);
             }
         }
@@ -315,6 +340,8 @@
         private final StatusBarPipelineFlags mStatusBarPipelineFlags;
         private final WifiViewModel mWifiViewModel;
         private final MobileContextProvider mMobileContextProvider;
+        private final MobileIconsViewModel mMobileIconsViewModel;
+
         protected final Context mContext;
         protected final int mIconSize;
         // Whether or not these icons show up in dumpsys
@@ -333,7 +360,9 @@
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
                 WifiViewModel wifiViewModel,
-                MobileContextProvider mobileContextProvider) {
+                MobileUiAdapter mobileUiAdapter,
+                MobileContextProvider mobileContextProvider
+        ) {
             mGroup = group;
             mLocation = location;
             mStatusBarPipelineFlags = statusBarPipelineFlags;
@@ -342,6 +371,14 @@
             mContext = group.getContext();
             mIconSize = mContext.getResources().getDimensionPixelSize(
                     com.android.internal.R.dimen.status_bar_icon_size);
+
+            if (statusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+                // This starts the flow for the new pipeline, and will notify us of changes
+                mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel();
+                MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
+            } else {
+                mMobileIconsViewModel = null;
+            }
         }
 
         public boolean isDemoable() {
@@ -394,6 +431,9 @@
 
                 case TYPE_MOBILE:
                     return addMobileIcon(index, slot, holder.getMobileState());
+
+                case TYPE_MOBILE_NEW:
+                    return addNewMobileIcon(index, slot, holder.getTag());
             }
 
             return null;
@@ -410,7 +450,7 @@
 
         @VisibleForTesting
         protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) {
-            final BaseStatusBarWifiView view;
+            final BaseStatusBarFrameLayout view;
             if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
                 view = onCreateModernStatusBarWifiView(slot);
                 // When [ModernStatusBarWifiView] is created, it will automatically apply the
@@ -429,17 +469,47 @@
         }
 
         @VisibleForTesting
-        protected StatusBarMobileView addMobileIcon(int index, String slot, MobileIconState state) {
+        protected StatusIconDisplayable addMobileIcon(
+                int index,
+                String slot,
+                MobileIconState state
+        ) {
+            if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+                throw new IllegalStateException("Attempting to add a mobile icon while the new "
+                        + "pipeline is enabled is not supported");
+            }
+
             // Use the `subId` field as a key to query for the correct context
-            StatusBarMobileView view = onCreateStatusBarMobileView(state.subId, slot);
-            view.applyMobileState(state);
-            mGroup.addView(view, index, onCreateLayoutParams());
+            StatusBarMobileView mobileView = onCreateStatusBarMobileView(state.subId, slot);
+            mobileView.applyMobileState(state);
+            mGroup.addView(mobileView, index, onCreateLayoutParams());
 
             if (mIsInDemoMode) {
                 Context mobileContext = mMobileContextProvider
                         .getMobileContextForSub(state.subId, mContext);
                 mDemoStatusIcons.addMobileView(state, mobileContext);
             }
+            return mobileView;
+        }
+
+        protected StatusIconDisplayable addNewMobileIcon(
+                int index,
+                String slot,
+                int subId
+        ) {
+            if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+                throw new IllegalStateException("Attempting to add a mobile icon using the new"
+                        + "pipeline, but the enabled flag is false.");
+            }
+
+            BaseStatusBarFrameLayout view = onCreateModernStatusBarMobileView(slot, subId);
+            mGroup.addView(view, index, onCreateLayoutParams());
+
+            if (mIsInDemoMode) {
+                // TODO (b/249790009): demo mode should be handled at the data layer in the
+                //  new pipeline
+            }
+
             return view;
         }
 
@@ -464,6 +534,15 @@
             return view;
         }
 
+        private ModernStatusBarMobileView onCreateModernStatusBarMobileView(
+                String slot, int subId) {
+            return ModernStatusBarMobileView
+                    .constructAndBind(
+                            mContext,
+                            slot,
+                            mMobileIconsViewModel.viewModelForSub(subId));
+        }
+
         protected LinearLayout.LayoutParams onCreateLayoutParams() {
             return new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize);
         }
@@ -519,6 +598,10 @@
                     return;
                 case TYPE_MOBILE:
                     onSetMobileIcon(viewIndex, holder.getMobileState());
+                    return;
+                case TYPE_MOBILE_NEW:
+                    // Nothing, the icon updates itself now
+                    return;
                 default:
                     break;
             }
@@ -542,9 +625,13 @@
         }
 
         public void onSetMobileIcon(int viewIndex, MobileIconState state) {
-            StatusBarMobileView view = (StatusBarMobileView) mGroup.getChildAt(viewIndex);
-            if (view != null) {
-                view.applyMobileState(state);
+            View view = mGroup.getChildAt(viewIndex);
+            if (view instanceof StatusBarMobileView) {
+                ((StatusBarMobileView) view).applyMobileState(state);
+            } else {
+                // ModernStatusBarMobileView automatically updates via the ViewModel
+                throw new IllegalStateException("Cannot update ModernStatusBarMobileView outside of"
+                        + "the new pipeline");
             }
 
             if (mIsInDemoMode) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index 7c31366..e106b9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -40,6 +40,7 @@
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.tuner.TunerService;
@@ -66,8 +67,8 @@
     private final StatusBarIconList mStatusBarIconList;
     private final ArrayList<IconManager> mIconGroups = new ArrayList<>();
     private final ArraySet<String> mIconHideList = new ArraySet<>();
-
-    private Context mContext;
+    private final StatusBarPipelineFlags mStatusBarPipelineFlags;
+    private final Context mContext;
 
     /** */
     @Inject
@@ -78,9 +79,12 @@
             ConfigurationController configurationController,
             TunerService tunerService,
             DumpManager dumpManager,
-            StatusBarIconList statusBarIconList) {
+            StatusBarIconList statusBarIconList,
+            StatusBarPipelineFlags statusBarPipelineFlags
+    ) {
         mStatusBarIconList = statusBarIconList;
         mContext = context;
+        mStatusBarPipelineFlags = statusBarPipelineFlags;
 
         configurationController.addCallback(this);
         commandQueue.addCallback(this);
@@ -220,6 +224,11 @@
      */
     @Override
     public void setMobileIcons(String slot, List<MobileIconState> iconStates) {
+        if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            Log.d(TAG, "ignoring old pipeline callbacks, because the new "
+                    + "pipeline frontend is enabled");
+            return;
+        }
         Slot mobileSlot = mStatusBarIconList.getSlot(slot);
 
         // Reverse the sort order to show icons with left to right([Slot1][Slot2]..).
@@ -227,7 +236,6 @@
         Collections.reverse(iconStates);
 
         for (MobileIconState state : iconStates) {
-
             StatusBarIconHolder holder = mobileSlot.getHolderForTag(state.subId);
             if (holder == null) {
                 holder = StatusBarIconHolder.fromMobileIconState(state);
@@ -239,6 +247,28 @@
         }
     }
 
+    @Override
+    public void setNewMobileIconSubIds(List<Integer> subIds) {
+        if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            Log.d(TAG, "ignoring new pipeline callback, "
+                    + "since the frontend is disabled");
+            return;
+        }
+        Slot mobileSlot = mStatusBarIconList.getSlot("mobile");
+
+        Collections.reverse(subIds);
+
+        for (Integer subId : subIds) {
+            StatusBarIconHolder holder = mobileSlot.getHolderForTag(subId);
+            if (holder == null) {
+                holder = StatusBarIconHolder.fromSubIdForModernMobileIcon(subId);
+                setIcon("mobile", holder);
+            } else {
+                // Don't have to do anything in the new world
+            }
+        }
+    }
+
     /**
      * Accept a list of CallIndicatorIconStates, and show the call strength icons.
      * @param slot statusbar slot for the call strength icons
@@ -384,8 +414,6 @@
         }
     }
 
-
-
     private void handleSet(String slotName, StatusBarIconHolder holder) {
         int viewIndex = mStatusBarIconList.getViewIndex(slotName, holder.getTag());
         mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
index af342dd..68a203e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.drawable.Icon;
@@ -25,6 +26,10 @@
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * Wraps {@link com.android.internal.statusbar.StatusBarIcon} so we can still have a uniform list
@@ -33,15 +38,35 @@
     public static final int TYPE_ICON = 0;
     public static final int TYPE_WIFI = 1;
     public static final int TYPE_MOBILE = 2;
+    /**
+     * TODO (b/249790733): address this once the new pipeline is in place
+     * This type exists so that the new pipeline (see {@link MobileIconViewModel}) can be used
+     * to inform the old view system about changes to the data set (the list of mobile icons). The
+     * design of the new pipeline should allow for removal of this icon holder type, and obsolete
+     * the need for this entire class.
+     *
+     * @deprecated This field only exists so the new status bar pipeline can interface with the
+     * view holder system.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE_NEW = 3;
+
+    @IntDef({
+            TYPE_ICON,
+            TYPE_WIFI,
+            TYPE_MOBILE,
+            TYPE_MOBILE_NEW
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface IconType {}
 
     private StatusBarIcon mIcon;
     private WifiIconState mWifiState;
     private MobileIconState mMobileState;
-    private int mType = TYPE_ICON;
+    private @IconType int mType = TYPE_ICON;
     private int mTag = 0;
 
     private StatusBarIconHolder() {
-
     }
 
     public static StatusBarIconHolder fromIcon(StatusBarIcon icon) {
@@ -80,6 +105,18 @@
     }
 
     /**
+     * ONLY for use with the new connectivity pipeline, where we only need a subscriptionID to
+     * determine icon ordering and building the correct view model
+     */
+    public static StatusBarIconHolder fromSubIdForModernMobileIcon(int subId) {
+        StatusBarIconHolder holder = new StatusBarIconHolder();
+        holder.mType = TYPE_MOBILE_NEW;
+        holder.mTag = subId;
+
+        return holder;
+    }
+
+    /**
      * Creates a new StatusBarIconHolder from a CallIndicatorIconState.
      */
     public static StatusBarIconHolder fromCallIndicatorState(
@@ -95,7 +132,7 @@
         return holder;
     }
 
-    public int getType() {
+    public @IconType int getType() {
         return mType;
     }
 
@@ -134,8 +171,12 @@
                 return mWifiState.visible;
             case TYPE_MOBILE:
                 return mMobileState.visible;
+            case TYPE_MOBILE_NEW:
+                //TODO (b/249790733), the new pipeline can control visibility via the ViewModel
+                return true;
 
-            default: return true;
+            default:
+                return true;
         }
     }
 
@@ -156,6 +197,10 @@
             case TYPE_MOBILE:
                 mMobileState.visible = visible;
                 break;
+
+            case TYPE_MOBILE_NEW:
+                //TODO (b/249790733), the new pipeline can control visibility via the ViewModel
+                break;
         }
     }
 
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 a0578fa..c09e2b6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -733,11 +733,6 @@
             if (dozing || mBouncer.needsFullscreenBouncer() || mOccluded) {
                 reset(dozing /* hideBouncerWhenShowing */);
             }
-
-            if (bouncerIsOrWillBeShowing()) {
-                // Ensure bouncer is not shown when dozing state changes.
-                hideBouncer(false);
-            }
             updateStates();
 
             if (!dozing) {
@@ -1131,8 +1126,8 @@
         if (occluded != mLastOccluded || mFirstUpdate) {
             mKeyguardStateController.notifyKeyguardState(showing, occluded);
         }
-        if ((showing && !occluded) != (mLastShowing && !mLastOccluded) || mFirstUpdate) {
-            mKeyguardUpdateManager.onKeyguardVisibilityChanged(showing && !occluded);
+        if (occluded != mLastOccluded || mShowing != showing || mFirstUpdate) {
+            mKeyguardUpdateManager.setKeyguardShowing(showing, occluded);
         }
         if (bouncerIsOrWillBeShowing != mLastBouncerIsOrWillBeShowing || mFirstUpdate
                 || bouncerShowing != mLastBouncerShowing) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 9a7c3fa..06d5542 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -16,6 +16,10 @@
 
 package com.android.systemui.statusbar.pipeline.dagger
 
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
@@ -30,4 +34,12 @@
 
     @Binds
     abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
+
+    @Binds
+    abstract fun mobileSubscriptionRepository(
+        impl: MobileSubscriptionRepositoryImpl
+    ): MobileSubscriptionRepository
+
+    @Binds
+    abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
new file mode 100644
index 0000000..46ccf32c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.model
+
+import android.annotation.IntRange
+import android.telephony.Annotation.DataActivityType
+import android.telephony.CellSignalStrength
+import android.telephony.TelephonyCallback.CarrierNetworkListener
+import android.telephony.TelephonyCallback.DataActivityListener
+import android.telephony.TelephonyCallback.DataConnectionStateListener
+import android.telephony.TelephonyCallback.DisplayInfoListener
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyCallback.SignalStrengthsListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyManager
+
+/**
+ * Data class containing all of the relevant information for a particular line of service, known as
+ * a Subscription in the telephony world. These models are the result of a single telephony listener
+ * which has many callbacks which each modify some particular field on this object.
+ *
+ * The design goal here is to de-normalize fields from the system into our model fields below. So
+ * any new field that needs to be tracked should be copied into this data class rather than
+ * threading complex system objects through the pipeline.
+ */
+data class MobileSubscriptionModel(
+    /** From [ServiceStateListener.onServiceStateChanged] */
+    val isEmergencyOnly: Boolean = false,
+
+    /** From [SignalStrengthsListener.onSignalStrengthsChanged] */
+    val isGsm: Boolean = false,
+    @IntRange(from = 0, to = 4)
+    val cdmaLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
+    @IntRange(from = 0, to = 4)
+    val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
+
+    /** Comes directly from [DataConnectionStateListener.onDataConnectionStateChanged] */
+    val dataConnectionState: Int? = null,
+
+    /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */
+    @DataActivityType val dataActivityDirection: Int? = null,
+
+    /** From [CarrierNetworkListener.onCarrierNetworkChange] */
+    val carrierNetworkChangeActive: Boolean? = null,
+
+    /** From [DisplayInfoListener.onDisplayInfoChanged] */
+    val displayInfo: TelephonyDisplayInfo? = null
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
new file mode 100644
index 0000000..36de2a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.CellSignalStrength
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyCallback.CarrierNetworkListener
+import android.telephony.TelephonyCallback.DataActivityListener
+import android.telephony.TelephonyCallback.DataConnectionStateListener
+import android.telephony.TelephonyCallback.DisplayInfoListener
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyCallback.SignalStrengthsListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/**
+ * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
+ * on various policy
+ */
+interface MobileSubscriptionRepository {
+    /** Observable list of current mobile subscriptions */
+    val subscriptionsFlow: Flow<List<SubscriptionInfo>>
+
+    /** Observable for the subscriptionId of the current mobile data connection */
+    val activeMobileDataSubscriptionId: Flow<Int>
+
+    /** Get or create an observable for the given subscription ID */
+    fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel>
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MobileSubscriptionRepositoryImpl
+@Inject
+constructor(
+    private val subscriptionManager: SubscriptionManager,
+    private val telephonyManager: TelephonyManager,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Application private val scope: CoroutineScope,
+) : MobileSubscriptionRepository {
+    private val subIdFlowCache: MutableMap<Int, StateFlow<MobileSubscriptionModel>> = mutableMapOf()
+
+    /**
+     * State flow that emits the set of mobile data subscriptions, each represented by its own
+     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
+     * info object, but for now we keep track of the infos themselves.
+     */
+    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
+                        override fun onSubscriptionsChanged() {
+                            trySend(Unit)
+                        }
+                    }
+
+                subscriptionManager.addOnSubscriptionsChangedListener(
+                    bgDispatcher.asExecutor(),
+                    callback,
+                )
+
+                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+            }
+            .mapLatest { fetchSubscriptionsList() }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
+
+    /** StateFlow that keeps track of the current active mobile data subscription */
+    override val activeMobileDataSubscriptionId: StateFlow<Int> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
+                        override fun onActiveDataSubscriptionIdChanged(subId: Int) {
+                            trySend(subId)
+                        }
+                    }
+
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+            }
+            .stateIn(
+                scope,
+                started = SharingStarted.WhileSubscribed(),
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID
+            )
+
+    /**
+     * Each mobile subscription needs its own flow, which comes from registering listeners on the
+     * system. Use this method to create those flows and cache them for reuse
+     */
+    override fun getFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> {
+        return subIdFlowCache[subId]
+            ?: createFlowForSubId(subId).also { subIdFlowCache[subId] = it }
+    }
+
+    @VisibleForTesting fun getSubIdFlowCache() = subIdFlowCache
+
+    private fun createFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> = run {
+        var state = MobileSubscriptionModel()
+        conflatedCallbackFlow {
+                val phony = telephonyManager.createForSubscriptionId(subId)
+                // TODO (b/240569788): log all of these into the connectivity logger
+                val callback =
+                    object :
+                        TelephonyCallback(),
+                        ServiceStateListener,
+                        SignalStrengthsListener,
+                        DataConnectionStateListener,
+                        DataActivityListener,
+                        CarrierNetworkListener,
+                        DisplayInfoListener {
+                        override fun onServiceStateChanged(serviceState: ServiceState) {
+                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+                            trySend(state)
+                        }
+                        override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+                            val cdmaLevel =
+                                signalStrength
+                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
+                                    .let { strengths ->
+                                        if (!strengths.isEmpty()) {
+                                            strengths[0].level
+                                        } else {
+                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+                                        }
+                                    }
+
+                            val primaryLevel = signalStrength.level
+
+                            state =
+                                state.copy(
+                                    cdmaLevel = cdmaLevel,
+                                    primaryLevel = primaryLevel,
+                                    isGsm = signalStrength.isGsm,
+                                )
+                            trySend(state)
+                        }
+                        override fun onDataConnectionStateChanged(
+                            dataState: Int,
+                            networkType: Int
+                        ) {
+                            state = state.copy(dataConnectionState = dataState)
+                            trySend(state)
+                        }
+                        override fun onDataActivity(direction: Int) {
+                            state = state.copy(dataActivityDirection = direction)
+                            trySend(state)
+                        }
+                        override fun onCarrierNetworkChange(active: Boolean) {
+                            state = state.copy(carrierNetworkChangeActive = active)
+                            trySend(state)
+                        }
+                        override fun onDisplayInfoChanged(
+                            telephonyDisplayInfo: TelephonyDisplayInfo
+                        ) {
+                            state = state.copy(displayInfo = telephonyDisplayInfo)
+                            trySend(state)
+                        }
+                    }
+                phony.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose {
+                    phony.unregisterTelephonyCallback(callback)
+                    // Release the cached flow
+                    subIdFlowCache.remove(subId)
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
+    }
+
+    private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
+        withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
new file mode 100644
index 0000000..77de849
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/**
+ * Repository to observe the state of [DeviceProvisionedController.isUserSetup]. This information
+ * can change some policy related to display
+ */
+interface UserSetupRepository {
+    /** Observable tracking [DeviceProvisionedController.isUserSetup] */
+    val isUserSetupFlow: Flow<Boolean>
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class UserSetupRepositoryImpl
+@Inject
+constructor(
+    private val deviceProvisionedController: DeviceProvisionedController,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Application scope: CoroutineScope,
+) : UserSetupRepository {
+    /** State flow that tracks [DeviceProvisionedController.isUserSetup] */
+    override val isUserSetupFlow: StateFlow<Boolean> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : DeviceProvisionedController.DeviceProvisionedListener {
+                        override fun onUserSetupChanged() {
+                            trySend(Unit)
+                        }
+                    }
+
+                deviceProvisionedController.addCallback(callback)
+
+                awaitClose { deviceProvisionedController.removeCallback(callback) }
+            }
+            .onStart { emit(Unit) }
+            .mapLatest { fetchUserSetupState() }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+
+    private suspend fun fetchUserSetupState(): Boolean =
+        withContext(bgDispatcher) { deviceProvisionedController.isCurrentUserSetup }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
new file mode 100644
index 0000000..40fe0f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.CarrierConfigManager
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.util.CarrierConfigTracker
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+interface MobileIconInteractor {
+    /** Identifier for RAT type indicator */
+    val iconGroup: Flow<SignalIcon.MobileIconGroup>
+    /** True if this line of service is emergency-only */
+    val isEmergencyOnly: Flow<Boolean>
+    /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
+    val level: Flow<Int>
+    /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
+    val numberOfLevels: Flow<Int>
+    /** True when we want to draw an icon that makes room for the exclamation mark */
+    val cutOut: Flow<Boolean>
+}
+
+/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
+class MobileIconInteractorImpl(
+    mobileStatusInfo: Flow<MobileSubscriptionModel>,
+) : MobileIconInteractor {
+    override val iconGroup: Flow<SignalIcon.MobileIconGroup> = flowOf(TelephonyIcons.THREE_G)
+    override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly }
+
+    override val level: Flow<Int> =
+        mobileStatusInfo.map { mobileModel ->
+            // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi]
+            if (mobileModel.isGsm) {
+                mobileModel.primaryLevel
+            } else {
+                mobileModel.cdmaLevel
+            }
+        }
+
+    /**
+     * This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL]
+     * once it's wired up inside of [CarrierConfigTracker]
+     */
+    override val numberOfLevels: Flow<Int> = flowOf(4)
+
+    /** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */
+    // TODO: find a better name for this?
+    override val cutOut: Flow<Boolean> = flowOf(false)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
new file mode 100644
index 0000000..8e67e19
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.util.CarrierConfigTracker
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+
+/**
+ * Business layer logic for mobile subscription icons
+ *
+ * Mobile indicators represent the UI for the (potentially filtered) list of [SubscriptionInfo]s
+ * that the system knows about. They obey policy that depends on OEM, carrier, and locale configs
+ */
+@SysUISingleton
+class MobileIconsInteractor
+@Inject
+constructor(
+    private val mobileSubscriptionRepo: MobileSubscriptionRepository,
+    private val carrierConfigTracker: CarrierConfigTracker,
+    userSetupRepo: UserSetupRepository,
+) {
+    private val activeMobileDataSubscriptionId =
+        mobileSubscriptionRepo.activeMobileDataSubscriptionId
+
+    private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> =
+        mobileSubscriptionRepo.subscriptionsFlow
+
+    /**
+     * Generally, SystemUI wants to show iconography for each subscription that is listed by
+     * [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only
+     * show a single representation of the pair of subscriptions. The docs define opportunistic as:
+     *
+     * "A subscription is opportunistic (if) the network it connects to has limited coverage"
+     * https://developer.android.com/reference/android/telephony/SubscriptionManager#setOpportunistic(boolean,%20int)
+     *
+     * In the case of opportunistic networks (typically CBRS), we will filter out one of the
+     * subscriptions based on
+     * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
+     * and by checking which subscription is opportunistic, or which one is active.
+     */
+    val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
+        combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId
+            ->
+            // Based on the old logic,
+            if (unfilteredSubs.size != 2) {
+                return@combine unfilteredSubs
+            }
+
+            val info1 = unfilteredSubs[0]
+            val info2 = unfilteredSubs[1]
+            // If both subscriptions are primary, show both
+            if (!info1.isOpportunistic && !info2.isOpportunistic) {
+                return@combine unfilteredSubs
+            }
+
+            // NOTE: at this point, we are now returning a single SubscriptionInfo
+
+            // If carrier required, always show the icon of the primary subscription.
+            // Otherwise, show whichever subscription is currently active for internet.
+            if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) {
+                // return the non-opportunistic info
+                return@combine if (info1.isOpportunistic) listOf(info2) else listOf(info1)
+            } else {
+                return@combine if (info1.subscriptionId == activeId) {
+                    listOf(info1)
+                } else {
+                    listOf(info2)
+                }
+            }
+        }
+
+    val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
+
+    /** Vends out new [MobileIconInteractor] for a particular subId */
+    fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
+        MobileIconInteractorImpl(mobileSubscriptionFlowForSubId(subId))
+
+    /**
+     * Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections
+     */
+    private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> =
+        mobileSubscriptionRepo.getFlowForSubId(subId)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
new file mode 100644
index 0000000..380017c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.phone.StatusBarIconController
+import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * This class is intended to provide a context to collect on the
+ * [MobileIconsInteractor.filteredSubscriptions] data source and supply a state flow that can
+ * control [StatusBarIconController] to keep the old UI in sync with the new data source.
+ *
+ * It also provides a mechanism to create a top-level view model for each IconManager to know about
+ * the list of available mobile lines of service for which we want to show icons.
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MobileUiAdapter
+@Inject
+constructor(
+    interactor: MobileIconsInteractor,
+    private val iconController: StatusBarIconController,
+    private val iconsViewModelFactory: MobileIconsViewModel.Factory,
+    @Application scope: CoroutineScope,
+) {
+    private val mobileSubIds: Flow<List<Int>> =
+        interactor.filteredSubscriptions.mapLatest { infos ->
+            infos.map { subscriptionInfo -> subscriptionInfo.subscriptionId }
+        }
+
+    /**
+     * We expose the list of tracked subscriptions as a flow of a list of ints, where each int is
+     * the subscriptionId of the relevant subscriptions. These act as a key into the layouts which
+     * house the mobile infos.
+     *
+     * NOTE: this should go away as the view presenter learns more about this data pipeline
+     */
+    private val mobileSubIdsState: StateFlow<List<Int>> =
+        mobileSubIds
+            .onEach {
+                // Notify the icon controller here so that it knows to add icons
+                iconController.setNewMobileIconSubIds(it)
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+
+    /**
+     * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's
+     * lifecycle. This will start collecting on [mobileSubIdsState] and link our new pipeline with
+     * the old view system.
+     */
+    fun createMobileIconsViewModel(): MobileIconsViewModel =
+        iconsViewModelFactory.create(mobileSubIdsState)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
new file mode 100644
index 0000000..1405b05
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.binder
+
+import android.content.res.ColorStateList
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.R
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
+
+object MobileIconBinder {
+    /** Binds the view to the view-model, continuing to update the former based on the latter */
+    @JvmStatic
+    fun bind(
+        view: ViewGroup,
+        viewModel: MobileIconViewModel,
+    ) {
+        val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
+        val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
+
+        view.isVisible = true
+        iconView.isVisible = true
+
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // Set the icon for the triangle
+                launch {
+                    viewModel.iconId.distinctUntilChanged().collect { iconId ->
+                        mobileDrawable.level = iconId
+                    }
+                }
+
+                // Set the tint
+                launch {
+                    viewModel.tint.collect { tint ->
+                        iconView.imageTintList = ColorStateList.valueOf(tint)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt
new file mode 100644
index 0000000..e7d5ee2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.binder
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+object MobileIconsBinder {
+    /**
+     * Start this ViewModel collecting on the list of mobile subscriptions in the scope of [view]
+     * which is passed in and managed by [IconManager]. Once the subscription list flow starts
+     * collecting, [MobileUiAdapter] will send updates to the icon manager.
+     */
+    @JvmStatic
+    fun bind(view: View, viewModel: MobileIconsViewModel) {
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.subscriptionIdsFlow.collect {
+                        // TODO(b/249790733): This is an empty collect, because [MobileUiAdapter]
+                        //  sets up a side-effect in this flow to trigger the methods on
+                        // [StatusBarIconController] which allows for this pipeline to be a data
+                        // source for the mobile icons.
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
new file mode 100644
index 0000000..ec4fa9c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.view
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import com.android.systemui.R
+import com.android.systemui.statusbar.BaseStatusBarFrameLayout
+import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
+import java.util.ArrayList
+
+class ModernStatusBarMobileView(
+    context: Context,
+    attrs: AttributeSet?,
+) : BaseStatusBarFrameLayout(context, attrs) {
+
+    private lateinit var slot: String
+    override fun getSlot() = slot
+
+    override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
+        // TODO
+    }
+
+    override fun setStaticDrawableColor(color: Int) {
+        // TODO
+    }
+
+    override fun setDecorColor(color: Int) {
+        // TODO
+    }
+
+    override fun setVisibleState(state: Int, animate: Boolean) {
+        // TODO
+    }
+
+    override fun getVisibleState(): Int {
+        return STATE_ICON
+    }
+
+    override fun isIconVisible(): Boolean {
+        return true
+    }
+
+    companion object {
+
+        /**
+         * Inflates a new instance of [ModernStatusBarMobileView], binds it to [viewModel], and
+         * returns it.
+         */
+        @JvmStatic
+        fun constructAndBind(
+            context: Context,
+            slot: String,
+            viewModel: MobileIconViewModel,
+        ): ModernStatusBarMobileView {
+            return (LayoutInflater.from(context)
+                    .inflate(R.layout.status_bar_mobile_signal_group_new, null)
+                    as ModernStatusBarMobileView)
+                .also {
+                    it.slot = slot
+                    MobileIconBinder.bind(it, viewModel)
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
new file mode 100644
index 0000000..cfabeba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import android.graphics.Color
+import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+
+/**
+ * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
+ * a single line of service via [MobileIconInteractor] and update the UI based on that
+ * subscription's information.
+ *
+ * There will be exactly one [MobileIconViewModel] per filtered subscription offered from
+ * [MobileIconsInteractor.filteredSubscriptions]
+ *
+ * TODO: figure out where carrier merged and VCN models go (probably here?)
+ */
+class MobileIconViewModel
+constructor(
+    val subscriptionId: Int,
+    iconInteractor: MobileIconInteractor,
+    logger: ConnectivityPipelineLogger,
+) {
+    /** An int consumable by [SignalDrawable] for display */
+    var iconId: Flow<Int> =
+        combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) {
+                level,
+                numberOfLevels,
+                cutOut ->
+                SignalDrawable.getState(level, numberOfLevels, cutOut)
+            }
+            .distinctUntilChanged()
+            .logOutputChange(logger, "iconId($subscriptionId)")
+
+    var tint: Flow<Int> = flowOf(Color.CYAN)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
new file mode 100644
index 0000000..24c1db9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(InternalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import javax.inject.Inject
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * View model for describing the system's current mobile cellular connections. The result is a list
+ * of [MobileIconViewModel]s which describe the individual icons and can be bound to
+ * [ModernStatusBarMobileView]
+ */
+class MobileIconsViewModel
+@Inject
+constructor(
+    val subscriptionIdsFlow: Flow<List<Int>>,
+    private val interactor: MobileIconsInteractor,
+    private val logger: ConnectivityPipelineLogger,
+) {
+    /** TODO: do we need to cache these? */
+    fun viewModelForSub(subId: Int): MobileIconViewModel =
+        MobileIconViewModel(
+            subId,
+            interactor.createMobileConnectionInteractorForSubId(subId),
+            logger
+        )
+
+    class Factory
+    @Inject
+    constructor(
+        private val interactor: MobileIconsInteractor,
+        private val logger: ConnectivityPipelineLogger,
+    ) {
+        fun create(subscriptionIdsFlow: Flow<List<Int>>): MobileIconsViewModel {
+            return MobileIconsViewModel(
+                subscriptionIdsFlow,
+                interactor,
+                logger,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
index 6c616ac..0cd9bd7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
@@ -22,7 +22,7 @@
 import android.view.Gravity
 import android.view.LayoutInflater
 import com.android.systemui.R
-import com.android.systemui.statusbar.BaseStatusBarWifiView
+import com.android.systemui.statusbar.BaseStatusBarFrameLayout
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
@@ -37,7 +37,7 @@
 class ModernStatusBarWifiView(
     context: Context,
     attrs: AttributeSet?
-) : BaseStatusBarWifiView(context, attrs) {
+) : BaseStatusBarFrameLayout(context, attrs) {
 
     private lateinit var slot: String
     private lateinit var binding: WifiViewBinder.Binding
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index f4d08e0..437d4d4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -435,7 +435,7 @@
         }
 
         @Override
-        public void onKeyguardVisibilityChanged(boolean showing) {
+        public void onKeyguardVisibilityChanged(boolean visible) {
             update(false /* updateAlways */);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index 712953e..494a4bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -91,11 +91,11 @@
     private final KeyguardUpdateMonitorCallback mInfoCallback =
             new KeyguardUpdateMonitorCallback() {
                 @Override
-                public void onKeyguardVisibilityChanged(boolean showing) {
-                    if (DEBUG) Log.d(TAG, String.format("onKeyguardVisibilityChanged %b", showing));
+                public void onKeyguardVisibilityChanged(boolean visible) {
+                    if (DEBUG) Log.d(TAG, String.format("onKeyguardVisibilityChanged %b", visible));
                     // Any time the keyguard is hidden, try to close the user switcher menu to
                     // restore keyguard to the default state
-                    if (!showing) {
+                    if (!visible) {
                         closeSwitcherIfOpenAndNotSimple(false);
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index a52e2af..91e20ee 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -167,11 +167,19 @@
      * @param removalReason a short string describing why the view was removed (timeout, state
      *     change, etc.)
      */
-    open fun removeView(removalReason: String) {
-        if (view == null) { return }
+    fun removeView(removalReason: String) {
+        if (shouldIgnoreViewRemoval(removalReason)) {
+            return
+        }
+        val currentView = view ?: return
+
+        animateViewOut(currentView) { windowManager.removeView(currentView) }
+
         logger.logChipRemoval(removalReason)
         configurationController.removeCallback(displayScaleListener)
-        windowManager.removeView(view)
+        // Re-set the view to null immediately (instead as part of the animation end runnable) so
+        // that if a new view event comes in while this view is animating out, we still display the
+        // new view appropriately.
         view = null
         info = null
         // No need to time the view out since it's already gone
@@ -179,6 +187,13 @@
     }
 
     /**
+     * Returns true if a view removal request should be ignored and false otherwise.
+     *
+     * Allows subclasses to keep the view visible for longer in certain circumstances.
+     */
+    open fun shouldIgnoreViewRemoval(removalReason: String): Boolean = false
+
+    /**
      * A method implemented by subclasses to update [currentView] based on [newInfo].
      */
     @CallSuper
@@ -190,7 +205,17 @@
      * A method that can be implemented by subclasses to do custom animations for when the view
      * appears.
      */
-    open fun animateViewIn(view: ViewGroup) {}
+    internal open fun animateViewIn(view: ViewGroup) {}
+
+    /**
+     * A method that can be implemented by subclasses to do custom animations for when the view
+     * disappears.
+     *
+     * @param onAnimationEnd an action that *must* be run once the animation finishes successfully.
+     */
+    internal open fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+        onAnimationEnd.run()
+    }
 }
 
 object TemporaryDisplayRemovalReason {
diff --git a/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java b/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java
index 5f7d745..a925e38 100644
--- a/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java
+++ b/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java
@@ -67,6 +67,8 @@
     private boolean mDefaultCarrierProvisionsWifiMergedNetworks;
     private boolean mDefaultShowOperatorNameConfigLoaded;
     private boolean mDefaultShowOperatorNameConfig;
+    private boolean mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded;
+    private boolean mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig;
 
     @Inject
     public CarrierConfigTracker(
@@ -207,6 +209,22 @@
     }
 
     /**
+     * Returns KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN value for
+     * the default carrier config.
+     */
+    public boolean getAlwaysShowPrimarySignalBarInOpportunisticNetworkDefault() {
+        if (!mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded) {
+            mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig = CarrierConfigManager
+                    .getDefaultConfig().getBoolean(CarrierConfigManager
+                            .KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN
+                    );
+            mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded = true;
+        }
+
+        return mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig;
+    }
+
+    /**
      * Returns the KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL value for the given subId, or the
      * default value if no override exists
      *
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 9aa71e9..63729e3 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -607,7 +607,7 @@
     public void testTriesToAuthenticate_whenKeyguard() {
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
     }
 
@@ -617,7 +617,7 @@
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
 
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
         verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
                 anyBoolean());
     }
@@ -630,7 +630,7 @@
 
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
         verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
                 anyBoolean());
     }
@@ -654,7 +654,7 @@
 
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
 
         // Stop scanning when bouncer becomes visible
@@ -668,7 +668,7 @@
 
     @Test
     public void testTriesToAuthenticate_whenAssistant() {
-        mKeyguardUpdateMonitor.setKeyguardOccluded(true);
+        mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
         mKeyguardUpdateMonitor.setAssistantVisible(true);
 
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
@@ -683,7 +683,7 @@
         mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */,
                 KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */,
                 new ArrayList<>());
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
     }
 
@@ -693,7 +693,7 @@
         mTestableLooper.processAllMessages();
         mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */,
                 KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */, new ArrayList<>());
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
         verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
                 anyBoolean());
     }
@@ -705,7 +705,7 @@
         when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(
                 KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
 
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
         verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
                 anyBoolean());
     }
@@ -717,7 +717,7 @@
         when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(
                 KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT);
 
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
     }
 
@@ -738,7 +738,7 @@
     public void testFaceAndFingerprintLockout_onlyFace() {
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
 
         faceAuthLockedOut();
 
@@ -749,7 +749,7 @@
     public void testFaceAndFingerprintLockout_onlyFingerprint() {
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
 
         mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback
                 .onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, "");
@@ -761,7 +761,7 @@
     public void testFaceAndFingerprintLockout() {
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
 
         faceAuthLockedOut();
         mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback
@@ -860,7 +860,7 @@
 
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
 
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
         verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(),
@@ -1033,8 +1033,7 @@
     public void testOccludingAppFingerprintListeningState() {
         // GIVEN keyguard isn't visible (app occluding)
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
-        mKeyguardUpdateMonitor.setKeyguardOccluded(true);
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(false);
+        mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
         when(mStrongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true);
 
         // THEN we shouldn't listen for fingerprints
@@ -1049,8 +1048,7 @@
     public void testOccludingAppRequestsFingerprint() {
         // GIVEN keyguard isn't visible (app occluding)
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
-        mKeyguardUpdateMonitor.setKeyguardOccluded(true);
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(false);
+        mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
 
         // WHEN an occluding app requests fp
         mKeyguardUpdateMonitor.requestFingerprintAuthOnOccludingApp(true);
@@ -1142,7 +1140,7 @@
         setKeyguardBouncerVisibility(false /* isVisible */);
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         when(mKeyguardBypassController.canBypass()).thenReturn(true);
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
 
         // WHEN status bar state reports a change to the keyguard that would normally indicate to
         // start running face auth
@@ -1153,8 +1151,9 @@
         // listening state to update
         assertThat(mKeyguardUpdateMonitor.isFaceDetectionRunning()).isEqualTo(false);
 
-        // WHEN biometric listening state is updated
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        // WHEN biometric listening state is updated when showing state changes from false => true
+        mKeyguardUpdateMonitor.setKeyguardShowing(false, false);
+        mKeyguardUpdateMonitor.setKeyguardShowing(true, false);
 
         // THEN face unlock is running
         assertThat(mKeyguardUpdateMonitor.isFaceDetectionRunning()).isEqualTo(true);
@@ -1520,7 +1519,7 @@
     public void testFingerprintCanAuth_whenCancellationNotReceivedAndAuthFailed() {
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        keyguardIsVisible();
 
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
         verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(),
@@ -1529,7 +1528,7 @@
         mKeyguardUpdateMonitor.onFaceAuthenticated(0, false);
         // Make sure keyguard is going away after face auth attempt, and that it calls
         // updateBiometricStateListeningState.
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(false);
+        mKeyguardUpdateMonitor.setKeyguardShowing(false, false);
         mTestableLooper.processAllMessages();
 
         verify(mHandler).postDelayed(mKeyguardUpdateMonitor.mFpCancelNotReceived,
@@ -1589,7 +1588,7 @@
     }
 
     private void keyguardIsVisible() {
-        mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true);
+        mKeyguardUpdateMonitor.setKeyguardShowing(true, false);
     }
 
     private void triggerAuthInterrupt() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
index 8fc0489..986e7cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
@@ -718,7 +718,7 @@
     }
 
     @Test
-    fun animatesViewRemovalFromStartToEnd() {
+    fun animatesViewRemovalFromStartToEnd_viewHasSiblings() {
         setUpRootWithChildren()
 
         val child = rootView.getChildAt(0)
@@ -742,6 +742,35 @@
     }
 
     @Test
+    fun animatesViewRemovalFromStartToEnd_viewHasNoSiblings() {
+        rootView = LinearLayout(mContext)
+        (rootView as LinearLayout).orientation = LinearLayout.HORIZONTAL
+        (rootView as LinearLayout).weightSum = 1f
+
+        val onlyChild = View(mContext)
+        rootView.addView(onlyChild)
+        forceLayout()
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            onlyChild,
+            destination = ViewHierarchyAnimator.Hotspot.LEFT,
+            interpolator = Interpolators.LINEAR
+        )
+
+        assertTrue(success)
+        assertNotNull(onlyChild.getTag(R.id.tag_animator))
+        checkBounds(onlyChild, l = 0, t = 0, r = 200, b = 100)
+        advanceAnimation(onlyChild, 0.5f)
+        checkBounds(onlyChild, l = 0, t = 0, r = 100, b = 100)
+        advanceAnimation(onlyChild, 1.0f)
+        checkBounds(onlyChild, l = 0, t = 0, r = 0, b = 100)
+        endAnimation(rootView)
+        endAnimation(onlyChild)
+        assertEquals(0, rootView.childCount)
+        assertFalse(onlyChild in rootView.children)
+    }
+
+    @Test
     fun animatesViewRemovalRespectingDestination() {
         // CENTER
         setUpRootWithChildren()
@@ -964,6 +993,60 @@
     }
 
     @Test
+    fun animateRemoval_runnableRunsWhenAnimationEnds() {
+        var runnableRun = false
+        val onAnimationEndRunnable = { runnableRun = true }
+
+        setUpRootWithChildren()
+        forceLayout()
+        val removedView = rootView.getChildAt(0)
+
+        ViewHierarchyAnimator.animateRemoval(
+            removedView,
+            onAnimationEnd = onAnimationEndRunnable
+        )
+        endAnimation(removedView)
+
+        assertEquals(true, runnableRun)
+    }
+
+    @Test
+    fun animateRemoval_runnableDoesNotRunWhenAnimationCancelled() {
+        var runnableRun = false
+        val onAnimationEndRunnable = { runnableRun = true }
+
+        setUpRootWithChildren()
+        forceLayout()
+        val removedView = rootView.getChildAt(0)
+
+        ViewHierarchyAnimator.animateRemoval(
+            removedView,
+            onAnimationEnd = onAnimationEndRunnable
+        )
+        cancelAnimation(removedView)
+
+        assertEquals(false, runnableRun)
+    }
+
+    @Test
+    fun animationRemoval_runnableDoesNotRunWhenOnlyPartwayThroughAnimation() {
+        var runnableRun = false
+        val onAnimationEndRunnable = { runnableRun = true }
+
+        setUpRootWithChildren()
+        forceLayout()
+        val removedView = rootView.getChildAt(0)
+
+        ViewHierarchyAnimator.animateRemoval(
+            removedView,
+            onAnimationEnd = onAnimationEndRunnable
+        )
+        advanceAnimation(removedView, 0.5f)
+
+        assertEquals(false, runnableRun)
+    }
+
+    @Test
     fun cleansUpListenersCorrectly() {
         val firstChild = View(mContext)
         firstChild.layoutParams = LinearLayout.LayoutParams(50 /* width */, 100 /* height */)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
index 37bb0c2..0b528a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
@@ -117,13 +117,12 @@
     }
 
     @Test
-    fun testFingerprintTrigger_KeyguardVisible_Ripple() {
-        // GIVEN fp exists, keyguard is visible, user doesn't need strong auth
+    fun testFingerprintTrigger_KeyguardShowing_Ripple() {
+        // GIVEN fp exists, keyguard is showing, user doesn't need strong auth
         val fpsLocation = Point(5, 5)
         `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation)
         controller.onViewAttached()
-        `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true)
-        `when`(keyguardUpdateMonitor.isDreaming).thenReturn(false)
+        `when`(keyguardStateController.isShowing).thenReturn(true)
         `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false)
 
         // WHEN fingerprint authenticated
@@ -140,39 +139,15 @@
     }
 
     @Test
-    fun testFingerprintTrigger_Dreaming_Ripple() {
-        // GIVEN fp exists, keyguard is visible, user doesn't need strong auth
-        val fpsLocation = Point(5, 5)
-        `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation)
-        controller.onViewAttached()
-        `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(false)
-        `when`(keyguardUpdateMonitor.isDreaming).thenReturn(true)
-        `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false)
-
-        // WHEN fingerprint authenticated
-        val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
-        verify(keyguardUpdateMonitor).registerCallback(captor.capture())
-        captor.value.onBiometricAuthenticated(
-                0 /* userId */,
-                BiometricSourceType.FINGERPRINT /* type */,
-                false /* isStrongBiometric */)
-
-        // THEN update sensor location and show ripple
-        verify(rippleView).setFingerprintSensorLocation(fpsLocation, 0f)
-        verify(rippleView).startUnlockedRipple(any())
-    }
-
-    @Test
-    fun testFingerprintTrigger_KeyguardNotVisible_NotDreaming_NoRipple() {
+    fun testFingerprintTrigger_KeyguardNotShowing_NoRipple() {
         // GIVEN fp exists & user doesn't need strong auth
         val fpsLocation = Point(5, 5)
         `when`(authController.udfpsLocation).thenReturn(fpsLocation)
         controller.onViewAttached()
         `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false)
 
-        // WHEN keyguard is NOT visible & fingerprint authenticated
-        `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(false)
-        `when`(keyguardUpdateMonitor.isDreaming).thenReturn(false)
+        // WHEN keyguard is NOT showing & fingerprint authenticated
+        `when`(keyguardStateController.isShowing).thenReturn(false)
         val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
         verify(keyguardUpdateMonitor).registerCallback(captor.capture())
         captor.value.onBiometricAuthenticated(
@@ -186,11 +161,11 @@
 
     @Test
     fun testFingerprintTrigger_StrongAuthRequired_NoRipple() {
-        // GIVEN fp exists & keyguard is visible
+        // GIVEN fp exists & keyguard is showing
         val fpsLocation = Point(5, 5)
         `when`(authController.udfpsLocation).thenReturn(fpsLocation)
         controller.onViewAttached()
-        `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true)
+        `when`(keyguardStateController.isShowing).thenReturn(true)
 
         // WHEN user needs strong auth & fingerprint authenticated
         `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(true)
@@ -207,12 +182,12 @@
 
     @Test
     fun testFaceTriggerBypassEnabled_Ripple() {
-        // GIVEN face auth sensor exists, keyguard is visible & strong auth isn't required
+        // GIVEN face auth sensor exists, keyguard is showing & strong auth isn't required
         val faceLocation = Point(5, 5)
         `when`(authController.faceSensorLocation).thenReturn(faceLocation)
         controller.onViewAttached()
 
-        `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true)
+        `when`(keyguardStateController.isShowing).thenReturn(true)
         `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false)
 
         // WHEN bypass is enabled & face authenticated
@@ -299,7 +274,7 @@
         val fpsLocation = Point(5, 5)
         `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation)
         controller.onViewAttached()
-        `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true)
+        `when`(keyguardStateController.isShowing).thenReturn(true)
         `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
 
         controller.showUnlockRipple(BiometricSourceType.FINGERPRINT)
@@ -317,7 +292,7 @@
         val faceLocation = Point(5, 5)
         `when`(authController.faceSensorLocation).thenReturn(faceLocation)
         controller.onViewAttached()
-        `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true)
+        `when`(keyguardStateController.isShowing).thenReturn(true)
         `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
         `when`(authController.isUdfpsFingerDown).thenReturn(true)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java
index d418836..7f55d38 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java
@@ -49,6 +49,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 
+import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 import java.util.function.BooleanSupplier;
 
@@ -148,8 +149,12 @@
         return false;
     }
 
-    private static void executeShellCommand(String cmd) {
-        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(cmd);
+    private void executeShellCommand(String cmd) {
+        try {
+            runShellCommand(cmd);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index eea2e95..7a15680 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -33,7 +34,6 @@
 import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -153,6 +153,21 @@
     }
 
     @Test
+    fun `isDozing - starts with correct initial value for isDozing`() = runBlockingTest {
+        var latest: Boolean? = null
+
+        whenever(statusBarStateController.isDozing).thenReturn(true)
+        var job = underTest.isDozing.onEach { latest = it }.launchIn(this)
+        assertThat(latest).isTrue()
+        job.cancel()
+
+        whenever(statusBarStateController.isDozing).thenReturn(false)
+        job = underTest.isDozing.onEach { latest = it }.launchIn(this)
+        assertThat(latest).isFalse()
+        job.cancel()
+    }
+
+    @Test
     fun dozeAmount() = runBlockingTest {
         val values = mutableListOf<Float>()
         val job = underTest.dozeAmount.onEach(values::add).launchIn(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index 37f6434..7c83cb7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
@@ -19,9 +19,7 @@
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.graphics.drawable.Drawable
-import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
-import com.android.internal.widget.CachingIconView
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.any
@@ -90,48 +88,6 @@
         assertThat(iconInfo.drawable).isEqualTo(appIconFromPackageName)
         assertThat(iconInfo.contentDescription).isEqualTo(APP_NAME)
     }
-
-    @Test
-    fun setIcon_viewHasIconAndContentDescription() {
-        val view = CachingIconView(context)
-        val icon = context.getDrawable(R.drawable.ic_celebration)!!
-        val contentDescription = "Happy birthday!"
-
-        MediaTttUtils.setIcon(view, icon, contentDescription)
-
-        assertThat(view.drawable).isEqualTo(icon)
-        assertThat(view.contentDescription).isEqualTo(contentDescription)
-    }
-
-    @Test
-    fun setIcon_iconSizeNull_viewSizeDoesNotChange() {
-        val view = CachingIconView(context)
-        val size = 456
-        view.layoutParams = FrameLayout.LayoutParams(size, size)
-
-        MediaTttUtils.setIcon(view, context.getDrawable(R.drawable.ic_cake)!!, "desc")
-
-        assertThat(view.layoutParams.width).isEqualTo(size)
-        assertThat(view.layoutParams.height).isEqualTo(size)
-    }
-
-    @Test
-    fun setIcon_iconSizeProvided_viewSizeUpdates() {
-        val view = CachingIconView(context)
-        val size = 456
-        view.layoutParams = FrameLayout.LayoutParams(size, size)
-
-        val newSize = 40
-        MediaTttUtils.setIcon(
-            view,
-            context.getDrawable(R.drawable.ic_cake)!!,
-            "desc",
-            iconSize = newSize
-        )
-
-        assertThat(view.layoutParams.width).isEqualTo(newSize)
-        assertThat(view.layoutParams.height).isEqualTo(newSize)
-    }
 }
 
 private const val PACKAGE_NAME = "com.android.systemui"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index d41ad48..775dc11 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -212,35 +212,27 @@
     }
 
     @Test
-    fun updateView_isAppIcon_usesAppIconSize() {
+    fun updateView_isAppIcon_usesAppIconPadding() {
         controllerReceiver.displayView(getChipReceiverInfo(packageName = PACKAGE_NAME))
+
         val chipView = getChipView()
-
-        chipView.measure(
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
-        )
-
-        val expectedSize =
-            context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver)
-        assertThat(chipView.getAppIconView().measuredWidth).isEqualTo(expectedSize)
-        assertThat(chipView.getAppIconView().measuredHeight).isEqualTo(expectedSize)
+        assertThat(chipView.getAppIconView().paddingLeft).isEqualTo(0)
+        assertThat(chipView.getAppIconView().paddingRight).isEqualTo(0)
+        assertThat(chipView.getAppIconView().paddingTop).isEqualTo(0)
+        assertThat(chipView.getAppIconView().paddingBottom).isEqualTo(0)
     }
 
     @Test
-    fun updateView_notAppIcon_usesGenericIconSize() {
+    fun updateView_notAppIcon_usesGenericIconPadding() {
         controllerReceiver.displayView(getChipReceiverInfo(packageName = null))
+
         val chipView = getChipView()
-
-        chipView.measure(
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
-        )
-
-        val expectedSize =
-            context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_size_receiver)
-        assertThat(chipView.getAppIconView().measuredWidth).isEqualTo(expectedSize)
-        assertThat(chipView.getAppIconView().measuredHeight).isEqualTo(expectedSize)
+        val expectedPadding =
+            context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding)
+        assertThat(chipView.getAppIconView().paddingLeft).isEqualTo(expectedPadding)
+        assertThat(chipView.getAppIconView().paddingRight).isEqualTo(expectedPadding)
+        assertThat(chipView.getAppIconView().paddingTop).isEqualTo(expectedPadding)
+        assertThat(chipView.getAppIconView().paddingBottom).isEqualTo(expectedPadding)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
index 098086a..eca3bed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media.taptotransfer.sender
 
 import android.app.StatusBarManager
+import android.content.Context
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.graphics.drawable.Drawable
@@ -37,9 +38,11 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -61,7 +64,7 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class MediaTttChipControllerSenderTest : SysuiTestCase() {
-    private lateinit var controllerSender: MediaTttChipControllerSender
+    private lateinit var controllerSender: TestMediaTttChipControllerSender
 
     @Mock
     private lateinit var packageManager: PackageManager
@@ -116,7 +119,7 @@
         whenever(lazyFalsingManager.get()).thenReturn(falsingManager)
         whenever(lazyFalsingCollector.get()).thenReturn(falsingCollector)
 
-        controllerSender = MediaTttChipControllerSender(
+        controllerSender = TestMediaTttChipControllerSender(
             commandQueue,
             context,
             logger,
@@ -821,6 +824,37 @@
     /** Helper method providing default parameters to not clutter up the tests. */
     private fun transferToThisDeviceFailed() =
         ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
+
+    private class TestMediaTttChipControllerSender(
+        commandQueue: CommandQueue,
+        context: Context,
+        @MediaTttReceiverLogger logger: MediaTttLogger,
+        windowManager: WindowManager,
+        mainExecutor: DelayableExecutor,
+        accessibilityManager: AccessibilityManager,
+        configurationController: ConfigurationController,
+        powerManager: PowerManager,
+        uiEventLogger: MediaTttSenderUiEventLogger,
+        falsingManager: Lazy<FalsingManager>,
+        falsingCollector: Lazy<FalsingCollector>,
+    ) : MediaTttChipControllerSender(
+        commandQueue,
+        context,
+        logger,
+        windowManager,
+        mainExecutor,
+        accessibilityManager,
+        configurationController,
+        powerManager,
+        uiEventLogger,
+        falsingManager,
+        falsingCollector,
+    ) {
+        override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+            // Just bypass the animation in tests
+            onAnimationEnd.run()
+        }
+    }
 }
 
 private const val APP_NAME = "Fake app name"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index 3cad2a0..b847ad0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -277,7 +277,7 @@
 
         // Then the layout changes
         assertThat(mController.shouldUseHorizontalLayout()).isTrue();
-        verify(mHorizontalLayoutListener).run(); // not invoked
+        verify(mHorizontalLayoutListener).run();
 
         // When it is rotated back to portrait
         mConfiguration.orientation = Configuration.ORIENTATION_PORTRAIT;
@@ -300,4 +300,24 @@
         verify(mQSTile).refreshState();
         verify(mOtherTile, never()).refreshState();
     }
+
+    @Test
+    public void configurationChange_onlySplitShadeConfigChanges_horizontalLayoutStatusUpdated() {
+        // Preconditions for horizontal layout
+        when(mMediaHost.getVisible()).thenReturn(true);
+        when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(false);
+        mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
+        mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener);
+        mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration);
+        assertThat(mController.shouldUseHorizontalLayout()).isTrue();
+        reset(mHorizontalLayoutListener);
+
+        // Only split shade status changes
+        when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(true);
+        mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration);
+
+        // Horizontal layout is updated accordingly.
+        assertThat(mController.shouldUseHorizontalLayout()).isFalse();
+        verify(mHorizontalLayoutListener).run();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
deleted file mode 100644
index 5432a74..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2017 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;
-
-import static org.junit.Assert.assertFalse;
-
-import android.os.Handler;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.Dependency;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.shade.ShadeController;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Verifies that particular sets of dependencies don't have dependencies on others. For example,
- * code managing notifications shouldn't directly depend on CentralSurfaces, since there are
- * platforms which want to manage notifications, but don't use CentralSurfaces.
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class NonPhoneDependencyTest extends SysuiTestCase {
-    @Mock private NotificationPresenter mPresenter;
-    @Mock private NotificationListContainer mListContainer;
-    @Mock private RemoteInputController.Delegate mDelegate;
-    @Mock private NotificationRemoteInputManager.Callback mRemoteInputManagerCallback;
-    @Mock private OnSettingsClickListener mOnSettingsClickListener;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
-        mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
-               new Handler(TestableLooper.get(this).getLooper()));
-    }
-
-    @Ignore("Causes binder calls which fail")
-    @Test
-    public void testNotificationManagementCodeHasNoDependencyOnStatusBarWindowManager() {
-        mDependency.injectMockDependency(ShadeController.class);
-        NotificationGutsManager gutsManager = Dependency.get(NotificationGutsManager.class);
-        NotificationLogger notificationLogger = Dependency.get(NotificationLogger.class);
-        NotificationMediaManager mediaManager = Dependency.get(NotificationMediaManager.class);
-        NotificationRemoteInputManager remoteInputManager =
-                Dependency.get(NotificationRemoteInputManager.class);
-        NotificationLockscreenUserManager lockscreenUserManager =
-                Dependency.get(NotificationLockscreenUserManager.class);
-        gutsManager.setUpWithPresenter(mPresenter, mListContainer,
-                mOnSettingsClickListener);
-        notificationLogger.setUpWithContainer(mListContainer);
-        mediaManager.setUpWithPresenter(mPresenter);
-        remoteInputManager.setUpWithCallback(mRemoteInputManagerCallback,
-                mDelegate);
-        lockscreenUserManager.setUpWithPresenter(mPresenter);
-
-        TestableLooper.get(this).processAllMessages();
-        assertFalse(mDependency.hasInstantiatedDependency(NotificationShadeWindowController.class));
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 853d1df..bdafa48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -88,6 +89,8 @@
     @Mock
     private NotificationClickNotifier mClickNotifier;
     @Mock
+    private OverviewProxyService mOverviewProxyService;
+    @Mock
     private KeyguardManager mKeyguardManager;
     @Mock
     private DeviceProvisionedController mDeviceProvisionedController;
@@ -344,6 +347,7 @@
                     (() -> mVisibilityProvider),
                     (() -> mNotifCollection),
                     mClickNotifier,
+                    (() -> mOverviewProxyService),
                     NotificationLockscreenUserManagerTest.this.mKeyguardManager,
                     mStatusBarStateController,
                     Handler.createAsync(Looper.myLooper()),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
new file mode 100644
index 0000000..16e2441
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
@@ -0,0 +1,321 @@
+/*
+ *
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import android.widget.RemoteViews
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationMemoryMonitorTest : SysuiTestCase() {
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_plainNotification() {
+        val notification = createBasicNotification().build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3316,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() {
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
+        val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = 0,
+            extras = 3316,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_customViewNotification_marksTrue() {
+        val notification =
+            createBasicNotification()
+                .setCustomContentView(
+                    RemoteViews(context.packageName, android.R.layout.list_content)
+                )
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3384,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = true,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() {
+        val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444)
+        val notification =
+            createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = 444444,
+            largeIcon = 0,
+            extras = 3212,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_bigPictureStyle() {
+        val bigPicture =
+            Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888))
+        val bigPictureIcon =
+            Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val notification =
+            createBasicNotification()
+                .setStyle(
+                    Notification.BigPictureStyle()
+                        .bigPicture(bigPicture)
+                        .bigLargeIcon(bigPictureIcon)
+                )
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 4092,
+            bigPicture = bigPicture.bitmap.allocationByteCount,
+            extender = 0,
+            style = "BigPictureStyle",
+            styleIcon = bigPictureIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_callingStyle() {
+        val personIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+        val fakeIntent =
+            PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+        val notification =
+            createBasicNotification()
+                .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent))
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 4084,
+            bigPicture = 0,
+            extender = 0,
+            style = "CallStyle",
+            styleIcon = personIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_messagingStyle() {
+        val personIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+        val message = Notification.MessagingStyle.Message("Message!", 4323, person)
+        val historicPersonIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888))
+        val historicPerson =
+            Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build()
+        val historicMessage =
+            Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson)
+
+        val notification =
+            createBasicNotification()
+                .setStyle(
+                    Notification.MessagingStyle(person)
+                        .addMessage(message)
+                        .addHistoricMessage(historicMessage)
+                )
+                .build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 5024,
+            bigPicture = 0,
+            extender = 0,
+            style = "MessagingStyle",
+            styleIcon =
+                personIcon.bitmap.allocationByteCount +
+                    historicPersonIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_carExtender() {
+        val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888)
+        val extender = Notification.CarExtender().setLargeIcon(carIcon)
+        val notification = createBasicNotification().extend(extender).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3612,
+            bigPicture = 0,
+            extender = 556656,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_tvWearExtender() {
+        val tvExtender = Notification.TvExtender().setChannel("channel2")
+        val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888)
+        val wearExtender = Notification.WearableExtender().setBackground(wearBackground)
+        val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build()
+        val nmm = createNMMWithNotifications(listOf(notification))
+        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3820,
+            bigPicture = 0,
+            extender = 388 + wearBackground.allocationByteCount,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    private fun createBasicNotification(): Notification.Builder {
+        val smallIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888))
+        val largeIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888))
+        return Notification.Builder(context)
+            .setSmallIcon(smallIcon)
+            .setLargeIcon(largeIcon)
+            .setContentTitle("This is a title")
+            .setContentText("This is content text.")
+    }
+
+    /** This will generate a nicer error message than comparing objects */
+    private fun assertNotificationObjectSizes(
+        memoryUse: NotificationMemoryUsage,
+        smallIcon: Int,
+        largeIcon: Int,
+        extras: Int,
+        bigPicture: Int,
+        extender: Int,
+        style: String?,
+        styleIcon: Int,
+        hasCustomView: Boolean
+    ) {
+        assertThat(memoryUse.packageName).isEqualTo("test_pkg")
+        assertThat(memoryUse.notificationId)
+            .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0"))
+        assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon)
+        assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon)
+        assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture)
+        if (style == null) {
+            assertThat(memoryUse.objectUsage.style).isNull()
+        } else {
+            assertThat(memoryUse.objectUsage.style).isEqualTo(style)
+        }
+        assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon)
+        assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView)
+    }
+
+    private fun getUseObject(
+        singleItemUseList: List<NotificationMemoryUsage>
+    ): NotificationMemoryUsage {
+        assertThat(singleItemUseList).hasSize(1)
+        return singleItemUseList[0]
+    }
+
+    private fun createNMMWithNotifications(
+        notifications: List<Notification>
+    ): NotificationMemoryMonitor {
+        val notifPipeline: NotifPipeline = mock()
+        val notificationEntries =
+            notifications.map { n ->
+                NotificationEntryBuilder().setTag("test").setNotification(n).build()
+            }
+        whenever(notifPipeline.allNotifs).thenReturn(notificationEntries)
+        return NotificationMemoryMonitor(notifPipeline, mock())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
index 682ff1f..81b8e98 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
@@ -32,6 +32,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.R;
+import com.android.internal.widget.NotificationActionListLayout;
 import com.android.internal.widget.NotificationExpandButton;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
@@ -142,4 +143,60 @@
         verify(mockExpandedEB, times(1)).requestAccessibilityFocus();
         verify(mockHeadsUpEB, times(0)).requestAccessibilityFocus();
     }
+
+    @Test
+    @UiThreadTest
+    public void testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
+        View mockContracted = mock(NotificationHeaderView.class);
+
+        View mockExpandedActions = mock(NotificationActionListLayout.class);
+        View mockExpanded = mock(NotificationHeaderView.class);
+        when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn(
+                mockExpandedActions);
+
+        View mockHeadsUpActions = mock(NotificationActionListLayout.class);
+        View mockHeadsUp = mock(NotificationHeaderView.class);
+        when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn(
+                mockHeadsUpActions);
+
+        mView.setContractedChild(mockContracted);
+        mView.setExpandedChild(mockExpanded);
+        mView.setHeadsUpChild(mockHeadsUp);
+
+        mView.setRemoteInputVisible(true);
+
+        verify(mockContracted, times(0)).findViewById(0);
+        verify(mockExpandedActions, times(1)).setImportantForAccessibility(
+                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+        verify(mockHeadsUpActions, times(1)).setImportantForAccessibility(
+                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
+        View mockContracted = mock(NotificationHeaderView.class);
+
+        View mockExpandedActions = mock(NotificationActionListLayout.class);
+        View mockExpanded = mock(NotificationHeaderView.class);
+        when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn(
+                mockExpandedActions);
+
+        View mockHeadsUpActions = mock(NotificationActionListLayout.class);
+        View mockHeadsUp = mock(NotificationHeaderView.class);
+        when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn(
+                mockHeadsUpActions);
+
+        mView.setContractedChild(mockContracted);
+        mView.setExpandedChild(mockExpanded);
+        mView.setHeadsUpChild(mockHeadsUp);
+
+        mView.setRemoteInputVisible(false);
+
+        verify(mockContracted, times(0)).findViewById(0);
+        verify(mockExpandedActions, times(1)).setImportantForAccessibility(
+                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+        verify(mockHeadsUpActions, times(1)).setImportantForAccessibility(
+                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index 34399b8..9c56c26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -44,6 +44,7 @@
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
 
@@ -80,6 +81,7 @@
                 StatusBarLocation.HOME,
                 mock(StatusBarPipelineFlags.class),
                 mock(WifiViewModel.class),
+                mock(MobileUiAdapter.class),
                 mMobileContextProvider,
                 mock(DarkIconDispatcher.class));
         testCallOnAdd_forManager(manager);
@@ -123,12 +125,14 @@
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
                 WifiViewModel wifiViewModel,
+                MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider contextProvider,
                 DarkIconDispatcher darkIconDispatcher) {
             super(group,
                     location,
                     statusBarPipelineFlags,
                     wifiViewModel,
+                    mobileUiAdapter,
                     contextProvider,
                     darkIconDispatcher);
         }
@@ -169,6 +173,7 @@
                     StatusBarLocation.HOME,
                     mock(StatusBarPipelineFlags.class),
                     mock(WifiViewModel.class),
+                    mock(MobileUiAdapter.class),
                     contextProvider);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index f52bfca..dcce61b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -23,11 +23,9 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -527,26 +525,4 @@
         mBouncerExpansionCallback.onVisibilityChanged(false);
         verify(mCentralSurfaces).setBouncerShowingOverDream(false);
     }
-
-
-    @Test
-    public void testSetDozing_bouncerShowing_Dozing() {
-        clearInvocations(mBouncer);
-        when(mBouncer.isShowing()).thenReturn(true);
-        doAnswer(invocation -> {
-            when(mBouncer.isShowing()).thenReturn(false);
-            return null;
-        }).when(mBouncer).hide(false);
-        mStatusBarKeyguardViewManager.onDozingChanged(true);
-        verify(mBouncer, times(1)).hide(false);
-    }
-
-    @Test
-    public void testSetDozing_bouncerShowing_notDozing() {
-        mStatusBarKeyguardViewManager.onDozingChanged(true);
-        when(mBouncer.isShowing()).thenReturn(true);
-        clearInvocations(mBouncer);
-        mStatusBarKeyguardViewManager.onDozingChanged(false);
-        verify(mBouncer, times(1)).hide(false);
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
new file mode 100644
index 0000000..0d15268
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileSubscriptionRepository : MobileSubscriptionRepository {
+    private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
+    override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
+
+    private val _activeMobileDataSubscriptionId =
+        MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+    override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
+
+    private val subIdFlows = mutableMapOf<Int, MutableStateFlow<MobileSubscriptionModel>>()
+    override fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> {
+        return subIdFlows[subId]
+            ?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it }
+    }
+
+    fun setSubscriptions(subs: List<SubscriptionInfo>) {
+        _subscriptionsFlow.value = subs
+    }
+
+    fun setActiveMobileDataSubscriptionId(subId: Int) {
+        _activeMobileDataSubscriptionId.value = subId
+    }
+
+    fun setMobileSubscriptionModel(model: MobileSubscriptionModel, subId: Int) {
+        val subscription = subIdFlows[subId] ?: throw Exception("no flow exists for this subId yet")
+        subscription.value = model
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
new file mode 100644
index 0000000..6c495c5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Defaults to `true` */
+class FakeUserSetupRepository : UserSetupRepository {
+    private val _isUserSetup: MutableStateFlow<Boolean> = MutableStateFlow(true)
+    override val isUserSetupFlow: Flow<Boolean> = _isUserSetup
+
+    fun setUserSetup(setup: Boolean) {
+        _isUserSetup.value = setup
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
new file mode 100644
index 0000000..316b795
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyCallback.CarrierNetworkListener
+import android.telephony.TelephonyCallback.DataActivityListener
+import android.telephony.TelephonyCallback.DataConnectionStateListener
+import android.telephony.TelephonyCallback.DisplayInfoListener
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyCallback.SignalStrengthsListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileSubscriptionRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: MobileSubscriptionRepositoryImpl
+
+    @Mock private lateinit var subscriptionManager: SubscriptionManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
+    private val scope = CoroutineScope(IMMEDIATE)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            MobileSubscriptionRepositoryImpl(
+                subscriptionManager,
+                telephonyManager,
+                IMMEDIATE,
+                scope,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun testSubscriptions_initiallyEmpty() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
+        }
+
+    @Test
+    fun testSubscriptions_listUpdates() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_removingSub_updatesList() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            // WHEN 2 networks show up
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // WHEN one network is removed
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // THEN the subscriptions list represents the newest change
+            assertThat(latest).isEqualTo(listOf(SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.activeMobileDataSubscriptionId.value)
+                .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_updates() =
+        runBlocking(IMMEDIATE) {
+            var active: Int? = null
+
+            val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this)
+
+            getActiveDataSubscriptionCallback().onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            assertThat(active).isEqualTo(SUB_2_ID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_default() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(MobileSubscriptionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_emergencyOnly() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val serviceState = ServiceState()
+            serviceState.isEmergencyOnly = true
+
+            getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+            assertThat(latest?.isEmergencyOnly).isEqualTo(true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_emergencyOnly_toggles() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<ServiceStateListener>()
+            val serviceState = ServiceState()
+            serviceState.isEmergencyOnly = true
+            callback.onServiceStateChanged(serviceState)
+            serviceState.isEmergencyOnly = false
+            callback.onServiceStateChanged(serviceState)
+
+            assertThat(latest?.isEmergencyOnly).isEqualTo(false)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_signalStrengths_levelsUpdate() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<SignalStrengthsListener>()
+            val strength = signalStrength(1, 2, true)
+            callback.onSignalStrengthsChanged(strength)
+
+            assertThat(latest?.isGsm).isEqualTo(true)
+            assertThat(latest?.primaryLevel).isEqualTo(1)
+            assertThat(latest?.cdmaLevel).isEqualTo(2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataConnectionState() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(100, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState).isEqualTo(100)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataActivity() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<DataActivityListener>()
+            callback.onDataActivity(3)
+
+            assertThat(latest?.dataActivityDirection).isEqualTo(3)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_carrierNetworkChange() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<CarrierNetworkListener>()
+            callback.onCarrierNetworkChange(true)
+
+            assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_displayInfo() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<DisplayInfoListener>()
+            val ti = mock<TelephonyDisplayInfo>()
+            callback.onDisplayInfoChanged(ti)
+
+            assertThat(latest?.displayInfo).isEqualTo(ti)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_isCached() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            val state1 = underTest.getFlowForSubId(SUB_1_ID)
+            val state2 = underTest.getFlowForSubId(SUB_1_ID)
+
+            assertThat(state1).isEqualTo(state2)
+        }
+
+    @Test
+    fun testFlowForSubId_isRemovedAfterFinish() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
+
+            var latest: MobileSubscriptionModel? = null
+
+            // Start collecting on some flow
+            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
+
+            // There should be once cached flow now
+            assertThat(underTest.getSubIdFlowCache().size).isEqualTo(1)
+
+            // When the job is canceled, the cache should be cleared
+            job.cancel()
+
+            assertThat(underTest.getSubIdFlowCache().size).isEqualTo(0)
+        }
+
+    private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
+        val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
+        verify(subscriptionManager)
+            .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+
+    private fun getActiveDataSubscriptionCallback(): ActiveDataSubscriptionIdListener =
+        getTelephonyCallbackForType()
+
+    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
+        val callbackCaptor = argumentCaptor<TelephonyCallback>()
+        verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.allValues
+    }
+
+    private inline fun <reified T> getTelephonyCallbackForType(): T {
+        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
+        assertThat(cbs.size).isEqualTo(1)
+        return cbs[0]
+    }
+
+    /** Convenience constructor for SignalStrength */
+    private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
+        val signalStrength = mock<SignalStrength>()
+        whenever(signalStrength.isGsm).thenReturn(isGsm)
+        whenever(signalStrength.level).thenReturn(gsmLevel)
+        val cdmaStrength =
+            mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
+        whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
+            .thenReturn(listOf(cdmaStrength))
+
+        return signalStrength
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private const val SUB_2_ID = 2
+        private val SUB_2 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt
new file mode 100644
index 0000000..91c233a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class UserSetupRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: UserSetupRepository
+    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+    private val scope = CoroutineScope(IMMEDIATE)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            UserSetupRepositoryImpl(
+                deviceProvisionedController,
+                IMMEDIATE,
+                scope,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun testUserSetup_defaultFalse() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+
+            val job = underTest.isUserSetupFlow.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun testUserSetup_updatesOnChange() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+
+            val job = underTest.isUserSetupFlow.onEach { latest = it }.launchIn(this)
+
+            whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
+            val callback = getDeviceProvisionedListener()
+            callback.onUserSetupChanged()
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    private fun getDeviceProvisionedListener(): DeviceProvisionedListener {
+        val captor = argumentCaptor<DeviceProvisionedListener>()
+        verify(deviceProvisionedController).addCallback(captor.capture())
+        return captor.value!!
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
new file mode 100644
index 0000000..8ec68f3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.CellSignalStrength
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileIconInteractor : MobileIconInteractor {
+    private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN)
+    override val iconGroup = _iconGroup
+
+    private val _isEmergencyOnly = MutableStateFlow<Boolean>(false)
+    override val isEmergencyOnly = _isEmergencyOnly
+
+    private val _level = MutableStateFlow<Int>(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+    override val level = _level
+
+    private val _numberOfLevels = MutableStateFlow<Int>(4)
+    override val numberOfLevels = _numberOfLevels
+
+    private val _cutOut = MutableStateFlow<Boolean>(false)
+    override val cutOut = _cutOut
+
+    fun setIconGroup(group: SignalIcon.MobileIconGroup) {
+        _iconGroup.value = group
+    }
+
+    fun setIsEmergencyOnly(emergency: Boolean) {
+        _isEmergencyOnly.value = emergency
+    }
+
+    fun setLevel(level: Int) {
+        _level.value = level
+    }
+
+    fun setNumberOfLevels(num: Int) {
+        _numberOfLevels.value = num
+    }
+
+    fun setCutOut(cutOut: Boolean) {
+        _cutOut.value = cutOut
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
new file mode 100644
index 0000000..2f07d9c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.CellSignalStrength
+import android.telephony.SubscriptionInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class MobileIconInteractorTest : SysuiTestCase() {
+    private lateinit var underTest: MobileIconInteractor
+    private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository()
+    private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID)
+
+    @Before
+    fun setUp() {
+        underTest = MobileIconInteractorImpl(sub1Flow)
+    }
+
+    @Test
+    fun gsm_level_default_unknown() =
+        runBlocking(IMMEDIATE) {
+            mobileSubscriptionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(isGsm = true),
+                SUB_1_ID
+            )
+
+            var latest: Int? = null
+            val job = underTest.level.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+
+            job.cancel()
+        }
+
+    @Test
+    fun gsm_usesGsmLevel() =
+        runBlocking(IMMEDIATE) {
+            mobileSubscriptionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(
+                    isGsm = true,
+                    primaryLevel = GSM_LEVEL,
+                    cdmaLevel = CDMA_LEVEL
+                ),
+                SUB_1_ID
+            )
+
+            var latest: Int? = null
+            val job = underTest.level.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(GSM_LEVEL)
+
+            job.cancel()
+        }
+
+    @Test
+    fun cdma_level_default_unknown() =
+        runBlocking(IMMEDIATE) {
+            mobileSubscriptionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(isGsm = false),
+                SUB_1_ID
+            )
+
+            var latest: Int? = null
+            val job = underTest.level.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+            job.cancel()
+        }
+
+    @Test
+    fun cdma_usesCdmaLevel() =
+        runBlocking(IMMEDIATE) {
+            mobileSubscriptionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(
+                    isGsm = false,
+                    primaryLevel = GSM_LEVEL,
+                    cdmaLevel = CDMA_LEVEL
+                ),
+                SUB_1_ID
+            )
+
+            var latest: Int? = null
+            val job = underTest.level.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(CDMA_LEVEL)
+
+            job.cancel()
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+
+        private const val GSM_LEVEL = 1
+        private const val CDMA_LEVEL = 2
+
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private const val SUB_2_ID = 2
+        private val SUB_2 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
new file mode 100644
index 0000000..89ad9cb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.SubscriptionInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
+import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class MobileIconsInteractorTest : SysuiTestCase() {
+    private lateinit var underTest: MobileIconsInteractor
+    private val userSetupRepository = FakeUserSetupRepository()
+    private val subscriptionsRepository = FakeMobileSubscriptionRepository()
+
+    @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            MobileIconsInteractor(
+                subscriptionsRepository,
+                carrierConfigTracker,
+                userSetupRepository,
+            )
+    }
+
+    @After fun tearDown() {}
+
+    @Test
+    fun filteredSubscriptions_default() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(listOf<SubscriptionInfo>())
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() =
+        runBlocking(IMMEDIATE) {
+            subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_3() =
+        runBlocking(IMMEDIATE) {
+            subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
+            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+            whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+                .thenReturn(false)
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            // Filtered subscriptions should show the active one when the config is false
+            assertThat(latest).isEqualTo(listOf(SUB_3_OPP))
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_4() =
+        runBlocking(IMMEDIATE) {
+            subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
+            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
+            whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+                .thenReturn(false)
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            // Filtered subscriptions should show the active one when the config is false
+            assertThat(latest).isEqualTo(listOf(SUB_4_OPP))
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_active_1() =
+        runBlocking(IMMEDIATE) {
+            subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
+            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
+            whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+                .thenReturn(true)
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            // Filtered subscriptions should show the primary (non-opportunistic) if the config is
+            // true
+            assertThat(latest).isEqualTo(listOf(SUB_1))
+
+            job.cancel()
+        }
+
+    @Test
+    fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_nonActive_1() =
+        runBlocking(IMMEDIATE) {
+            subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
+            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+            whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+                .thenReturn(true)
+
+            var latest: List<SubscriptionInfo>? = null
+            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+
+            // Filtered subscriptions should show the primary (non-opportunistic) if the config is
+            // true
+            assertThat(latest).isEqualTo(listOf(SUB_1))
+
+            job.cancel()
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private const val SUB_2_ID = 2
+        private val SUB_2 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+
+        private const val SUB_3_ID = 3
+        private val SUB_3_OPP =
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_3_ID)
+                whenever(it.isOpportunistic).thenReturn(true)
+            }
+
+        private const val SUB_4_ID = 4
+        private val SUB_4_OPP =
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_4_ID)
+                whenever(it.isOpportunistic).thenReturn(true)
+            }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
new file mode 100644
index 0000000..b374abb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.settingslib.graph.SignalDrawable
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class MobileIconViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: MobileIconViewModel
+    private val interactor = FakeMobileIconInteractor()
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        interactor.apply {
+            setLevel(1)
+            setCutOut(false)
+            setIconGroup(TelephonyIcons.THREE_G)
+            setIsEmergencyOnly(false)
+            setNumberOfLevels(4)
+        }
+        underTest = MobileIconViewModel(SUB_1_ID, interactor, logger)
+    }
+
+    @Test
+    fun iconId_correctLevel_notCutout() =
+        runBlocking(IMMEDIATE) {
+            var latest: Int? = null
+            val job = underTest.iconId.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(SignalDrawable.getState(1, 4, false))
+
+            job.cancel()
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SUB_1_ID = 1
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index 921b7ef..7cb2806 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -61,6 +61,8 @@
     @Mock
     private lateinit var powerManager: PowerManager
 
+    private var shouldIgnoreViewRemoval: Boolean = false
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -205,6 +207,26 @@
         verify(windowManager, never()).removeView(any())
     }
 
+    @Test
+    fun removeView_shouldIgnoreRemovalFalse_viewRemoved() {
+        shouldIgnoreViewRemoval = false
+        underTest.displayView(getState())
+
+        underTest.removeView("reason")
+
+        verify(windowManager).removeView(any())
+    }
+
+    @Test
+    fun removeView_shouldIgnoreRemovalTrue_viewNotRemoved() {
+        shouldIgnoreViewRemoval = true
+        underTest.displayView(getState())
+
+        underTest.removeView("reason")
+
+        verify(windowManager, never()).removeView(any())
+    }
+
     private fun getState(name: String = "name") = ViewInfo(name)
 
     private fun getConfigurationListener(): ConfigurationListener {
@@ -240,6 +262,10 @@
             super.updateView(newInfo, currentView)
             mostRecentViewInfo = newInfo
         }
+
+        override fun shouldIgnoreViewRemoval(removalReason: String): Boolean {
+            return shouldIgnoreViewRemoval
+        }
     }
 
     inner class ViewInfo(val name: String) : TemporaryViewInfo {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
index 2be67ed..23c7a61 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
@@ -70,6 +70,10 @@
     }
 
     @Override
+    public void setNewMobileIconSubIds(List<Integer> subIds) {
+    }
+
+    @Override
     public void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states) {
     }
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 0d08db9..736914a 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1365,7 +1365,8 @@
                     break;
                 case MSG_II_SET_HEARING_AID_VOLUME:
                     synchronized (mDeviceStateLock) {
-                        mBtHelper.setHearingAidVolume(msg.arg1, msg.arg2);
+                        mBtHelper.setHearingAidVolume(msg.arg1, msg.arg2,
+                                mDeviceInventory.isHearingAidConnected());
                     }
                     break;
                 case MSG_II_SET_LE_AUDIO_OUT_VOLUME: {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index ee0d79f..35da73e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -377,7 +377,8 @@
                         makeLeAudioDeviceUnavailable(address, btInfo.mAudioSystemDevice);
                     } else if (switchToAvailable) {
                         makeLeAudioDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
-                                streamType, btInfo.mVolume, btInfo.mAudioSystemDevice,
+                                streamType, btInfo.mVolume == -1 ? -1 : btInfo.mVolume * 10,
+                                btInfo.mAudioSystemDevice,
                                 "onSetBtActiveDevice");
                     }
                     break;
@@ -1161,6 +1162,22 @@
                 .record();
     }
 
+    /**
+     * Returns whether a device of type DEVICE_OUT_HEARING_AID is connected.
+     * Visibility by APM plays no role
+     * @return true if a DEVICE_OUT_HEARING_AID is connected, false otherwise.
+     */
+    boolean isHearingAidConnected() {
+        synchronized (mDevicesLock) {
+            for (DeviceInfo di : mConnectedDevices.values()) {
+                if (di.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
     @GuardedBy("mDevicesLock")
     private void makeLeAudioDeviceAvailable(String address, String name, int streamType,
             int volumeIndex, int device, String eventSource) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0b6b890..745555c 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -41,10 +41,12 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.app.AlarmManager;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
 import android.app.IUidObserver;
 import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.app.role.OnRoleHoldersChangedListener;
 import android.app.role.RoleManager;
 import android.bluetooth.BluetoothAdapter;
@@ -1185,6 +1187,8 @@
         mSafeMediaVolumeIndex = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_safe_media_volume_index) * 10;
 
+        mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+
         mUseFixedVolume = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_useFixedVolume);
 
@@ -1202,7 +1206,7 @@
         mPlaybackMonitor =
                 new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM],
                         device -> onMuteAwaitConnectionTimeout(device));
-        mPlaybackMonitor.registerPlaybackCallback(mVoicePlaybackActivityMonitor, true);
+        mPlaybackMonitor.registerPlaybackCallback(mPlaybackActivityMonitor, true);
 
         mMediaFocusControl = new MediaFocusControl(mContext, mPlaybackMonitor);
 
@@ -1308,6 +1312,7 @@
 
         intentFilter.addAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
         intentFilter.addAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
+        intentFilter.addAction(ACTION_CHECK_MUSIC_ACTIVE);
 
         mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null,
                 Context.RECEIVER_EXPORTED);
@@ -1927,13 +1932,7 @@
         if (state == AudioService.CONNECTION_STATE_CONNECTED) {
             // DEVICE_OUT_HDMI is now connected
             if (mSafeMediaVolumeDevices.contains(AudioSystem.DEVICE_OUT_HDMI)) {
-                sendMsg(mAudioHandler,
-                        MSG_CHECK_MUSIC_ACTIVE,
-                        SENDMSG_REPLACE,
-                        0,
-                        0,
-                        caller,
-                        MUSIC_ACTIVE_POLL_PERIOD_MS);
+                scheduleMusicActiveCheck();
             }
 
             if (isPlatformTelevision()) {
@@ -3822,8 +3821,9 @@
     }
 
     private AtomicBoolean mVoicePlaybackActive = new AtomicBoolean(false);
+    private AtomicBoolean mMediaPlaybackActive = new AtomicBoolean(false);
 
-    private final IPlaybackConfigDispatcher mVoicePlaybackActivityMonitor =
+    private final IPlaybackConfigDispatcher mPlaybackActivityMonitor =
             new IPlaybackConfigDispatcher.Stub() {
         @Override
         public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs,
@@ -3836,19 +3836,26 @@
 
     private void onPlaybackConfigChange(List<AudioPlaybackConfiguration> configs) {
         boolean voiceActive = false;
+        boolean mediaActive = false;
         for (AudioPlaybackConfiguration config : configs) {
             final int usage = config.getAudioAttributes().getUsage();
-            if ((usage == AudioAttributes.USAGE_VOICE_COMMUNICATION
-                    || usage == AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
-                    && config.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
+            if (!config.isActive()) {
+                continue;
+            }
+            if (usage == AudioAttributes.USAGE_VOICE_COMMUNICATION
+                    || usage == AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) {
                 voiceActive = true;
-                break;
+            }
+            if (usage == AudioAttributes.USAGE_MEDIA || usage == AudioAttributes.USAGE_GAME) {
+                mediaActive = true;
             }
         }
         if (mVoicePlaybackActive.getAndSet(voiceActive) != voiceActive) {
             updateHearingAidVolumeOnVoiceActivityUpdate();
         }
-
+        if (mMediaPlaybackActive.getAndSet(mediaActive) != mediaActive && mediaActive) {
+            scheduleMusicActiveCheck();
+        }
         // Update playback active state for all apps in audio mode stack.
         // When the audio mode owner becomes active, replace any delayed MSG_UPDATE_AUDIO_MODE
         // and request an audio mode update immediately. Upon any other change, queue the message
@@ -4011,7 +4018,7 @@
         }
     }
 
-    private void setLeAudioVolumeOnModeUpdate(int mode) {
+    private void setLeAudioVolumeOnModeUpdate(int mode, int streamType, int device) {
         switch (mode) {
             case AudioSystem.MODE_IN_COMMUNICATION:
             case AudioSystem.MODE_IN_CALL:
@@ -4025,8 +4032,6 @@
                 return;
         }
 
-        int streamType = getBluetoothContextualVolumeStream(mode);
-
         // Currently, DEVICE_OUT_BLE_HEADSET is the only output type for LE_AUDIO profile.
         // (See AudioDeviceBroker#createBtDeviceInfo())
         int index = mStreamStates[streamType].getIndex(AudioSystem.DEVICE_OUT_BLE_HEADSET);
@@ -4037,6 +4042,7 @@
                     + index + " maxIndex=" + maxIndex + " streamType=" + streamType);
         }
         mDeviceBroker.postSetLeAudioVolumeIndex(index, maxIndex, streamType);
+        mDeviceBroker.postApplyVolumeOnDevice(streamType, device, "setLeAudioVolumeOnModeUpdate");
     }
 
     private void setStreamVolume(int streamType, int index, int flags,
@@ -5417,7 +5423,7 @@
 
                 // Forcefully set LE audio volume as a workaround, since the value of 'device'
                 // is not DEVICE_OUT_BLE_* even when BLE is connected.
-                setLeAudioVolumeOnModeUpdate(mode);
+                setLeAudioVolumeOnModeUpdate(mode, streamType, device);
 
                 // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
                 // connections not started by the application changing the mode when pid changes
@@ -6032,30 +6038,52 @@
         return mContentResolver;
     }
 
+    private void scheduleMusicActiveCheck() {
+        synchronized (mSafeMediaVolumeStateLock) {
+            cancelMusicActiveCheck();
+            mMusicActiveIntent = PendingIntent.getBroadcast(mContext,
+                REQUEST_CODE_CHECK_MUSIC_ACTIVE,
+                new Intent(ACTION_CHECK_MUSIC_ACTIVE),
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+            mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                    SystemClock.elapsedRealtime()
+                    + MUSIC_ACTIVE_POLL_PERIOD_MS, mMusicActiveIntent);
+        }
+    }
+
+    private void cancelMusicActiveCheck() {
+        synchronized (mSafeMediaVolumeStateLock) {
+            if (mMusicActiveIntent != null) {
+                mAlarmManager.cancel(mMusicActiveIntent);
+                mMusicActiveIntent = null;
+            }
+        }
+    }
     private void onCheckMusicActive(String caller) {
         synchronized (mSafeMediaVolumeStateLock) {
             if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE) {
                 int device = getDeviceForStream(AudioSystem.STREAM_MUSIC);
-
-                if (mSafeMediaVolumeDevices.contains(device)) {
-                    sendMsg(mAudioHandler,
-                            MSG_CHECK_MUSIC_ACTIVE,
-                            SENDMSG_REPLACE,
-                            0,
-                            0,
-                            caller,
-                            MUSIC_ACTIVE_POLL_PERIOD_MS);
+                if (mSafeMediaVolumeDevices.contains(device)
+                        && mAudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) {
+                    scheduleMusicActiveCheck();
                     int index = mStreamStates[AudioSystem.STREAM_MUSIC].getIndex(device);
-                    if (mAudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)
-                            && (index > safeMediaVolumeIndex(device))) {
+                    if (index > safeMediaVolumeIndex(device)) {
                         // Approximate cumulative active music time
-                        mMusicActiveMs += MUSIC_ACTIVE_POLL_PERIOD_MS;
+                        long curTimeMs = SystemClock.elapsedRealtime();
+                        if (mLastMusicActiveTimeMs != 0) {
+                            mMusicActiveMs += (int) (curTimeMs - mLastMusicActiveTimeMs);
+                        }
+                        mLastMusicActiveTimeMs = curTimeMs;
+                        Log.i(TAG, "onCheckMusicActive() mMusicActiveMs: " + mMusicActiveMs);
                         if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) {
                             setSafeMediaVolumeEnabled(true, caller);
                             mMusicActiveMs = 0;
                         }
                         saveMusicActiveMs();
                     }
+                } else {
+                    cancelMusicActiveCheck();
+                    mLastMusicActiveTimeMs = 0;
                 }
             }
         }
@@ -6124,6 +6152,7 @@
                         } else {
                             // We have existing playback time recorded, already confirmed.
                             mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
+                            mLastMusicActiveTimeMs = 0;
                         }
                     }
                 } else {
@@ -8633,13 +8662,7 @@
     @VisibleForTesting
     public void checkMusicActive(int deviceType, String caller) {
         if (mSafeMediaVolumeDevices.contains(deviceType)) {
-            sendMsg(mAudioHandler,
-                    MSG_CHECK_MUSIC_ACTIVE,
-                    SENDMSG_REPLACE,
-                    0,
-                    0,
-                    caller,
-                    MUSIC_ACTIVE_POLL_PERIOD_MS);
+            scheduleMusicActiveCheck();
         }
     }
 
@@ -8764,6 +8787,8 @@
                                 suspendedPackages[i], suspendedUids[i]);
                     }
                 }
+            } else if (action.equals(ACTION_CHECK_MUSIC_ACTIVE)) {
+                onCheckMusicActive(ACTION_CHECK_MUSIC_ACTIVE);
             }
         }
     } // end class AudioServiceBroadcastReceiver
@@ -9709,12 +9734,20 @@
     // When this time reaches UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX, the safe media volume is re-enabled
     // automatically. mMusicActiveMs is rounded to a multiple of MUSIC_ACTIVE_POLL_PERIOD_MS.
     private int mMusicActiveMs;
+    private long mLastMusicActiveTimeMs = 0;
+    private PendingIntent mMusicActiveIntent = null;
+    private AlarmManager mAlarmManager;
+
     private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours
     private static final int MUSIC_ACTIVE_POLL_PERIOD_MS = 60000;  // 1 minute polling interval
     private static final int SAFE_VOLUME_CONFIGURE_TIMEOUT_MS = 30000;  // 30s after boot completed
     // check playback or record activity every 6 seconds for UIDs owning mode IN_COMMUNICATION
     private static final int CHECK_MODE_FOR_UID_PERIOD_MS = 6000;
 
+    private static final String ACTION_CHECK_MUSIC_ACTIVE =
+            AudioService.class.getSimpleName() + ".CHECK_MUSIC_ACTIVE";
+    private static final int REQUEST_CODE_CHECK_MUSIC_ACTIVE = 1;
+
     private int safeMediaVolumeIndex(int device) {
         if (!mSafeMediaVolumeDevices.contains(device)) {
             return MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC];
@@ -9736,14 +9769,9 @@
                 } else if (!on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)) {
                     mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
                     mMusicActiveMs = 1;  // nonzero = confirmed
+                    mLastMusicActiveTimeMs = 0;
                     saveMusicActiveMs();
-                    sendMsg(mAudioHandler,
-                            MSG_CHECK_MUSIC_ACTIVE,
-                            SENDMSG_REPLACE,
-                            0,
-                            0,
-                            caller,
-                            MUSIC_ACTIVE_POLL_PERIOD_MS);
+                    scheduleMusicActiveCheck();
                 }
             }
         }
@@ -9785,7 +9813,9 @@
     public void disableSafeMediaVolume(String callingPackage) {
         enforceVolumeController("disable the safe media volume");
         synchronized (mSafeMediaVolumeStateLock) {
+            final long identity = Binder.clearCallingIdentity();
             setSafeMediaVolumeEnabled(false, callingPackage);
+            Binder.restoreCallingIdentity(identity);
             if (mPendingVolumeCommand != null) {
                 onSetStreamVolume(mPendingVolumeCommand.mStreamType,
                                   mPendingVolumeCommand.mIndex,
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index d0f5470..6cd42f8 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -424,7 +424,8 @@
         mLeAudio.setVolume(volume);
     }
 
-    /*package*/ synchronized void setHearingAidVolume(int index, int streamType) {
+    /*package*/ synchronized void setHearingAidVolume(int index, int streamType,
+            boolean isHeadAidConnected) {
         if (mHearingAid == null) {
             if (AudioService.DEBUG_VOL) {
                 Log.i(TAG, "setHearingAidVolume: null mHearingAid");
@@ -441,8 +442,11 @@
             Log.i(TAG, "setHearingAidVolume: calling mHearingAid.setVolume idx="
                     + index + " gain=" + gainDB);
         }
-        AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
-                AudioServiceEvents.VolumeEvent.VOL_SET_HEARING_AID_VOL, index, gainDB));
+        // do not log when hearing aid is not connected to avoid confusion when reading dumpsys
+        if (isHeadAidConnected) {
+            AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
+                    AudioServiceEvents.VolumeEvent.VOL_SET_HEARING_AID_VOL, index, gainDB));
+        }
         mHearingAid.setVolume(gainDB);
     }
 
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 5a2fb18..dad9584 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -571,7 +571,8 @@
     /**
      * Called when there has been user activity.
      */
-    public void onUserActivity(int displayGroupId, int event, int uid) {
+    public void onUserActivity(int displayGroupId, @PowerManager.UserActivityEvent int event,
+            int uid) {
         if (DEBUG) {
             Slog.d(TAG, "onUserActivity: event=" + event + ", uid=" + uid);
         }
diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java
index fec61ac..9fe53fb 100644
--- a/services/core/java/com/android/server/power/PowerGroup.java
+++ b/services/core/java/com/android/server/power/PowerGroup.java
@@ -74,6 +74,8 @@
     private long mLastPowerOnTime;
     private long mLastUserActivityTime;
     private long mLastUserActivityTimeNoChangeLights;
+    @PowerManager.UserActivityEvent
+    private int mLastUserActivityEvent;
     /** Timestamp (milliseconds since boot) of the last time the power group was awoken.*/
     private long mLastWakeTime;
     /** Timestamp (milliseconds since boot) of the last time the power group was put to sleep. */
@@ -244,7 +246,7 @@
         return true;
     }
 
-    boolean dozeLocked(long eventTime, int uid, int reason) {
+    boolean dozeLocked(long eventTime, int uid, @PowerManager.GoToSleepReason int reason) {
         if (eventTime < getLastWakeTimeLocked() || !isInteractive(mWakefulness)) {
             return false;
         }
@@ -253,9 +255,14 @@
         try {
             reason = Math.min(PowerManager.GO_TO_SLEEP_REASON_MAX,
                     Math.max(reason, PowerManager.GO_TO_SLEEP_REASON_MIN));
+            long millisSinceLastUserActivity = eventTime - Math.max(
+                    mLastUserActivityTimeNoChangeLights, mLastUserActivityTime);
             Slog.i(TAG, "Powering off display group due to "
-                    + PowerManager.sleepReasonToString(reason)  + " (groupId= " + getGroupId()
-                    + ", uid= " + uid + ")...");
+                    + PowerManager.sleepReasonToString(reason)
+                    + " (groupId= " + getGroupId() + ", uid= " + uid
+                    + ", millisSinceLastUserActivity=" + millisSinceLastUserActivity
+                    + ", lastUserActivityEvent=" + PowerManager.userActivityEventToString(
+                    mLastUserActivityEvent) + ")...");
 
             setSandmanSummonedLocked(/* isSandmanSummoned= */ true);
             setWakefulnessLocked(WAKEFULNESS_DOZING, eventTime, uid, reason, /* opUid= */ 0,
@@ -266,14 +273,16 @@
         return true;
     }
 
-    boolean sleepLocked(long eventTime, int uid, int reason) {
+    boolean sleepLocked(long eventTime, int uid, @PowerManager.GoToSleepReason int reason) {
         if (eventTime < mLastWakeTime || getWakefulnessLocked() == WAKEFULNESS_ASLEEP) {
             return false;
         }
 
         Trace.traceBegin(Trace.TRACE_TAG_POWER, "sleepPowerGroup");
         try {
-            Slog.i(TAG, "Sleeping power group (groupId=" + getGroupId() + ", uid=" + uid + ")...");
+            Slog.i(TAG,
+                    "Sleeping power group (groupId=" + getGroupId() + ", uid=" + uid + ", reason="
+                            + PowerManager.sleepReasonToString(reason) + ")...");
             setSandmanSummonedLocked(/* isSandmanSummoned= */ true);
             setWakefulnessLocked(WAKEFULNESS_ASLEEP, eventTime, uid, reason, /* opUid= */0,
                     /* opPackageName= */ null, /* details= */ null);
@@ -287,16 +296,20 @@
         return mLastUserActivityTime;
     }
 
-    void setLastUserActivityTimeLocked(long lastUserActivityTime) {
+    void setLastUserActivityTimeLocked(long lastUserActivityTime,
+            @PowerManager.UserActivityEvent int event) {
         mLastUserActivityTime = lastUserActivityTime;
+        mLastUserActivityEvent = event;
     }
 
     public long getLastUserActivityTimeNoChangeLightsLocked() {
         return mLastUserActivityTimeNoChangeLights;
     }
 
-    public void setLastUserActivityTimeNoChangeLightsLocked(long time) {
+    public void setLastUserActivityTimeNoChangeLightsLocked(long time,
+            @PowerManager.UserActivityEvent int event) {
         mLastUserActivityTimeNoChangeLights = time;
+        mLastUserActivityEvent = event;
     }
 
     public int getUserActivitySummaryLocked() {
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index dbf05f1..3a05fc3 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -1169,6 +1169,7 @@
                 return;
             }
 
+            Slog.i(TAG, "onFlip(): Face " + (isFaceDown ? "down." : "up."));
             mIsFaceDown = isFaceDown;
             if (isFaceDown) {
                 final long currentTime = mClock.uptimeMillis();
@@ -1888,12 +1889,13 @@
 
     // Called from native code.
     @SuppressWarnings("unused")
-    private void userActivityFromNative(long eventTime, int event, int displayId, int flags) {
+    private void userActivityFromNative(long eventTime, @PowerManager.UserActivityEvent int event,
+            int displayId, int flags) {
         userActivityInternal(displayId, eventTime, event, flags, Process.SYSTEM_UID);
     }
 
-    private void userActivityInternal(int displayId, long eventTime, int event, int flags,
-            int uid) {
+    private void userActivityInternal(int displayId, long eventTime,
+            @PowerManager.UserActivityEvent int event, int flags, int uid) {
         synchronized (mLock) {
             if (displayId == Display.INVALID_DISPLAY) {
                 if (userActivityNoUpdateLocked(eventTime, event, flags, uid)) {
@@ -1944,11 +1946,12 @@
 
     @GuardedBy("mLock")
     private boolean userActivityNoUpdateLocked(final PowerGroup powerGroup, long eventTime,
-            int event, int flags, int uid) {
+            @PowerManager.UserActivityEvent int event, int flags, int uid) {
         final int groupId = powerGroup.getGroupId();
         if (DEBUG_SPEW) {
             Slog.d(TAG, "userActivityNoUpdateLocked: groupId=" + groupId
-                    + ", eventTime=" + eventTime + ", event=" + event
+                    + ", eventTime=" + eventTime
+                    + ", event=" + PowerManager.userActivityEventToString(event)
                     + ", flags=0x" + Integer.toHexString(flags) + ", uid=" + uid);
         }
 
@@ -1983,7 +1986,7 @@
             if ((flags & PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS) != 0) {
                 if (eventTime > powerGroup.getLastUserActivityTimeNoChangeLightsLocked()
                         && eventTime > powerGroup.getLastUserActivityTimeLocked()) {
-                    powerGroup.setLastUserActivityTimeNoChangeLightsLocked(eventTime);
+                    powerGroup.setLastUserActivityTimeNoChangeLightsLocked(eventTime, event);
                     mDirty |= DIRTY_USER_ACTIVITY;
                     if (event == PowerManager.USER_ACTIVITY_EVENT_BUTTON) {
                         mDirty |= DIRTY_QUIESCENT;
@@ -1993,7 +1996,7 @@
                 }
             } else {
                 if (eventTime > powerGroup.getLastUserActivityTimeLocked()) {
-                    powerGroup.setLastUserActivityTimeLocked(eventTime);
+                    powerGroup.setLastUserActivityTimeLocked(eventTime, event);
                     mDirty |= DIRTY_USER_ACTIVITY;
                     if (event == PowerManager.USER_ACTIVITY_EVENT_BUTTON) {
                         mDirty |= DIRTY_QUIESCENT;
@@ -2020,7 +2023,8 @@
             @WakeReason int reason, String details, int uid, String opPackageName, int opUid) {
         if (DEBUG_SPEW) {
             Slog.d(TAG, "wakePowerGroupLocked: eventTime=" + eventTime
-                    + ", groupId=" + powerGroup.getGroupId() + ", uid=" + uid);
+                    + ", groupId=" + powerGroup.getGroupId()
+                    + ", reason=" + PowerManager.wakeReasonToString(reason) + ", uid=" + uid);
         }
         if (mForceSuspendActive || !mSystemReady) {
             return;
@@ -2043,11 +2047,11 @@
 
     @GuardedBy("mLock")
     private boolean dozePowerGroupLocked(final PowerGroup powerGroup, long eventTime,
-            int reason, int uid) {
+            @GoToSleepReason int reason, int uid) {
         if (DEBUG_SPEW) {
             Slog.d(TAG, "dozePowerGroup: eventTime=" + eventTime
-                    + ", groupId=" + powerGroup.getGroupId() + ", reason=" + reason
-                    + ", uid=" + uid);
+                    + ", groupId=" + powerGroup.getGroupId()
+                    + ", reason=" + PowerManager.sleepReasonToString(reason) + ", uid=" + uid);
         }
 
         if (!mSystemReady || !mBootCompleted) {
@@ -2058,10 +2062,12 @@
     }
 
     @GuardedBy("mLock")
-    private boolean sleepPowerGroupLocked(final PowerGroup powerGroup, long eventTime, int reason,
-            int uid) {
+    private boolean sleepPowerGroupLocked(final PowerGroup powerGroup, long eventTime,
+            @GoToSleepReason int reason, int uid) {
         if (DEBUG_SPEW) {
-            Slog.d(TAG, "sleepPowerGroup: eventTime=" + eventTime + ", uid=" + uid);
+            Slog.d(TAG, "sleepPowerGroup: eventTime=" + eventTime
+                    + ", groupId=" + powerGroup.getGroupId()
+                    + ", reason=" + PowerManager.sleepReasonToString(reason) + ", uid=" + uid);
         }
         if (!mBootCompleted || !mSystemReady) {
             return false;
@@ -2122,8 +2128,10 @@
             case WAKEFULNESS_DOZING:
                 traceMethodName = "goToSleep";
                 Slog.i(TAG, "Going to sleep due to " + PowerManager.sleepReasonToString(reason)
-                        + " (uid " + uid + ")...");
-
+                        + " (uid " + uid + ", screenOffTimeout=" + mScreenOffTimeoutSetting
+                        + ", activityTimeoutWM=" + mUserActivityTimeoutOverrideFromWindowManager
+                        + ", maxDimRatio=" + mMaximumScreenDimRatioConfig
+                        + ", maxDimDur=" + mMaximumScreenDimDurationConfig + ")...");
                 mLastGlobalSleepTime = eventTime;
                 mLastGlobalSleepReason = reason;
                 mDozeStartInProgress = true;
@@ -4207,7 +4215,7 @@
     void onUserActivity() {
         synchronized (mLock) {
             mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP).setLastUserActivityTimeLocked(
-                    mClock.uptimeMillis());
+                    mClock.uptimeMillis(), PowerManager.USER_ACTIVITY_EVENT_OTHER);
         }
     }
 
@@ -5590,7 +5598,8 @@
         }
 
         @Override // Binder call
-        public void userActivity(int displayId, long eventTime, int event, int flags) {
+        public void userActivity(int displayId, long eventTime,
+                @PowerManager.UserActivityEvent int event, int flags) {
             final long now = mClock.uptimeMillis();
             if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER)
                     != PackageManager.PERMISSION_GRANTED
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index f888ff6..7170773 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -687,7 +687,7 @@
      */
     public void lockUser(int userId) {
         mLockPatternUtils.requireStrongAuth(
-                StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, userId);
+                StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, userId);
         try {
             WindowManagerGlobal.getWindowManagerService().lockNow(null);
         } catch (RemoteException e) {
@@ -2084,7 +2084,7 @@
             if (mStrongAuthTracker.isTrustAllowedForUser(mUserId)) {
                 if (DEBUG) Slog.d(TAG, "Revoking all trust because of trust timeout");
                 mLockPatternUtils.requireStrongAuth(
-                        mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, mUserId);
+                        mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, mUserId);
             }
             maybeLockScreen(mUserId);
         }
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 0707b81..30454d4 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -50,6 +50,7 @@
 import android.util.SparseArray;
 import android.view.RemoteAnimationAdapter;
 import android.view.WindowManager;
+import android.window.RemoteTransition;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
@@ -563,14 +564,39 @@
             return false;
         }
         mService.mRootWindowContainer.startPowerModeLaunchIfNeeded(true /* forceSend */, r);
-        final ActivityMetricsLogger.LaunchingState launchingState =
-                mSupervisor.getActivityMetricsLogger().notifyActivityLaunching(intent);
-        final Task task = r.getTask();
-        mService.deferWindowLayout();
-        try {
+        final RemoteTransition remote = options.getRemoteTransition();
+        if (remote != null && rootTask.mTransitionController.isCollecting()) {
+            final Transition transition = new Transition(WindowManager.TRANSIT_TO_FRONT,
+                    0 /* flags */, rootTask.mTransitionController,
+                    mService.mWindowManager.mSyncEngine);
+            // Special case: we are entering recents while an existing transition is running. In
+            // this case, we know it's safe to "defer" the activity launch, so lets do so now so
+            // that it can get its own transition and thus update launcher correctly.
+            mService.mWindowManager.mSyncEngine.queueSyncSet(
+                    () -> rootTask.mTransitionController.moveToCollecting(transition),
+                    () -> {
+                        final Task task = r.getTask();
+                        task.mTransitionController.requestStartTransition(transition,
+                                task, remote, null /* displayChange */);
+                        task.mTransitionController.collect(task);
+                        startExistingRecentsIfPossibleInner(intent, options, r, task, rootTask);
+                    });
+        } else {
+            final Task task = r.getTask();
             task.mTransitionController.requestTransitionIfNeeded(WindowManager.TRANSIT_TO_FRONT,
                     0 /* flags */, task, task /* readyGroupRef */,
                     options.getRemoteTransition(), null /* displayChange */);
+            startExistingRecentsIfPossibleInner(intent, options, r, task, rootTask);
+        }
+        return true;
+    }
+
+    void startExistingRecentsIfPossibleInner(Intent intent, ActivityOptions options,
+            ActivityRecord r, Task task, Task rootTask) {
+        final ActivityMetricsLogger.LaunchingState launchingState =
+                mSupervisor.getActivityMetricsLogger().notifyActivityLaunching(intent);
+        mService.deferWindowLayout();
+        try {
             r.mTransitionController.setTransientLaunch(r,
                     TaskDisplayArea.getRootTaskAbove(rootTask));
             task.moveToFront("startExistingRecents");
@@ -582,7 +608,6 @@
             task.mInResumeTopActivity = false;
             mService.continueWindowLayout();
         }
-        return true;
     }
 
     void registerRemoteAnimationForNextActivityStart(String packageName,
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index b67d2ef..9f45f02 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3546,12 +3546,16 @@
      * {@link android.window.TaskFragmentOrganizer}
      */
     TaskFragmentParentInfo getTaskFragmentParentInfo() {
-        return new TaskFragmentParentInfo(getConfiguration(), getDisplayId(), isVisibleRequested());
+        return new TaskFragmentParentInfo(getConfiguration(), getDisplayId(),
+                shouldBeVisible(null /* starting */));
     }
 
     @Override
     void onActivityVisibleRequestedChanged() {
-        if (mVisibleRequested != isVisibleRequested()) {
+        final boolean prevVisibleRequested = mVisibleRequested;
+        // mVisibleRequested is updated in super method.
+        super.onActivityVisibleRequestedChanged();
+        if (prevVisibleRequested != mVisibleRequested) {
             sendTaskFragmentParentInfoChangedIfNeeded();
         }
     }
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 058a066..d178676 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2690,12 +2690,26 @@
             return;
         }
         mVisibleRequested = isVisibleRequested;
-        final TaskFragment parentTf = getParent().asTaskFragment();
+        final WindowContainer<?> parent = getParent();
+        if (parent == null) {
+            return;
+        }
+        final TaskFragment parentTf = parent.asTaskFragment();
         if (parentTf != null) {
             parentTf.onActivityVisibleRequestedChanged();
         }
     }
 
+    @Nullable
+    @Override
+    TaskFragment getTaskFragment(Predicate<TaskFragment> callback) {
+        final TaskFragment taskFragment = super.getTaskFragment(callback);
+        if (taskFragment != null) {
+            return taskFragment;
+        }
+        return callback.test(this) ? this : null;
+    }
+
     String toFullString() {
         final StringBuilder sb = new StringBuilder(128);
         sb.append(this);
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 2d5c989..867833a 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -920,6 +920,7 @@
         for (int i = 0, n = pendingEvents.size(); i < n; i++) {
             final PendingTaskFragmentEvent event = pendingEvents.get(i);
             final Task task = event.mTaskFragment != null ? event.mTaskFragment.getTask() : null;
+            // TODO(b/251132298): move visibility check to the client side.
             if (task != null && (task.lastActiveTime <= event.mDeferTime
                     || !(isTaskVisible(task, visibleTasks, invisibleTasks)
                     || shouldSendEventWhenTaskInvisible(event)))) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 1404de2..0b23359 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -195,6 +195,7 @@
         mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
+        assertTaskFragmentParentInfoChangedTransaction(mTask);
         assertTaskFragmentAppearedTransaction();
     }
 
@@ -365,6 +366,7 @@
         mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
+        assertTaskFragmentParentInfoChangedTransaction(task);
         assertActivityReparentedToTaskTransaction(task.mTaskId, activity.intent, activity.token);
     }
 
@@ -1205,7 +1207,8 @@
 
     /**
      * Creates a {@link TaskFragment} with the {@link WindowContainerTransaction}. Calls
-     * {@link WindowOrganizerController#applyTransaction} to apply the transaction,
+     * {@link WindowOrganizerController#applyTransaction(WindowContainerTransaction)} to apply the
+     * transaction,
      */
     private void createTaskFragmentFromOrganizer(WindowContainerTransaction wct,
             ActivityRecord ownerActivity, IBinder fragmentToken) {
@@ -1239,8 +1242,8 @@
         final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
         assertFalse(changes.isEmpty());
 
-        // Appeared will come with parent info changed.
-        final TaskFragmentTransaction.Change change = changes.get(changes.size() - 1);
+        // Use remove to verify multiple transaction changes.
+        final TaskFragmentTransaction.Change change = changes.remove(0);
         assertEquals(TYPE_TASK_FRAGMENT_APPEARED, change.getType());
         assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo());
         assertEquals(mFragmentToken, change.getTaskFragmentToken());
@@ -1253,8 +1256,8 @@
         final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
         assertFalse(changes.isEmpty());
 
-        // InfoChanged may come with parent info changed.
-        final TaskFragmentTransaction.Change change = changes.get(changes.size() - 1);
+        // Use remove to verify multiple transaction changes.
+        final TaskFragmentTransaction.Change change = changes.remove(0);
         assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, change.getType());
         assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo());
         assertEquals(mFragmentToken, change.getTaskFragmentToken());
@@ -1266,7 +1269,9 @@
         final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
         final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
         assertFalse(changes.isEmpty());
-        final TaskFragmentTransaction.Change change = changes.get(0);
+
+        // Use remove to verify multiple transaction changes.
+        final TaskFragmentTransaction.Change change = changes.remove(0);
         assertEquals(TYPE_TASK_FRAGMENT_VANISHED, change.getType());
         assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo());
         assertEquals(mFragmentToken, change.getTaskFragmentToken());
@@ -1278,7 +1283,9 @@
         final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
         final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
         assertFalse(changes.isEmpty());
-        final TaskFragmentTransaction.Change change = changes.get(0);
+
+        // Use remove to verify multiple transaction changes.
+        final TaskFragmentTransaction.Change change = changes.remove(0);
         assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, change.getType());
         assertEquals(task.mTaskId, change.getTaskId());
         assertEquals(task.getTaskFragmentParentInfo(), change.getTaskFragmentParentInfo());
@@ -1290,7 +1297,9 @@
         final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
         final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
         assertFalse(changes.isEmpty());
-        final TaskFragmentTransaction.Change change = changes.get(0);
+
+        // Use remove to verify multiple transaction changes.
+        final TaskFragmentTransaction.Change change = changes.remove(0);
         assertEquals(TYPE_TASK_FRAGMENT_ERROR, change.getType());
         assertEquals(mErrorToken, change.getErrorCallbackToken());
         final Bundle errorBundle = change.getErrorBundle();
@@ -1306,7 +1315,9 @@
         final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
         final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
         assertFalse(changes.isEmpty());
-        final TaskFragmentTransaction.Change change = changes.get(0);
+
+        // Use remove to verify multiple transaction changes.
+        final TaskFragmentTransaction.Change change = changes.remove(0);
         assertEquals(TYPE_ACTIVITY_REPARENTED_TO_TASK, change.getType());
         assertEquals(taskId, change.getTaskId());
         assertEquals(intent, change.getActivityIntent());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 76cd19b..68ac1d6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1454,6 +1454,21 @@
         verify(tfBehind, never()).resumeTopActivity(any(), any(), anyBoolean());
     }
 
+    @Test
+    public void testGetTaskFragment() {
+        final Task parentTask = createTask(mDisplayContent);
+        final TaskFragment tf0 = createTaskFragmentWithParentTask(parentTask);
+        final TaskFragment tf1 = createTaskFragmentWithParentTask(parentTask);
+
+        assertNull("Could not find it because there's no organized TaskFragment",
+                parentTask.getTaskFragment(TaskFragment::isOrganizedTaskFragment));
+
+        doReturn(true).when(tf0).isOrganizedTaskFragment();
+
+        assertEquals("tf0 must be return because it's the organized TaskFragment.",
+                tf0, parentTask.getTaskFragment(TaskFragment::isOrganizedTaskFragment));
+    }
+
     private Task getTestTask() {
         final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
         return task.getBottomMostTask();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 139e440..b99fd16 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -718,6 +718,10 @@
         activity.mVisibleRequested = true;
     }
 
+    static TaskFragment createTaskFragmentWithParentTask(@NonNull Task parentTask) {
+        return createTaskFragmentWithParentTask(parentTask, false /* createEmbeddedTask */);
+    }
+
     /**
      * Creates a {@link TaskFragment} and attach it to the {@code parentTask}.
      *