Merge "Create AppList for SpaPrivilegedLib"
diff --git a/api/OWNERS b/api/OWNERS
index 4d8ed03..bf6216c 100644
--- a/api/OWNERS
+++ b/api/OWNERS
@@ -3,7 +3,7 @@
 # Modularization team
 file:platform/packages/modules/common:/OWNERS
 
-per-file Android.bp = file:platform/build/soong:/OWNERS
+per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
 
 # For metalava team to disable lint checks in platform
-per-file Android.bp = aurimas@google.com,emberrose@google.com,sjgilbert@google.com
\ No newline at end of file
+per-file Android.bp = aurimas@google.com,emberrose@google.com,sjgilbert@google.com
diff --git a/config/preloaded-classes-denylist b/config/preloaded-classes-denylist
index 02f2df6..502d8c6 100644
--- a/config/preloaded-classes-denylist
+++ b/config/preloaded-classes-denylist
@@ -9,3 +9,4 @@
 android.net.rtp.AudioStream
 android.net.rtp.RtpStream
 java.util.concurrent.ThreadLocalRandom
+com.android.internal.jank.InteractionJankMonitor$InstanceHolder
diff --git a/core/api/current.txt b/core/api/current.txt
index e043339..795c430 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -52986,6 +52986,22 @@
     method public android.view.inputmethod.CursorAnchorInfo.Builder setSelectionRange(int, int);
   }
 
+  public final class DeleteGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.graphics.RectF getDeletionArea();
+    method public int getGranularity();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.DeleteGesture> CREATOR;
+  }
+
+  public static final class DeleteGesture.Builder {
+    ctor public DeleteGesture.Builder();
+    method @NonNull public android.view.inputmethod.DeleteGesture build();
+    method @NonNull public android.view.inputmethod.DeleteGesture.Builder setDeletionArea(@NonNull android.graphics.RectF);
+    method @NonNull public android.view.inputmethod.DeleteGesture.Builder setFallbackText(@Nullable String);
+    method @NonNull public android.view.inputmethod.DeleteGesture.Builder setGranularity(int);
+  }
+
   public final class EditorBoundsInfo implements android.os.Parcelable {
     method public int describeContents();
     method @Nullable public android.graphics.RectF getEditorBounds();
@@ -53080,6 +53096,12 @@
     field public int token;
   }
 
+  public abstract class HandwritingGesture {
+    method @Nullable public String getFallbackText();
+    field public static final int GRANULARITY_CHARACTER = 2; // 0x2
+    field public static final int GRANULARITY_WORD = 1; // 0x1
+  }
+
   public final class InlineSuggestion implements android.os.Parcelable {
     method public int describeContents();
     method @NonNull public android.view.inputmethod.InlineSuggestionInfo getInfo();
@@ -53170,6 +53192,7 @@
     method @Nullable public CharSequence getTextBeforeCursor(@IntRange(from=0) int, int);
     method public boolean performContextMenuAction(int);
     method public boolean performEditorAction(int);
+    method public default void performHandwritingGesture(@NonNull android.view.inputmethod.HandwritingGesture, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.IntConsumer);
     method public boolean performPrivateCommand(String, android.os.Bundle);
     method public default boolean performSpellCheck();
     method public boolean reportFullscreenMode(boolean);
@@ -53391,6 +53414,38 @@
     method public android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder setSubtypeNameResId(int);
   }
 
+  public final class InsertGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.graphics.PointF getInsertionPoint();
+    method @Nullable public String getTextToInsert();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.InsertGesture> CREATOR;
+  }
+
+  public static final class InsertGesture.Builder {
+    ctor public InsertGesture.Builder();
+    method @NonNull public android.view.inputmethod.InsertGesture build();
+    method @NonNull public android.view.inputmethod.InsertGesture.Builder setFallbackText(@Nullable String);
+    method @NonNull public android.view.inputmethod.InsertGesture.Builder setInsertionPoint(@NonNull android.graphics.PointF);
+    method @NonNull public android.view.inputmethod.InsertGesture.Builder setTextToInsert(@NonNull String);
+  }
+
+  public final class SelectGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getGranularity();
+    method @NonNull public android.graphics.RectF getSelectionArea();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.SelectGesture> CREATOR;
+  }
+
+  public static final class SelectGesture.Builder {
+    ctor public SelectGesture.Builder();
+    method @NonNull public android.view.inputmethod.SelectGesture build();
+    method @NonNull public android.view.inputmethod.SelectGesture.Builder setFallbackText(@Nullable String);
+    method @NonNull public android.view.inputmethod.SelectGesture.Builder setGranularity(int);
+    method @NonNull public android.view.inputmethod.SelectGesture.Builder setSelectionArea(@NonNull android.graphics.RectF);
+  }
+
   public final class SurroundingText implements android.os.Parcelable {
     ctor public SurroundingText(@NonNull CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0xffffffff) int);
     method public int describeContents();
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 25ef6e8..c2b315f 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -980,7 +980,8 @@
     boolean mEnterAnimationComplete;
 
     private boolean mIsInMultiWindowMode;
-    private boolean mIsInPictureInPictureMode;
+    /** @hide */
+    boolean mIsInPictureInPictureMode;
 
     private boolean mShouldDockBigOverlays;
 
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index b383d7d..db76816 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -4177,7 +4177,8 @@
     private void schedulePauseWithUserLeavingHint(ActivityClientRecord r) {
         final ClientTransaction transaction = ClientTransaction.obtain(this.mAppThread, r.token);
         transaction.setLifecycleStateRequest(PauseActivityItem.obtain(r.activity.isFinishing(),
-                /* userLeaving */ true, r.activity.mConfigChangeFlags, /* dontReport */ false));
+                /* userLeaving */ true, r.activity.mConfigChangeFlags, /* dontReport */ false,
+                /* autoEnteringPip */ false));
         executeTransaction(transaction);
     }
 
@@ -4965,12 +4966,18 @@
 
     @Override
     public void handlePauseActivity(ActivityClientRecord r, boolean finished, boolean userLeaving,
-            int configChanges, PendingTransactionActions pendingActions, String reason) {
+            int configChanges, boolean autoEnteringPip, PendingTransactionActions pendingActions,
+            String reason) {
         if (userLeaving) {
             performUserLeavingActivity(r);
         }
 
         r.activity.mConfigChangeFlags |= configChanges;
+        if (autoEnteringPip) {
+            // Set mIsInPictureInPictureMode earlier in case of auto-enter-pip, see also
+            // {@link Activity#enterPictureInPictureMode(PictureInPictureParams)}.
+            r.activity.mIsInPictureInPictureMode = true;
+        }
         performPauseActivity(r, finished, reason, pendingActions);
 
         // Make sure any pending writes are now committed.
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index 389da2d..f322ca9 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -96,8 +96,8 @@
 
     /** Pause the activity. */
     public abstract void handlePauseActivity(@NonNull ActivityClientRecord r, boolean finished,
-            boolean userLeaving, int configChanges, PendingTransactionActions pendingActions,
-            String reason);
+            boolean userLeaving, int configChanges, boolean autoEnteringPip,
+            PendingTransactionActions pendingActions, String reason);
 
     /**
      * Resume the activity.
diff --git a/core/java/android/app/servertransaction/PauseActivityItem.java b/core/java/android/app/servertransaction/PauseActivityItem.java
index 813e0f9..965e761 100644
--- a/core/java/android/app/servertransaction/PauseActivityItem.java
+++ b/core/java/android/app/servertransaction/PauseActivityItem.java
@@ -39,13 +39,14 @@
     private boolean mUserLeaving;
     private int mConfigChanges;
     private boolean mDontReport;
+    private boolean mAutoEnteringPip;
 
     @Override
     public void execute(ClientTransactionHandler client, ActivityClientRecord r,
             PendingTransactionActions pendingActions) {
         Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
-        client.handlePauseActivity(r, mFinished, mUserLeaving, mConfigChanges, pendingActions,
-                "PAUSE_ACTIVITY_ITEM");
+        client.handlePauseActivity(r, mFinished, mUserLeaving, mConfigChanges, mAutoEnteringPip,
+                pendingActions, "PAUSE_ACTIVITY_ITEM");
         Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
     }
 
@@ -71,7 +72,7 @@
 
     /** Obtain an instance initialized with provided params. */
     public static PauseActivityItem obtain(boolean finished, boolean userLeaving, int configChanges,
-            boolean dontReport) {
+            boolean dontReport, boolean autoEnteringPip) {
         PauseActivityItem instance = ObjectPool.obtain(PauseActivityItem.class);
         if (instance == null) {
             instance = new PauseActivityItem();
@@ -80,6 +81,7 @@
         instance.mUserLeaving = userLeaving;
         instance.mConfigChanges = configChanges;
         instance.mDontReport = dontReport;
+        instance.mAutoEnteringPip = autoEnteringPip;
 
         return instance;
     }
@@ -94,6 +96,7 @@
         instance.mUserLeaving = false;
         instance.mConfigChanges = 0;
         instance.mDontReport = true;
+        instance.mAutoEnteringPip = false;
 
         return instance;
     }
@@ -105,6 +108,7 @@
         mUserLeaving = false;
         mConfigChanges = 0;
         mDontReport = false;
+        mAutoEnteringPip = false;
         ObjectPool.recycle(this);
     }
 
@@ -117,6 +121,7 @@
         dest.writeBoolean(mUserLeaving);
         dest.writeInt(mConfigChanges);
         dest.writeBoolean(mDontReport);
+        dest.writeBoolean(mAutoEnteringPip);
     }
 
     /** Read from Parcel. */
@@ -125,6 +130,7 @@
         mUserLeaving = in.readBoolean();
         mConfigChanges = in.readInt();
         mDontReport = in.readBoolean();
+        mAutoEnteringPip = in.readBoolean();
     }
 
     public static final @NonNull Creator<PauseActivityItem> CREATOR =
@@ -148,7 +154,8 @@
         }
         final PauseActivityItem other = (PauseActivityItem) o;
         return mFinished == other.mFinished && mUserLeaving == other.mUserLeaving
-                && mConfigChanges == other.mConfigChanges && mDontReport == other.mDontReport;
+                && mConfigChanges == other.mConfigChanges && mDontReport == other.mDontReport
+                && mAutoEnteringPip == other.mAutoEnteringPip;
     }
 
     @Override
@@ -158,12 +165,14 @@
         result = 31 * result + (mUserLeaving ? 1 : 0);
         result = 31 * result + mConfigChanges;
         result = 31 * result + (mDontReport ? 1 : 0);
+        result = 31 * result + (mAutoEnteringPip ? 1 : 0);
         return result;
     }
 
     @Override
     public String toString() {
         return "PauseActivityItem{finished=" + mFinished + ",userLeaving=" + mUserLeaving
-                + ",configChanges=" + mConfigChanges + ",dontReport=" + mDontReport + "}";
+                + ",configChanges=" + mConfigChanges + ",dontReport=" + mDontReport
+                + ",autoEnteringPip=" + mAutoEnteringPip + "}";
     }
 }
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index 25ff8a7..de1d38a 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -227,7 +227,8 @@
                     break;
                 case ON_PAUSE:
                     mTransactionHandler.handlePauseActivity(r, false /* finished */,
-                            false /* userLeaving */, 0 /* configChanges */, mPendingActions,
+                            false /* userLeaving */, 0 /* configChanges */,
+                            false /* autoEnteringPip */, mPendingActions,
                             "LIFECYCLER_PAUSE_ACTIVITY");
                     break;
                 case ON_STOP:
diff --git a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
index e38e611..3260713 100644
--- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
+++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
@@ -17,6 +17,7 @@
 package android.inputmethodservice;
 
 import android.annotation.AnyThread;
+import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.Bundle;
@@ -24,9 +25,13 @@
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.DeleteGesture;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.HandwritingGesture;
 import android.view.inputmethod.InputContentInfo;
+import android.view.inputmethod.InsertGesture;
+import android.view.inputmethod.SelectGesture;
 import android.view.inputmethod.SurroundingText;
 import android.view.inputmethod.TextAttribute;
 
@@ -35,6 +40,8 @@
 import com.android.internal.inputmethod.InputConnectionCommandHeader;
 
 import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
 
 /**
  * A stateless wrapper of {@link com.android.internal.inputmethod.IRemoteInputConnection} to
@@ -591,6 +598,27 @@
         }
     }
 
+    @AnyThread
+    public void performHandwritingGesture(
+            @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
+            @Nullable IntConsumer consumer) {
+        // TODO(b/210039666): implement resultReceiver
+        try {
+            if (gesture instanceof SelectGesture) {
+                mConnection.performHandwritingSelectGesture(
+                        createHeader(), (SelectGesture) gesture, null);
+            } else if (gesture instanceof InsertGesture) {
+                mConnection.performHandwritingInsertGesture(
+                        createHeader(), (InsertGesture) gesture, null);
+            } else if (gesture instanceof DeleteGesture) {
+                mConnection.performHandwritingDeleteGesture(
+                        createHeader(), (DeleteGesture) gesture, null);
+            }
+        } catch (RemoteException e) {
+            // TODO(b/210039666): return result
+        }
+    }
+
     /**
      * Invokes {@link IRemoteInputConnection#requestCursorUpdates(InputConnectionCommandHeader, int,
      * int, AndroidFuture)}.
diff --git a/core/java/android/inputmethodservice/RemoteInputConnection.java b/core/java/android/inputmethodservice/RemoteInputConnection.java
index 2711c4f..694293c 100644
--- a/core/java/android/inputmethodservice/RemoteInputConnection.java
+++ b/core/java/android/inputmethodservice/RemoteInputConnection.java
@@ -17,6 +17,7 @@
 package android.inputmethodservice;
 
 import android.annotation.AnyThread;
+import android.annotation.CallbackExecutor;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -28,6 +29,7 @@
 import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.HandwritingGesture;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputContentInfo;
 import android.view.inputmethod.SurroundingText;
@@ -41,6 +43,8 @@
 
 import java.lang.ref.WeakReference;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
 
 /**
  * Takes care of remote method invocations of {@link InputConnection} in the IME side.
@@ -411,6 +415,13 @@
     }
 
     @AnyThread
+    public void performHandwritingGesture(
+            @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
+            @Nullable IntConsumer consumer) {
+        mInvoker.performHandwritingGesture(gesture, executor, consumer);
+    }
+
+    @AnyThread
     public boolean requestCursorUpdates(int cursorUpdateMode) {
         if (mCancellationGroup.isCanceled()) {
             return false;
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index ca0af2c..738f9c9 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -1995,7 +1995,8 @@
     /** @hide */
     public UserManager(Context context, IUserManager service) {
         mService = service;
-        mContext = context.getApplicationContext();
+        Context appContext = context.getApplicationContext();
+        mContext = (appContext == null ? context : appContext);
         mUserId = context.getUserId();
     }
 
diff --git a/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java b/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java
index 95bcda5..9292e96 100644
--- a/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java
+++ b/core/java/android/service/selectiontoolbar/RemoteSelectionToolbar.java
@@ -1317,7 +1317,6 @@
         contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
         contentContainer.setTag(FloatingToolbar.FLOATING_TOOLBAR_TAG);
-        contentContainer.setContentDescription(FloatingToolbar.FLOATING_TOOLBAR_TAG);
         contentContainer.setClipToOutline(true);
         return contentContainer;
     }
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index b73ff901..0338ceb 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -97,10 +97,6 @@
     /** @hide */
     public static final String SETTINGS_AUTO_TEXT_WRAPPING = "settings_auto_text_wrapping";
 
-    /** Flag to enable/disable guest mode UX changes as mentioned in b/214031645
-     *  @hide
-     */
-    public static final String SETTINGS_GUEST_MODE_UX_CHANGES = "settings_guest_mode_ux_changes";
 
     /** Support Clear Calling feature.
      *  @hide
@@ -150,7 +146,6 @@
         DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "true");
         DEFAULT_FLAGS.put(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE, "true");
         DEFAULT_FLAGS.put(SETTINGS_AUTO_TEXT_WRAPPING, "false");
-        DEFAULT_FLAGS.put(SETTINGS_GUEST_MODE_UX_CHANGES, "true");
         DEFAULT_FLAGS.put(SETTINGS_ENABLE_CLEAR_CALLING, "false");
         DEFAULT_FLAGS.put(SETTINGS_ACCESSIBILITY_SIMPLE_CURSOR, "false");
         DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_UI, "false");
diff --git a/core/java/android/view/inputmethod/DeleteGesture.aidl b/core/java/android/view/inputmethod/DeleteGesture.aidl
new file mode 100644
index 0000000..e9f31dd
--- /dev/null
+++ b/core/java/android/view/inputmethod/DeleteGesture.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.view.inputmethod;
+
+parcelable DeleteGesture;
\ No newline at end of file
diff --git a/core/java/android/view/inputmethod/DeleteGesture.java b/core/java/android/view/inputmethod/DeleteGesture.java
new file mode 100644
index 0000000..257254e
--- /dev/null
+++ b/core/java/android/view/inputmethod/DeleteGesture.java
@@ -0,0 +1,193 @@
+/*
+ * 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 android.view.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.widget.TextView;
+
+import java.util.Objects;
+
+/**
+ * A sub-class of {@link HandwritingGesture} for deleting an area of text.
+ * This class holds the information required for deletion of text in
+ * toolkit widgets like {@link TextView}.
+ */
+public final class DeleteGesture extends HandwritingGesture implements Parcelable {
+
+    private @Granularity int mGranularity;
+    private RectF mArea;
+
+    private DeleteGesture(@Granularity int granularity, RectF area, String fallbackText) {
+        mArea = area;
+        mGranularity = granularity;
+        mFallbackText = fallbackText;
+    }
+
+    private DeleteGesture(@NonNull final Parcel source) {
+        mFallbackText = source.readString8();
+        mGranularity = source.readInt();
+        mArea = source.readTypedObject(RectF.CREATOR);
+    }
+
+    /**
+     * Returns Granular level on which text should be operated.
+     * @see HandwritingGesture#GRANULARITY_CHARACTER
+     * @see HandwritingGesture#GRANULARITY_WORD
+     */
+    @Granularity
+    public int getGranularity() {
+        return mGranularity;
+    }
+
+    /**
+     * Returns the deletion area {@link RectF} in screen coordinates.
+     *
+     * Getter for deletion area set with {@link DeleteGesture.Builder#setDeletionArea(RectF)}.
+     * {@code null} if area was not set.
+     */
+    @NonNull
+    public RectF getDeletionArea() {
+        return mArea;
+    }
+
+    /**
+     * Builder for {@link DeleteGesture}. This class is not designed to be thread-safe.
+     */
+    public static final class Builder {
+        private int mGranularity;
+        private RectF mArea;
+        private String mFallbackText;
+
+        /**
+         * Set text deletion granularity. Intersecting words/characters will be
+         * included in the operation.
+         * @param granularity {@link HandwritingGesture#GRANULARITY_WORD} or
+         * {@link HandwritingGesture#GRANULARITY_CHARACTER}.
+         * @return {@link Builder}.
+         */
+        @NonNull
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder setGranularity(@Granularity int granularity) {
+            mGranularity = granularity;
+            return this;
+        }
+
+        /**
+         * Set rectangular single/multiline text deletion area intersecting with text.
+         *
+         * The resulting deletion would be performed for all text intersecting rectangle. The
+         * deletion includes the first word/character in the rectangle, and the last
+         * word/character in the rectangle, and includes  everything in between even if it's not
+         * in the rectangle.
+         *
+         * Intersection is determined using
+         * {@link #setGranularity(int)}. e.g. {@link HandwritingGesture#GRANULARITY_WORD} includes
+         * all the words with their width/height center included in the deletion rectangle.
+         * @param area {@link RectF} (in screen coordinates) for which text will be deleted.
+         * @see HandwritingGesture#GRANULARITY_WORD
+         * @see HandwritingGesture#GRANULARITY_CHARACTER
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setDeletionArea(@NonNull RectF area) {
+            mArea = area;
+            return this;
+        }
+
+        /**
+         * Set fallback text that will be committed at current cursor position if there is no
+         * applicable text beneath the area of gesture.
+         * @param fallbackText text to set
+         */
+        @NonNull
+        public Builder setFallbackText(@Nullable String fallbackText) {
+            mFallbackText = fallbackText;
+            return this;
+        }
+
+        /**
+         * @return {@link DeleteGesture} using parameters in this {@link DeleteGesture.Builder}.
+         * @throws IllegalArgumentException if one or more positional parameters are not specified.
+         */
+        @NonNull
+        public DeleteGesture build() {
+            if (mArea == null || mArea.isEmpty()) {
+                throw new IllegalArgumentException("Deletion area must be set.");
+            }
+            if (mGranularity <= GRANULARITY_UNDEFINED) {
+                throw new IllegalArgumentException("Deletion granularity must be set.");
+            }
+            return new DeleteGesture(mGranularity, mArea, mFallbackText);
+        }
+    }
+
+    /**
+     * Used to make this class parcelable.
+     */
+    public static final @android.annotation.NonNull Creator<DeleteGesture> CREATOR =
+            new Creator<DeleteGesture>() {
+        @Override
+        public DeleteGesture createFromParcel(Parcel source) {
+            return new DeleteGesture(source);
+        }
+
+        @Override
+        public DeleteGesture[] newArray(int size) {
+            return new DeleteGesture[size];
+        }
+    };
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mArea, mGranularity, mFallbackText);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof DeleteGesture)) return false;
+
+        DeleteGesture that = (DeleteGesture) o;
+
+        if (mGranularity != that.mGranularity) return false;
+        if (!Objects.equals(mFallbackText, that.mFallbackText)) return false;
+        return Objects.equals(mArea, that.mArea);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Used to package this object into a {@link Parcel}.
+     *
+     * @param dest The {@link Parcel} to be written.
+     * @param flags The flags used for parceling.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(mFallbackText);
+        dest.writeInt(mGranularity);
+        dest.writeTypedObject(mArea, flags);
+    }
+}
diff --git a/core/java/android/view/inputmethod/HandwritingGesture.java b/core/java/android/view/inputmethod/HandwritingGesture.java
new file mode 100644
index 0000000..15824ae
--- /dev/null
+++ b/core/java/android/view/inputmethod/HandwritingGesture.java
@@ -0,0 +1,94 @@
+/*
+ * 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 android.view.inputmethod;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.graphics.RectF;
+import android.inputmethodservice.InputMethodService;
+import android.view.MotionEvent;
+
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
+
+/**
+ * Base class for Stylus handwriting gesture.
+ *
+ * During a stylus handwriting session, user can perform a stylus gesture operation like
+ * {@link SelectGesture}, {@link DeleteGesture}, {@link InsertGesture} on an
+ * area of text. IME is responsible for listening to Stylus {@link MotionEvent} using
+ * {@link InputMethodService#onStylusHandwritingMotionEvent} and interpret if it can translate to a
+ * gesture operation.
+ * While creating Gesture operations {@link SelectGesture}, {@link DeleteGesture},
+ * , {@code Granularity} helps pick the correct granular level of text like word level
+ * {@link #GRANULARITY_WORD}, or character level {@link #GRANULARITY_CHARACTER}.
+ *
+ * @see InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)
+ * @see InputMethodService#onStartStylusHandwriting()
+ */
+public abstract class HandwritingGesture {
+
+    HandwritingGesture() {}
+
+    static final int GRANULARITY_UNDEFINED = 0;
+
+    /**
+     * Operate text per word basis. e.g. if selection includes width-wise center of the word,
+     * whole word is selected.
+     * <p> Strategy of operating at a granular level is maintained in the UI toolkit.
+     *     A character/word/line is included if its center is within the gesture rectangle.
+     *     e.g. if a selection {@link RectF} with {@link #GRANULARITY_WORD} includes width-wise
+     *     center of the word, it should be selected.
+     *     Similarly, text in a line should be included in the operation if rectangle includes
+     *     line height center.</p>
+     * Refer to https://www.unicode.org/reports/tr29/#Word_Boundaries for more detail on how word
+     * breaks are decided.
+     */
+    public static final int GRANULARITY_WORD = 1;
+
+    /**
+     * Operate on text per character basis. i.e. each character is selected based on its
+     * intersection with selection rectangle.
+     * <p> Strategy of operating at a granular level is maintained in the UI toolkit.
+     *     A character/word/line is included if its center is within the gesture rectangle.
+     *     e.g. if a selection {@link RectF} with {@link #GRANULARITY_CHARACTER} includes width-wise
+     *     center of the character, it should be selected.
+     *     Similarly, text in a line should be included in the operation if rectangle includes
+     *     line height center.</p>
+     */
+    public static final int GRANULARITY_CHARACTER = 2;
+
+    /**
+     * Granular level on which text should be operated.
+     */
+    @IntDef({GRANULARITY_CHARACTER, GRANULARITY_WORD})
+    @interface Granularity {}
+
+    @Nullable
+    String mFallbackText;
+
+    /**
+     * The fallback text that will be committed at current cursor position if there is no applicable
+     * text beneath the area of gesture.
+     * For example, select can fail if gesture is drawn over area that has no text beneath.
+     * example 2: join can fail if the gesture is drawn over text but there is no whitespace.
+     */
+    @Nullable
+    public String getFallbackText() {
+        return mFallbackText;
+    }
+}
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index dac1be6..7b0270a1 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -16,6 +16,7 @@
 
 package android.view.inputmethod;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -31,6 +32,8 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
 
 /**
  * The InputConnection interface is the communication channel from an
@@ -968,6 +971,17 @@
     boolean performPrivateCommand(String action, Bundle data);
 
     /**
+     * Perform a handwriting gesture on text.
+     *
+     * @param gesture the gesture to perform
+     * @param executor if the caller passes a non-null consumer  TODO(b/210039666): complete doc
+     * @param consumer if the caller passes a non-null receiver, the editor must invoke this
+     */
+    default void performHandwritingGesture(
+            @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
+            @Nullable IntConsumer consumer) {}
+
+    /**
      * The editor is requested to call
      * {@link InputMethodManager#updateCursorAnchorInfo(android.view.View, CursorAnchorInfo)} at
      * once, as soon as possible, regardless of cursor/anchor position changes. This flag can be
diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java
index 7a88a75..56beddf 100644
--- a/core/java/android/view/inputmethod/InputConnectionWrapper.java
+++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java
@@ -16,6 +16,7 @@
 
 package android.view.inputmethod;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -25,6 +26,9 @@
 
 import com.android.internal.util.Preconditions;
 
+import java.util.concurrent.Executor;
+import java.util.function.IntConsumer;
+
 /**
  * <p>Wrapper class for proxying calls to another InputConnection.  Subclass and have fun!
  */
@@ -323,6 +327,17 @@
      * @throws NullPointerException if the target is {@code null}.
      */
     @Override
+    public void performHandwritingGesture(
+            @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
+            @Nullable IntConsumer consumer) {
+        mTarget.performHandwritingGesture(gesture, executor, consumer);
+    }
+
+    /**
+     * {@inheritDoc}
+     * @throws NullPointerException if the target is {@code null}.
+     */
+    @Override
     public boolean requestCursorUpdates(int cursorUpdateMode) {
         return mTarget.requestCursorUpdates(cursorUpdateMode);
     }
diff --git a/core/java/android/view/inputmethod/InsertGesture.aidl b/core/java/android/view/inputmethod/InsertGesture.aidl
new file mode 100644
index 0000000..9cdb14a
--- /dev/null
+++ b/core/java/android/view/inputmethod/InsertGesture.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.view.inputmethod;
+
+parcelable InsertGesture;
\ No newline at end of file
diff --git a/core/java/android/view/inputmethod/InsertGesture.java b/core/java/android/view/inputmethod/InsertGesture.java
new file mode 100644
index 0000000..2cf015a
--- /dev/null
+++ b/core/java/android/view/inputmethod/InsertGesture.java
@@ -0,0 +1,172 @@
+/*
+ * 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 android.view.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.graphics.PointF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * A sub-class of {@link HandwritingGesture} for inserting text at the defined insertion point.
+ * This class holds the information required for insertion of text in
+ * toolkit widgets like {@link TextView}.
+ */
+public final class InsertGesture extends HandwritingGesture implements Parcelable {
+
+    private String mTextToInsert;
+    private PointF mPoint;
+
+    private InsertGesture(String text, PointF point, String fallbackText) {
+        mPoint = point;
+        mTextToInsert = text;
+        mFallbackText = fallbackText;
+    }
+
+    private InsertGesture(final Parcel source) {
+        mFallbackText = source.readString8();
+        mTextToInsert = source.readString8();
+        mPoint = source.readTypedObject(PointF.CREATOR);
+    }
+
+    /** Returns the text that will be inserted at {@link #getInsertionPoint()} **/
+    @Nullable
+    public String getTextToInsert() {
+        return mTextToInsert;
+    }
+
+    /**
+     * Returns the insertion point {@link PointF} (in screen coordinates) where
+     * {@link #getTextToInsert()} will be inserted.
+     */
+    @Nullable
+    public PointF getInsertionPoint() {
+        return mPoint;
+    }
+
+    /**
+     * Builder for {@link InsertGesture}. This class is not designed to be thread-safe.
+     */
+    public static final class Builder {
+        private String mText;
+        private PointF mPoint;
+        private String mFallbackText;
+
+        /** set the text that will be inserted at {@link #setInsertionPoint(PointF)} **/
+        @NonNull
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder setTextToInsert(@NonNull String text) {
+            mText = text;
+            return this;
+        }
+
+        /**
+         * Sets the insertion point (in screen coordinates) where {@link #setTextToInsert(String)}
+         * should be inserted.
+         */
+        @NonNull
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder setInsertionPoint(@NonNull PointF point) {
+            mPoint = point;
+            return this;
+        }
+
+        /**
+         * Set fallback text that will be committed at current cursor position if there is no
+         * applicable text beneath the area of gesture.
+         * @param fallbackText text to set
+         */
+        @NonNull
+        public Builder setFallbackText(@Nullable String fallbackText) {
+            mFallbackText = fallbackText;
+            return this;
+        }
+
+        /**
+         * @return {@link InsertGesture} using parameters in this {@link InsertGesture.Builder}.
+         * @throws IllegalArgumentException if one or more positional parameters are not specified.
+         */
+        @NonNull
+        public InsertGesture build() {
+            if (mPoint == null) {
+                throw new IllegalArgumentException("Insertion point must be set.");
+            }
+            if (TextUtils.isEmpty(mText)) {
+                throw new IllegalArgumentException("Text to insert must be non-empty.");
+            }
+            return new InsertGesture(mText, mPoint, mFallbackText);
+        }
+    }
+
+    /**
+     * Used to make this class parcelable.
+     */
+    public static final @android.annotation.NonNull Creator<InsertGesture> CREATOR =
+            new Creator<InsertGesture>() {
+        @Override
+        public InsertGesture createFromParcel(Parcel source) {
+            return new InsertGesture(source);
+        }
+
+        @Override
+        public InsertGesture[] newArray(int size) {
+            return new InsertGesture[size];
+        }
+    };
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mPoint, mTextToInsert, mFallbackText);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof InsertGesture)) return false;
+
+        InsertGesture that = (InsertGesture) o;
+
+        if (!Objects.equals(mFallbackText, that.mFallbackText)) return false;
+        if (!Objects.equals(mTextToInsert, that.mTextToInsert)) return false;
+        return Objects.equals(mPoint, that.mPoint);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Used to package this object into a {@link Parcel}.
+     *
+     * @param dest The {@link Parcel} to be written.
+     * @param flags The flags used for parceling.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(mFallbackText);
+        dest.writeString8(mTextToInsert);
+        dest.writeTypedObject(mPoint, flags);
+    }
+}
diff --git a/core/java/android/view/inputmethod/SelectGesture.aidl b/core/java/android/view/inputmethod/SelectGesture.aidl
new file mode 100644
index 0000000..65da4f3
--- /dev/null
+++ b/core/java/android/view/inputmethod/SelectGesture.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.view.inputmethod;
+
+parcelable SelectGesture;
\ No newline at end of file
diff --git a/core/java/android/view/inputmethod/SelectGesture.java b/core/java/android/view/inputmethod/SelectGesture.java
new file mode 100644
index 0000000..f3cd71e
--- /dev/null
+++ b/core/java/android/view/inputmethod/SelectGesture.java
@@ -0,0 +1,191 @@
+/*
+ * 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 android.view.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.widget.TextView;
+
+import java.util.Objects;
+
+/**
+ * A sub-class of {@link HandwritingGesture} for selecting an area of text.
+ * This class holds the information required for selection of text in
+ * toolkit widgets like {@link TextView}.
+ */
+public final class SelectGesture extends HandwritingGesture implements Parcelable {
+
+    private @Granularity int mGranularity;
+    private RectF mArea;
+
+    private SelectGesture(int granularity, RectF area, String fallbackText) {
+        mArea = area;
+        mGranularity = granularity;
+        mFallbackText = fallbackText;
+    }
+
+    private SelectGesture(@NonNull Parcel source) {
+        mFallbackText = source.readString8();
+        mGranularity = source.readInt();
+        mArea = source.readTypedObject(RectF.CREATOR);
+    }
+
+    /**
+     * Returns Granular level on which text should be operated.
+     * @see #GRANULARITY_CHARACTER
+     * @see #GRANULARITY_WORD
+     */
+    @Granularity
+    public int getGranularity() {
+        return mGranularity;
+    }
+
+    /**
+     * Returns the Selection area {@link RectF} in screen coordinates.
+     *
+     * Getter for selection area set with {@link Builder#setSelectionArea(RectF)}. {@code null}
+     * if area was not set.
+     */
+    @NonNull
+    public RectF getSelectionArea() {
+        return mArea;
+    }
+
+
+    /**
+     * Builder for {@link SelectGesture}. This class is not designed to be thread-safe.
+     */
+    public static final class Builder {
+        private int mGranularity;
+        private RectF mArea;
+        private String mFallbackText;
+
+        /**
+         * Define text selection granularity. Intersecting words/characters will be
+         * included in the operation.
+         * @param granularity {@link HandwritingGesture#GRANULARITY_WORD} or
+         * {@link HandwritingGesture#GRANULARITY_CHARACTER}.
+         * @return {@link Builder}.
+         */
+        @NonNull
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder setGranularity(@Granularity int granularity) {
+            mGranularity = granularity;
+            return this;
+        }
+
+        /**
+         * Set rectangular single/multiline text selection area intersecting with text.
+         *
+         * The resulting selection would be performed for all text intersecting rectangle. The
+         * selection includes the first word/character in the  rectangle, and the last
+         * word/character in the rectangle, and includes  everything in between even if it's not
+         * in the rectangle.
+         *
+         * Intersection is determined using
+         * {@link #setGranularity(int)}. e.g. {@link HandwritingGesture#GRANULARITY_WORD} includes
+         * all the words with their width/height center included in the selection rectangle.
+         * @param area {@link RectF} (in screen coordinates) for which text will be selection.
+         */
+        @NonNull
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder setSelectionArea(@NonNull RectF area) {
+            mArea = area;
+            return this;
+        }
+
+        /**
+         * Set fallback text that will be committed at current cursor position if there is no
+         * applicable text beneath the area of gesture.
+         * @param fallbackText text to set
+         */
+        @NonNull
+        public Builder setFallbackText(@Nullable String fallbackText) {
+            mFallbackText = fallbackText;
+            return this;
+        }
+
+        /**
+         * @return {@link SelectGesture} using parameters in this {@link InsertGesture.Builder}.
+         * @throws IllegalArgumentException if one or more positional parameters are not specified.
+         */
+        @NonNull
+        public SelectGesture build() {
+            if (mArea == null || mArea.isEmpty()) {
+                throw new IllegalArgumentException("Selection area must be set.");
+            }
+            if (mGranularity <= GRANULARITY_UNDEFINED) {
+                throw new IllegalArgumentException("Selection granularity must be set.");
+            }
+            return new SelectGesture(mGranularity, mArea, mFallbackText);
+        }
+    }
+
+    /**
+     * Used to make this class parcelable.
+     */
+    public static final @android.annotation.NonNull Parcelable.Creator<SelectGesture> CREATOR =
+            new Parcelable.Creator<SelectGesture>() {
+        @Override
+        public SelectGesture createFromParcel(Parcel source) {
+            return new SelectGesture(source);
+        }
+
+        @Override
+        public SelectGesture[] newArray(int size) {
+            return new SelectGesture[size];
+        }
+    };
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mGranularity, mArea, mFallbackText);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof SelectGesture)) return false;
+
+        SelectGesture that = (SelectGesture) o;
+
+        if (mGranularity != that.mGranularity) return false;
+        if (!Objects.equals(mFallbackText, that.mFallbackText)) return false;
+        return Objects.equals(mArea, that.mArea);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Used to package this object into a {@link Parcel}.
+     *
+     * @param dest The {@link Parcel} to be written.
+     * @param flags The flags used for parceling.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(mFallbackText);
+        dest.writeInt(mGranularity);
+        dest.writeTypedObject(mArea, flags);
+    }
+}
diff --git a/core/java/android/window/ITaskFragmentOrganizerController.aidl b/core/java/android/window/ITaskFragmentOrganizerController.aidl
index 8407d10..884ca77 100644
--- a/core/java/android/window/ITaskFragmentOrganizerController.aidl
+++ b/core/java/android/window/ITaskFragmentOrganizerController.aidl
@@ -16,8 +16,10 @@
 
 package android.window;
 
+import android.os.IBinder;
 import android.view.RemoteAnimationDefinition;
 import android.window.ITaskFragmentOrganizer;
+import android.window.WindowContainerTransaction;
 
 /** @hide */
 interface ITaskFragmentOrganizerController {
@@ -46,8 +48,15 @@
     void unregisterRemoteAnimations(in ITaskFragmentOrganizer organizer, int taskId);
 
     /**
-      * Checks if an activity organized by a {@link android.window.TaskFragmentOrganizer} and
-      * only occupies a portion of Task bounds.
-      */
+     * Checks if an activity organized by a {@link android.window.TaskFragmentOrganizer} and
+     * only occupies a portion of Task bounds.
+     */
     boolean isActivityEmbedded(in IBinder activityToken);
+
+    /**
+     * Notifies the server that the organizer has finished handling the given transaction. The
+     * server should apply the given {@link WindowContainerTransaction} for the necessary changes.
+     */
+    void onTransactionHandled(in ITaskFragmentOrganizer organizer, in IBinder transactionToken,
+        in WindowContainerTransaction wct);
 }
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index cd15df84..7359172 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -26,7 +26,6 @@
 import android.annotation.CallSuper;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SuppressLint;
 import android.annotation.TestApi;
 import android.content.Intent;
 import android.content.res.Configuration;
@@ -141,6 +140,28 @@
     }
 
     /**
+     * Notifies the server that the organizer has finished handling the given transaction. The
+     * server should apply the given {@link WindowContainerTransaction} for the necessary changes.
+     *
+     * @param transactionToken  {@link TaskFragmentTransaction#getTransactionToken()} from
+     *                          {@link #onTransactionReady(TaskFragmentTransaction)}
+     * @param wct               {@link WindowContainerTransaction} that the server should apply for
+     *                          update of the transaction.
+     * @see com.android.server.wm.WindowOrganizerController#enforceTaskPermission for permission
+     * requirement.
+     * @hide
+     */
+    public void onTransactionHandled(@NonNull IBinder transactionToken,
+            @NonNull WindowContainerTransaction wct) {
+        wct.setTaskFragmentOrganizer(mInterface);
+        try {
+            getController().onTransactionHandled(mInterface, transactionToken, wct);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Called when a TaskFragment is created and organized by this organizer.
      *
      * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
@@ -227,12 +248,8 @@
     /**
      * Called when the transaction is ready so that the organizer can update the TaskFragments based
      * on the changes in transaction.
-     * Note: {@link WindowOrganizer#applyTransaction} permission requirement is conditional for
-     * {@link TaskFragmentOrganizer}.
-     * @see com.android.server.wm.WindowOrganizerController#enforceTaskPermission
      * @hide
      */
-    @SuppressLint("AndroidFrameworkRequiresPermission")
     public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
@@ -274,8 +291,9 @@
                             "Unknown TaskFragmentEvent=" + change.getType());
             }
         }
-        // TODO(b/240519866): notify TaskFragmentOrganizerController that the transition is done.
-        applyTransaction(wct);
+
+        // Notify the server, and the server should apply the WindowContainerTransaction.
+        onTransactionHandled(transaction.getTransactionToken(), wct);
     }
 
     @Override
diff --git a/core/java/android/window/TaskFragmentTransaction.java b/core/java/android/window/TaskFragmentTransaction.java
index 07e8e8c..84a5fea 100644
--- a/core/java/android/window/TaskFragmentTransaction.java
+++ b/core/java/android/window/TaskFragmentTransaction.java
@@ -23,6 +23,7 @@
 import android.annotation.Nullable;
 import android.content.Intent;
 import android.content.res.Configuration;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Parcel;
@@ -41,19 +42,31 @@
  */
 public final class TaskFragmentTransaction implements Parcelable {
 
+    /** Unique token to represent this transaction. */
+    private final IBinder mTransactionToken;
+
+    /** Changes in this transaction. */
     private final ArrayList<Change> mChanges = new ArrayList<>();
 
-    public TaskFragmentTransaction() {}
+    public TaskFragmentTransaction() {
+        mTransactionToken = new Binder();
+    }
 
     private TaskFragmentTransaction(Parcel in) {
+        mTransactionToken = in.readStrongBinder();
         in.readTypedList(mChanges, Change.CREATOR);
     }
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeStrongBinder(mTransactionToken);
         dest.writeTypedList(mChanges);
     }
 
+    public IBinder getTransactionToken() {
+        return mTransactionToken;
+    }
+
     /** Adds a {@link Change} to this transaction. */
     public void addChange(@Nullable Change change) {
         if (change != null) {
@@ -74,7 +87,9 @@
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
-        sb.append("TaskFragmentTransaction{changes=[");
+        sb.append("TaskFragmentTransaction{token=");
+        sb.append(mTransactionToken);
+        sb.append(" changes=[");
         for (int i = 0; i < mChanges.size(); ++i) {
             if (i > 0) {
                 sb.append(',');
diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
index fcdcb2d..8f6bc43 100644
--- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
+++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -217,7 +217,11 @@
             case TYPE_HEADER_ALL_OTHERS:
                 TextView textView = (TextView) itemView;
                 if (itemType == TYPE_HEADER_SUGGESTED) {
-                    setTextTo(textView, R.string.language_picker_section_suggested);
+                   if (mCountryMode) {
+                        setTextTo(textView, R.string.language_picker_regions_section_suggested);
+                    } else {
+                        setTextTo(textView, R.string.language_picker_section_suggested);
+                    }
                 } else {
                     if (mCountryMode) {
                         setTextTo(textView, R.string.region_picker_section_all);
diff --git a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
index a7dd6f1..7a219c6 100644
--- a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
+++ b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
@@ -17,11 +17,15 @@
 package com.android.internal.inputmethod;
 
 import android.os.Bundle;
+import android.os.ResultReceiver;
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.DeleteGesture;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputContentInfo;
+import android.view.inputmethod.InsertGesture;
+import android.view.inputmethod.SelectGesture;
 import android.view.inputmethod.TextAttribute;
 
 import com.android.internal.infra.AndroidFuture;
@@ -86,6 +90,15 @@
     void performPrivateCommand(in InputConnectionCommandHeader header, String action,
             in Bundle data);
 
+    void performHandwritingSelectGesture(in InputConnectionCommandHeader header,
+            in SelectGesture gesture, in ResultReceiver resultReceiver);
+
+    void performHandwritingInsertGesture(in InputConnectionCommandHeader header,
+            in InsertGesture gesture, in ResultReceiver resultReceiver);
+
+    void performHandwritingDeleteGesture(in InputConnectionCommandHeader header,
+            in DeleteGesture gesture, in ResultReceiver resultReceiver);
+
     void setComposingRegion(in InputConnectionCommandHeader header, int start, int end);
 
     void setComposingRegionWithTextAttribute(in InputConnectionCommandHeader header, int start,
diff --git a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java
index b63ce1b..c65a69f 100644
--- a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java
+++ b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java
@@ -31,6 +31,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.ResultReceiver;
 import android.os.Trace;
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
@@ -39,11 +40,15 @@
 import android.view.ViewRootImpl;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.DeleteGesture;
 import android.view.inputmethod.DumpableInputConnection;
 import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.HandwritingGesture;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputContentInfo;
 import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InsertGesture;
+import android.view.inputmethod.SelectGesture;
 import android.view.inputmethod.TextAttribute;
 import android.view.inputmethod.TextSnapshot;
 
@@ -970,6 +975,67 @@
 
     @Dispatching(cancellable = true)
     @Override
+    public void performHandwritingSelectGesture(
+            InputConnectionCommandHeader header, SelectGesture gesture,
+            ResultReceiver resultReceiver) {
+        performHandwritingGestureInternal(header, gesture, resultReceiver);
+    }
+
+    @Dispatching(cancellable = true)
+    @Override
+    public void performHandwritingInsertGesture(
+            InputConnectionCommandHeader header, InsertGesture gesture,
+            ResultReceiver resultReceiver) {
+        performHandwritingGestureInternal(header, gesture, resultReceiver);
+    }
+
+    @Dispatching(cancellable = true)
+    @Override
+    public void performHandwritingDeleteGesture(
+            InputConnectionCommandHeader header, DeleteGesture gesture,
+            ResultReceiver resultReceiver) {
+        performHandwritingGestureInternal(header, gesture, resultReceiver);
+    }
+
+    private <T extends HandwritingGesture> void performHandwritingGestureInternal(
+            InputConnectionCommandHeader header,  T gesture, ResultReceiver resultReceiver) {
+        dispatchWithTracing("performHandwritingGesture", () -> {
+            if (header.mSessionId != mCurrentSessionId.get()) {
+                return;  // cancelled
+            }
+            InputConnection ic = getInputConnection();
+            if (ic == null || !isActive()) {
+                Log.w(TAG, "performHandwritingGesture on inactive InputConnection");
+                return;
+            }
+            // TODO(b/210039666): implement resultReceiver
+            ic.performHandwritingGesture(gesture, null, null);
+        });
+    }
+
+    /**
+     * Dispatches {@link InputConnection#requestCursorUpdates(int)}.
+     *
+     * <p>This method is intended to be called only from {@link InputMethodManager}.</p>
+     * @param cursorUpdateMode the mode for {@link InputConnection#requestCursorUpdates(int, int)}
+     * @param cursorUpdateFilter the filter for
+     *      {@link InputConnection#requestCursorUpdates(int, int)}
+     * @param imeDisplayId displayId on which IME is displayed.
+     */
+    @Dispatching(cancellable = true)
+    public void requestCursorUpdatesFromImm(int cursorUpdateMode, int cursorUpdateFilter,
+            int imeDisplayId) {
+        final int currentSessionId = mCurrentSessionId.get();
+        dispatchWithTracing("requestCursorUpdatesFromImm", () -> {
+            if (currentSessionId != mCurrentSessionId.get()) {
+                return;  // cancelled
+            }
+            requestCursorUpdatesInternal(cursorUpdateMode, cursorUpdateFilter, imeDisplayId);
+        });
+    }
+
+    @Dispatching(cancellable = true)
+    @Override
     public void requestCursorUpdates(InputConnectionCommandHeader header, int cursorUpdateMode,
             int imeDisplayId, AndroidFuture future /* T=Boolean */) {
         dispatchWithTracing("requestCursorUpdates", future, () -> {
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 72de78c..fc4e041 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -87,6 +87,7 @@
 import android.annotation.NonNull;
 import android.annotation.UiThread;
 import android.annotation.WorkerThread;
+import android.app.ActivityThread;
 import android.content.Context;
 import android.os.Build;
 import android.os.Handler;
@@ -292,7 +293,10 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL,
     };
 
-    private static volatile InteractionJankMonitor sInstance;
+    private static class InstanceHolder {
+        public static final InteractionJankMonitor INSTANCE =
+            new InteractionJankMonitor(new HandlerThread(DEFAULT_WORKER_NAME));
+    }
 
     private final DeviceConfig.OnPropertiesChangedListener mPropertiesChangedListener =
             this::updateProperties;
@@ -384,15 +388,7 @@
      * @return instance of InteractionJankMonitor
      */
     public static InteractionJankMonitor getInstance() {
-        // Use DCL here since this method might be invoked very often.
-        if (sInstance == null) {
-            synchronized (InteractionJankMonitor.class) {
-                if (sInstance == null) {
-                    sInstance = new InteractionJankMonitor(new HandlerThread(DEFAULT_WORKER_NAME));
-                }
-            }
-        }
-        return sInstance;
+        return InstanceHolder.INSTANCE;
     }
 
     /**
@@ -402,6 +398,11 @@
      */
     @VisibleForTesting
     public InteractionJankMonitor(@NonNull HandlerThread worker) {
+        // Check permission early.
+        DeviceConfig.enforceReadPermission(
+            ActivityThread.currentApplication().getApplicationContext(),
+            DeviceConfig.NAMESPACE_INTERACTION_JANK_MONITOR);
+
         mRunningTrackers = new SparseArray<>();
         mTimeoutActions = new SparseArray<>();
         mWorker = worker;
diff --git a/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java b/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java
index 95a4e12..bc729f1 100644
--- a/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java
+++ b/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java
@@ -1475,7 +1475,6 @@
         contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
         contentContainer.setTag(FloatingToolbar.FLOATING_TOOLBAR_TAG);
-        contentContainer.setContentDescription(FloatingToolbar.FLOATING_TOOLBAR_TAG);
         contentContainer.setClipToOutline(true);
         return contentContainer;
     }
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index 671e634..7f50204 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -49,7 +49,7 @@
 per-file *Zygote* = file:/ZYGOTE_OWNERS
 per-file core_jni_helpers.* = file:/ZYGOTE_OWNERS
 per-file fd_utils.* = file:/ZYGOTE_OWNERS
-per-file Android.bp = file:platform/build/soong:/OWNERS
+per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
 per-file android_animation_* = file:/core/java/android/animation/OWNERS
 per-file android_app_admin_* = file:/core/java/android/app/admin/OWNERS
 per-file android_hardware_Usb* = file:/services/usb/OWNERS
diff --git a/core/res/res/layout/floating_popup_container.xml b/core/res/res/layout/floating_popup_container.xml
index ca03737..776a35d 100644
--- a/core/res/res/layout/floating_popup_container.xml
+++ b/core/res/res/layout/floating_popup_container.xml
@@ -16,6 +16,7 @@
 */
 -->
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/floating_popup_container"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:padding="0dp"
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index b7da6ae..7a2c866 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5422,8 +5422,10 @@
     <!-- Hint text in a search edit box (used to filter long language / country lists) [CHAR LIMIT=25] -->
     <string name="search_language_hint">Type language name</string>
 
-    <!-- List section subheader for the language picker, containing a list of suggested languages determined by the default region [CHAR LIMIT=30] -->
+    <!-- List section subheader for the language picker, containing a list of suggested languages [CHAR LIMIT=30] -->
     <string name="language_picker_section_suggested">Suggested</string>
+    <!-- "List section subheader for the language picker, containing a list of suggested regions available for that language [CHAR LIMIT=30] -->
+    <string name="language_picker_regions_section_suggested">Suggested</string>
 
     <!-- List section subheader for the language picker, containing a list of suggested languages determined by the default region [CHAR LIMIT=30] -->
     <string name="language_picker_section_suggested_bilingual">Suggested languages</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d60fc20..91f9bef 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3141,6 +3141,7 @@
   <java-symbol type="string" name="language_picker_section_all" />
   <java-symbol type="string" name="region_picker_section_all" />
   <java-symbol type="string" name="language_picker_section_suggested" />
+  <java-symbol type="string" name="language_picker_regions_section_suggested" />
   <java-symbol type="string" name="language_picker_section_suggested_bilingual" />
   <java-symbol type="string" name="region_picker_section_suggested_bilingual" />
   <java-symbol type="string" name="language_selection_title" />
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index 0a5a4d5..993ecf6 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -221,15 +221,15 @@
 
     @Test
     public void testRecyclePauseActivityItemItem() {
-        PauseActivityItem emptyItem = PauseActivityItem.obtain(false, false, 0, false);
-        PauseActivityItem item = PauseActivityItem.obtain(true, true, 5, true);
+        PauseActivityItem emptyItem = PauseActivityItem.obtain(false, false, 0, false, false);
+        PauseActivityItem item = PauseActivityItem.obtain(true, true, 5, true, true);
         assertNotSame(item, emptyItem);
         assertFalse(item.equals(emptyItem));
 
         item.recycle();
         assertEquals(item, emptyItem);
 
-        PauseActivityItem item2 = PauseActivityItem.obtain(true, false, 5, true);
+        PauseActivityItem item2 = PauseActivityItem.obtain(true, false, 5, true, true);
         assertSame(item, item2);
         assertFalse(item2.equals(emptyItem));
     }
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index e9bbdbe..b292d7d 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -234,7 +234,8 @@
     public void testPause() {
         // Write to parcel
         PauseActivityItem item = PauseActivityItem.obtain(true /* finished */,
-                true /* userLeaving */, 135 /* configChanges */, true /* dontReport */);
+                true /* userLeaving */, 135 /* configChanges */, true /* dontReport */,
+                true /* autoEnteringPip */);
         writeAndPrepareForReading(item);
 
         // Read from parcel and assert
diff --git a/core/tests/coretests/src/android/widget/FloatingToolbarUtils.java b/core/tests/coretests/src/android/widget/FloatingToolbarUtils.java
index c6f5924..2d3ed95 100644
--- a/core/tests/coretests/src/android/widget/FloatingToolbarUtils.java
+++ b/core/tests/coretests/src/android/widget/FloatingToolbarUtils.java
@@ -16,13 +16,12 @@
 
 package android.widget;
 
-import static com.android.internal.widget.floatingtoolbar.FloatingToolbar.FLOATING_TOOLBAR_TAG;
-
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.content.res.Resources;
 import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.Until;
 
@@ -33,25 +32,27 @@
 final class FloatingToolbarUtils {
 
     private final UiDevice mDevice;
+    private static final BySelector TOOLBAR_CONTAINER_SELECTOR =
+            By.res("android", "floating_popup_container");
 
     FloatingToolbarUtils() {
         mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
     }
 
     void waitForFloatingToolbarPopup() {
-        mDevice.wait(Until.findObject(By.desc(FLOATING_TOOLBAR_TAG)), 500);
+        mDevice.wait(Until.findObject(TOOLBAR_CONTAINER_SELECTOR), 500);
     }
 
     void assertFloatingToolbarIsDisplayed() {
         waitForFloatingToolbarPopup();
-        assertThat(mDevice.hasObject(By.desc(FLOATING_TOOLBAR_TAG))).isTrue();
+        assertThat(mDevice.hasObject(TOOLBAR_CONTAINER_SELECTOR)).isTrue();
     }
 
     void assertFloatingToolbarContainsItem(String itemLabel) {
         waitForFloatingToolbarPopup();
         assertWithMessage("Expected to find item labelled [" + itemLabel + "]")
                 .that(mDevice.hasObject(
-                        By.desc(FLOATING_TOOLBAR_TAG).hasDescendant(By.text(itemLabel))))
+                        TOOLBAR_CONTAINER_SELECTOR.hasDescendant(By.text(itemLabel))))
                 .isTrue();
     }
 
@@ -59,14 +60,14 @@
         waitForFloatingToolbarPopup();
         assertWithMessage("Expected to not find item labelled [" + itemLabel + "]")
                 .that(mDevice.hasObject(
-                        By.desc(FLOATING_TOOLBAR_TAG).hasDescendant(By.text(itemLabel))))
+                        TOOLBAR_CONTAINER_SELECTOR.hasDescendant(By.text(itemLabel))))
                 .isFalse();
     }
 
     void assertFloatingToolbarContainsItemAtIndex(String itemLabel, int index) {
         waitForFloatingToolbarPopup();
         assertWithMessage("Expected to find item labelled [" + itemLabel + "] at index " + index)
-                .that(mDevice.findObject(By.desc(FLOATING_TOOLBAR_TAG))
+                .that(mDevice.findObject(TOOLBAR_CONTAINER_SELECTOR)
                         .findObjects(By.clickable(true))
                         .get(index)
                         .getChildren()
@@ -77,7 +78,7 @@
 
     void clickFloatingToolbarItem(String label) {
         waitForFloatingToolbarPopup();
-        mDevice.findObject(By.desc(FLOATING_TOOLBAR_TAG))
+        mDevice.findObject(TOOLBAR_CONTAINER_SELECTOR)
                 .findObject(By.text(label))
                 .click();
     }
@@ -85,13 +86,13 @@
     void clickFloatingToolbarOverflowItem(String label) {
         // TODO: There might be a benefit to combining this with "clickFloatingToolbarItem" method.
         waitForFloatingToolbarPopup();
-        mDevice.findObject(By.desc(FLOATING_TOOLBAR_TAG))
+        mDevice.findObject(TOOLBAR_CONTAINER_SELECTOR)
                 .findObject(By.desc(str(R.string.floating_toolbar_open_overflow_description)))
                 .click();
         mDevice.wait(
-                Until.findObject(By.desc(FLOATING_TOOLBAR_TAG).hasDescendant(By.text(label))),
+                Until.findObject(TOOLBAR_CONTAINER_SELECTOR.hasDescendant(By.text(label))),
                 1000);
-        mDevice.findObject(By.desc(FLOATING_TOOLBAR_TAG))
+        mDevice.findObject(TOOLBAR_CONTAINER_SELECTOR)
                 .findObject(By.text(label))
                 .click();
     }
diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
index f4a6f02..613eddd 100644
--- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
+++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
@@ -298,8 +298,8 @@
 
         private void pauseActivity(ActivityClientRecord r) {
             mThread.handlePauseActivity(r, false /* finished */,
-                    false /* userLeaving */, 0 /* configChanges */, null /* pendingActions */,
-                    "test");
+                    false /* userLeaving */, 0 /* configChanges */, false /* autoEnteringPip */,
+                    null /* pendingActions */, "test");
         }
 
         private void stopActivity(ActivityClientRecord r) {
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 03d22ac..748032b 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -2065,6 +2065,12 @@
       "group": "WM_DEBUG_CONFIGURATION",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-108248992": {
+      "message": "Defer transition ready for TaskFragmentTransaction=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+    },
     "-106400104": {
       "message": "Preload recents with %s",
       "level": "DEBUG",
@@ -2113,6 +2119,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
+    "-79016993": {
+      "message": "Continue transition ready for TaskFragmentTransaction=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java"
+    },
     "-70719599": {
       "message": "Unregister remote animations for organizer=%s uid=%d pid=%d",
       "level": "VERBOSE",
diff --git a/data/fonts/fonts.xml b/data/fonts/fonts.xml
index f8c015f..8e2a59c 100644
--- a/data/fonts/fonts.xml
+++ b/data/fonts/fonts.xml
@@ -39,7 +39,11 @@
           <axis tag="wdth" stylevalue="100" />
           <axis tag="wght" stylevalue="300" />
         </font>
-        <font weight="400" style="normal">RobotoStatic-Regular.ttf</font>
+        <font weight="400" style="normal">Roboto-Regular.ttf
+          <axis tag="ital" stylevalue="0" />
+          <axis tag="wdth" stylevalue="100" />
+          <axis tag="wght" stylevalue="400" />
+        </font>
         <font weight="500" style="normal">Roboto-Regular.ttf
           <axis tag="ital" stylevalue="0" />
           <axis tag="wdth" stylevalue="100" />
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
new file mode 100644
index 0000000..cc4db93
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -0,0 +1,234 @@
+/*
+ * 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.activityembedding;
+
+import static android.graphics.Matrix.MSCALE_X;
+import static android.graphics.Matrix.MTRANS_X;
+import static android.graphics.Matrix.MTRANS_Y;
+
+import android.annotation.CallSuper;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Wrapper to handle the ActivityEmbedding animation update in one
+ * {@link SurfaceControl.Transaction}.
+ */
+class ActivityEmbeddingAnimationAdapter {
+
+    /**
+     * If {@link #mOverrideLayer} is set to this value, we don't want to override the surface layer.
+     */
+    private static final int LAYER_NO_OVERRIDE = -1;
+
+    final Animation mAnimation;
+    final TransitionInfo.Change mChange;
+    final SurfaceControl mLeash;
+
+    final Transformation mTransformation = new Transformation();
+    final float[] mMatrix = new float[9];
+    final float[] mVecs = new float[4];
+    final Rect mRect = new Rect();
+    private boolean mIsFirstFrame = true;
+    private int mOverrideLayer = LAYER_NO_OVERRIDE;
+
+    ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
+            @NonNull TransitionInfo.Change change) {
+        this(animation, change, change.getLeash());
+    }
+
+    /**
+     * @param leash the surface to animate, which is not necessary the same as
+     * {@link TransitionInfo.Change#getLeash()}, it can be a screenshot for example.
+     */
+    ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
+            @NonNull TransitionInfo.Change change, @NonNull SurfaceControl leash) {
+        mAnimation = animation;
+        mChange = change;
+        mLeash = leash;
+    }
+
+    /**
+     * Surface layer to be set at the first frame of the animation. We will not set the layer if it
+     * is set to {@link #LAYER_NO_OVERRIDE}.
+     */
+    final void overrideLayer(int layer) {
+        mOverrideLayer = layer;
+    }
+
+    /** Called on frame update. */
+    final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
+        if (mIsFirstFrame) {
+            t.show(mLeash);
+            if (mOverrideLayer != LAYER_NO_OVERRIDE) {
+                t.setLayer(mLeash, mOverrideLayer);
+            }
+            mIsFirstFrame = false;
+        }
+
+        // Extract the transformation to the current time.
+        mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()),
+                mTransformation);
+        t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+        onAnimationUpdateInner(t);
+    }
+
+    /** To be overridden by subclasses to adjust the animation surface change. */
+    void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+        final Point offset = mChange.getEndRelOffset();
+        mTransformation.getMatrix().postTranslate(offset.x, offset.y);
+        t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+        t.setAlpha(mLeash, mTransformation.getAlpha());
+        // Get current animation position.
+        final int positionX = Math.round(mMatrix[MTRANS_X]);
+        final int positionY = Math.round(mMatrix[MTRANS_Y]);
+        // The exiting surface starts at position: Change#getEndRelOffset() and moves with
+        // positionX varying. Offset our crop region by the amount we have slided so crop
+        // regions stays exactly on the original container in split.
+        final int cropOffsetX = offset.x - positionX;
+        final int cropOffsetY = offset.y - positionY;
+        final Rect cropRect = new Rect();
+        cropRect.set(mChange.getEndAbsBounds());
+        // Because window crop uses absolute position.
+        cropRect.offsetTo(0, 0);
+        cropRect.offset(cropOffsetX, cropOffsetY);
+        t.setCrop(mLeash, cropRect);
+    }
+
+    /** Called after animation finished. */
+    @CallSuper
+    void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
+        onAnimationUpdate(t, mAnimation.getDuration());
+    }
+
+    final long getDurationHint() {
+        return mAnimation.computeDurationHint();
+    }
+
+    /**
+     * Should be used when the {@link TransitionInfo.Change} is in split with others, and wants to
+     * animate together as one. This adapter will offset the animation leash to make the animate of
+     * two windows look like a single window.
+     */
+    static class SplitAdapter extends ActivityEmbeddingAnimationAdapter {
+        private final boolean mIsLeftHalf;
+        private final int mWholeAnimationWidth;
+
+        /**
+         * @param isLeftHalf whether this is the left half of the animation.
+         * @param wholeAnimationWidth the whole animation windows width.
+         */
+        SplitAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
+                boolean isLeftHalf, int wholeAnimationWidth) {
+            super(animation, change);
+            mIsLeftHalf = isLeftHalf;
+            mWholeAnimationWidth = wholeAnimationWidth;
+            if (wholeAnimationWidth == 0) {
+                throw new IllegalArgumentException("SplitAdapter must provide wholeAnimationWidth");
+            }
+        }
+
+        @Override
+        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+            final Point offset = mChange.getEndRelOffset();
+            float posX = offset.x;
+            final float posY = offset.y;
+            // This window is half of the whole animation window. Offset left/right to make it
+            // look as one with the other half.
+            mTransformation.getMatrix().getValues(mMatrix);
+            final int changeWidth = mChange.getEndAbsBounds().width();
+            final float scaleX = mMatrix[MSCALE_X];
+            final float totalOffset = mWholeAnimationWidth * (1 - scaleX) / 2;
+            final float curOffset = changeWidth * (1 - scaleX) / 2;
+            final float offsetDiff = totalOffset - curOffset;
+            if (mIsLeftHalf) {
+                posX += offsetDiff;
+            } else {
+                posX -= offsetDiff;
+            }
+            mTransformation.getMatrix().postTranslate(posX, posY);
+            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+            t.setAlpha(mLeash, mTransformation.getAlpha());
+        }
+    }
+
+    /**
+     * Should be used for the animation of the snapshot of a {@link TransitionInfo.Change} that has
+     * size change.
+     */
+    static class SnapshotAdapter extends ActivityEmbeddingAnimationAdapter {
+
+        SnapshotAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
+                @NonNull SurfaceControl snapshotLeash) {
+            super(animation, change, snapshotLeash);
+        }
+
+        @Override
+        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+            // Snapshot should always be placed at the top left of the animation leash.
+            mTransformation.getMatrix().postTranslate(0, 0);
+            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+            t.setAlpha(mLeash, mTransformation.getAlpha());
+        }
+
+        @Override
+        void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
+            super.onAnimationEnd(t);
+            // Remove the screenshot leash after animation is finished.
+            t.remove(mLeash);
+        }
+    }
+
+    /**
+     * Should be used for the animation of the {@link TransitionInfo.Change} that has size change.
+     */
+    static class BoundsChangeAdapter extends ActivityEmbeddingAnimationAdapter {
+
+        BoundsChangeAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change) {
+            super(animation, change);
+        }
+
+        @Override
+        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+            final Point offset = mChange.getEndRelOffset();
+            mTransformation.getMatrix().postTranslate(offset.x, offset.y);
+            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+            t.setAlpha(mLeash, mTransformation.getAlpha());
+
+            // The following applies an inverse scale to the clip-rect so that it crops "after" the
+            // scale instead of before.
+            mVecs[1] = mVecs[2] = 0;
+            mVecs[0] = mVecs[3] = 1;
+            mTransformation.getMatrix().mapVectors(mVecs);
+            mVecs[0] = 1.f / mVecs[0];
+            mVecs[3] = 1.f / mVecs[3];
+            final Rect clipRect = mTransformation.getClipRect();
+            mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
+            mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
+            mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
+            mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
+            t.setCrop(mLeash, mRect);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
new file mode 100644
index 0000000..7e0795d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -0,0 +1,290 @@
+/*
+ * 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.activityembedding;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.common.ScreenshotUtils;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+/** To run the ActivityEmbedding animations. */
+class ActivityEmbeddingAnimationRunner {
+
+    private static final String TAG = "ActivityEmbeddingAnimR";
+
+    private final ActivityEmbeddingController mController;
+    @VisibleForTesting
+    final ActivityEmbeddingAnimationSpec mAnimationSpec;
+
+    ActivityEmbeddingAnimationRunner(@NonNull Context context,
+            @NonNull ActivityEmbeddingController controller) {
+        mController = controller;
+        mAnimationSpec = new ActivityEmbeddingAnimationSpec(context);
+    }
+
+    /** Creates and starts animation for ActivityEmbedding transition. */
+    void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction) {
+        final Animator animator = createAnimator(info, startTransaction, finishTransaction,
+                () -> mController.onAnimationFinished(transition));
+        startTransaction.apply();
+        animator.start();
+    }
+
+    /**
+     * Sets transition animation scale settings value.
+     * @param scale The setting value of transition animation scale.
+     */
+    void setAnimScaleSetting(float scale) {
+        mAnimationSpec.setAnimScaleSetting(scale);
+    }
+
+    /** Creates the animator for the given {@link TransitionInfo}. */
+    @VisibleForTesting
+    @NonNull
+    Animator createAnimator(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Runnable animationFinishCallback) {
+        final List<ActivityEmbeddingAnimationAdapter> adapters =
+                createAnimationAdapters(info, startTransaction);
+        long duration = 0;
+        for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+            duration = Math.max(duration, adapter.getDurationHint());
+        }
+        final ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+        animator.setDuration(duration);
+        animator.addUpdateListener((anim) -> {
+            // Update all adapters in the same transaction.
+            final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+            for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+                adapter.onAnimationUpdate(t, animator.getCurrentPlayTime());
+            }
+            t.apply();
+        });
+        animator.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {}
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+                for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+                    adapter.onAnimationEnd(t);
+                }
+                t.apply();
+                animationFinishCallback.run();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {}
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {}
+        });
+        return animator;
+    }
+
+    /**
+     * Creates list of {@link ActivityEmbeddingAnimationAdapter} to handle animations on all window
+     * changes.
+     */
+    @NonNull
+    private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters(
+            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
+        for (TransitionInfo.Change change : info.getChanges()) {
+            if (change.getMode() == TRANSIT_CHANGE
+                    && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
+                return createChangeAnimationAdapters(info, startTransaction);
+            }
+        }
+        if (Transitions.isClosingType(info.getType())) {
+            return createCloseAnimationAdapters(info);
+        }
+        return createOpenAnimationAdapters(info);
+    }
+
+    @NonNull
+    private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters(
+            @NonNull TransitionInfo info) {
+        return createOpenCloseAnimationAdapters(info, true /* isOpening */,
+                mAnimationSpec::loadOpenAnimation);
+    }
+
+    @NonNull
+    private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters(
+            @NonNull TransitionInfo info) {
+        return createOpenCloseAnimationAdapters(info, false /* isOpening */,
+                mAnimationSpec::loadCloseAnimation);
+    }
+
+    /**
+     * Creates {@link ActivityEmbeddingAnimationAdapter} for OPEN and CLOSE types of transition.
+     * @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type.
+     */
+    @NonNull
+    private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters(
+            @NonNull TransitionInfo info, boolean isOpening,
+            @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider) {
+        // We need to know if the change window is only a partial of the whole animation screen.
+        // If so, we will need to adjust it to make the whole animation screen looks like one.
+        final List<TransitionInfo.Change> openingChanges = new ArrayList<>();
+        final List<TransitionInfo.Change> closingChanges = new ArrayList<>();
+        final Rect openingWholeScreenBounds = new Rect();
+        final Rect closingWholeScreenBounds = new Rect();
+        for (TransitionInfo.Change change : info.getChanges()) {
+            final Rect bounds = new Rect(change.getEndAbsBounds());
+            final Point offset = change.getEndRelOffset();
+            bounds.offsetTo(offset.x, offset.y);
+            if (Transitions.isOpeningType(change.getMode())) {
+                openingChanges.add(change);
+                openingWholeScreenBounds.union(bounds);
+            } else {
+                closingChanges.add(change);
+                closingWholeScreenBounds.union(bounds);
+            }
+        }
+
+        // For OPEN transition, open windows should be above close windows.
+        // For CLOSE transition, open windows should be below close windows.
+        int offsetLayer = TYPE_LAYER_OFFSET;
+        final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
+        for (TransitionInfo.Change change : openingChanges) {
+            final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
+                    change, animationProvider, openingWholeScreenBounds);
+            if (isOpening) {
+                adapter.overrideLayer(offsetLayer++);
+            }
+            adapters.add(adapter);
+        }
+        for (TransitionInfo.Change change : closingChanges) {
+            final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
+                    change, animationProvider, closingWholeScreenBounds);
+            if (!isOpening) {
+                adapter.overrideLayer(offsetLayer++);
+            }
+            adapters.add(adapter);
+        }
+        return adapters;
+    }
+
+    @NonNull
+    private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter(
+            @NonNull TransitionInfo.Change change,
+            @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider,
+            @NonNull Rect wholeAnimationBounds) {
+        final Animation animation = animationProvider.apply(change, wholeAnimationBounds);
+        final Rect bounds = new Rect(change.getEndAbsBounds());
+        final Point offset = change.getEndRelOffset();
+        bounds.offsetTo(offset.x, offset.y);
+        if (bounds.left == wholeAnimationBounds.left
+                && bounds.right != wholeAnimationBounds.right) {
+            // This is the left split of the whole animation window.
+            return new ActivityEmbeddingAnimationAdapter.SplitAdapter(animation, change,
+                    true /* isLeftHalf */, wholeAnimationBounds.width());
+        } else if (bounds.left != wholeAnimationBounds.left
+                && bounds.right == wholeAnimationBounds.right) {
+            // This is the right split of the whole animation window.
+            return new ActivityEmbeddingAnimationAdapter.SplitAdapter(animation, change,
+                    false /* isLeftHalf */, wholeAnimationBounds.width());
+        }
+        // Open/close window that fills the whole animation.
+        return new ActivityEmbeddingAnimationAdapter(animation, change);
+    }
+
+    @NonNull
+    private List<ActivityEmbeddingAnimationAdapter> createChangeAnimationAdapters(
+            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
+        final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
+        for (TransitionInfo.Change change : info.getChanges()) {
+            if (change.getMode() == TRANSIT_CHANGE
+                    && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
+                // This is the window with bounds change.
+                final WindowContainerToken parentToken = change.getParent();
+                final Rect parentBounds;
+                if (parentToken != null) {
+                    TransitionInfo.Change parentChange = info.getChange(parentToken);
+                    parentBounds = parentChange != null
+                            ? parentChange.getEndAbsBounds()
+                            : change.getEndAbsBounds();
+                } else {
+                    parentBounds = change.getEndAbsBounds();
+                }
+                final Animation[] animations =
+                        mAnimationSpec.createChangeBoundsChangeAnimations(change, parentBounds);
+                // Adapter for the starting screenshot leash.
+                final SurfaceControl screenshotLeash = createScreenshot(change, startTransaction);
+                if (screenshotLeash != null) {
+                    // The screenshot leash will be removed in SnapshotAdapter#onAnimationEnd
+                    adapters.add(new ActivityEmbeddingAnimationAdapter.SnapshotAdapter(
+                            animations[0], change, screenshotLeash));
+                } else {
+                    Log.e(TAG, "Failed to take screenshot for change=" + change);
+                }
+                // Adapter for the ending bounds changed leash.
+                adapters.add(new ActivityEmbeddingAnimationAdapter.BoundsChangeAdapter(
+                        animations[1], change));
+                continue;
+            }
+
+            // These are the other windows that don't have bounds change in the same transition.
+            final Animation animation;
+            if (!TransitionInfo.isIndependent(change, info)) {
+                // No-op if it will be covered by the changing parent window.
+                animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
+            } else if (Transitions.isClosingType(change.getMode())) {
+                animation = mAnimationSpec.createChangeBoundsCloseAnimation(change);
+            } else {
+                animation = mAnimationSpec.createChangeBoundsOpenAnimation(change);
+            }
+            adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change));
+        }
+        return adapters;
+    }
+
+    /** Takes a screenshot of the given {@link TransitionInfo.Change} surface. */
+    @Nullable
+    private SurfaceControl createScreenshot(@NonNull TransitionInfo.Change change,
+            @NonNull SurfaceControl.Transaction startTransaction) {
+        final Rect cropBounds = new Rect(change.getStartAbsBounds());
+        cropBounds.offsetTo(0, 0);
+        return ScreenshotUtils.takeScreenshot(startTransaction, change.getLeash(), cropBounds,
+                Integer.MAX_VALUE);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
new file mode 100644
index 0000000..6f06f28
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -0,0 +1,212 @@
+/*
+ * 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.activityembedding;
+
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.ClipRectAnimation;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.TranslateAnimation;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
+import com.android.wm.shell.transition.Transitions;
+
+/** Animation spec for ActivityEmbedding transition. */
+// TODO(b/206557124): provide an easier way to customize animation
+class ActivityEmbeddingAnimationSpec {
+
+    private static final String TAG = "ActivityEmbeddingAnimSpec";
+    private static final int CHANGE_ANIMATION_DURATION = 517;
+    private static final int CHANGE_ANIMATION_FADE_DURATION = 80;
+    private static final int CHANGE_ANIMATION_FADE_OFFSET = 30;
+
+    private final Context mContext;
+    private final TransitionAnimation mTransitionAnimation;
+    private final Interpolator mFastOutExtraSlowInInterpolator;
+    private final LinearInterpolator mLinearInterpolator;
+    private float mTransitionAnimationScaleSetting;
+
+    ActivityEmbeddingAnimationSpec(@NonNull Context context) {
+        mContext = context;
+        mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
+        mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
+                mContext, android.R.interpolator.fast_out_extra_slow_in);
+        mLinearInterpolator = new LinearInterpolator();
+    }
+
+    /**
+     * Sets transition animation scale settings value.
+     * @param scale The setting value of transition animation scale.
+     */
+    void setAnimScaleSetting(float scale) {
+        mTransitionAnimationScaleSetting = scale;
+    }
+
+    /** For window that doesn't need to be animated. */
+    @NonNull
+    static Animation createNoopAnimation(@NonNull TransitionInfo.Change change) {
+        // Noop but just keep the window showing/hiding.
+        final float alpha = Transitions.isClosingType(change.getMode()) ? 0f : 1f;
+        return new AlphaAnimation(alpha, alpha);
+    }
+
+    /** Animation for window that is opening in a change transition. */
+    @NonNull
+    Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change) {
+        final Rect bounds = change.getEndAbsBounds();
+        final Point offset = change.getEndRelOffset();
+        // The window will be animated in from left or right depends on its position.
+        final int startLeft = offset.x == 0 ? -bounds.width() : bounds.width();
+
+        // The position should be 0-based as we will post translate in
+        // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+        final Animation animation = new TranslateAnimation(startLeft, 0, 0, 0);
+        animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+        animation.setDuration(CHANGE_ANIMATION_DURATION);
+        animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+        animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        return animation;
+    }
+
+    /** Animation for window that is closing in a change transition. */
+    @NonNull
+    Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change) {
+        final Rect bounds = change.getEndAbsBounds();
+        final Point offset = change.getEndRelOffset();
+        // The window will be animated out to left or right depends on its position.
+        final int endLeft = offset.x == 0 ? -bounds.width() : bounds.width();
+
+        // The position should be 0-based as we will post translate in
+        // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+        final Animation animation = new TranslateAnimation(0, endLeft, 0, 0);
+        animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+        animation.setDuration(CHANGE_ANIMATION_DURATION);
+        animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+        animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        return animation;
+    }
+
+    /**
+     * Animation for window that is changing (bounds change) in a change transition.
+     * @return the return array always has two elements. The first one is for the start leash, and
+     *         the second one is for the end leash.
+     */
+    @NonNull
+    Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change,
+            @NonNull Rect parentBounds) {
+        // Both start bounds and end bounds are in screen coordinates. We will post translate
+        // to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+        final Rect startBounds = change.getStartAbsBounds();
+        final Rect endBounds = change.getEndAbsBounds();
+        float scaleX = ((float) startBounds.width()) / endBounds.width();
+        float scaleY = ((float) startBounds.height()) / endBounds.height();
+        // Start leash is a child of the end leash. Reverse the scale so that the start leash won't
+        // be scaled up with its parent.
+        float startScaleX = 1.f / scaleX;
+        float startScaleY = 1.f / scaleY;
+
+        // The start leash will be fade out.
+        final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */);
+        final Animation startAlpha = new AlphaAnimation(1f, 0f);
+        startAlpha.setInterpolator(mLinearInterpolator);
+        startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
+        startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
+        startSet.addAnimation(startAlpha);
+        final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
+                startScaleY);
+        startScale.setInterpolator(mFastOutExtraSlowInInterpolator);
+        startScale.setDuration(CHANGE_ANIMATION_DURATION);
+        startSet.addAnimation(startScale);
+        startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
+                endBounds.height());
+        startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+        // The end leash will be moved into the end position while scaling.
+        final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
+        endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
+        final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
+        endScale.setDuration(CHANGE_ANIMATION_DURATION);
+        endSet.addAnimation(endScale);
+        // The position should be 0-based as we will post translate in
+        // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+        final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
+                0, 0);
+        endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
+        endSet.addAnimation(endTranslate);
+        // The end leash is resizing, we should update the window crop based on the clip rect.
+        final Rect startClip = new Rect(startBounds);
+        final Rect endClip = new Rect(endBounds);
+        startClip.offsetTo(0, 0);
+        endClip.offsetTo(0, 0);
+        final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
+        clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
+        endSet.addAnimation(clipAnim);
+        endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
+                parentBounds.height());
+        endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+        return new Animation[]{startSet, endSet};
+    }
+
+    @NonNull
+    Animation loadOpenAnimation(@NonNull TransitionInfo.Change change,
+            @NonNull Rect wholeAnimationBounds) {
+        final boolean isEnter = Transitions.isOpeningType(change.getMode());
+        final Animation animation;
+        // TODO(b/207070762):
+        // 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit
+        // 2. Implement edgeExtension version
+        animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+                ? R.anim.task_fragment_open_enter
+                : R.anim.task_fragment_open_exit);
+        final Rect bounds = change.getEndAbsBounds();
+        animation.initialize(bounds.width(), bounds.height(),
+                wholeAnimationBounds.width(), wholeAnimationBounds.height());
+        animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        return animation;
+    }
+
+    @NonNull
+    Animation loadCloseAnimation(@NonNull TransitionInfo.Change change,
+            @NonNull Rect wholeAnimationBounds) {
+        final boolean isEnter = Transitions.isOpeningType(change.getMode());
+        final Animation animation;
+        // TODO(b/207070762):
+        // 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit
+        // 2. Implement edgeExtension version
+        animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+                ? R.anim.task_fragment_close_enter
+                : R.anim.task_fragment_close_exit);
+        final Rect bounds = change.getEndAbsBounds();
+        animation.initialize(bounds.width(), bounds.height(),
+                wholeAnimationBounds.width(), wholeAnimationBounds.height());
+        animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        return animation;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index b305897..e0004fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -18,8 +18,11 @@
 
 import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
 
+import static java.util.Objects.requireNonNull;
+
 import android.content.Context;
 import android.os.IBinder;
+import android.util.ArrayMap;
 import android.view.SurfaceControl;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
@@ -28,6 +31,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
@@ -37,15 +41,37 @@
 public class ActivityEmbeddingController implements Transitions.TransitionHandler {
 
     private final Context mContext;
-    private final Transitions mTransitions;
+    @VisibleForTesting
+    final Transitions mTransitions;
+    @VisibleForTesting
+    final ActivityEmbeddingAnimationRunner mAnimationRunner;
 
-    public ActivityEmbeddingController(Context context, ShellInit shellInit,
-            Transitions transitions) {
-        mContext = context;
-        mTransitions = transitions;
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            shellInit.addInitCallback(this::onInit, this);
-        }
+    /**
+     * Keeps track of the currently-running transition callback associated with each transition
+     * token.
+     */
+    private final ArrayMap<IBinder, Transitions.TransitionFinishCallback> mTransitionCallbacks =
+            new ArrayMap<>();
+
+    private ActivityEmbeddingController(@NonNull Context context, @NonNull ShellInit shellInit,
+            @NonNull Transitions transitions) {
+        mContext = requireNonNull(context);
+        mTransitions = requireNonNull(transitions);
+        mAnimationRunner = new ActivityEmbeddingAnimationRunner(context, this);
+
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    /**
+     * Creates {@link ActivityEmbeddingController}, returns {@code null} if the feature is not
+     * supported.
+     */
+    @Nullable
+    public static ActivityEmbeddingController create(@NonNull Context context,
+            @NonNull ShellInit shellInit, @NonNull Transitions transitions) {
+        return Transitions.ENABLE_SHELL_TRANSITIONS
+                ? new ActivityEmbeddingController(context, shellInit, transitions)
+                : null;
     }
 
     /** Registers to handle transitions. */
@@ -66,9 +92,9 @@
             }
         }
 
-        // TODO(b/207070762) Implement AE animation.
-        startTransaction.apply();
-        finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+        // Start ActivityEmbedding animation.
+        mTransitionCallbacks.put(transition, finishCallback);
+        mAnimationRunner.startAnimation(transition, info, startTransaction, finishTransaction);
         return true;
     }
 
@@ -79,6 +105,21 @@
         return null;
     }
 
+    @Override
+    public void setAnimScaleSetting(float scale) {
+        mAnimationRunner.setAnimScaleSetting(scale);
+    }
+
+    /** Called when the animation is finished. */
+    void onAnimationFinished(@NonNull IBinder transition) {
+        final Transitions.TransitionFinishCallback callback =
+                mTransitionCallbacks.remove(transition);
+        if (callback == null) {
+            throw new IllegalStateException("No finish callback found");
+        }
+        callback.onTransitionFinished(null /* wct */, null /* wctCB */);
+    }
+
     private static boolean isEmbedded(@NonNull TransitionInfo.Change change) {
         return (change.getFlags() & FLAG_IS_EMBEDDED) != 0;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 2c02006..99b8885 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -821,7 +821,7 @@
     /**
      * Description of current bubble state.
      */
-    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+    public void dump(@NonNull PrintWriter pw) {
         pw.print("key: "); pw.println(mKey);
         pw.print("  showInShade:   "); pw.println(showInShade());
         pw.print("  showDot:       "); pw.println(showDot());
@@ -831,7 +831,7 @@
         pw.print("  suppressNotif: "); pw.println(shouldSuppressNotification());
         pw.print("  autoExpand:    "); pw.println(shouldAutoExpand());
         if (mExpandedView != null) {
-            mExpandedView.dump(pw, args);
+            mExpandedView.dump(pw);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index de26b549..dcbb272 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -72,7 +72,6 @@
 import android.service.notification.NotificationListenerService.RankingMap;
 import android.util.Log;
 import android.util.Pair;
-import android.util.Slog;
 import android.util.SparseArray;
 import android.view.View;
 import android.view.ViewGroup;
@@ -100,6 +99,7 @@
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
@@ -159,6 +159,7 @@
     private final TaskViewTransitions mTaskViewTransitions;
     private final SyncTransactionQueue mSyncQueue;
     private final ShellController mShellController;
+    private final ShellCommandHandler mShellCommandHandler;
 
     // Used to post to main UI thread
     private final ShellExecutor mMainExecutor;
@@ -229,6 +230,7 @@
   
     public BubbleController(Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ShellController shellController,
             BubbleData data,
             @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
@@ -252,6 +254,7 @@
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
         mContext = context;
+        mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
         mLauncherApps = launcherApps;
         mBarService = statusBarService == null
@@ -431,6 +434,7 @@
         mCurrentProfiles = userProfiles;
 
         mShellController.addConfigurationChangeListener(this);
+        mShellCommandHandler.addDumpCallback(this::dump, this);
     }
 
     @VisibleForTesting
@@ -538,7 +542,6 @@
 
         if (mNotifEntryToExpandOnShadeUnlock != null) {
             expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
-            mNotifEntryToExpandOnShadeUnlock = null;
         }
 
         updateStack();
@@ -925,15 +928,6 @@
         return (isSummary && isSuppressedSummary) || isSuppressedBubble;
     }
 
-    private void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback) {
-        if (mBubbleData.isSummarySuppressed(groupKey)) {
-            mBubbleData.removeSuppressedSummary(groupKey);
-            if (callback != null) {
-                callback.accept(mBubbleData.getSummaryKey(groupKey));
-            }
-        }
-    }
-
     /** Promote the provided bubble from the overflow view. */
     public void promoteBubbleFromOverflow(Bubble bubble) {
         mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
@@ -1519,14 +1513,15 @@
     /**
      * Description of current bubble state.
      */
-    private void dump(PrintWriter pw, String[] args) {
+    private void dump(PrintWriter pw, String prefix) {
         pw.println("BubbleController state:");
-        mBubbleData.dump(pw, args);
+        mBubbleData.dump(pw);
         pw.println();
         if (mStackView != null) {
-            mStackView.dump(pw, args);
+            mStackView.dump(pw);
         }
         pw.println();
+        mImpl.mCachedState.dump(pw);
     }
 
     /**
@@ -1711,28 +1706,12 @@
         }
 
         @Override
-        public boolean isStackExpanded() {
-            return mCachedState.isStackExpanded();
-        }
-
-        @Override
         @Nullable
         public Bubble getBubbleWithShortcutId(String shortcutId) {
             return mCachedState.getBubbleWithShortcutId(shortcutId);
         }
 
         @Override
-        public void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
-                Executor callbackExecutor) {
-            mMainExecutor.execute(() -> {
-                Consumer<String> cb = callback != null
-                        ? (key) -> callbackExecutor.execute(() -> callback.accept(key))
-                        : null;
-                BubbleController.this.removeSuppressedSummaryIfNecessary(groupKey, cb);
-            });
-        }
-
-        @Override
         public void collapseStack() {
             mMainExecutor.execute(() -> {
                 BubbleController.this.collapseStack();
@@ -1761,13 +1740,6 @@
         }
 
         @Override
-        public void openBubbleOverflow() {
-            mMainExecutor.execute(() -> {
-                BubbleController.this.openBubbleOverflow();
-            });
-        }
-
-        @Override
         public boolean handleDismissalInterception(BubbleEntry entry,
                 @Nullable List<BubbleEntry> children, IntConsumer removeCallback,
                 Executor callbackExecutor) {
@@ -1882,18 +1854,6 @@
             mMainExecutor.execute(
                     () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded));
         }
-
-        @Override
-        public void dump(PrintWriter pw, String[] args) {
-            try {
-                mMainExecutor.executeBlocking(() -> {
-                    BubbleController.this.dump(pw, args);
-                    mCachedState.dump(pw);
-                });
-            } catch (InterruptedException e) {
-                Slog.e(TAG, "Failed to dump BubbleController in 2s");
-            }
-        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index fa86c84..c64133f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -1136,7 +1136,7 @@
     /**
      * Description of current bubble data state.
      */
-    public void dump(PrintWriter pw, String[] args) {
+    public void dump(PrintWriter pw) {
         pw.print("selected: ");
         pw.println(mSelectedBubble != null
                 ? mSelectedBubble.getKey()
@@ -1147,13 +1147,13 @@
         pw.print("stack bubble count:    ");
         pw.println(mBubbles.size());
         for (Bubble bubble : mBubbles) {
-            bubble.dump(pw, args);
+            bubble.dump(pw);
         }
 
         pw.print("overflow bubble count:    ");
         pw.println(mOverflowBubbles.size());
         for (Bubble bubble : mOverflowBubbles) {
-            bubble.dump(pw, args);
+            bubble.dump(pw);
         }
 
         pw.print("summaryKeys: ");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 2666a0e..cfbe1b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -1044,7 +1044,7 @@
     /**
      * Description of current expanded view state.
      */
-    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+    public void dump(@NonNull PrintWriter pw) {
         pw.print("BubbleExpandedView");
         pw.print("  taskId:               "); pw.println(mTaskId);
         pw.print("  stackView:            "); pw.println(mStackView);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 2d0be06..5bf88b1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -299,7 +299,7 @@
     private BubblesNavBarGestureTracker mBubblesNavBarGestureTracker;
 
     /** Description of current animation controller state. */
-    public void dump(PrintWriter pw, String[] args) {
+    public void dump(PrintWriter pw) {
         pw.println("Stack view state:");
 
         String bubblesOnScreen = BubbleDebugConfig.formatBubblesString(
@@ -313,8 +313,8 @@
         pw.print("  expandedContainerMatrix: ");
         pw.println(mExpandedViewContainer.getAnimationMatrix());
 
-        mStackAnimationController.dump(pw, args);
-        mExpandedAnimationController.dump(pw, args);
+        mStackAnimationController.dump(pw);
+        mExpandedAnimationController.dump(pw);
 
         if (mExpandedBubble != null) {
             pw.println("Expanded bubble state:");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 37b96ff..0e97e9e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -35,7 +35,6 @@
 
 import com.android.wm.shell.common.annotations.ExternalThread;
 
-import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 import java.util.HashMap;
@@ -91,18 +90,6 @@
      */
     boolean isBubbleExpanded(String key);
 
-    /** @return {@code true} if stack of bubbles is expanded or not. */
-    boolean isStackExpanded();
-
-    /**
-     * Removes a group key indicating that the summary for this group should no longer be
-     * suppressed.
-     *
-     * @param callback If removed, this callback will be called with the summary key of the group
-     */
-    void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
-            Executor callbackExecutor);
-
     /** Tell the stack of bubbles to collapse. */
     void collapseStack();
 
@@ -130,9 +117,6 @@
     /** Called for any taskbar changes. */
     void onTaskbarChanged(Bundle b);
 
-    /** Open the overflow view. */
-    void openBubbleOverflow();
-
     /**
      * We intercept notification entries (including group summaries) dismissed by the user when
      * there is an active bubble associated with it. We do this so that developers can still
@@ -252,9 +236,6 @@
      */
     void onUserRemoved(int removedUserId);
 
-    /** Description of current bubble state. */
-    void dump(PrintWriter pw, String[] args);
-
     /** Listener to find out about stack expansion / collapse events. */
     interface BubbleExpandListener {
         /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index b521cb6a..ae434bc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -468,7 +468,7 @@
     }
 
     /** Description of current animation controller state. */
-    public void dump(PrintWriter pw, String[] args) {
+    public void dump(PrintWriter pw) {
         pw.println("ExpandedAnimationController state:");
         pw.print("  isActive:          "); pw.println(isActiveController());
         pw.print("  animatingExpand:   "); pw.println(mAnimatingExpand);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
index 0a1b4d7..4e2cbfd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
@@ -431,7 +431,7 @@
     }
 
     /** Description of current animation controller state. */
-    public void dump(PrintWriter pw, String[] args) {
+    public void dump(PrintWriter pw) {
         pw.println("StackAnimationController state:");
         pw.print("  isActive:             "); pw.println(isActiveController());
         pw.print("  restingStackPos:      ");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
index e22c951..8022e9b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
@@ -66,6 +66,7 @@
     @Provides
     static Optional<Pip> providePip(
             Context context,
+            ShellInit shellInit,
             ShellController shellController,
             TvPipBoundsState tvPipBoundsState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
@@ -84,6 +85,7 @@
         return Optional.of(
                 TvPipController.create(
                         context,
+                        shellInit,
                         shellController,
                         tvPipBoundsState,
                         tvPipBoundsAlgorithm,
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 a6a04cf..ceaa64e 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
@@ -627,11 +627,12 @@
 
     @WMSingleton
     @Provides
-    static ActivityEmbeddingController provideActivityEmbeddingController(
+    static Optional<ActivityEmbeddingController> provideActivityEmbeddingController(
             Context context,
             ShellInit shellInit,
             Transitions transitions) {
-        return new ActivityEmbeddingController(context, shellInit, transitions);
+        return Optional.ofNullable(
+                ActivityEmbeddingController.create(context, shellInit, transitions));
     }
 
     //
@@ -686,7 +687,7 @@
             Optional<RecentTasksController> recentTasksOptional,
             Optional<OneHandedController> oneHandedControllerOptional,
             Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
-            ActivityEmbeddingController activityEmbeddingOptional,
+            Optional<ActivityEmbeddingController> activityEmbeddingOptional,
             Transitions transitions,
             StartingWindowController startingWindow,
             @ShellCreateTriggerOverride Optional<Object> overriddenCreateTrigger) {
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 2bcc134..4fe3255 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
@@ -145,6 +145,7 @@
     @Provides
     static BubbleController provideBubbleController(Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ShellController shellController,
             BubbleData data,
             FloatingContentCoordinator floatingContentCoordinator,
@@ -165,7 +166,7 @@
             @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
-        return new BubbleController(context, shellInit, shellController, data,
+        return new BubbleController(context, shellInit, shellCommandHandler, shellController, data,
                 null /* synchronizer */, floatingContentCoordinator,
                 new BubbleDataRepository(context, launcherApps, mainExecutor),
                 statusBarService, windowManager, windowManagerShellWrapper, userManager,
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 76c0f41..7129165 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
@@ -37,16 +37,6 @@
     }
 
     /**
-     * Return one handed settings enabled or not.
-     */
-    boolean isOneHandedEnabled();
-
-    /**
-     * Return swipe to notification settings enabled or not.
-     */
-    boolean isSwipeToNotificationEnabled();
-
-    /**
      * Enters one handed mode.
      */
     void startOneHanded();
@@ -80,9 +70,4 @@
      * transition start or finish
      */
     void registerTransitionCallback(OneHandedTransitionCallback callback);
-
-    /**
-     * Notifies when user switch complete
-     */
-    void onUserSwitch(int userId);
 }
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 9149204..e0c4fe8 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
@@ -59,6 +59,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.UserChangeListener;
 
 import java.io.PrintWriter;
 
@@ -67,7 +68,7 @@
  */
 public class OneHandedController implements RemoteCallable<OneHandedController>,
         DisplayChangeController.OnDisplayChangingListener, ConfigurationChangeListener,
-        KeyguardChangeListener {
+        KeyguardChangeListener, UserChangeListener {
     private static final String TAG = "OneHandedController";
 
     private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE =
@@ -76,8 +77,8 @@
 
     public static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
 
-    private volatile boolean mIsOneHandedEnabled;
-    private volatile boolean mIsSwipeToNotificationEnabled;
+    private boolean mIsOneHandedEnabled;
+    private boolean mIsSwipeToNotificationEnabled;
     private boolean mIsShortcutEnabled;
     private boolean mTaskChangeToExit;
     private boolean mLockedDisabled;
@@ -294,6 +295,7 @@
         mState.addSListeners(mTutorialHandler);
         mShellController.addConfigurationChangeListener(this);
         mShellController.addKeyguardChangeListener(this);
+        mShellController.addUserChangeListener(this);
     }
 
     public OneHanded asOneHanded() {
@@ -627,7 +629,8 @@
         stopOneHanded();
     }
 
-    private void onUserSwitch(int newUserId) {
+    @Override
+    public void onUserChanged(int newUserId, @NonNull Context userContext) {
         unregisterSettingObservers();
         mUserId = newUserId;
         registerSettingObservers(newUserId);
@@ -718,18 +721,6 @@
         }
 
         @Override
-        public boolean isOneHandedEnabled() {
-            // This is volatile so return directly
-            return mIsOneHandedEnabled;
-        }
-
-        @Override
-        public boolean isSwipeToNotificationEnabled() {
-            // This is volatile so return directly
-            return mIsSwipeToNotificationEnabled;
-        }
-
-        @Override
         public void startOneHanded() {
             mMainExecutor.execute(() -> {
                 OneHandedController.this.startOneHanded();
@@ -770,13 +761,6 @@
                 OneHandedController.this.registerTransitionCallback(callback);
             });
         }
-
-        @Override
-        public void onUserSwitch(int userId) {
-            mMainExecutor.execute(() -> {
-                OneHandedController.this.onUserSwitch(userId);
-            });
-        }
     }
 
     /**
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 93172f8..c06881a 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
@@ -51,12 +51,6 @@
     }
 
     /**
-     * Registers the session listener for the current user.
-     */
-    default void registerSessionListenerForCurrentUser() {
-    }
-
-    /**
      * Sets both shelf visibility and its height.
      *
      * @param visible visibility of shelf.
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 fc97f31..ac3407d 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
@@ -92,6 +92,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.UserChangeListener;
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
@@ -105,7 +106,8 @@
  * Manages the picture-in-picture (PIP) UI and states for Phones.
  */
 public class PipController implements PipTransitionController.PipTransitionCallback,
-        RemoteCallable<PipController>, ConfigurationChangeListener, KeyguardChangeListener {
+        RemoteCallable<PipController>, ConfigurationChangeListener, KeyguardChangeListener,
+        UserChangeListener {
     private static final String TAG = "PipController";
 
     private Context mContext;
@@ -528,7 +530,7 @@
                 });
 
         mOneHandedController.ifPresent(controller -> {
-            controller.asOneHanded().registerTransitionCallback(
+            controller.registerTransitionCallback(
                     new OneHandedTransitionCallback() {
                         @Override
                         public void onStartFinished(Rect bounds) {
@@ -542,8 +544,11 @@
                     });
         });
 
+        mMediaController.registerSessionListenerForCurrentUser();
+
         mShellController.addConfigurationChangeListener(this);
         mShellController.addKeyguardChangeListener(this);
+        mShellController.addUserChangeListener(this);
     }
 
     @Override
@@ -557,6 +562,12 @@
     }
 
     @Override
+    public void onUserChanged(int newUserId, @NonNull Context userContext) {
+        // Re-register the media session listener when switching users
+        mMediaController.registerSessionListenerForCurrentUser();
+    }
+
+    @Override
     public void onConfigurationChanged(Configuration newConfig) {
         mPipBoundsAlgorithm.onConfigurationChanged(mContext);
         mTouchHandler.onConfigurationChanged();
@@ -644,10 +655,6 @@
         }
     }
 
-    private void registerSessionListenerForCurrentUser() {
-        mMediaController.registerSessionListenerForCurrentUser();
-    }
-
     private void onSystemUiStateChanged(boolean isValidState, int flag) {
         mTouchHandler.onSystemUiStateChanged(isValidState);
     }
@@ -968,13 +975,6 @@
         }
 
         @Override
-        public void registerSessionListenerForCurrentUser() {
-            mMainExecutor.execute(() -> {
-                PipController.this.registerSessionListenerForCurrentUser();
-            });
-        }
-
-        @Override
         public void setShelfHeight(boolean visible, int height) {
             mMainExecutor.execute(() -> {
                 PipController.this.setShelfHeight(visible, height);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index a24d9618..4e1b046 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -32,6 +32,8 @@
 import android.os.RemoteException;
 import android.view.Gravity;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.WindowManagerShellWrapper;
@@ -51,6 +53,8 @@
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.UserChangeListener;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -64,7 +68,7 @@
 public class TvPipController implements PipTransitionController.PipTransitionCallback,
         TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
         TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener,
-        ConfigurationChangeListener {
+        ConfigurationChangeListener, UserChangeListener {
     private static final String TAG = "TvPipController";
     static final boolean DEBUG = false;
 
@@ -105,6 +109,11 @@
     private final PipMediaController mPipMediaController;
     private final TvPipNotificationController mPipNotificationController;
     private final TvPipMenuController mTvPipMenuController;
+    private final PipTransitionController mPipTransitionController;
+    private final TaskStackListenerImpl mTaskStackListener;
+    private final PipParamsChangedForwarder mPipParamsChangedForwarder;
+    private final DisplayController mDisplayController;
+    private final WindowManagerShellWrapper mWmShellWrapper;
     private final ShellExecutor mMainExecutor;
     private final TvPipImpl mImpl = new TvPipImpl();
 
@@ -121,6 +130,7 @@
 
     public static Pip create(
             Context context,
+            ShellInit shellInit,
             ShellController shellController,
             TvPipBoundsState tvPipBoundsState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
@@ -138,6 +148,7 @@
             ShellExecutor mainExecutor) {
         return new TvPipController(
                 context,
+                shellInit,
                 shellController,
                 tvPipBoundsState,
                 tvPipBoundsAlgorithm,
@@ -157,6 +168,7 @@
 
     private TvPipController(
             Context context,
+            ShellInit shellInit,
             ShellController shellController,
             TvPipBoundsState tvPipBoundsState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
@@ -170,11 +182,12 @@
             TaskStackListenerImpl taskStackListener,
             PipParamsChangedForwarder pipParamsChangedForwarder,
             DisplayController displayController,
-            WindowManagerShellWrapper wmShell,
+            WindowManagerShellWrapper wmShellWrapper,
             ShellExecutor mainExecutor) {
         mContext = context;
         mMainExecutor = mainExecutor;
         mShellController = shellController;
+        mDisplayController = displayController;
 
         mTvPipBoundsState = tvPipBoundsState;
         mTvPipBoundsState.setDisplayId(context.getDisplayId());
@@ -193,16 +206,32 @@
 
         mAppOpsListener = pipAppOpsListener;
         mPipTaskOrganizer = pipTaskOrganizer;
-        pipTransitionController.registerPipTransitionCallback(this);
+        mPipTransitionController = pipTransitionController;
+        mPipParamsChangedForwarder = pipParamsChangedForwarder;
+        mTaskStackListener = taskStackListener;
+        mWmShellWrapper = wmShellWrapper;
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        mPipTransitionController.registerPipTransitionCallback(this);
 
         loadConfigurations();
 
-        registerPipParamsChangedListener(pipParamsChangedForwarder);
-        registerTaskStackListenerCallback(taskStackListener);
-        registerWmShellPinnedStackListener(wmShell);
-        displayController.addDisplayWindowListener(this);
+        registerPipParamsChangedListener(mPipParamsChangedForwarder);
+        registerTaskStackListenerCallback(mTaskStackListener);
+        registerWmShellPinnedStackListener(mWmShellWrapper);
+        registerSessionListenerForCurrentUser();
+        mDisplayController.addDisplayWindowListener(this);
 
         mShellController.addConfigurationChangeListener(this);
+        mShellController.addUserChangeListener(this);
+    }
+
+    @Override
+    public void onUserChanged(int newUserId, @NonNull Context userContext) {
+        // Re-register the media session listener when switching users
+        registerSessionListenerForCurrentUser();
     }
 
     @Override
@@ -679,11 +708,6 @@
     }
 
     private class TvPipImpl implements Pip {
-        @Override
-        public void registerSessionListenerForCurrentUser() {
-            mMainExecutor.execute(() -> {
-                TvPipController.this.registerSessionListenerForCurrentUser();
-            });
-        }
+        // Not used
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
index 1c0b358..9df8631 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
@@ -21,13 +21,13 @@
  */
 public interface KeyguardChangeListener {
     /**
-     * Notifies the Shell that the keyguard is showing (and if so, whether it is occluded).
+     * Called when the keyguard is showing (and if so, whether it is occluded).
      */
     default void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
             boolean animatingDismiss) {}
 
     /**
-     * Notifies the Shell when the keyguard dismiss animation has finished.
+     * Called when the keyguard dismiss animation has finished.
      *
      * TODO(b/206741900) deprecate this path once we're able to animate the PiP window as part of
      * keyguard dismiss animation.
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 52ffb46..5799394 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
@@ -25,7 +25,9 @@
 
 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 androidx.annotation.NonNull;
@@ -36,6 +38,7 @@
 import com.android.wm.shell.common.annotations.ExternalThread;
 
 import java.io.PrintWriter;
+import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
@@ -53,6 +56,9 @@
             new CopyOnWriteArrayList<>();
     private final CopyOnWriteArrayList<KeyguardChangeListener> mKeyguardChangeListeners =
             new CopyOnWriteArrayList<>();
+    private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners =
+            new CopyOnWriteArrayList<>();
+
     private Configuration mLastConfiguration;
 
 
@@ -102,6 +108,22 @@
         mKeyguardChangeListeners.remove(listener);
     }
 
+    /**
+     * Adds a new user-change listener. The user change callbacks are not made in any
+     * particular order.
+     */
+    public void addUserChangeListener(UserChangeListener listener) {
+        mUserChangeListeners.remove(listener);
+        mUserChangeListeners.add(listener);
+    }
+
+    /**
+     * Removes an existing user-change listener.
+     */
+    public void removeUserChangeListener(UserChangeListener listener) {
+        mUserChangeListeners.remove(listener);
+    }
+
     @VisibleForTesting
     void onConfigurationChanged(Configuration newConfig) {
         // The initial config is send on startup and doesn't trigger listener callbacks
@@ -144,6 +166,8 @@
 
     @VisibleForTesting
     void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) {
+        ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard visibility changed: visible=%b "
+                + "occluded=%b animatingDismiss=%b", visible, occluded, animatingDismiss);
         for (KeyguardChangeListener listener : mKeyguardChangeListeners) {
             listener.onKeyguardVisibilityChanged(visible, occluded, animatingDismiss);
         }
@@ -151,17 +175,35 @@
 
     @VisibleForTesting
     void onKeyguardDismissAnimationFinished() {
+        ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard dismiss animation finished");
         for (KeyguardChangeListener listener : mKeyguardChangeListeners) {
             listener.onKeyguardDismissAnimationFinished();
         }
     }
 
+    @VisibleForTesting
+    void onUserChanged(int newUserId, @NonNull Context userContext) {
+        ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User changed: id=%d", newUserId);
+        for (UserChangeListener listener : mUserChangeListeners) {
+            listener.onUserChanged(newUserId, userContext);
+        }
+    }
+
+    @VisibleForTesting
+    void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+        ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User profiles changed");
+        for (UserChangeListener listener : mUserChangeListeners) {
+            listener.onUserProfilesChanged(profiles);
+        }
+    }
+
     public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
         pw.println(prefix + TAG);
         pw.println(innerPrefix + "mConfigChangeListeners=" + mConfigChangeListeners.size());
         pw.println(innerPrefix + "mLastConfiguration=" + mLastConfiguration);
         pw.println(innerPrefix + "mKeyguardChangeListeners=" + mKeyguardChangeListeners.size());
+        pw.println(innerPrefix + "mUserChangeListeners=" + mUserChangeListeners.size());
     }
 
     /**
@@ -220,5 +262,17 @@
             mMainExecutor.execute(() ->
                     ShellController.this.onKeyguardDismissAnimationFinished());
         }
+
+        @Override
+        public void onUserChanged(int newUserId, @NonNull Context userContext) {
+            mMainExecutor.execute(() ->
+                    ShellController.this.onUserChanged(newUserId, userContext));
+        }
+
+        @Override
+        public void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+            mMainExecutor.execute(() ->
+                    ShellController.this.onUserProfilesChanged(profiles));
+        }
     }
 }
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 254c253..2108c82 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
@@ -16,9 +16,14 @@
 
 package com.android.wm.shell.sysui;
 
+import android.content.Context;
+import android.content.pm.UserInfo;
 import android.content.res.Configuration;
 
+import androidx.annotation.NonNull;
+
 import java.io.PrintWriter;
+import java.util.List;
 
 /**
  * General interface for notifying the Shell of common SysUI events like configuration or keyguard
@@ -59,4 +64,14 @@
      * Notifies the Shell when the keyguard dismiss animation has finished.
      */
     default void onKeyguardDismissAnimationFinished() {}
+
+    /**
+     * Notifies the Shell when the user changes.
+     */
+    default void onUserChanged(int newUserId, @NonNull Context userContext) {}
+
+    /**
+     * Notifies the Shell when a profile belonging to the user changes.
+     */
+    default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java
new file mode 100644
index 0000000..3d0909f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.content.pm.UserInfo;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Callbacks for when the user or user's profiles changes.
+ */
+public interface UserChangeListener {
+    /**
+     * Called when the current (parent) user changes.
+     */
+    default void onUserChanged(int newUserId, @NonNull Context userContext) {}
+
+    /**
+     * Called when a profile belonging to the user changes.
+     */
+    default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
+}
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 26d0ec6..29d25bc 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
@@ -219,6 +219,8 @@
                     + "use ShellInit callbacks to ensure proper ordering");
         }
         mHandlers.add(handler);
+        // Set initial scale settings.
+        handler.setAnimScaleSetting(mTransitionAnimationScaleSetting);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: %s",
                 handler.getClass().getSimpleName());
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
new file mode 100644
index 0000000..b2e45a6
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -0,0 +1,79 @@
+/*
+ * 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.activityembedding;
+
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.window.TransitionInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Tests for {@link ActivityEmbeddingAnimationRunner}.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:ActivityEmbeddingAnimationRunnerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase {
+
+    @Before
+    public void setup() {
+        super.setUp();
+        doNothing().when(mController).onAnimationFinished(any());
+    }
+
+    @Test
+    public void testStartAnimation() {
+        final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+        final TransitionInfo.Change embeddingChange = createChange();
+        embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+        info.addChange(embeddingChange);
+        doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+
+        mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction);
+
+        final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class);
+        verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction),
+                finishCallback.capture());
+        verify(mStartTransaction).apply();
+        verify(mAnimator).start();
+        verifyNoMoreInteractions(mFinishTransaction);
+        verify(mController, never()).onAnimationFinished(any());
+
+        // Call onAnimationFinished() when the animation is finished.
+        finishCallback.getValue().run();
+
+        verify(mController).onAnimationFinished(mTransition);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
new file mode 100644
index 0000000..84befdd
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
@@ -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.wm.shell.activityembedding;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.mock;
+
+import android.animation.Animator;
+import android.annotation.CallSuper;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** TestBase for ActivityEmbedding animation. */
+abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase {
+
+    @Mock
+    ShellInit mShellInit;
+    @Mock
+    Transitions mTransitions;
+    @Mock
+    IBinder mTransition;
+    @Mock
+    SurfaceControl.Transaction mStartTransaction;
+    @Mock
+    SurfaceControl.Transaction mFinishTransaction;
+    @Mock
+    Transitions.TransitionFinishCallback mFinishCallback;
+    @Mock
+    Animator mAnimator;
+
+    ActivityEmbeddingController mController;
+    ActivityEmbeddingAnimationRunner mAnimRunner;
+    ActivityEmbeddingAnimationSpec mAnimSpec;
+
+    @CallSuper
+    @Before
+    public void setUp() {
+        assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+        MockitoAnnotations.initMocks(this);
+        mController = ActivityEmbeddingController.create(mContext, mShellInit, mTransitions);
+        assertNotNull(mController);
+        mAnimRunner = mController.mAnimationRunner;
+        assertNotNull(mAnimRunner);
+        mAnimSpec = mAnimRunner.mAnimationSpec;
+        assertNotNull(mAnimSpec);
+        spyOn(mController);
+        spyOn(mAnimRunner);
+        spyOn(mAnimSpec);
+    }
+
+    /** Creates a mock {@link TransitionInfo.Change}. */
+    static TransitionInfo.Change createChange() {
+        return new TransitionInfo.Change(mock(WindowContainerToken.class),
+                mock(SurfaceControl.class));
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
index bfe3b54..cf43b00 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -16,52 +16,117 @@
 
 package com.android.wm.shell.activityembedding;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
 
-import static org.junit.Assume.assumeTrue;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 
-import android.content.Context;
+import android.window.TransitionInfo;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.transition.Transitions;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 
 /**
- * Tests for the activity embedding controller.
+ * Tests for {@link ActivityEmbeddingController}.
  *
  * Build/Install/Run:
  *  atest WMShellUnitTests:ActivityEmbeddingControllerTests
  */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class ActivityEmbeddingControllerTests extends ShellTestCase {
-
-    private @Mock Context mContext;
-    private @Mock ShellInit mShellInit;
-    private @Mock Transitions mTransitions;
-    private ActivityEmbeddingController mController;
+public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimationTestBase {
 
     @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mController = spy(new ActivityEmbeddingController(mContext, mShellInit, mTransitions));
+    public void setup() {
+        super.setUp();
+        doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
     }
 
     @Test
-    public void instantiate_addInitCallback() {
-        assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
-        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    public void testInstantiate() {
+        verify(mShellInit).addInitCallback(any(), any());
+    }
+
+    @Test
+    public void testOnInit() {
+        mController.onInit();
+
+        verify(mTransitions).addHandler(mController);
+    }
+
+    @Test
+    public void testSetAnimScaleSetting() {
+        mController.setAnimScaleSetting(1.0f);
+
+        verify(mAnimRunner).setAnimScaleSetting(1.0f);
+        verify(mAnimSpec).setAnimScaleSetting(1.0f);
+    }
+
+    @Test
+    public void testStartAnimation_containsNonActivityEmbeddingChange() {
+        final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+        final TransitionInfo.Change embeddingChange = createChange();
+        embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+        final TransitionInfo.Change nonEmbeddingChange = createChange();
+        info.addChange(embeddingChange);
+        info.addChange(nonEmbeddingChange);
+
+        // No-op
+        assertFalse(mController.startAnimation(mTransition, info, mStartTransaction,
+                mFinishTransaction, mFinishCallback));
+        verify(mAnimRunner, never()).startAnimation(any(), any(), any(), any());
+        verifyNoMoreInteractions(mStartTransaction);
+        verifyNoMoreInteractions(mFinishTransaction);
+        verifyNoMoreInteractions(mFinishCallback);
+    }
+
+    @Test
+    public void testStartAnimation_onlyActivityEmbeddingChange() {
+        final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+        final TransitionInfo.Change embeddingChange = createChange();
+        embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+        info.addChange(embeddingChange);
+
+        // No-op
+        assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
+                mFinishTransaction, mFinishCallback));
+        verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction,
+                mFinishTransaction);
+        verify(mStartTransaction).apply();
+        verifyNoMoreInteractions(mFinishTransaction);
+    }
+
+    @Test
+    public void testOnAnimationFinished() {
+        // Should not call finish when there is no transition.
+        assertThrows(IllegalStateException.class,
+                () -> mController.onAnimationFinished(mTransition));
+
+        final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+        final TransitionInfo.Change embeddingChange = createChange();
+        embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+        info.addChange(embeddingChange);
+        mController.startAnimation(mTransition, info, mStartTransaction,
+                mFinishTransaction, mFinishCallback);
+
+        verify(mFinishCallback, never()).onTransitionFinished(any(), any());
+        mController.onAnimationFinished(mTransition);
+        verify(mFinishCallback).onTransitionFinished(any(), any());
+
+        // Should not call finish when the finish has already been called.
+        assertThrows(IllegalStateException.class,
+                () -> mController.onAnimationFinished(mTransition));
     }
 }
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 90645ce..cf8297e 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
@@ -171,6 +171,11 @@
     }
 
     @Test
+    public void testControllerRegistersUserChangeListener() {
+        verify(mMockShellController, times(1)).addUserChangeListener(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 9ed8d84..eb5726b 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
@@ -23,6 +23,7 @@
 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.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -77,9 +78,9 @@
 public class PipControllerTest extends ShellTestCase {
     private PipController mPipController;
     private ShellInit mShellInit;
+    private ShellController mShellController;
 
     @Mock private ShellCommandHandler mMockShellCommandHandler;
-    @Mock private ShellController mMockShellController;
     @Mock private DisplayController mMockDisplayController;
     @Mock private PhonePipMenuController mMockPhonePipMenuController;
     @Mock private PipAppOpsListener mMockPipAppOpsListener;
@@ -110,8 +111,10 @@
             return null;
         }).when(mMockExecutor).execute(any());
         mShellInit = spy(new ShellInit(mMockExecutor));
+        mShellController = spy(new ShellController(mShellInit, mMockShellCommandHandler,
+                mMockExecutor));
         mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler,
-                mMockShellController, mMockDisplayController, mMockPipAppOpsListener,
+                mShellController, mMockDisplayController, mMockPipAppOpsListener,
                 mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
                 mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
                 mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
@@ -135,12 +138,22 @@
 
     @Test
     public void instantiatePipController_registerConfigChangeListener() {
-        verify(mMockShellController, times(1)).addConfigurationChangeListener(any());
+        verify(mShellController, times(1)).addConfigurationChangeListener(any());
     }
 
     @Test
     public void instantiatePipController_registerKeyguardChangeListener() {
-        verify(mMockShellController, times(1)).addKeyguardChangeListener(any());
+        verify(mShellController, times(1)).addKeyguardChangeListener(any());
+    }
+
+    @Test
+    public void instantiatePipController_registerUserChangeListener() {
+        verify(mShellController, times(1)).addUserChangeListener(any());
+    }
+
+    @Test
+    public void instantiatePipController_registerMediaListener() {
+        verify(mMockPipMediaController, times(1)).registerSessionListenerForCurrentUser();
     }
 
     @Test
@@ -167,7 +180,7 @@
 
         ShellInit shellInit = new ShellInit(mMockExecutor);
         assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler,
-                mMockShellController, mMockDisplayController, mMockPipAppOpsListener,
+                mShellController, mMockDisplayController, mMockPipAppOpsListener,
                 mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
                 mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
                 mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
@@ -264,4 +277,11 @@
 
         verify(mMockPipBoundsState).setKeepClearAreas(Set.of(keepClearArea), Set.of());
     }
+
+    @Test
+    public void onUserChangeRegisterMediaListener() {
+        reset(mMockPipMediaController);
+        mShellController.asShell().onUserChanged(100, mContext);
+        verify(mMockPipMediaController, times(1)).registerSessionListenerForCurrentUser();
+    }
 }
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 39e58ff..d6ddba9 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
@@ -17,11 +17,15 @@
 package com.android.wm.shell.sysui;
 
 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.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
@@ -35,6 +39,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Locale;
 
 @SmallTest
@@ -42,22 +48,29 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class ShellControllerTest extends ShellTestCase {
 
+    private static final int TEST_USER_ID = 100;
+
     @Mock
     private ShellInit mShellInit;
     @Mock
     private ShellCommandHandler mShellCommandHandler;
     @Mock
     private ShellExecutor mExecutor;
+    @Mock
+    private Context mTestUserContext;
 
     private ShellController mController;
     private TestConfigurationChangeListener mConfigChangeListener;
     private TestKeyguardChangeListener mKeyguardChangeListener;
+    private TestUserChangeListener mUserChangeListener;
+
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mKeyguardChangeListener = new TestKeyguardChangeListener();
         mConfigChangeListener = new TestConfigurationChangeListener();
+        mUserChangeListener = new TestUserChangeListener();
         mController = new ShellController(mShellInit, mShellCommandHandler, mExecutor);
         mController.onConfigurationChanged(getConfigurationCopy());
     }
@@ -68,6 +81,46 @@
     }
 
     @Test
+    public void testAddUserChangeListener_ensureCallback() {
+        mController.addUserChangeListener(mUserChangeListener);
+
+        mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+        assertTrue(mUserChangeListener.userChanged == 1);
+        assertTrue(mUserChangeListener.lastUserContext == mTestUserContext);
+    }
+
+    @Test
+    public void testDoubleAddUserChangeListener_ensureSingleCallback() {
+        mController.addUserChangeListener(mUserChangeListener);
+        mController.addUserChangeListener(mUserChangeListener);
+
+        mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+        assertTrue(mUserChangeListener.userChanged == 1);
+        assertTrue(mUserChangeListener.lastUserContext == mTestUserContext);
+    }
+
+    @Test
+    public void testAddRemoveUserChangeListener_ensureNoCallback() {
+        mController.addUserChangeListener(mUserChangeListener);
+        mController.removeUserChangeListener(mUserChangeListener);
+
+        mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+        assertTrue(mUserChangeListener.userChanged == 0);
+        assertTrue(mUserChangeListener.lastUserContext == null);
+    }
+
+    @Test
+    public void testUserProfilesChanged() {
+        mController.addUserChangeListener(mUserChangeListener);
+
+        ArrayList<UserInfo> profiles = new ArrayList<>();
+        profiles.add(mock(UserInfo.class));
+        profiles.add(mock(UserInfo.class));
+        mController.onUserProfilesChanged(profiles);
+        assertTrue(mUserChangeListener.lastUserProfiles.equals(profiles));
+    }
+
+    @Test
     public void testAddKeyguardChangeListener_ensureCallback() {
         mController.addKeyguardChangeListener(mKeyguardChangeListener);
 
@@ -332,4 +385,27 @@
             dismissAnimationFinished++;
         }
     }
+
+    private class TestUserChangeListener implements UserChangeListener {
+        // Counts of number of times each of the callbacks are called
+        public int userChanged;
+        public int lastUserId;
+        public Context lastUserContext;
+        public int userProfilesChanged;
+        public List<? extends UserInfo> lastUserProfiles;
+
+
+        @Override
+        public void onUserChanged(int newUserId, @NonNull Context userContext) {
+            userChanged++;
+            lastUserId = newUserId;
+            lastUserContext = userContext;
+        }
+
+        @Override
+        public void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+            userProfilesChanged++;
+            lastUserProfiles = profiles;
+        }
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index b9c4030..a822e18 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -600,6 +600,9 @@
      * Returns the WifiInfo for the underlying WiFi network of the VCN network, returns null if the
      * input NetworkCapabilities is not for a VCN network with underlying WiFi network.
      *
+     * TODO(b/238425913): Move this method to be inside systemui not settingslib once we've migrated
+     *   off of {@link WifiStatusTracker} and {@link NetworkControllerImpl}.
+     *
      * @param networkCapabilities NetworkCapabilities of the network.
      */
     @Nullable
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 652e281..78dea89 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -85,6 +85,7 @@
     <uses-permission android:name="android.permission.CONTROL_VPN" />
     <uses-permission android:name="android.permission.PEERS_MAC_ADDRESS"/>
     <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"/>
+    <uses-permission android:name="android.permission.NETWORK_STACK"/>
     <!-- Physical hardware -->
     <uses-permission android:name="android.permission.MANAGE_USB" />
     <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" />
diff --git a/packages/SystemUI/res-keyguard/values-land/dimens.xml b/packages/SystemUI/res-keyguard/values-land/dimens.xml
index 4e92884f..a4e7a5f 100644
--- a/packages/SystemUI/res-keyguard/values-land/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-land/dimens.xml
@@ -22,7 +22,6 @@
     <dimen name="keyguard_eca_top_margin">0dp</dimen>
     <dimen name="keyguard_eca_bottom_margin">2dp</dimen>
     <dimen name="keyguard_password_height">26dp</dimen>
-    <dimen name="num_pad_entry_row_margin_bottom">0dp</dimen>
 
     <!-- The size of PIN text in the PIN unlock method. -->
     <integer name="scaled_password_text_size">26</integer>
diff --git a/packages/SystemUI/res-keyguard/values-sw360dp-land/dimens.xml b/packages/SystemUI/res-keyguard/values-sw360dp-land/dimens.xml
index f465be4..0421135 100644
--- a/packages/SystemUI/res-keyguard/values-sw360dp-land/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw360dp-land/dimens.xml
@@ -22,7 +22,6 @@
     <dimen name="keyguard_eca_top_margin">4dp</dimen>
     <dimen name="keyguard_eca_bottom_margin">4dp</dimen>
     <dimen name="keyguard_password_height">50dp</dimen>
-    <dimen name="num_pad_entry_row_margin_bottom">4dp</dimen>
 
     <!-- The size of PIN text in the PIN unlock method. -->
     <integer name="scaled_password_text_size">40</integer>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index acf3e4d..32871f0 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -86,7 +86,7 @@
 
     <!-- Spacing around each button used for PIN view -->
     <dimen name="num_pad_key_width">72dp</dimen>
-    <dimen name="num_pad_entry_row_margin_bottom">16dp</dimen>
+    <dimen name="num_pad_entry_row_margin_bottom">12dp</dimen>
     <dimen name="num_pad_row_margin_bottom">6dp</dimen>
     <dimen name="num_pad_key_margin_end">12dp</dimen>
 
diff --git a/packages/SystemUI/res/layout/new_status_bar_wifi_group.xml b/packages/SystemUI/res/layout/new_status_bar_wifi_group.xml
new file mode 100644
index 0000000..753ba2f
--- /dev/null
+++ b/packages/SystemUI/res/layout/new_status_bar_wifi_group.xml
@@ -0,0 +1,28 @@
+<?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.wifi.ui.view.ModernStatusBarWifiView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/wifi_combo"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:gravity="center_vertical" >
+
+    <include layout="@layout/status_bar_wifi_group_inner" />
+
+</com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView>
diff --git a/packages/SystemUI/res/layout/status_bar_wifi_group.xml b/packages/SystemUI/res/layout/status_bar_wifi_group.xml
index 35cce25..6cb6993b 100644
--- a/packages/SystemUI/res/layout/status_bar_wifi_group.xml
+++ b/packages/SystemUI/res/layout/status_bar_wifi_group.xml
@@ -18,70 +18,11 @@
 -->
 <com.android.systemui.statusbar.StatusBarWifiView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:systemui="http://schemas.android.com/apk/res-auto"
     android:id="@+id/wifi_combo"
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
     android:gravity="center_vertical" >
 
-    <com.android.keyguard.AlphaOptimizedLinearLayout
-        android:id="@+id/wifi_group"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="center_vertical"
-        android:layout_marginStart="2.5dp"
-    >
-        <FrameLayout
-                android:id="@+id/inout_container"
-                android:layout_height="17dp"
-                android:layout_width="wrap_content"
-                android:gravity="center_vertical" >
-            <ImageView
-                android:id="@+id/wifi_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/wifi_out"
-                android:layout_height="wrap_content"
-                android:layout_width="wrap_content"
-                android:src="@drawable/ic_activity_up"
-                android:paddingEnd="2dp"
-                android:visibility="gone"
-            />
-        </FrameLayout>
-        <FrameLayout
-            android:id="@+id/wifi_combo"
-            android:layout_height="wrap_content"
-            android:layout_width="wrap_content"
-            android:gravity="center_vertical" >
-            <com.android.systemui.statusbar.AlphaOptimizedImageView
-                android:id="@+id/wifi_signal"
-                android:layout_height="@dimen/status_bar_wifi_signal_size"
-                android:layout_width="@dimen/status_bar_wifi_signal_size" />
-        </FrameLayout>
+    <include layout="@layout/status_bar_wifi_group_inner" />
 
-        <View
-            android:id="@+id/wifi_signal_spacer"
-            android:layout_width="@dimen/status_bar_wifi_signal_spacer_width"
-            android:layout_height="4dp"
-            android:visibility="gone" />
-
-        <!-- Looks like CarStatusBar uses this... -->
-        <ViewStub
-            android:id="@+id/connected_device_signals_stub"
-            android:layout="@layout/connected_device_signal"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content" />
-
-        <View
-            android:id="@+id/wifi_airplane_spacer"
-            android:layout_width="@dimen/status_bar_airplane_spacer_width"
-            android:layout_height="4dp"
-            android:visibility="gone"
-        />
-    </com.android.keyguard.AlphaOptimizedLinearLayout>
-</com.android.systemui.statusbar.StatusBarWifiView>
+</com.android.systemui.statusbar.StatusBarWifiView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml b/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml
new file mode 100644
index 0000000..0ea0653
--- /dev/null
+++ b/packages/SystemUI/res/layout/status_bar_wifi_group_inner.xml
@@ -0,0 +1,82 @@
+<?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">
+
+    <com.android.keyguard.AlphaOptimizedLinearLayout
+        android:id="@+id/wifi_group"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:layout_marginStart="2.5dp"
+    >
+        <FrameLayout
+                android:id="@+id/inout_container"
+                android:layout_height="17dp"
+                android:layout_width="wrap_content"
+                android:gravity="center_vertical" >
+            <ImageView
+                android:id="@+id/wifi_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/wifi_out"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:src="@drawable/ic_activity_up"
+                android:paddingEnd="2dp"
+                android:visibility="gone"
+            />
+        </FrameLayout>
+        <FrameLayout
+            android:id="@+id/wifi_combo"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:gravity="center_vertical" >
+            <com.android.systemui.statusbar.AlphaOptimizedImageView
+                android:id="@+id/wifi_signal"
+                android:layout_height="@dimen/status_bar_wifi_signal_size"
+                android:layout_width="@dimen/status_bar_wifi_signal_size" />
+        </FrameLayout>
+
+        <View
+            android:id="@+id/wifi_signal_spacer"
+            android:layout_width="@dimen/status_bar_wifi_signal_spacer_width"
+            android:layout_height="4dp"
+            android:visibility="gone" />
+
+        <!-- Looks like CarStatusBar uses this... -->
+        <ViewStub
+            android:id="@+id/connected_device_signals_stub"
+            android:layout="@layout/connected_device_signal"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <View
+            android:id="@+id/wifi_airplane_spacer"
+            android:layout_width="@dimen/status_bar_airplane_spacer_width"
+            android:layout_height="4dp"
+            android:visibility="gone"
+        />
+    </com.android.keyguard.AlphaOptimizedLinearLayout>
+</merge>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7c1fdd5..ae30089 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1179,7 +1179,6 @@
     <item name="shutdown_scrim_behind_alpha" format="float" type="dimen">0.95</item>
 
     <!-- Output switcher panel related dimensions -->
-    <dimen name="media_output_dialog_list_margin">12dp</dimen>
     <dimen name="media_output_dialog_list_max_height">355dp</dimen>
     <dimen name="media_output_dialog_header_album_icon_size">72dp</dimen>
     <dimen name="media_output_dialog_header_back_icon_size">32dp</dimen>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
index b29dc83..22bffda 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
@@ -49,6 +49,7 @@
 import android.view.animation.Interpolator;
 import android.view.animation.LinearInterpolator;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.UiEventLoggerImpl;
@@ -99,6 +100,7 @@
     private @WindowInsetsController.Behavior
     int mBehavior = WindowInsetsController.BEHAVIOR_DEFAULT;
     private int mNavBarMode;
+    private boolean mTaskBarVisible = false;
     private boolean mSkipOverrideUserLockPrefsOnce;
     private final int mLightIconColor;
     private final int mDarkIconColor;
@@ -422,6 +424,7 @@
     }
 
     public void onTaskbarStateChange(boolean visible, boolean stashed) {
+        mTaskBarVisible = visible;
         if (getRotationButton() == null) {
             return;
         }
@@ -438,9 +441,12 @@
      * Return true when either the task bar is visible or it's in visual immersive mode.
      */
     @SuppressLint("InlinedApi")
-    private boolean canShowRotationButton() {
-        return mIsNavigationBarShowing || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT
-                || isGesturalMode(mNavBarMode);
+    @VisibleForTesting
+    boolean canShowRotationButton() {
+        return mIsNavigationBarShowing
+            || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT
+            || isGesturalMode(mNavBarMode)
+            || mTaskBarVisible;
     }
 
     @DrawableRes
@@ -624,4 +630,3 @@
         }
     }
 }
-
diff --git a/packages/SystemUI/src/com/android/systemui/ActivityIntentHelper.java b/packages/SystemUI/src/com/android/systemui/ActivityIntentHelper.java
index 43b3929..df65bcf 100644
--- a/packages/SystemUI/src/com/android/systemui/ActivityIntentHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/ActivityIntentHelper.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui;
 
+import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -34,12 +35,12 @@
 @SysUISingleton
 public class ActivityIntentHelper {
 
-    private final Context mContext;
+    private final PackageManager mPm;
 
     @Inject
     public ActivityIntentHelper(Context context) {
         // TODO: inject a package manager, not a context.
-        mContext = context;
+        mPm = context.getPackageManager();
     }
 
     /**
@@ -57,6 +58,15 @@
     }
 
     /**
+     * @see #wouldLaunchResolverActivity(Intent, int)
+     */
+    public boolean wouldPendingLaunchResolverActivity(PendingIntent intent, int currentUserId) {
+        ActivityInfo targetActivityInfo = getPendingTargetActivityInfo(intent, currentUserId,
+                false /* onlyDirectBootAware */);
+        return targetActivityInfo == null;
+    }
+
+    /**
      * Returns info about the target Activity of a given intent, or null if the intent does not
      * resolve to a specific component meeting the requirements.
      *
@@ -68,19 +78,45 @@
      */
     public ActivityInfo getTargetActivityInfo(Intent intent, int currentUserId,
             boolean onlyDirectBootAware) {
-        PackageManager packageManager = mContext.getPackageManager();
-        int flags = PackageManager.MATCH_DEFAULT_ONLY;
+        int flags = PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA;
         if (!onlyDirectBootAware) {
             flags |= PackageManager.MATCH_DIRECT_BOOT_AWARE
                     | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
         }
-        final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser(
+        final List<ResolveInfo> appList = mPm.queryIntentActivitiesAsUser(
                 intent, flags, currentUserId);
         if (appList.size() == 0) {
             return null;
         }
-        ResolveInfo resolved = packageManager.resolveActivityAsUser(intent,
-                flags | PackageManager.GET_META_DATA, currentUserId);
+        if (appList.size() == 1) {
+            return appList.get(0).activityInfo;
+        }
+        ResolveInfo resolved = mPm.resolveActivityAsUser(intent, flags, currentUserId);
+        if (resolved == null || wouldLaunchResolverActivity(resolved, appList)) {
+            return null;
+        } else {
+            return resolved.activityInfo;
+        }
+    }
+
+    /**
+     * @see #getTargetActivityInfo(Intent, int, boolean)
+     */
+    public ActivityInfo getPendingTargetActivityInfo(PendingIntent intent, int currentUserId,
+            boolean onlyDirectBootAware) {
+        int flags = PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA;
+        if (!onlyDirectBootAware) {
+            flags |= PackageManager.MATCH_DIRECT_BOOT_AWARE
+                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+        }
+        final List<ResolveInfo> appList = intent.queryIntentComponents(flags);
+        if (appList.size() == 0) {
+            return null;
+        }
+        if (appList.size() == 1) {
+            return appList.get(0).activityInfo;
+        }
+        ResolveInfo resolved = mPm.resolveActivityAsUser(intent.getIntent(), flags, currentUserId);
         if (resolved == null || wouldLaunchResolverActivity(resolved, appList)) {
             return null;
         } else {
@@ -104,6 +140,17 @@
     }
 
     /**
+     * @see #wouldShowOverLockscreen(Intent, int)
+     */
+    public boolean wouldPendingShowOverLockscreen(PendingIntent intent, int currentUserId) {
+        ActivityInfo targetActivityInfo = getPendingTargetActivityInfo(intent,
+                currentUserId, false /* onlyDirectBootAware */);
+        return targetActivityInfo != null
+                && (targetActivityInfo.flags & (ActivityInfo.FLAG_SHOW_WHEN_LOCKED
+                | ActivityInfo.FLAG_SHOW_FOR_ALL_USERS)) > 0;
+    }
+
+    /**
      * Determines if sending the given intent would result in starting an Intent resolver activity,
      * instead of resolving to a specific component.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/GuestSessionNotification.java b/packages/SystemUI/src/com/android/systemui/GuestSessionNotification.java
index b0eaab9..fa9a83e 100644
--- a/packages/SystemUI/src/com/android/systemui/GuestSessionNotification.java
+++ b/packages/SystemUI/src/com/android/systemui/GuestSessionNotification.java
@@ -25,7 +25,6 @@
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.util.FeatureFlagUtils;
 
 import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.systemui.util.NotificationChannels;
@@ -59,10 +58,8 @@
     }
 
     void createPersistentNotification(UserInfo userInfo, boolean isGuestFirstLogin) {
-        if (!FeatureFlagUtils.isEnabled(mContext,
-                FeatureFlagUtils.SETTINGS_GUEST_MODE_UX_CHANGES)
-                || !userInfo.isGuest()) {
-            // we create a persistent notification only if enabled and only for guests
+        if (!userInfo.isGuest()) {
+            // we create a persistent notification only for guests
             return;
         }
         String contentText;
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 2dadf57..e549a96 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -241,7 +241,6 @@
                 notifCollection,
                 notifPipeline,
                 sysUiState,
-                dumpManager,
                 sysuiMainExecutor));
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
index 7e4a108..823255c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
@@ -113,7 +113,7 @@
     }
 
     void setExtraStatusBarItemViews(List<View> views) {
-        mSystemStatusViewGroup.removeAllViews();
+        removeAllStatusBarItemViews();
         views.forEach(view -> mSystemStatusViewGroup.addView(view));
     }
 
@@ -121,4 +121,8 @@
         final View statusIcon = findViewById(resId);
         return Objects.requireNonNull(statusIcon);
     }
+
+    void removeAllStatusBarItemViews() {
+        mSystemStatusViewGroup.removeAllViews();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index 65cfae1..6f50550 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -192,6 +192,7 @@
         mDreamOverlayNotificationCountProvider.ifPresent(
                 provider -> provider.removeCallback(mNotificationCountCallback));
         mStatusBarItemsProvider.removeCallback(mStatusBarItemsProviderCallback);
+        mView.removeAllStatusBarItemViews();
         mTouchInsetSession.clear();
 
         mIsAttached = false;
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 1f356cb..677f0a2 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -154,7 +154,11 @@
     public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE =
             new ReleasedFlag(603, false);
 
-    public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE = new UnreleasedFlag(604, true);
+    public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_BACKEND =
+            new UnreleasedFlag(604, true);
+
+    public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_FRONTEND =
+            new UnreleasedFlag(605, true);
 
     /***************************************/
     // 700 - dialer/calls
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 19c6249..c4e3d4e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -251,9 +251,21 @@
             Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface)
 
         view.contentDescription = view.context.getString(viewModel.contentDescriptionResourceId)
-        view.setOnClickListener {
+        view.isClickable = viewModel.isClickable
+        if (viewModel.isClickable) {
+            view.setOnClickListener(OnClickListener(viewModel, falsingManager))
+        } else {
+            view.setOnClickListener(null)
+        }
+    }
+
+    private class OnClickListener(
+        private val viewModel: KeyguardQuickAffordanceViewModel,
+        private val falsingManager: FalsingManager,
+    ) : View.OnClickListener {
+        override fun onClick(view: View) {
             if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                return@setOnClickListener
+                return
             }
 
             if (viewModel.configKey != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 01d5e5c..e3ebac6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.doze.util.BurnInHelperWrapper
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -37,6 +38,23 @@
     private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
     private val burnInHelperWrapper: BurnInHelperWrapper,
 ) {
+    /**
+     * Whether quick affordances are "opaque enough" to be considered visible to and interactive by
+     * the user. If they are not interactive, user input should not be allowed on them.
+     *
+     * Note that there is a margin of error, where we allow very, very slightly transparent views to
+     * be considered "fully opaque" for the purpose of being interactive. This is to accommodate the
+     * error margin of floating point arithmetic.
+     *
+     * A view that is visible but with an alpha of less than our threshold either means it's not
+     * fully done fading in or is fading/faded out. Either way, it should not be
+     * interactive/clickable unless "fully opaque" to avoid issues like in b/241830987.
+     */
+    private val areQuickAffordancesFullyOpaque: Flow<Boolean> =
+        bottomAreaInteractor.alpha
+            .map { alpha -> alpha >= AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD }
+            .distinctUntilChanged()
+
     /** An observable for the view-model of the "start button" quick affordance. */
     val startButton: Flow<KeyguardQuickAffordanceViewModel> =
         button(KeyguardQuickAffordancePosition.BOTTOM_START)
@@ -77,14 +95,19 @@
         return combine(
                 quickAffordanceInteractor.quickAffordance(position),
                 bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(),
-            ) { model, animateReveal ->
-                model.toViewModel(animateReveal)
+                areQuickAffordancesFullyOpaque,
+            ) { model, animateReveal, isFullyOpaque ->
+                model.toViewModel(
+                    animateReveal = animateReveal,
+                    isClickable = isFullyOpaque,
+                )
             }
             .distinctUntilChanged()
     }
 
     private fun KeyguardQuickAffordanceModel.toViewModel(
         animateReveal: Boolean,
+        isClickable: Boolean,
     ): KeyguardQuickAffordanceViewModel {
         return when (this) {
             is KeyguardQuickAffordanceModel.Visible ->
@@ -100,8 +123,20 @@
                             animationController = parameters.animationController,
                         )
                     },
+                    isClickable = isClickable,
                 )
             is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
         }
     }
+
+    companion object {
+        // We select a value that's less than 1.0 because we want floating point math precision to
+        // not be a factor in determining whether the affordance UI is fully opaque. The number we
+        // choose needs to be close enough 1.0 such that the user can't easily tell the difference
+        // between the UI with an alpha at the threshold and when the alpha is 1.0. At the same
+        // time, we don't want the number to be too close to 1.0 such that there is a chance that we
+        // never treat the affordance UI as "fully opaque" as that would risk making it forever not
+        // clickable.
+        @VisibleForTesting const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index 985ab62..b1de27d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -31,6 +31,7 @@
     val icon: ContainedDrawable = ContainedDrawable.WithResource(0),
     @StringRes val contentDescriptionResourceId: Int = 0,
     val onClicked: (OnClickedParameters) -> Unit = {},
+    val isClickable: Boolean = false,
 ) {
     data class OnClickedParameters(
         val configKey: KClass<out KeyguardQuickAffordanceConfig>,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index e360d10..ee59561 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.dialog;
 
+import android.annotation.DrawableRes;
 import android.content.res.ColorStateList;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
@@ -42,9 +43,6 @@
     private static final String TAG = "MediaOutputAdapter";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    private ViewGroup mConnectedItem;
-    private boolean mIncludeDynamicGroup;
-
     public MediaOutputAdapter(MediaOutputController controller) {
         super(controller);
         setHasStableIds(true);
@@ -102,141 +100,90 @@
         void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) {
             super.onBind(device, topMargin, bottomMargin, position);
             boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice();
-            final boolean currentlyConnected = !mIncludeDynamicGroup
-                    && isCurrentlyConnected(device);
+            final boolean currentlyConnected = isCurrentlyConnected(device);
             boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
-            if (currentlyConnected) {
-                mConnectedItem = mContainerLayout;
-            }
-            mCheckBox.setVisibility(View.GONE);
-            mStatusIcon.setVisibility(View.GONE);
-            mEndTouchArea.setVisibility(View.GONE);
-            mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
-            mContainerLayout.setOnClickListener(null);
-            mContainerLayout.setContentDescription(null);
-            mTitleText.setTextColor(mController.getColorItemContent());
-            mSubTitleText.setTextColor(mController.getColorItemContent());
-            mTwoLineTitleText.setTextColor(mController.getColorItemContent());
-            mSeekBar.getProgressDrawable().setColorFilter(
-                    new PorterDuffColorFilter(mController.getColorSeekbarProgress(),
-                            PorterDuff.Mode.SRC_IN));
             if (mCurrentActivePosition == position) {
                 mCurrentActivePosition = -1;
             }
 
-            if (mController.isTransferring()) {
+            if (mController.isAnyDeviceTransferring()) {
                 if (device.getState() == MediaDeviceState.STATE_CONNECTING
                         && !mController.hasAdjustVolumeUserRestriction()) {
                     setUpDeviceIcon(device);
-                    mProgressBar.getIndeterminateDrawable().setColorFilter(
-                            new PorterDuffColorFilter(
-                                    mController.getColorItemContent(),
-                                    PorterDuff.Mode.SRC_IN));
-                    setSingleLineLayout(getItemTitle(device), true /* bFocused */,
-                            false /* showSeekBar*/,
-                            true /* showProgressBar */, false /* showStatus */);
+                    updateProgressBarColor();
+                    setSingleLineLayout(getItemTitle(device), false /* showSeekBar*/,
+                            true /* showProgressBar */, false /* showCheckBox */,
+                            false /* showEndTouchArea */);
                 } else {
                     setUpDeviceIcon(device);
-                    setSingleLineLayout(getItemTitle(device), false /* bFocused */);
+                    setSingleLineLayout(getItemTitle(device));
                 }
             } else {
                 // Set different layout for each device
                 if (device.isMutingExpectedDevice()
                         && !mController.isCurrentConnectedDeviceRemote()) {
-                    mTitleIcon.setImageDrawable(
-                            mContext.getDrawable(R.drawable.media_output_icon_volume));
-                    mTitleIcon.setColorFilter(mController.getColorItemContent());
-                    mTitleText.setTextColor(mController.getColorItemContent());
-                    setSingleLineLayout(getItemTitle(device), true /* bFocused */,
-                            false /* showSeekBar */,
-                            false /* showProgressBar */, false /* showStatus */);
+                    updateTitleIcon(R.drawable.media_output_icon_volume,
+                            mController.getColorItemContent());
                     initMutingExpectedDevice();
                     mCurrentActivePosition = position;
-                    mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
+                    updateContainerClickListener(v -> onItemClick(v, device));
+                    setSingleLineLayout(getItemTitle(device));
                 } else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
                     setUpDeviceIcon(device);
-                    mStatusIcon.setImageDrawable(
-                            mContext.getDrawable(R.drawable.media_output_status_failed));
-                    mStatusIcon.setColorFilter(mController.getColorItemContent());
-                    setTwoLineLayout(device, false /* bFocused */,
-                            false /* showSeekBar */, false /* showProgressBar */,
-                            true /* showSubtitle */, true /* showStatus */);
+                    updateConnectionFailedStatusIcon();
                     mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
-                    mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
+                    updateContainerClickListener(v -> onItemClick(v, device));
+                    setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */,
+                            false /* showProgressBar */, true /* showSubtitle */,
+                            true /* showStatus */);
                 } else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
                     setUpDeviceIcon(device);
-                    mProgressBar.getIndeterminateDrawable().setColorFilter(
-                            new PorterDuffColorFilter(
-                                    mController.getColorItemContent(),
-                                    PorterDuff.Mode.SRC_IN));
-                    setSingleLineLayout(getItemTitle(device), true /* bFocused */,
-                            false /* showSeekBar*/,
-                            true /* showProgressBar */, false /* showStatus */);
+                    updateProgressBarColor();
+                    setSingleLineLayout(getItemTitle(device), false /* showSeekBar*/,
+                            true /* showProgressBar */, false /* showCheckBox */,
+                            false /* showEndTouchArea */);
                 } else if (mController.getSelectedMediaDevice().size() > 1
                         && isDeviceIncluded(mController.getSelectedMediaDevice(), device)) {
                     boolean isDeviceDeselectable = isDeviceIncluded(
                             mController.getDeselectableMediaDevice(), device);
-                    mTitleText.setTextColor(mController.getColorItemContent());
-                    mTitleIcon.setImageDrawable(
-                            mContext.getDrawable(R.drawable.media_output_icon_volume));
-                    mTitleIcon.setColorFilter(mController.getColorItemContent());
-                    setSingleLineLayout(getItemTitle(device), true /* bFocused */,
-                            true /* showSeekBar */,
-                            false /* showProgressBar */, false /* showStatus */);
+                    updateTitleIcon(R.drawable.media_output_icon_volume,
+                            mController.getColorItemContent());
+                    updateGroupableCheckBox(true, isDeviceDeselectable, device);
+                    updateEndClickArea(device, isDeviceDeselectable);
                     setUpContentDescriptionForView(mContainerLayout, false, device);
-                    mCheckBox.setOnCheckedChangeListener(null);
-                    mCheckBox.setVisibility(View.VISIBLE);
-                    mCheckBox.setChecked(true);
-                    mCheckBox.setOnCheckedChangeListener(isDeviceDeselectable
-                            ? (buttonView, isChecked) -> onGroupActionTriggered(false, device)
-                            : null);
-                    mCheckBox.setEnabled(isDeviceDeselectable);
-                    setCheckBoxColor(mCheckBox, mController.getColorItemContent());
+                    setSingleLineLayout(getItemTitle(device), true /* showSeekBar */,
+                            false /* showProgressBar */, true /* showCheckBox */,
+                            true /* showEndTouchArea */);
                     initSeekbar(device, isCurrentSeekbarInvisible);
-                    mEndTouchArea.setVisibility(View.VISIBLE);
-                    mEndTouchArea.setOnClickListener(null);
-                    mEndTouchArea.setOnClickListener(
-                            isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null);
-                    mEndTouchArea.setImportantForAccessibility(
-                            View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-                    setUpContentDescriptionForView(mEndTouchArea, true, device);
                 } else if (!mController.hasAdjustVolumeUserRestriction()
                         && currentlyConnected) {
                     if (isMutingExpectedDeviceExist
                             && !mController.isCurrentConnectedDeviceRemote()) {
                         // mark as disconnected and set special click listener
                         setUpDeviceIcon(device);
-                        setSingleLineLayout(getItemTitle(device), false /* bFocused */);
-                        mContainerLayout.setOnClickListener(v -> cancelMuteAwaitConnection());
+                        updateContainerClickListener(v -> cancelMuteAwaitConnection());
+                        setSingleLineLayout(getItemTitle(device));
                     } else {
-                        mTitleIcon.setImageDrawable(
-                                mContext.getDrawable(R.drawable.media_output_icon_volume));
-                        mTitleIcon.setColorFilter(mController.getColorItemContent());
-                        mTitleText.setTextColor(mController.getColorItemContent());
-                        setSingleLineLayout(getItemTitle(device), true /* bFocused */,
-                                true /* showSeekBar */,
-                                false /* showProgressBar */, false /* showStatus */);
-                        initSeekbar(device, isCurrentSeekbarInvisible);
+                        updateTitleIcon(R.drawable.media_output_icon_volume,
+                                mController.getColorItemContent());
                         setUpContentDescriptionForView(mContainerLayout, false, device);
                         mCurrentActivePosition = position;
+                        setSingleLineLayout(getItemTitle(device), true /* showSeekBar */,
+                                false /* showProgressBar */, false /* showCheckBox */,
+                                false /* showEndTouchArea */);
+                        initSeekbar(device, isCurrentSeekbarInvisible);
                     }
                 } else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
                     setUpDeviceIcon(device);
-                    mCheckBox.setOnCheckedChangeListener(null);
-                    mCheckBox.setVisibility(View.VISIBLE);
-                    mCheckBox.setChecked(false);
-                    mCheckBox.setOnCheckedChangeListener(
-                            (buttonView, isChecked) -> onGroupActionTriggered(true, device));
-                    mEndTouchArea.setVisibility(View.VISIBLE);
-                    mContainerLayout.setOnClickListener(v -> onGroupActionTriggered(true, device));
-                    setCheckBoxColor(mCheckBox, mController.getColorItemContent());
-                    setSingleLineLayout(getItemTitle(device), false /* bFocused */,
-                            false /* showSeekBar */,
-                            false /* showProgressBar */, false /* showStatus */);
+                    updateGroupableCheckBox(false, true, device);
+                    updateContainerClickListener(v -> onGroupActionTriggered(true, device));
+                    setSingleLineLayout(getItemTitle(device), false /* showSeekBar */,
+                            false /* showProgressBar */, true /* showCheckBox */,
+                            true /* showEndTouchArea */);
                 } else {
                     setUpDeviceIcon(device);
-                    setSingleLineLayout(getItemTitle(device), false /* bFocused */);
-                    mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
+                    setSingleLineLayout(getItemTitle(device));
+                    updateContainerClickListener(v -> onItemClick(v, device));
                 }
             }
         }
@@ -248,15 +195,56 @@
                     ColorStateList(states, colors));
         }
 
+        private void updateConnectionFailedStatusIcon() {
+            mStatusIcon.setImageDrawable(
+                    mContext.getDrawable(R.drawable.media_output_status_failed));
+            mStatusIcon.setColorFilter(mController.getColorItemContent());
+        }
+
+        private void updateProgressBarColor() {
+            mProgressBar.getIndeterminateDrawable().setColorFilter(
+                    new PorterDuffColorFilter(
+                            mController.getColorItemContent(),
+                            PorterDuff.Mode.SRC_IN));
+        }
+
+        public void updateEndClickArea(MediaDevice device, boolean isDeviceDeselectable) {
+            mEndTouchArea.setOnClickListener(null);
+            mEndTouchArea.setOnClickListener(
+                    isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null);
+            mEndTouchArea.setImportantForAccessibility(
+                    View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+            setUpContentDescriptionForView(mEndTouchArea, true, device);
+        }
+
+        private void updateGroupableCheckBox(boolean isSelected, boolean isGroupable,
+                MediaDevice device) {
+            mCheckBox.setOnCheckedChangeListener(null);
+            mCheckBox.setChecked(isSelected);
+            mCheckBox.setOnCheckedChangeListener(
+                    isGroupable ? (buttonView, isChecked) -> onGroupActionTriggered(!isSelected,
+                            device) : null);
+            mCheckBox.setEnabled(isGroupable);
+            setCheckBoxColor(mCheckBox, mController.getColorItemContent());
+        }
+
+        private void updateTitleIcon(@DrawableRes int id, int color) {
+            mTitleIcon.setImageDrawable(mContext.getDrawable(id));
+            mTitleIcon.setColorFilter(color);
+        }
+
+        private void updateContainerClickListener(View.OnClickListener listener) {
+            mContainerLayout.setOnClickListener(listener);
+        }
+
         @Override
         void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) {
             if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
                 mTitleText.setTextColor(mController.getColorItemContent());
                 mCheckBox.setVisibility(View.GONE);
-                setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new),
-                        false /* bFocused */);
-                final Drawable d = mContext.getDrawable(R.drawable.ic_add);
-                mTitleIcon.setImageDrawable(d);
+                setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new));
+                final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
+                mTitleIcon.setImageDrawable(addDrawable);
                 mTitleIcon.setColorFilter(mController.getColorItemContent());
                 mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
             }
@@ -273,7 +261,7 @@
         }
 
         private void onItemClick(View view, MediaDevice device) {
-            if (mController.isTransferring()) {
+            if (mController.isAnyDeviceTransferring()) {
                 return;
             }
             if (isCurrentlyConnected(device)) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 43b0287..3f7b226 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -63,8 +63,6 @@
 
     protected final MediaOutputController mController;
 
-    private int mMargin;
-
     Context mContext;
     View mHolderView;
     boolean mIsDragging;
@@ -82,8 +80,6 @@
     public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
             int viewType) {
         mContext = viewGroup.getContext();
-        mMargin = mContext.getResources().getDimensionPixelSize(
-                R.dimen.media_output_dialog_list_margin);
         mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item,
                 viewGroup, false);
 
@@ -168,16 +164,28 @@
 
         void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) {
             mDeviceId = device.getId();
+            mCheckBox.setVisibility(View.GONE);
+            mStatusIcon.setVisibility(View.GONE);
+            mEndTouchArea.setVisibility(View.GONE);
+            mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+            mContainerLayout.setOnClickListener(null);
+            mContainerLayout.setContentDescription(null);
+            mTitleText.setTextColor(mController.getColorItemContent());
+            mSubTitleText.setTextColor(mController.getColorItemContent());
+            mTwoLineTitleText.setTextColor(mController.getColorItemContent());
+            mSeekBar.getProgressDrawable().setColorFilter(
+                    new PorterDuffColorFilter(mController.getColorSeekbarProgress(),
+                            PorterDuff.Mode.SRC_IN));
         }
 
         abstract void onBind(int customizedItem, boolean topMargin, boolean bottomMargin);
 
-        void setSingleLineLayout(CharSequence title, boolean bFocused) {
-            setSingleLineLayout(title, bFocused, false, false, false);
+        void setSingleLineLayout(CharSequence title) {
+            setSingleLineLayout(title, false, false, false, false);
         }
 
-        void setSingleLineLayout(CharSequence title, boolean bFocused, boolean showSeekBar,
-                boolean showProgressBar, boolean showStatus) {
+        void setSingleLineLayout(CharSequence title, boolean showSeekBar,
+                boolean showProgressBar, boolean showCheckBox, boolean showEndTouchArea) {
             mTwoLineLayout.setVisibility(View.GONE);
             boolean isActive = showSeekBar || showProgressBar;
             if (!mCornerAnimator.isRunning()) {
@@ -188,10 +196,6 @@
                                 .mutate() : mContext.getDrawable(
                                         R.drawable.media_output_item_background)
                                 .mutate();
-                backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
-                        isActive ? mController.getColorConnectedItemBackground()
-                                : mController.getColorItemBackground(),
-                        PorterDuff.Mode.SRC_IN));
                 mItemLayout.setBackground(backgroundDrawable);
                 if (showSeekBar) {
                     final ClipDrawable clipDrawable =
@@ -201,27 +205,21 @@
                             (GradientDrawable) clipDrawable.getDrawable();
                     progressDrawable.setCornerRadius(mController.getActiveRadius());
                 }
-            } else {
-                mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
-                        isActive ? mController.getColorConnectedItemBackground()
-                                : mController.getColorItemBackground(),
-                        PorterDuff.Mode.SRC_IN));
             }
+            mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
+                    isActive ? mController.getColorConnectedItemBackground()
+                            : mController.getColorItemBackground(),
+                    PorterDuff.Mode.SRC_IN));
             mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
             mSeekBar.setAlpha(1);
             mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
             if (!showSeekBar) {
                 mSeekBar.resetVolume();
             }
-            mStatusIcon.setVisibility(showStatus ? View.VISIBLE : View.GONE);
             mTitleText.setText(title);
             mTitleText.setVisibility(View.VISIBLE);
-        }
-
-        void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
-                boolean showProgressBar, boolean showSubtitle) {
-            setTwoLineLayout(device, null, bFocused, showSeekBar, showProgressBar, showSubtitle,
-                    false);
+            mCheckBox.setVisibility(showCheckBox ? View.VISIBLE : View.GONE);
+            mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE);
         }
 
         void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
@@ -230,12 +228,6 @@
                     showStatus);
         }
 
-        void setTwoLineLayout(CharSequence title, boolean bFocused, boolean showSeekBar,
-                boolean showProgressBar, boolean showSubtitle) {
-            setTwoLineLayout(null, title, bFocused, showSeekBar, showProgressBar, showSubtitle,
-                    false);
-        }
-
         private void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused,
                 boolean showSeekBar, boolean showProgressBar, boolean showSubtitle,
                 boolean showStatus) {
@@ -254,20 +246,11 @@
             mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
             mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
             mTwoLineTitleText.setTranslationY(0);
-            if (device == null) {
-                mTwoLineTitleText.setText(title);
-            } else {
-                mTwoLineTitleText.setText(getItemTitle(device));
-            }
-
-            if (bFocused) {
-                mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString(
-                                com.android.internal.R.string.config_headlineFontFamilyMedium),
-                        Typeface.NORMAL));
-            } else {
-                mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString(
-                        com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL));
-            }
+            mTwoLineTitleText.setText(device == null ? title : getItemTitle(device));
+            mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString(
+                            bFocused ? com.android.internal.R.string.config_headlineFontFamilyMedium
+                                    : com.android.internal.R.string.config_headlineFontFamily),
+                    Typeface.NORMAL));
         }
 
         void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) {
@@ -327,35 +310,6 @@
             mItemLayout.setBackground(backgroundDrawable);
         }
 
-        void initSessionSeekbar() {
-            disableSeekBar();
-            mSeekBar.setMax(mController.getSessionVolumeMax());
-            mSeekBar.setMin(0);
-            final int currentVolume = mController.getSessionVolume();
-            if (mSeekBar.getProgress() != currentVolume) {
-                mSeekBar.setProgress(currentVolume, true);
-            }
-            mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
-                @Override
-                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-                    if (!fromUser) {
-                        return;
-                    }
-                    mController.adjustSessionVolume(progress);
-                }
-
-                @Override
-                public void onStartTrackingTouch(SeekBar seekBar) {
-                    mIsDragging = true;
-                }
-
-                @Override
-                public void onStopTrackingTouch(SeekBar seekBar) {
-                    mIsDragging = false;
-                }
-            });
-        }
-
         private void animateCornerAndVolume(int fromProgress, int toProgress) {
             final GradientDrawable layoutBackgroundDrawable =
                     (GradientDrawable) mItemLayout.getBackground();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index f7d80e0..96817c9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -411,7 +411,7 @@
                 device.getId());
         boolean isSelectedDeviceInGroup = getSelectedMediaDevice().size() > 1
                 && getSelectedMediaDevice().contains(device);
-        return (!hasAdjustVolumeUserRestriction() && isConnected && !isTransferring())
+        return (!hasAdjustVolumeUserRestriction() && isConnected && !isAnyDeviceTransferring())
                 || isSelectedDeviceInGroup;
     }
 
@@ -708,7 +708,7 @@
                 UserHandle.of(UserHandle.myUserId()));
     }
 
-    boolean isTransferring() {
+    boolean isAnyDeviceTransferring() {
         synchronized (mMediaDevicesLock) {
             for (MediaDevice device : mMediaDevices) {
                 if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
index 2278938..3a0ac1b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
@@ -226,7 +226,7 @@
 
         appIconView.contentDescription = appNameOverride ?: iconInfo.iconName
         appIconView.setImageDrawable(appIconDrawableOverride ?: iconInfo.icon)
-        return appIconView.contentDescription.toString()
+        return appIconView.contentDescription
     }
 
     /**
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 196ea22..00a22f2 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
@@ -141,12 +141,13 @@
 
     override fun updateChipView(newChipInfo: ChipReceiverInfo, currentChipView: ViewGroup) {
         super.updateChipView(newChipInfo, currentChipView)
-        setIcon(
+        val iconName = setIcon(
                 currentChipView,
                 newChipInfo.routeInfo.clientPackageName,
                 newChipInfo.appIconDrawableOverride,
                 newChipInfo.appNameOverride
         )
+        currentChipView.contentDescription = iconName
     }
 
     override fun animateChipIn(chipView: ViewGroup) {
@@ -159,6 +160,8 @@
                 .alpha(1f)
                 .setDuration(5.frames)
                 .start()
+        // Using withEndAction{} doesn't apply a11y focus when screen is unlocked.
+        appIconView.postOnAnimation { chipView.requestAccessibilityFocus() }
         startRipple(chipView.requireViewById(R.id.ripple))
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
index eeb1010..2a6cf66 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
@@ -80,7 +80,8 @@
             FeatureFlags featureFlags,
             VariableDateViewController.Factory variableDateViewControllerFactory,
             BatteryMeterViewController batteryMeterViewController,
-            StatusBarContentInsetsProvider statusBarContentInsetsProvider) {
+            StatusBarContentInsetsProvider statusBarContentInsetsProvider,
+            StatusBarIconController.TintedIconManager.Factory tintedIconManagerFactory) {
         super(view);
         mPrivacyIconsController = headerPrivacyIconsController;
         mStatusBarIconController = statusBarIconController;
@@ -103,7 +104,7 @@
                 mView.requireViewById(R.id.date_clock)
         );
 
-        mIconManager = new StatusBarIconController.TintedIconManager(mIconContainer, featureFlags);
+        mIconManager = tintedIconManagerFactory.create(mIconContainer);
         mDemoModeReceiver = new ClockDemoModeReceiver(mClockView);
         mColorExtractor = colorExtractor;
         mOnColorsChangedListener = (extractor, which) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index a8993bc..8b37aab 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -70,7 +70,7 @@
 public class TakeScreenshotService extends Service {
     private static final String TAG = logTag(TakeScreenshotService.class);
 
-    private ScreenshotController mScreenshot;
+    private final ScreenshotController mScreenshot;
 
     private final UserManager mUserManager;
     private final DevicePolicyManager mDevicePolicyManager;
@@ -152,10 +152,7 @@
         if (DEBUG_SERVICE) {
             Log.d(TAG, "onUnbind");
         }
-        if (mScreenshot != null) {
-            mScreenshot.removeWindow();
-            mScreenshot = null;
-        }
+        mScreenshot.removeWindow();
         unregisterReceiver(mCloseSystemDialogs);
         return false;
     }
@@ -163,10 +160,7 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        if (mScreenshot != null) {
-            mScreenshot.onDestroy();
-            mScreenshot = null;
-        }
+        mScreenshot.onDestroy();
         if (DEBUG_SERVICE) {
             Log.d(TAG, "onDestroy");
         }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
index 5e908d9..1558ac5 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
@@ -76,6 +76,6 @@
          * Notifies that the current user's profiles have changed.
          */
         @JvmDefault
-        fun onProfilesChanged(profiles: List<UserInfo>) {}
+        fun onProfilesChanged(profiles: List<@JvmSuppressWildcards UserInfo>) {}
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index 0f9ac36..fab70fc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -77,6 +77,7 @@
 class LargeScreenShadeHeaderController @Inject constructor(
     @Named(LARGE_SCREEN_SHADE_HEADER) private val header: View,
     private val statusBarIconController: StatusBarIconController,
+    private val tintedIconManagerFactory: StatusBarIconController.TintedIconManager.Factory,
     private val privacyIconsController: HeaderPrivacyIconsController,
     private val insetsProvider: StatusBarContentInsetsProvider,
     private val configurationController: ConfigurationController,
@@ -259,7 +260,7 @@
         batteryMeterViewController.ignoreTunerUpdates()
         batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
 
-        iconManager = StatusBarIconController.TintedIconManager(iconContainer, featureFlags)
+        iconManager = tintedIconManagerFactory.create(iconContainer)
         iconManager.setTint(
             Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary)
         )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt
new file mode 100644
index 0000000..4d53064
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt
@@ -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.systemui.statusbar
+
+import android.content.Context
+import android.util.AttributeSet
+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]).
+ *
+ * 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(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttrs: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttrs), StatusIconDisplayable
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
index a6986d7..5aee62e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
@@ -28,7 +28,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;
 
@@ -41,8 +40,7 @@
 /**
  * Start small: StatusBarWifiView will be able to layout from a WifiIconState
  */
-public class StatusBarWifiView extends FrameLayout implements DarkReceiver,
-        StatusIconDisplayable {
+public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkReceiver {
     private static final String TAG = "StatusBarWifiView";
 
     /// Used to show etc dots
@@ -80,11 +78,6 @@
         super(context, attrs, defStyleAttr);
     }
 
-    public StatusBarWifiView(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-    }
-
     public void setSlot(String slot) {
         mSlot = slot;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
index 3c449ad..7097568 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
@@ -23,7 +23,7 @@
 /** */
 public class WifiIcons {
 
-    static final int[] WIFI_FULL_ICONS = {
+    public static final int[] WIFI_FULL_ICONS = {
             com.android.internal.R.drawable.ic_wifi_signal_0,
             com.android.internal.R.drawable.ic_wifi_signal_1,
             com.android.internal.R.drawable.ic_wifi_signal_2,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index b0a5adb..6f06759 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -4065,7 +4065,7 @@
             final PendingIntent intent, @Nullable final Runnable intentSentUiThreadCallback,
             @Nullable ActivityLaunchAnimator.Controller animationController) {
         final boolean willLaunchResolverActivity = intent.isActivity()
-                && mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(),
+                && mActivityIntentHelper.wouldPendingLaunchResolverActivity(intent,
                 mLockscreenUserManager.getCurrentUserId());
 
         boolean animate = !willLaunchResolverActivity
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index fc8e7d5..deb6150 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -225,6 +225,7 @@
 
     public void addDemoWifiView(WifiIconState state) {
         Log.d(TAG, "addDemoWifiView: ");
+        // TODO(b/238425913): Migrate this view to {@code ModernStatusBarWifiView}.
         StatusBarWifiView view = StatusBarWifiView.fromContext(mContext, state.slot);
 
         int viewIndex = getChildCount();
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 30b640b..ed186ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -40,6 +40,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.BaseStatusBarWifiView;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusBarMobileView;
 import com.android.systemui.statusbar.StatusBarWifiView;
@@ -47,12 +48,16 @@
 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.pipeline.wifi.ui.view.ModernStatusBarWifiView;
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
 import com.android.systemui.util.Assert;
 
 import java.util.ArrayList;
 import java.util.List;
 
 import javax.inject.Inject;
+import javax.inject.Provider;
 
 public interface StatusBarIconController {
 
@@ -128,8 +133,12 @@
         private final DarkIconDispatcher mDarkIconDispatcher;
         private int mIconHPadding;
 
-        public DarkIconManager(LinearLayout linearLayout, FeatureFlags featureFlags) {
-            super(linearLayout, featureFlags);
+        public DarkIconManager(
+                LinearLayout linearLayout,
+                FeatureFlags featureFlags,
+                StatusBarPipelineFlags statusBarPipelineFlags,
+                Provider<WifiViewModel> wifiViewModelProvider) {
+            super(linearLayout, featureFlags, statusBarPipelineFlags, wifiViewModelProvider);
             mIconHPadding = mContext.getResources().getDimensionPixelSize(
                     R.dimen.status_bar_icon_padding);
             mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class);
@@ -183,14 +192,40 @@
             mDarkIconDispatcher.removeDarkReceiver(mDemoStatusIcons);
             super.exitDemoMode();
         }
+
+        @SysUISingleton
+        public static class Factory {
+            private final FeatureFlags mFeatureFlags;
+            private final StatusBarPipelineFlags mStatusBarPipelineFlags;
+            private final Provider<WifiViewModel> mWifiViewModelProvider;
+
+            @Inject
+            public Factory(
+                    FeatureFlags featureFlags,
+                    StatusBarPipelineFlags statusBarPipelineFlags,
+                    Provider<WifiViewModel> wifiViewModelProvider) {
+                mFeatureFlags = featureFlags;
+                mStatusBarPipelineFlags = statusBarPipelineFlags;
+                mWifiViewModelProvider = wifiViewModelProvider;
+            }
+
+            public DarkIconManager create(LinearLayout group) {
+                return new DarkIconManager(
+                        group, mFeatureFlags, mStatusBarPipelineFlags, mWifiViewModelProvider);
+            }
+        }
     }
 
     /** */
     class TintedIconManager extends IconManager {
         private int mColor;
 
-        public TintedIconManager(ViewGroup group, FeatureFlags featureFlags) {
-            super(group, featureFlags);
+        public TintedIconManager(
+                ViewGroup group,
+                FeatureFlags featureFlags,
+                StatusBarPipelineFlags statusBarPipelineFlags,
+                Provider<WifiViewModel> wifiViewModelProvider) {
+            super(group, featureFlags, statusBarPipelineFlags, wifiViewModelProvider);
         }
 
         @Override
@@ -223,14 +258,22 @@
         @SysUISingleton
         public static class Factory {
             private final FeatureFlags mFeatureFlags;
+            private final StatusBarPipelineFlags mStatusBarPipelineFlags;
+            private final Provider<WifiViewModel> mWifiViewModelProvider;
 
             @Inject
-            public Factory(FeatureFlags featureFlags) {
+            public Factory(
+                    FeatureFlags featureFlags,
+                    StatusBarPipelineFlags statusBarPipelineFlags,
+                    Provider<WifiViewModel> wifiViewModelProvider) {
                 mFeatureFlags = featureFlags;
+                mStatusBarPipelineFlags = statusBarPipelineFlags;
+                mWifiViewModelProvider = wifiViewModelProvider;
             }
 
             public TintedIconManager create(ViewGroup group) {
-                return new TintedIconManager(group, mFeatureFlags);
+                return new TintedIconManager(
+                        group, mFeatureFlags, mStatusBarPipelineFlags, mWifiViewModelProvider);
             }
         }
     }
@@ -239,8 +282,10 @@
      * Turns info from StatusBarIconController into ImageViews in a ViewGroup.
      */
     class IconManager implements DemoModeCommandReceiver {
-        private final FeatureFlags mFeatureFlags;
         protected final ViewGroup mGroup;
+        private final FeatureFlags mFeatureFlags;
+        private final StatusBarPipelineFlags mStatusBarPipelineFlags;
+        private final Provider<WifiViewModel> mWifiViewModelProvider;
         protected final Context mContext;
         protected final int mIconSize;
         // Whether or not these icons show up in dumpsys
@@ -254,9 +299,15 @@
 
         protected ArrayList<String> mBlockList = new ArrayList<>();
 
-        public IconManager(ViewGroup group, FeatureFlags featureFlags) {
-            mFeatureFlags = featureFlags;
+        public IconManager(
+                ViewGroup group,
+                FeatureFlags featureFlags,
+                StatusBarPipelineFlags statusBarPipelineFlags,
+                Provider<WifiViewModel> wifiViewModelProvider) {
             mGroup = group;
+            mFeatureFlags = featureFlags;
+            mStatusBarPipelineFlags = statusBarPipelineFlags;
+            mWifiViewModelProvider = wifiViewModelProvider;
             mContext = group.getContext();
             mIconSize = mContext.getResources().getDimensionPixelSize(
                     com.android.internal.R.dimen.status_bar_icon_size);
@@ -308,7 +359,7 @@
                     return addIcon(index, slot, blocked, holder.getIcon());
 
                 case TYPE_WIFI:
-                    return addSignalIcon(index, slot, holder.getWifiState());
+                    return addWifiIcon(index, slot, holder.getWifiState());
 
                 case TYPE_MOBILE:
                     return addMobileIcon(index, slot, holder.getMobileState());
@@ -327,9 +378,17 @@
         }
 
         @VisibleForTesting
-        protected StatusBarWifiView addSignalIcon(int index, String slot, WifiIconState state) {
-            StatusBarWifiView view = onCreateStatusBarWifiView(slot);
-            view.applyWifiState(state);
+        protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) {
+            final BaseStatusBarWifiView view;
+            if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+                view = onCreateModernStatusBarWifiView(slot);
+                // When [ModernStatusBarWifiView] is created, it will automatically apply the
+                // correct view state so we don't need to call applyWifiState.
+            } else {
+                StatusBarWifiView wifiView = onCreateStatusBarWifiView(slot);
+                wifiView.applyWifiState(state);
+                view = wifiView;
+            }
             mGroup.addView(view, index, onCreateLayoutParams());
 
             if (mIsInDemoMode) {
@@ -359,6 +418,11 @@
             return view;
         }
 
+        private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) {
+            return ModernStatusBarWifiView.constructAndBind(
+                    mContext, slot, mWifiViewModelProvider.get());
+        }
+
         private StatusBarMobileView onCreateStatusBarMobileView(String slot) {
             StatusBarMobileView view = StatusBarMobileView.fromContext(mContext, slot);
             return view;
@@ -415,9 +479,8 @@
                     onSetIcon(viewIndex, holder.getIcon());
                     return;
                 case TYPE_WIFI:
-                    onSetSignalIcon(viewIndex, holder.getWifiState());
+                    onSetWifiIcon(viewIndex, holder.getWifiState());
                     return;
-
                 case TYPE_MOBILE:
                     onSetMobileIcon(viewIndex, holder.getMobileState());
                 default:
@@ -425,10 +488,16 @@
             }
         }
 
-        public void onSetSignalIcon(int viewIndex, WifiIconState state) {
-            StatusBarWifiView wifiView = (StatusBarWifiView) mGroup.getChildAt(viewIndex);
-            if (wifiView != null) {
-                wifiView.applyWifiState(state);
+        public void onSetWifiIcon(int viewIndex, WifiIconState state) {
+            View view = mGroup.getChildAt(viewIndex);
+            if (view instanceof StatusBarWifiView) {
+                ((StatusBarWifiView) view).applyWifiState(state);
+            } else if (view instanceof ModernStatusBarWifiView) {
+                // ModernStatusBarWifiView will automatically apply state based on its callbacks, so
+                // we don't need to call applyWifiState.
+            } else {
+                throw new IllegalStateException("View at " + viewIndex + " must be of type "
+                        + "StatusBarWifiView or ModernStatusBarWifiView");
             }
 
             if (mIsInDemoMode) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 374f091..5cd2ba1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -223,12 +223,12 @@
 
         boolean isActivityIntent = intent != null && intent.isActivity() && !isBubble;
         final boolean willLaunchResolverActivity = isActivityIntent
-                && mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(),
+                && mActivityIntentHelper.wouldPendingLaunchResolverActivity(intent,
                 mLockscreenUserManager.getCurrentUserId());
         final boolean animate = !willLaunchResolverActivity
                 && mCentralSurfaces.shouldAnimateLaunch(isActivityIntent);
         boolean showOverLockscreen = mKeyguardStateController.isShowing() && intent != null
-                && mActivityIntentHelper.wouldShowOverLockscreen(intent.getIntent(),
+                && mActivityIntentHelper.wouldPendingShowOverLockscreen(intent,
                 mLockscreenUserManager.getCurrentUserId());
         ActivityStarter.OnDismissAction postKeyguardAction = new ActivityStarter.OnDismissAction() {
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 40b9a15..70af77e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -259,8 +259,9 @@
         final boolean isActivity = pendingIntent.isActivity();
         if (isActivity || appRequestedAuth) {
             mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent);
-            final boolean afterKeyguardGone = mActivityIntentHelper.wouldLaunchResolverActivity(
-                    pendingIntent.getIntent(), mLockscreenUserManager.getCurrentUserId());
+            final boolean afterKeyguardGone = mActivityIntentHelper
+                    .wouldPendingLaunchResolverActivity(pendingIntent,
+                            mLockscreenUserManager.getCurrentUserId());
             mActivityStarter.dismissKeyguardThenExecute(() -> {
                 mActionClickLogger.logKeyguardGone(pendingIntent);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 200f45f..fb5b096 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -286,6 +286,7 @@
             PanelExpansionStateManager panelExpansionStateManager,
             FeatureFlags featureFlags,
             StatusBarIconController statusBarIconController,
+            StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory,
             StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
             KeyguardStateController keyguardStateController,
             NotificationPanelViewController notificationPanelViewController,
@@ -306,6 +307,7 @@
                 panelExpansionStateManager,
                 featureFlags,
                 statusBarIconController,
+                darkIconManagerFactory,
                 statusBarHideIconsForBouncerManager,
                 keyguardStateController,
                 notificationPanelViewController,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index f61b488..5fd72b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -124,6 +124,7 @@
     private final StatusBarIconController mStatusBarIconController;
     private final CarrierConfigTracker mCarrierConfigTracker;
     private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
+    private final StatusBarIconController.DarkIconManager.Factory mDarkIconManagerFactory;
     private final SecureSettings mSecureSettings;
     private final Executor mMainExecutor;
     private final DumpManager mDumpManager;
@@ -172,6 +173,7 @@
             PanelExpansionStateManager panelExpansionStateManager,
             FeatureFlags featureFlags,
             StatusBarIconController statusBarIconController,
+            StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory,
             StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
             KeyguardStateController keyguardStateController,
             NotificationPanelViewController notificationPanelViewController,
@@ -193,6 +195,7 @@
         mFeatureFlags = featureFlags;
         mStatusBarIconController = statusBarIconController;
         mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
+        mDarkIconManagerFactory = darkIconManagerFactory;
         mKeyguardStateController = keyguardStateController;
         mNotificationPanelViewController = notificationPanelViewController;
         mStatusBarStateController = statusBarStateController;
@@ -232,7 +235,7 @@
             mStatusBar.restoreHierarchyState(
                     savedInstanceState.getSparseParcelableArray(EXTRA_PANEL_STATE));
         }
-        mDarkIconManager = new DarkIconManager(view.findViewById(R.id.statusIcons), mFeatureFlags);
+        mDarkIconManager = mDarkIconManagerFactory.create(view.findViewById(R.id.statusIcons));
         mDarkIconManager.setShouldLog(true);
         updateBlockedIcons();
         mStatusBarIconController.addIconGroup(mDarkIconManager);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt
deleted file mode 100644
index 780a02d..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline
-
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo
-import kotlinx.coroutines.flow.StateFlow
-
-/**
- * Interface exposing a flow for raw connectivity information. Clients should collect on
- * [rawConnectivityInfoFlow] to get updates on connectivity information.
- *
- * Note: [rawConnectivityInfoFlow] should be a *hot* flow, so that we only create one instance of it
- * and all clients get references to the same flow.
- *
- * This will be used for the new status bar pipeline to compile information we need to display some
- * of the icons in the RHS of the status bar.
- */
-interface ConnectivityInfoCollector {
-    val rawConnectivityInfoFlow: StateFlow<RawConnectivityInfo>
-}
-
-/**
- * An object containing all of the raw connectivity information.
- *
- * Importantly, all the information in this object should not be processed at all (i.e., the data
- * that we receive from callbacks should be piped straight into this object and not be filtered,
- * manipulated, or processed in any way). Instead, any listeners on
- * [ConnectivityInfoCollector.rawConnectivityInfoFlow] can do the processing.
- *
- * This allows us to keep all the processing in one place which is beneficial for logging and
- * debugging purposes.
- */
-data class RawConnectivityInfo(
-        val networkCapabilityInfo: Map<Int, NetworkCapabilityInfo> = emptyMap(),
-)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt
deleted file mode 100644
index 99798f9..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilitiesRepo
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/**
- * The real implementation of [ConnectivityInfoCollector] that will collect information from all the
- * relevant connectivity callbacks and compile it into [rawConnectivityInfoFlow].
- */
-@SysUISingleton
-class ConnectivityInfoCollectorImpl @Inject constructor(
-        networkCapabilitiesRepo: NetworkCapabilitiesRepo,
-        @Application scope: CoroutineScope,
-) : ConnectivityInfoCollector {
-    override val rawConnectivityInfoFlow: StateFlow<RawConnectivityInfo> =
-            // TODO(b/238425913): Collect all the separate flows for individual raw information into
-            //   this final flow.
-            networkCapabilitiesRepo.dataStream
-                    .map {
-                        RawConnectivityInfo(networkCapabilityInfo = it)
-                    }
-                    .stateIn(scope, started = Lazily, initialValue = RawConnectivityInfo())
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
index 64c47f6..fe84674 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
@@ -20,31 +20,21 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
 import javax.inject.Inject
 import javax.inject.Provider
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
 import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 /**
- * A processor that transforms raw connectivity information that we get from callbacks and turns it
- * into a list of displayable connectivity information.
+ * A temporary object that collects on [WifiViewModel] flows for debugging purposes.
  *
- * This will be used for the new status bar pipeline to calculate the list of icons that should be
- * displayed in the RHS of the status bar.
- *
- * Anyone can listen to [processedInfoFlow] to get updates to the processed data.
+ * This will eventually get migrated to a view binder that will use the flow outputs to set state on
+ * views. For now, this just collects on flows so that the information gets logged.
  */
 @SysUISingleton
 class ConnectivityInfoProcessor @Inject constructor(
-        connectivityInfoCollector: ConnectivityInfoCollector,
         context: Context,
         // TODO(b/238425913): Don't use the application scope; instead, use the status bar view's
         // scope so we only do work when there's UI that cares about it.
@@ -52,23 +42,8 @@
         private val statusBarPipelineFlags: StatusBarPipelineFlags,
         private val wifiViewModelProvider: Provider<WifiViewModel>,
 ) : CoreStartable(context) {
-    // Note: This flow will not start running until a client calls `collect` on it, which means that
-    // [connectivityInfoCollector]'s flow will also not start anything until that `collect` call
-    // happens.
-    // TODO(b/238425913): Delete this.
-    val processedInfoFlow: Flow<ProcessedConnectivityInfo> =
-            if (!statusBarPipelineFlags.isNewPipelineEnabled())
-                emptyFlow()
-            else connectivityInfoCollector.rawConnectivityInfoFlow
-                    .map { it.process() }
-                    .stateIn(
-                            scope,
-                            started = Lazily,
-                            initialValue = ProcessedConnectivityInfo()
-                    )
-
     override fun start() {
-        if (!statusBarPipelineFlags.isNewPipelineEnabled()) {
+        if (!statusBarPipelineFlags.isNewPipelineBackendEnabled()) {
             return
         }
         // TODO(b/238425913): The view binder should do this instead. For now, do it here so we can
@@ -77,17 +52,4 @@
             wifiViewModelProvider.get().isActivityInVisible.collect { }
         }
     }
-
-    private fun RawConnectivityInfo.process(): ProcessedConnectivityInfo {
-        // TODO(b/238425913): Actually process the raw info into meaningful data.
-        return ProcessedConnectivityInfo(this.networkCapabilityInfo)
-    }
 }
-
-/**
- * An object containing connectivity info that has been processed into data that can be directly
- * used by the status bar (and potentially other SysUI areas) to display icons.
- */
-data class ProcessedConnectivityInfo(
-        val networkCapabilityInfo: Map<Int, NetworkCapabilityInfo> = emptyMap(),
-)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
index 589cdb8..9b8b643 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
@@ -25,13 +25,28 @@
 @SysUISingleton
 class StatusBarPipelineFlags @Inject constructor(private val featureFlags: FeatureFlags) {
     /**
-     * Returns true if we should run the new pipeline.
+     * Returns true if we should run the new pipeline backend.
      *
-     * TODO(b/238425913): We may want to split this out into:
-     *   (1) isNewPipelineLoggingEnabled(), where the new pipeline runs and logs its decisions but
-     *       doesn't change the UI at all.
-     *   (2) isNewPipelineEnabled(), where the new pipeline runs and does change the UI (and the old
-     *       pipeline doesn't change the UI).
+     * The new pipeline backend hooks up to all our external callbacks, logs those callback inputs,
+     * and logs the output state.
      */
-    fun isNewPipelineEnabled(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE)
+    fun isNewPipelineBackendEnabled(): Boolean =
+        featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_BACKEND)
+
+    /**
+     * Returns true if we should run the new pipeline frontend *and* backend.
+     *
+     * The new pipeline frontend will use the outputted state from the new backend and will make the
+     * correct changes to the UI.
+     */
+    fun isNewPipelineFrontendEnabled(): Boolean =
+        isNewPipelineBackendEnabled() &&
+            featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_FRONTEND)
+
+    /**
+     * Returns true if we should apply some coloring to icons that were rendered with the new
+     * pipeline to help with debugging.
+     */
+    // For now, just always apply the debug coloring if we've enabled frontend rendering.
+    fun useNewPipelineDebugColoring(): Boolean = isNewPipelineFrontendEnabled()
 }
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 7abe19e7b..88eccb5 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
@@ -17,8 +17,6 @@
 package com.android.systemui.statusbar.pipeline.dagger
 
 import com.android.systemui.CoreStartable
-import com.android.systemui.statusbar.pipeline.ConnectivityInfoCollector
-import com.android.systemui.statusbar.pipeline.ConnectivityInfoCollectorImpl
 import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl
@@ -36,10 +34,5 @@
     abstract fun bindConnectivityInfoProcessor(cip: ConnectivityInfoProcessor): CoreStartable
 
     @Binds
-    abstract fun provideConnectivityInfoCollector(
-            impl: ConnectivityInfoCollectorImpl
-    ): ConnectivityInfoCollector
-
-    @Binds
     abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index a5fff5e..2a89309 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -31,6 +31,9 @@
 class ConnectivityPipelineLogger @Inject constructor(
     @StatusBarConnectivityLog private val buffer: LogBuffer,
 ) {
+    /**
+     * Logs a change in one of the **raw inputs** to the connectivity pipeline.
+     */
     fun logInputChange(callbackName: String, changeInfo: String) {
         buffer.log(
                 SB_LOGGING_TAG,
@@ -45,6 +48,41 @@
         )
     }
 
+    /**
+     * Logs a **data transformation** that we performed within the connectivity pipeline.
+     */
+    fun logTransformation(transformationName: String, oldValue: Any?, newValue: Any?) {
+        if (oldValue == newValue) {
+            buffer.log(
+                SB_LOGGING_TAG,
+                LogLevel.INFO,
+                {
+                    str1 = transformationName
+                    str2 = oldValue.toString()
+                },
+                {
+                    "Transform: $str1: $str2 (transformation didn't change it)"
+                }
+            )
+        } else {
+            buffer.log(
+                SB_LOGGING_TAG,
+                LogLevel.INFO,
+                {
+                    str1 = transformationName
+                    str2 = oldValue.toString()
+                    str3 = newValue.toString()
+                },
+                {
+                    "Transform: $str1: $str2 -> $str3"
+                }
+            )
+        }
+    }
+
+    /**
+     * Logs a change in one of the **outputs** to the connectivity pipeline.
+     */
     fun logOutputChange(outputParamName: String, changeInfo: String) {
         buffer.log(
                 SB_LOGGING_TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt
deleted file mode 100644
index 1b73322..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.wifi.data.model
-
-/** Provides information about the current wifi state. */
-data class WifiModel(
-    /** See [android.net.wifi.WifiInfo.ssid]. */
-    val ssid: String? = null,
-    /** See [android.net.wifi.WifiInfo.isPasspointAp]. */
-    val isPasspointAccessPoint: Boolean = false,
-    /** See [android.net.wifi.WifiInfo.isOsuAp]. */
-    val isOnlineSignUpForPasspointAccessPoint: Boolean = false,
-    /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */
-    val passpointProviderFriendlyName: String? = null,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
new file mode 100644
index 0000000..5566fa6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.wifi.data.model
+
+/** Provides information about the current wifi network. */
+sealed class WifiNetworkModel {
+    /** A model representing that we have no active wifi network. */
+    object Inactive : WifiNetworkModel()
+
+    /** Provides information about an active wifi network. */
+    class Active(
+        /**
+         * The [android.net.Network.netId] we received from
+         * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
+         *
+         * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId].
+         */
+        val networkId: Int,
+
+        /** See [android.net.wifi.WifiInfo.ssid]. */
+        val ssid: String? = null,
+
+        /** See [android.net.wifi.WifiInfo.isPasspointAp]. */
+        val isPasspointAccessPoint: Boolean = false,
+
+        /** See [android.net.wifi.WifiInfo.isOsuAp]. */
+        val isOnlineSignUpForPasspointAccessPoint: Boolean = false,
+
+        /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */
+        val passpointProviderFriendlyName: String? = null,
+    ) : WifiNetworkModel()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt
deleted file mode 100644
index 6c0a445..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.systemui.statusbar.pipeline.wifi.data.repository
-
-import android.annotation.SuppressLint
-import android.net.ConnectivityManager
-import android.net.Network
-import android.net.NetworkCapabilities
-import android.net.NetworkRequest
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.stateIn
-
-/**
- * Repository that contains all relevant [NetworkCapabilities] for the current networks.
- *
- * TODO(b/238425913): Figure out how to merge this with [WifiRepository].
- */
-@SysUISingleton
-class NetworkCapabilitiesRepo @Inject constructor(
-    connectivityManager: ConnectivityManager,
-    @Application scope: CoroutineScope,
-    logger: ConnectivityPipelineLogger,
-) {
-    @SuppressLint("MissingPermission")
-    val dataStream: StateFlow<Map<Int, NetworkCapabilityInfo>> = run {
-        var state = emptyMap<Int, NetworkCapabilityInfo>()
-        callbackFlow {
-                val networkRequest: NetworkRequest =
-                    NetworkRequest.Builder()
-                        .clearCapabilities()
-                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
-                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
-                        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
-                        .build()
-                val callback =
-                    // TODO (b/240569788): log these using [LogBuffer]
-                    object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
-                        override fun onCapabilitiesChanged(
-                            network: Network,
-                            networkCapabilities: NetworkCapabilities
-                        ) {
-                            logger.logOnCapabilitiesChanged(network, networkCapabilities)
-                            state =
-                                state.toMutableMap().also {
-                                    it[network.getNetId()] =
-                                        NetworkCapabilityInfo(network, networkCapabilities)
-                                }
-                            trySend(state)
-                        }
-
-                        override fun onLost(network: Network) {
-                            logger.logOnLost(network)
-                            state = state.toMutableMap().also { it.remove(network.getNetId()) }
-                            trySend(state)
-                        }
-                    }
-                connectivityManager.registerNetworkCallback(networkRequest, callback)
-
-                awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
-            }
-            .stateIn(scope, started = Lazily, initialValue = state)
-    }
-}
-
-/** contains info about network capabilities. */
-data class NetworkCapabilityInfo(
-    val network: Network,
-    val capabilities: NetworkCapabilities,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 012dde5..43fbabc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -16,16 +16,26 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
+import android.annotation.SuppressLint
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiManager.TrafficStateCallback
 import android.util.Log
+import com.android.settingslib.Utils
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -38,9 +48,9 @@
  */
 interface WifiRepository {
     /**
-     * Observable for the current state of wifi; `null` when there is no active wifi.
+     * Observable for the current wifi network.
      */
-    val wifiModel: Flow<WifiModel?>
+    val wifiNetwork: Flow<WifiNetworkModel>
 
     /**
      * Observable for the current wifi network activity.
@@ -51,14 +61,57 @@
 /** Real implementation of [WifiRepository]. */
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
+@SuppressLint("MissingPermission")
 class WifiRepositoryImpl @Inject constructor(
+        connectivityManager: ConnectivityManager,
         wifiManager: WifiManager?,
         @Main mainExecutor: Executor,
         logger: ConnectivityPipelineLogger,
 ) : WifiRepository {
+    override val wifiNetwork: Flow<WifiNetworkModel> = conflatedCallbackFlow {
+        var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
 
-    // TODO(b/238425913): Actually implement the wifiModel flow.
-    override val wifiModel: Flow<WifiModel?> = flowOf(WifiModel(ssid = "AB"))
+        val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+            override fun onCapabilitiesChanged(
+                network: Network,
+                networkCapabilities: NetworkCapabilities
+            ) {
+                logger.logOnCapabilitiesChanged(network, networkCapabilities)
+
+                val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
+                if (wifiInfo?.isPrimary == true) {
+                    val wifiNetworkModel = wifiInfoToModel(wifiInfo, network.getNetId())
+                    logger.logTransformation(
+                        WIFI_NETWORK_CALLBACK_NAME,
+                        oldValue = currentWifi,
+                        newValue = wifiNetworkModel
+                    )
+                    currentWifi = wifiNetworkModel
+                    trySend(wifiNetworkModel)
+                }
+            }
+
+            override fun onLost(network: Network) {
+                logger.logOnLost(network)
+                val wifi = currentWifi
+                if (wifi is WifiNetworkModel.Active && wifi.networkId == network.getNetId()) {
+                    val newNetworkModel = WifiNetworkModel.Inactive
+                    logger.logTransformation(
+                        WIFI_NETWORK_CALLBACK_NAME,
+                        oldValue = wifi,
+                        newValue = newNetworkModel
+                    )
+                    currentWifi = newNetworkModel
+                    trySend(newNetworkModel)
+                }
+            }
+        }
+
+        trySend(WIFI_NETWORK_DEFAULT)
+        connectivityManager.registerNetworkCallback(WIFI_NETWORK_CALLBACK_REQUEST, callback)
+
+        awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+    }
 
     override val wifiActivity: Flow<WifiActivityModel> =
             if (wifiManager == null) {
@@ -71,8 +124,8 @@
                         trySend(trafficStateToWifiActivityModel(state))
                     }
 
-                    wifiManager.registerTrafficStateCallback(mainExecutor, callback)
                     trySend(ACTIVITY_DEFAULT)
+                    wifiManager.registerTrafficStateCallback(mainExecutor, callback)
 
                     awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
                 }
@@ -80,6 +133,13 @@
 
     companion object {
         val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+        // Start out with no known wifi network.
+        // Note: [WifiStatusTracker] (the old implementation of connectivity logic) does do an
+        // initial fetch to get a starting wifi network. But, it uses a deprecated API
+        // [WifiManager.getConnectionInfo()], and the deprecation doc indicates to just use
+        // [ConnectivityManager.NetworkCallback] results instead. So, for now we'll just rely on the
+        // NetworkCallback inside [wifiNetwork] for our wifi network information.
+        val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive
 
         private fun trafficStateToWifiActivityModel(state: Int): WifiActivityModel {
             return WifiActivityModel(
@@ -90,6 +150,30 @@
             )
         }
 
+        private fun networkCapabilitiesToWifiInfo(
+            networkCapabilities: NetworkCapabilities
+        ): WifiInfo? {
+            return when {
+                networkCapabilities.hasTransport(TRANSPORT_WIFI) ->
+                    networkCapabilities.transportInfo as WifiInfo?
+                networkCapabilities.hasTransport(TRANSPORT_CELLULAR) ->
+                    // Sometimes, cellular networks can act as wifi networks (known as VCN --
+                    // virtual carrier network). So, see if this cellular network has wifi info.
+                    Utils.tryGetWifiInfoForVcn(networkCapabilities)
+                else -> null
+            }
+        }
+
+        private fun wifiInfoToModel(wifiInfo: WifiInfo, networkId: Int): WifiNetworkModel {
+            return WifiNetworkModel.Active(
+                networkId,
+                wifiInfo.ssid,
+                wifiInfo.isPasspointAp,
+                wifiInfo.isOsuAp,
+                wifiInfo.passpointProviderFriendlyName
+            )
+        }
+
         private fun prettyPrintActivity(activity: Int): String {
             return when (activity) {
                 TrafficStateCallback.DATA_ACTIVITY_NONE -> "NONE"
@@ -99,5 +183,15 @@
                 else -> "INVALID"
             }
         }
+
+        private val WIFI_NETWORK_CALLBACK_REQUEST: NetworkRequest =
+            NetworkRequest.Builder()
+                .clearCapabilities()
+                .addCapability(NET_CAPABILITY_NOT_VPN)
+                .addTransportType(TRANSPORT_WIFI)
+                .addTransportType(TRANSPORT_CELLULAR)
+                .build()
+
+        private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index f705399..a9da63b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -18,6 +18,7 @@
 
 import android.net.wifi.WifiManager
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -34,13 +35,15 @@
 class WifiInteractor @Inject constructor(
         repository: WifiRepository,
 ) {
-    private val ssid: Flow<String?> = repository.wifiModel.map { info ->
-        when {
-            info == null -> null
-            info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint ->
-                info.passpointProviderFriendlyName
-            info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid
-            else -> null
+    private val ssid: Flow<String?> = repository.wifiNetwork.map { info ->
+        when (info) {
+            is WifiNetworkModel.Inactive -> null
+            is WifiNetworkModel.Active -> when {
+                info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint ->
+                    info.passpointProviderFriendlyName
+                info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid
+                else -> null
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
new file mode 100644
index 0000000..b06aaf4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.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.wifi.ui.binder
+
+import android.content.res.ColorStateList
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/**
+ * Binds a wifi icon in the status bar to its view-model.
+ *
+ * To use this properly, users should maintain a one-to-one relationship between the [View] and the
+ * view-binding, binding each view only once. It is okay and expected for the same instance of the
+ * view-model to be reused for multiple view/view-binder bindings.
+ */
+@OptIn(InternalCoroutinesApi::class)
+object WifiViewBinder {
+    /** Binds the view to the view-model, continuing to update the former based on the latter. */
+    @JvmStatic
+    fun bind(
+        view: ViewGroup,
+        viewModel: WifiViewModel,
+    ) {
+        val iconView = view.requireViewById<ImageView>(R.id.wifi_signal)
+
+        view.isVisible = true
+        iconView.isVisible = true
+        iconView.setImageDrawable(view.context.getDrawable(WIFI_FULL_ICONS[2]))
+
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.tint.collect { tint ->
+                        iconView.imageTintList = ColorStateList.valueOf(tint)
+                    }
+                }
+            }
+        }
+
+        // TODO(b/238425913): Hook up to [viewModel] to render actual changes to the wifi icon.
+    }
+}
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
new file mode 100644
index 0000000..c14a897
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.wifi.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.BaseStatusBarWifiView
+import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
+
+/**
+ * A new and more modern implementation of [com.android.systemui.statusbar.StatusBarWifiView] that
+ * is updated by [WifiViewBinder].
+ */
+class ModernStatusBarWifiView(
+    context: Context,
+    attrs: AttributeSet?
+) : BaseStatusBarWifiView(context, attrs) {
+
+    private lateinit var slot: String
+
+    override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
+        // TODO(b/238425913)
+    }
+
+    override fun getSlot() = slot
+
+    override fun setStaticDrawableColor(color: Int) {
+        // TODO(b/238425913)
+    }
+
+    override fun setDecorColor(color: Int) {
+        // TODO(b/238425913)
+    }
+
+    override fun setVisibleState(state: Int, animate: Boolean) {
+        // TODO(b/238425913)
+    }
+
+    override fun getVisibleState(): Int {
+        // TODO(b/238425913)
+        return STATE_ICON
+    }
+
+    override fun isIconVisible(): Boolean {
+        // TODO(b/238425913)
+        return true
+    }
+
+    /** Set the slot name for this view. */
+    private fun setSlot(slotName: String) {
+        this.slot = slotName
+    }
+
+    companion object {
+        /**
+         * Inflates a new instance of [ModernStatusBarWifiView], binds it to [viewModel], and
+         * returns it.
+         */
+        @JvmStatic
+        fun constructAndBind(
+            context: Context,
+            slot: String,
+            viewModel: WifiViewModel,
+        ): ModernStatusBarWifiView {
+            return (
+                LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null)
+                    as ModernStatusBarWifiView
+                ).also {
+                    it.setSlot(slot)
+                    WifiViewBinder.bind(it, viewModel)
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index b990eb7..7a26260 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -16,12 +16,15 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
+import android.graphics.Color
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flowOf
 
 /**
@@ -30,9 +33,10 @@
  * TODO(b/238425913): Hook this up to the real status bar wifi view using a view binder.
  */
 class WifiViewModel @Inject constructor(
-        private val constants: WifiConstants,
-        private val logger: ConnectivityPipelineLogger,
-        private val interactor: WifiInteractor,
+    statusBarPipelineFlags: StatusBarPipelineFlags,
+    private val constants: WifiConstants,
+    private val logger: ConnectivityPipelineLogger,
+    private val interactor: WifiInteractor,
 ) {
     val isActivityInVisible: Flow<Boolean>
         get() =
@@ -42,4 +46,11 @@
                 interactor.hasActivityIn
             }
                 .logOutputChange(logger, "activityInVisible")
+
+    /** The tint that should be applied to the icon. */
+    val tint: Flow<Int> = if (!statusBarPipelineFlags.useNewPipelineDebugColoring()) {
+        emptyFlow()
+    } else {
+        flowOf(Color.CYAN)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index dfcdaef..836d571 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -45,7 +45,6 @@
 import android.provider.Settings;
 import android.telephony.TelephonyCallback;
 import android.text.TextUtils;
-import android.util.FeatureFlagUtils;
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -287,10 +286,6 @@
         refreshUsers(UserHandle.USER_NULL);
     }
 
-    private static boolean isEnableGuestModeUxChanges(Context context) {
-        return FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_GUEST_MODE_UX_CHANGES);
-    }
-
     /**
      * Refreshes users from UserManager.
      *
@@ -549,17 +544,9 @@
         }
 
         if (currUserInfo != null && currUserInfo.isGuest()) {
-            if (isEnableGuestModeUxChanges(mContext)) {
-                showExitGuestDialog(currUserId, currUserInfo.isEphemeral(),
-                        record.resolveId(), dialogShower);
-                return;
-            } else {
-                if (currUserInfo.isEphemeral()) {
-                    showExitGuestDialog(currUserId, currUserInfo.isEphemeral(),
-                            record.resolveId(), dialogShower);
-                    return;
-                }
-            }
+            showExitGuestDialog(currUserId, currUserInfo.isEphemeral(),
+                    record.resolveId(), dialogShower);
+            return;
         }
 
         if (dialogShower != null) {
@@ -1056,14 +1043,8 @@
         public String getName(Context context, UserRecord item) {
             if (item.isGuest) {
                 if (item.isCurrent) {
-                    if (isEnableGuestModeUxChanges(context)) {
-                        return context.getString(
-                                com.android.settingslib.R.string.guest_exit_quick_settings_button);
-                    } else {
-                        return context.getString(mController.mGuestUserAutoCreated
-                            ? com.android.settingslib.R.string.guest_reset_guest
-                            : com.android.settingslib.R.string.guest_exit_guest);
-                    }
+                    return context.getString(
+                            com.android.settingslib.R.string.guest_exit_quick_settings_button);
                 } else {
                     if (item.info != null) {
                         return context.getString(com.android.internal.R.string.guest_name);
@@ -1080,13 +1061,8 @@
                                             ? com.android.settingslib.R.string.guest_resetting
                                             : com.android.internal.R.string.guest_name);
                         } else {
-                            if (isEnableGuestModeUxChanges(context)) {
-                                // we always show "guest" as string, instead of "add guest"
-                                return context.getString(com.android.internal.R.string.guest_name);
-                            } else {
-                                return context.getString(
-                                        com.android.settingslib.R.string.guest_new_guest);
-                            }
+                            // we always show "guest" as string, instead of "add guest"
+                            return context.getString(com.android.internal.R.string.guest_name);
                         }
                     }
                 }
@@ -1108,11 +1084,7 @@
         protected static Drawable getIconDrawable(Context context, UserRecord item) {
             int iconRes;
             if (item.isAddUser) {
-                if (isEnableGuestModeUxChanges(context)) {
-                    iconRes = R.drawable.ic_add;
-                } else {
-                    iconRes = R.drawable.ic_account_circle_filled;
-                }
+                iconRes = R.drawable.ic_add;
             } else if (item.isGuest) {
                 iconRes = R.drawable.ic_account_circle;
             } else if (item.isAddSupervisedUser) {
@@ -1289,46 +1261,32 @@
         ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral,
                     int targetId) {
             super(context);
-            if (isEnableGuestModeUxChanges(context)) {
-                if (isGuestEphemeral) {
-                    setTitle(context.getString(
-                                com.android.settingslib.R.string.guest_exit_dialog_title));
-                    setMessage(context.getString(
-                                com.android.settingslib.R.string.guest_exit_dialog_message));
-                    setButton(DialogInterface.BUTTON_NEUTRAL,
-                            context.getString(android.R.string.cancel), this);
-                    setButton(DialogInterface.BUTTON_POSITIVE,
-                            context.getString(
-                                com.android.settingslib.R.string.guest_exit_dialog_button), this);
-                } else {
-                    setTitle(context.getString(
-                                com.android.settingslib
-                                    .R.string.guest_exit_dialog_title_non_ephemeral));
-                    setMessage(context.getString(
-                                com.android.settingslib
-                                    .R.string.guest_exit_dialog_message_non_ephemeral));
-                    setButton(DialogInterface.BUTTON_NEUTRAL,
-                            context.getString(android.R.string.cancel), this);
-                    setButton(DialogInterface.BUTTON_NEGATIVE,
-                            context.getString(
-                                com.android.settingslib.R.string.guest_exit_clear_data_button),
-                            this);
-                    setButton(DialogInterface.BUTTON_POSITIVE,
-                            context.getString(
-                                com.android.settingslib.R.string.guest_exit_save_data_button),
-                            this);
-                }
-            } else {
-                setTitle(mGuestUserAutoCreated
-                        ? com.android.settingslib.R.string.guest_reset_guest_dialog_title
-                        : com.android.settingslib.R.string.guest_remove_guest_dialog_title);
-                setMessage(context.getString(R.string.guest_exit_guest_dialog_message));
+            if (isGuestEphemeral) {
+                setTitle(context.getString(
+                            com.android.settingslib.R.string.guest_exit_dialog_title));
+                setMessage(context.getString(
+                            com.android.settingslib.R.string.guest_exit_dialog_message));
                 setButton(DialogInterface.BUTTON_NEUTRAL,
                         context.getString(android.R.string.cancel), this);
                 setButton(DialogInterface.BUTTON_POSITIVE,
-                        context.getString(mGuestUserAutoCreated
-                            ? com.android.settingslib.R.string.guest_reset_guest_confirm_button
-                            : com.android.settingslib.R.string.guest_remove_guest_confirm_button),
+                        context.getString(
+                            com.android.settingslib.R.string.guest_exit_dialog_button), this);
+            } else {
+                setTitle(context.getString(
+                            com.android.settingslib
+                                .R.string.guest_exit_dialog_title_non_ephemeral));
+                setMessage(context.getString(
+                            com.android.settingslib
+                                .R.string.guest_exit_dialog_message_non_ephemeral));
+                setButton(DialogInterface.BUTTON_NEUTRAL,
+                        context.getString(android.R.string.cancel), this);
+                setButton(DialogInterface.BUTTON_NEGATIVE,
+                        context.getString(
+                            com.android.settingslib.R.string.guest_exit_clear_data_button),
+                        this);
+                setButton(DialogInterface.BUTTON_POSITIVE,
+                        context.getString(
+                            com.android.settingslib.R.string.guest_exit_save_data_button),
                         this);
             }
             SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
@@ -1345,39 +1303,29 @@
             if (mFalsingManager.isFalseTap(penalty)) {
                 return;
             }
-            if (isEnableGuestModeUxChanges(getContext())) {
-                if (mIsGuestEphemeral) {
-                    if (which == DialogInterface.BUTTON_POSITIVE) {
-                        mDialogLaunchAnimator.dismissStack(this);
-                        // Ephemeral guest: exit guest, guest is removed by the system
-                        // on exit, since its marked ephemeral
-                        exitGuestUser(mGuestId, mTargetId, false);
-                    } else if (which == DialogInterface.BUTTON_NEGATIVE) {
-                        // Cancel clicked, do nothing
-                        cancel();
-                    }
-                } else {
-                    if (which == DialogInterface.BUTTON_POSITIVE) {
-                        mDialogLaunchAnimator.dismissStack(this);
-                        // Non-ephemeral guest: exit guest, guest is not removed by the system
-                        // on exit, since its marked non-ephemeral
-                        exitGuestUser(mGuestId, mTargetId, false);
-                    } else if (which == DialogInterface.BUTTON_NEGATIVE) {
-                        mDialogLaunchAnimator.dismissStack(this);
-                        // Non-ephemeral guest: remove guest and then exit
-                        exitGuestUser(mGuestId, mTargetId, true);
-                    } else if (which == DialogInterface.BUTTON_NEUTRAL) {
-                        // Cancel clicked, do nothing
-                        cancel();
-                    }
+            if (mIsGuestEphemeral) {
+                if (which == DialogInterface.BUTTON_POSITIVE) {
+                    mDialogLaunchAnimator.dismissStack(this);
+                    // Ephemeral guest: exit guest, guest is removed by the system
+                    // on exit, since its marked ephemeral
+                    exitGuestUser(mGuestId, mTargetId, false);
+                } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+                    // Cancel clicked, do nothing
+                    cancel();
                 }
             } else {
-                if (which == BUTTON_NEUTRAL) {
-                    cancel();
-                } else {
-                    mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE);
+                if (which == DialogInterface.BUTTON_POSITIVE) {
                     mDialogLaunchAnimator.dismissStack(this);
-                    removeGuestUser(mGuestId, mTargetId);
+                    // Non-ephemeral guest: exit guest, guest is not removed by the system
+                    // on exit, since its marked non-ephemeral
+                    exitGuestUser(mGuestId, mTargetId, false);
+                } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+                    mDialogLaunchAnimator.dismissStack(this);
+                    // Non-ephemeral guest: remove guest and then exit
+                    exitGuestUser(mGuestId, mTargetId, true);
+                } else if (which == DialogInterface.BUTTON_NEUTRAL) {
+                    // Cancel clicked, do nothing
+                    cancel();
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/IpcSerializer.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/IpcSerializer.kt
new file mode 100644
index 0000000..c0331e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/IpcSerializer.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.util.kotlin
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.runBlocking
+
+/**
+ * A utility for handling incoming IPCs from a Binder interface in the order that they are received.
+ *
+ * This class serves as a replacement for the common [android.os.Handler] message-queue pattern,
+ * where IPCs can arrive on arbitrary threads and are all enqueued onto a queue and processed by the
+ * Handler in-order.
+ *
+ *     class MyService : Service() {
+ *
+ *       private val serializer = IpcSerializer()
+ *
+ *       // Need to invoke process() in order to actually process IPCs sent over the serializer.
+ *       override fun onStart(...) = lifecycleScope.launch {
+ *         serializer.process()
+ *       }
+ *
+ *       // In your binder implementation, use runSerializedBlocking to enqueue a function onto
+ *       // the serializer.
+ *       override fun onBind(intent: Intent?) = object : IAidlService.Stub() {
+ *         override fun ipcMethodFoo() = serializer.runSerializedBlocking {
+ *           ...
+ *         }
+ *
+ *         override fun ipcMethodBar() = serializer.runSerializedBlocking {
+ *           ...
+ *         }
+ *       }
+ *     }
+ */
+class IpcSerializer {
+
+    private val channel = Channel<Pair<CompletableDeferred<Unit>, Job>>()
+
+    /**
+     * Runs functions enqueued via usage of [runSerialized] and [runSerializedBlocking] serially.
+     * This method will never complete normally, so it must be launched in its own coroutine; if
+     * this is not actively running, no enqueued functions will be evaluated.
+     */
+    suspend fun process(): Nothing {
+        for ((start, finish) in channel) {
+            // Signal to the sender that serializer has reached this message
+            start.complete(Unit)
+            // Wait to hear from the sender that it has finished running it's work, before handling
+            // the next message
+            finish.join()
+        }
+        error("Unexpected end of serialization channel")
+    }
+
+    /**
+     * Enqueues [block] for evaluation by the serializer, suspending the caller until it has
+     * completed. It is up to the caller to define what thread this is evaluated in, determined
+     * by the [kotlin.coroutines.CoroutineContext] used.
+     */
+    suspend fun <R> runSerialized(block: suspend () -> R): R {
+        val start = CompletableDeferred(Unit)
+        val finish = CompletableDeferred(Unit)
+        // Enqueue our message on the channel.
+        channel.send(start to finish)
+        // Wait for the serializer to reach our message
+        start.await()
+        // Now evaluate the block
+        val result = block()
+        // Notify the serializer that we've completed evaluation
+        finish.complete(Unit)
+        return result
+    }
+
+    /**
+     * Enqueues [block] for evaluation by the serializer, blocking the binder thread until it has
+     * completed. Evaluation occurs on the binder thread, so methods like
+     * [android.os.Binder.getCallingUid] that depend on the current thread will work as expected.
+     */
+    fun <R> runSerializedBlocking(block: suspend () -> R): R = runBlocking { runSerialized(block) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index aeab2df..199048e 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -50,9 +50,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -77,7 +75,6 @@
 import com.android.wm.shell.bubbles.BubbleEntry;
 import com.android.wm.shell.bubbles.Bubbles;
 
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -92,7 +89,7 @@
  * The SysUi side bubbles manager which communicate with other SysUi components.
  */
 @SysUISingleton
-public class BubblesManager implements Dumpable {
+public class BubblesManager {
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubblesManager" : TAG_BUBBLES;
 
@@ -137,7 +134,6 @@
             CommonNotifCollection notifCollection,
             NotifPipeline notifPipeline,
             SysUiState sysUiState,
-            DumpManager dumpManager,
             Executor sysuiMainExecutor) {
         if (bubblesOptional.isPresent()) {
             return new BubblesManager(context,
@@ -156,7 +152,6 @@
                     notifCollection,
                     notifPipeline,
                     sysUiState,
-                    dumpManager,
                     sysuiMainExecutor);
         } else {
             return null;
@@ -180,7 +175,6 @@
             CommonNotifCollection notifCollection,
             NotifPipeline notifPipeline,
             SysUiState sysUiState,
-            DumpManager dumpManager,
             Executor sysuiMainExecutor) {
         mContext = context;
         mBubbles = bubbles;
@@ -203,8 +197,6 @@
 
         setupNotifPipeline();
 
-        dumpManager.registerDumpable(TAG, this);
-
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
             public void onKeyguardShowingChanged() {
@@ -648,11 +640,6 @@
         }
     }
 
-    @Override
-    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
-        mBubbles.dump(pw, args);
-    }
-
     /** Checks whether bubbles are enabled for this user, handles negative userIds. */
     public static boolean areBubblesEnabled(@NonNull Context context, @NonNull UserHandle user) {
         if (user.getIdentifier() < 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index eba2795..a4a59fc 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -29,14 +29,16 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
 
 import android.content.Context;
+import android.content.pm.UserInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.inputmethodservice.InputMethodService;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.view.KeyEvent;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
@@ -47,11 +49,11 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.tracing.ProtoTraceable;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.UserInfoController;
 import com.android.systemui.tracing.ProtoTracer;
 import com.android.systemui.tracing.nano.SystemUiTraceProto;
 import com.android.wm.shell.nano.WmShellTraceProto;
@@ -66,6 +68,7 @@
 
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.Executor;
 
@@ -115,7 +118,7 @@
     private final SysUiState mSysUiState;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final ProtoTracer mProtoTracer;
-    private final UserInfoController mUserInfoController;
+    private final UserTracker mUserTracker;
     private final Executor mSysUiMainExecutor;
 
     // Listeners and callbacks. Note that we prefer member variable over anonymous class here to
@@ -144,9 +147,20 @@
                     mShell.onKeyguardDismissAnimationFinished();
                 }
             };
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
+                    mShell.onUserChanged(newUser, userContext);
+                }
+
+                @Override
+                public void onProfilesChanged(@NonNull List<UserInfo> profiles) {
+                    mShell.onUserProfilesChanged(profiles);
+                }
+            };
 
     private boolean mIsSysUiStateValid;
-    private KeyguardUpdateMonitorCallback mOneHandedKeyguardCallback;
     private WakefulnessLifecycle.Observer mWakefulnessObserver;
 
     @Inject
@@ -163,7 +177,7 @@
             SysUiState sysUiState,
             ProtoTracer protoTracer,
             WakefulnessLifecycle wakefulnessLifecycle,
-            UserInfoController userInfoController,
+            UserTracker userTracker,
             @Main Executor sysUiMainExecutor) {
         super(context);
         mShell = shell;
@@ -178,7 +192,7 @@
         mOneHandedOptional = oneHandedOptional;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mProtoTracer = protoTracer;
-        mUserInfoController = userInfoController;
+        mUserTracker = userTracker;
         mSysUiMainExecutor = sysUiMainExecutor;
     }
 
@@ -192,8 +206,9 @@
         mKeyguardStateController.addCallback(mKeyguardStateCallback);
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
 
-        // TODO: Consider piping config change and other common calls to a shell component to
-        //  delegate internally
+        // Subscribe to user changes
+        mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
+
         mProtoTracer.add(this);
         mCommandQueue.addCallback(this);
         mPipOptional.ifPresent(this::initPip);
@@ -214,10 +229,6 @@
             mIsSysUiStateValid = (sysUiStateFlag & INVALID_SYSUI_STATE_MASK) == 0;
             pip.onSystemUiStateChanged(mIsSysUiStateValid, sysUiStateFlag);
         });
-
-        // The media session listener needs to be re-registered when switching users
-        mUserInfoController.addCallback((String name, Drawable picture, String userAccount) ->
-                pip.registerSessionListenerForCurrentUser());
     }
 
     @VisibleForTesting
@@ -267,15 +278,6 @@
             }
         });
 
-        // TODO: Either move into ShellInterface or register a receiver on the Shell side directly
-        mOneHandedKeyguardCallback = new KeyguardUpdateMonitorCallback() {
-            @Override
-            public void onUserSwitchComplete(int userId) {
-                oneHanded.onUserSwitch(userId);
-            }
-        };
-        mKeyguardUpdateMonitor.registerCallback(mOneHandedKeyguardCallback);
-
         mWakefulnessObserver =
                 new WakefulnessLifecycle.Observer() {
                     @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 01309f8..7f6b79b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -308,6 +308,12 @@
     }
 
     @Test
+    public void testOnViewDetachedRemovesViews() {
+        mController.onViewDetached();
+        verify(mView).removeAllStatusBarItemViews();
+    }
+
+    @Test
     public void testWifiIconHiddenWhenWifiBecomesAvailable() {
         // Make sure wifi starts out unavailable when onViewAttached is called, and then returns
         // true on the second query.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 19491f4..14b85b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -37,6 +37,8 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
+import kotlin.math.max
+import kotlin.math.min
 import kotlin.reflect.KClass
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -127,6 +129,7 @@
         val testConfig =
             TestConfig(
                 isVisible = true,
+                isClickable = true,
                 icon = mock(),
                 canShowWhileLocked = false,
                 intent = Intent("action"),
@@ -154,6 +157,7 @@
         val config =
             TestConfig(
                 isVisible = true,
+                isClickable = true,
                 icon = mock(),
                 canShowWhileLocked = false,
                 intent = null, // This will cause it to tell the system that the click was handled.
@@ -201,6 +205,7 @@
         val testConfig =
             TestConfig(
                 isVisible = true,
+                isClickable = true,
                 icon = mock(),
                 canShowWhileLocked = false,
                 intent = Intent("action"),
@@ -260,6 +265,7 @@
             testConfig =
                 TestConfig(
                     isVisible = true,
+                    isClickable = true,
                     icon = mock(),
                     canShowWhileLocked = true,
                 )
@@ -269,6 +275,7 @@
             testConfig =
                 TestConfig(
                     isVisible = true,
+                    isClickable = true,
                     icon = mock(),
                     canShowWhileLocked = false,
                 )
@@ -342,6 +349,129 @@
         job.cancel()
     }
 
+    @Test
+    fun `isClickable - true when alpha at threshold`() = runBlockingTest {
+        repository.setKeyguardShowing(true)
+        repository.setBottomAreaAlpha(
+            KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD
+        )
+
+        val testConfig =
+            TestConfig(
+                isVisible = true,
+                isClickable = true,
+                icon = mock(),
+                canShowWhileLocked = false,
+                intent = Intent("action"),
+            )
+        val configKey =
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                testConfig = testConfig,
+            )
+
+        var latest: KeyguardQuickAffordanceViewModel? = null
+        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+
+        assertQuickAffordanceViewModel(
+            viewModel = latest,
+            testConfig = testConfig,
+            configKey = configKey,
+        )
+        job.cancel()
+    }
+
+    @Test
+    fun `isClickable - true when alpha above threshold`() = runBlockingTest {
+        repository.setKeyguardShowing(true)
+        var latest: KeyguardQuickAffordanceViewModel? = null
+        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+        repository.setBottomAreaAlpha(
+            min(1f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f),
+        )
+
+        val testConfig =
+            TestConfig(
+                isVisible = true,
+                isClickable = true,
+                icon = mock(),
+                canShowWhileLocked = false,
+                intent = Intent("action"),
+            )
+        val configKey =
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                testConfig = testConfig,
+            )
+
+        assertQuickAffordanceViewModel(
+            viewModel = latest,
+            testConfig = testConfig,
+            configKey = configKey,
+        )
+        job.cancel()
+    }
+
+    @Test
+    fun `isClickable - false when alpha below threshold`() = runBlockingTest {
+        repository.setKeyguardShowing(true)
+        var latest: KeyguardQuickAffordanceViewModel? = null
+        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+        repository.setBottomAreaAlpha(
+            max(0f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f),
+        )
+
+        val testConfig =
+            TestConfig(
+                isVisible = true,
+                isClickable = false,
+                icon = mock(),
+                canShowWhileLocked = false,
+                intent = Intent("action"),
+            )
+        val configKey =
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                testConfig = testConfig,
+            )
+
+        assertQuickAffordanceViewModel(
+            viewModel = latest,
+            testConfig = testConfig,
+            configKey = configKey,
+        )
+        job.cancel()
+    }
+
+    @Test
+    fun `isClickable - false when alpha at zero`() = runBlockingTest {
+        repository.setKeyguardShowing(true)
+        var latest: KeyguardQuickAffordanceViewModel? = null
+        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+        repository.setBottomAreaAlpha(0f)
+
+        val testConfig =
+            TestConfig(
+                isVisible = true,
+                isClickable = false,
+                icon = mock(),
+                canShowWhileLocked = false,
+                intent = Intent("action"),
+            )
+        val configKey =
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                testConfig = testConfig,
+            )
+
+        assertQuickAffordanceViewModel(
+            viewModel = latest,
+            testConfig = testConfig,
+            configKey = configKey,
+        )
+        job.cancel()
+    }
+
     private suspend fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
         repository.setDozeAmount(dozeAmount)
         return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
@@ -384,6 +514,7 @@
     ) {
         checkNotNull(viewModel)
         assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
+        assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable)
         if (testConfig.isVisible) {
             assertThat(viewModel.icon).isEqualTo(testConfig.icon)
             viewModel.onClicked.invoke(
@@ -404,6 +535,7 @@
 
     private data class TestConfig(
         val isVisible: Boolean,
+        val isClickable: Boolean = false,
         val icon: ContainedDrawable? = null,
         val canShowWhileLocked: Boolean = false,
         val intent: Intent? = null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt
new file mode 100644
index 0000000..373af5c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt
@@ -0,0 +1,57 @@
+/*
+ *  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.lifecycle
+
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.executor.TaskExecutor
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Test rule that makes ArchTaskExecutor main thread assertions pass. There is one such assert
+ * in LifecycleRegistry.
+ */
+class InstantTaskExecutorRule : TestWatcher() {
+    // TODO(b/240620122): This is a copy of
+    //  androidx/arch/core/executor/testing/InstantTaskExecutorRule which should be replaced
+    //  with a dependency on the real library once b/ is cleared.
+    override fun starting(description: Description) {
+        super.starting(description)
+        ArchTaskExecutor.getInstance()
+            .setDelegate(
+                object : TaskExecutor() {
+                    override fun executeOnDiskIO(runnable: Runnable) {
+                        runnable.run()
+                    }
+
+                    override fun postToMainThread(runnable: Runnable) {
+                        runnable.run()
+                    }
+
+                    override fun isMainThread(): Boolean {
+                        return true
+                    }
+                }
+            )
+    }
+
+    override fun finished(description: Description) {
+        super.finished(description)
+        ArchTaskExecutor.getInstance().setDelegate(null)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
index 80f3e46..91a6de6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
@@ -20,8 +20,6 @@
 import android.testing.TestableLooper.RunWithLooper
 import android.view.View
 import android.view.ViewTreeObserver
-import androidx.arch.core.executor.ArchTaskExecutor
-import androidx.arch.core.executor.TaskExecutor
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.test.filters.SmallTest
@@ -35,8 +33,6 @@
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
-import org.junit.rules.TestWatcher
-import org.junit.runner.Description
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 import org.mockito.Mock
@@ -282,38 +278,4 @@
             _invocations.add(Invocation(lifecycleOwner))
         }
     }
-
-    /**
-     * Test rule that makes ArchTaskExecutor main thread assertions pass. There is one such assert
-     * in LifecycleRegistry.
-     */
-    class InstantTaskExecutorRule : TestWatcher() {
-        // TODO(b/240620122): This is a copy of
-        //  androidx/arch/core/executor/testing/InstantTaskExecutorRule which should be replaced
-        //  with a dependency on the real library once b/ is cleared.
-        override fun starting(description: Description) {
-            super.starting(description)
-            ArchTaskExecutor.getInstance()
-                .setDelegate(
-                    object : TaskExecutor() {
-                        override fun executeOnDiskIO(runnable: Runnable) {
-                            runnable.run()
-                        }
-
-                        override fun postToMainThread(runnable: Runnable) {
-                            runnable.run()
-                        }
-
-                        override fun isMainThread(): Boolean {
-                            return true
-                        }
-                    }
-                )
-        }
-
-        override fun finished(description: Description) {
-            super.finished(description)
-            ArchTaskExecutor.getInstance().setDelegate(null)
-        }
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 260bb87..22ecb4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -78,7 +78,7 @@
 
         when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
         when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
-        when(mMediaOutputController.isTransferring()).thenReturn(false);
+        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
         when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
         when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
         when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
@@ -208,7 +208,7 @@
 
     @Test
     public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
-        when(mMediaOutputController.isTransferring()).thenReturn(true);
+        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
         when(mMediaDevice1.getState()).thenReturn(
                 LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -224,7 +224,7 @@
 
     @Test
     public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() {
-        when(mMediaOutputController.isTransferring()).thenReturn(true);
+        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
         when(mMediaDevice2.getState()).thenReturn(
                 LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
index be14cc5..cb4f08e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
@@ -93,6 +93,10 @@
     private lateinit var featureFlags: FeatureFlags
     @Mock
     private lateinit var insetsProvider: StatusBarContentInsetsProvider
+    @Mock
+    private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
+    @Mock
+    private lateinit var iconManager: StatusBarIconController.TintedIconManager
 
     private val qsExpansionPathInterpolator = QSExpansionPathInterpolator()
 
@@ -106,6 +110,7 @@
         `when`(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
         `when`(variableDateViewControllerFactory.create(any()))
                 .thenReturn(variableDateViewController)
+        `when`(iconManagerFactory.create(any())).thenReturn(iconManager)
         `when`(view.resources).thenReturn(mContext.resources)
         `when`(view.isAttachedToWindow).thenReturn(true)
         `when`(view.context).thenReturn(context)
@@ -122,7 +127,8 @@
                 featureFlags,
                 variableDateViewControllerFactory,
                 batteryMeterViewController,
-                insetsProvider
+                insetsProvider,
+                iconManagerFactory,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index ed1a13b..20c6d9a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -91,6 +91,10 @@
     @Mock
     private lateinit var statusBarIconController: StatusBarIconController
     @Mock
+    private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
+    @Mock
+    private lateinit var iconManager: StatusBarIconController.TintedIconManager
+    @Mock
     private lateinit var qsCarrierGroupController: QSCarrierGroupController
     @Mock
     private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
@@ -169,6 +173,8 @@
         }
         whenever(view.visibility).thenAnswer { _ -> viewVisibility }
 
+        whenever(iconManagerFactory.create(any())).thenReturn(iconManager)
+
         whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(true)
         whenever(featureFlags.isEnabled(Flags.NEW_HEADER)).thenReturn(true)
 
@@ -178,6 +184,7 @@
         controller = LargeScreenShadeHeaderController(
             view,
             statusBarIconController,
+            iconManagerFactory,
             privacyIconsController,
             insetsProvider,
             configurationController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
index 02b26db..eeb61bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
@@ -43,6 +43,8 @@
     @Mock private lateinit var view: View
     @Mock private lateinit var statusIcons: StatusIconContainer
     @Mock private lateinit var statusBarIconController: StatusBarIconController
+    @Mock private lateinit var iconManagerFactory: StatusBarIconController.TintedIconManager.Factory
+    @Mock private lateinit var iconManager: StatusBarIconController.TintedIconManager
     @Mock private lateinit var qsCarrierGroupController: QSCarrierGroupController
     @Mock private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
     @Mock private lateinit var featureFlags: FeatureFlags
@@ -91,10 +93,12 @@
         whenever(view.visibility).thenAnswer { _ -> viewVisibility }
         whenever(variableDateViewControllerFactory.create(any()))
             .thenReturn(variableDateViewController)
+        whenever(iconManagerFactory.create(any())).thenReturn(iconManager)
         whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(false)
         mLargeScreenShadeHeaderController = LargeScreenShadeHeaderController(
                 view,
                 statusBarIconController,
+                iconManagerFactory,
                 privacyIconsController,
                 insetsProvider,
                 configurationController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt
new file mode 100644
index 0000000..9393a4f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.shared.rotation
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.Display
+import android.view.WindowInsetsController
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@RunWithLooper
+class RotationButtonControllerTest : SysuiTestCase() {
+
+  private lateinit var mController: RotationButtonController
+
+  @Before
+  fun setUp() {
+    mController = RotationButtonController(
+      mContext,
+      /* lightIconColor = */ 0,
+      /* darkIconColor = */ 0,
+      /* iconCcwStart0ResId = */ 0,
+      /* iconCcwStart90ResId = */ 0,
+      /* iconCwStart0ResId = */ 0,
+      /* iconCwStart90ResId = */ 0
+    ) { 0 }
+  }
+
+  @Test
+  fun ifGestural_showRotationSuggestion() {
+    mController.onNavigationBarWindowVisibilityChange( /* showing = */ false)
+    mController.onBehaviorChanged(Display.DEFAULT_DISPLAY,
+                                  WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE)
+    mController.onNavigationModeChanged(WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON)
+    mController.onTaskbarStateChange( /* visible = */ false, /* stashed = */ false)
+    assertThat(mController.canShowRotationButton()).isFalse()
+
+    mController.onNavigationModeChanged(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL)
+
+    assertThat(mController.canShowRotationButton()).isTrue()
+  }
+
+  @Test
+  fun ifTaskbarVisible_showRotationSuggestion() {
+    mController.onNavigationBarWindowVisibilityChange( /* showing = */ false)
+    mController.onBehaviorChanged(Display.DEFAULT_DISPLAY,
+                                    WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE)
+    mController.onNavigationModeChanged(WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON)
+    mController.onTaskbarStateChange( /* visible = */ false, /* stashed = */ false)
+    assertThat(mController.canShowRotationButton()).isFalse()
+
+    mController.onTaskbarStateChange( /* visible = */ true, /* stashed = */ false)
+
+    assertThat(mController.canShowRotationButton()).isTrue()
+  }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index 11e502f..6cf1a12 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -47,7 +47,6 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.battery.BatteryMeterViewController;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -89,7 +88,9 @@
     @Mock
     private StatusBarIconController mStatusBarIconController;
     @Mock
-    private FeatureFlags mFeatureFlags;
+    private StatusBarIconController.TintedIconManager.Factory mIconManagerFactory;
+    @Mock
+    private StatusBarIconController.TintedIconManager mIconManager;
     @Mock
     private BatteryMeterViewController mBatteryMeterViewController;
     @Mock
@@ -129,6 +130,8 @@
 
         MockitoAnnotations.initMocks(this);
 
+        when(mIconManagerFactory.create(any())).thenReturn(mIconManager);
+
         allowTestableLooperAsMainThread();
         TestableLooper.get(this).runWithLooper(() -> {
             mKeyguardStatusBarView =
@@ -148,7 +151,7 @@
                 mBatteryController,
                 mUserInfoController,
                 mStatusBarIconController,
-                new StatusBarIconController.TintedIconManager.Factory(mFeatureFlags),
+                mIconManagerFactory,
                 mBatteryMeterViewController,
                 mNotificationPanelViewStateProvider,
                 mKeyguardStateController,
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 0f1c40b..a6b7e51 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
@@ -40,12 +40,16 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager;
 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.wifi.ui.viewmodel.WifiViewModel;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import javax.inject.Provider;
+
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
 @SmallTest
@@ -67,7 +71,11 @@
     @Test
     public void testSetCalledOnAdd_DarkIconManager() {
         LinearLayout layout = new LinearLayout(mContext);
-        TestDarkIconManager manager = new TestDarkIconManager(layout, mock(FeatureFlags.class));
+        TestDarkIconManager manager = new TestDarkIconManager(
+                layout,
+                mock(FeatureFlags.class),
+                mock(StatusBarPipelineFlags.class),
+                () -> mock(WifiViewModel.class));
         testCallOnAdd_forManager(manager);
     }
 
@@ -104,8 +112,12 @@
     private static class TestDarkIconManager extends DarkIconManager
             implements TestableIconManager {
 
-        TestDarkIconManager(LinearLayout group, FeatureFlags featureFlags) {
-            super(group, featureFlags);
+        TestDarkIconManager(
+                LinearLayout group,
+                FeatureFlags featureFlags,
+                StatusBarPipelineFlags statusBarPipelineFlags,
+                Provider<WifiViewModel> wifiViewModelProvider) {
+            super(group, featureFlags, statusBarPipelineFlags, wifiViewModelProvider);
         }
 
         @Override
@@ -123,7 +135,7 @@
         }
 
         @Override
-        protected StatusBarWifiView addSignalIcon(int index, String slot, WifiIconState state) {
+        protected StatusBarWifiView addWifiIcon(int index, String slot, WifiIconState state) {
             StatusBarWifiView mock = mock(StatusBarWifiView.class);
             mGroup.addView(mock, index);
             return mock;
@@ -140,7 +152,10 @@
 
     private static class TestIconManager extends IconManager implements TestableIconManager {
         TestIconManager(ViewGroup group) {
-            super(group, mock(FeatureFlags.class));
+            super(group,
+                    mock(FeatureFlags.class),
+                    mock(StatusBarPipelineFlags.class),
+                    () -> mock(WifiViewModel.class));
         }
 
         @Override
@@ -158,7 +173,7 @@
         }
 
         @Override
-        protected StatusBarWifiView addSignalIcon(int index, String slot, WifiIconState state) {
+        protected StatusBarWifiView addWifiIcon(int index, String slot, WifiIconState state) {
             StatusBarWifiView mock = mock(StatusBarWifiView.class);
             mGroup.addView(mock, index);
             return mock;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index de43a1f..2b80508 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -37,7 +37,6 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.content.Intent;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -135,8 +134,6 @@
     @Mock
     private PendingIntent mContentIntent;
     @Mock
-    private Intent mContentIntentInner;
-    @Mock
     private OnUserInteractionCallback mOnUserInteractionCallback;
     @Mock
     private Runnable mFutureDismissalRunnable;
@@ -159,7 +156,6 @@
         MockitoAnnotations.initMocks(this);
         when(mContentIntent.isActivity()).thenReturn(true);
         when(mContentIntent.getCreatorUserHandle()).thenReturn(UserHandle.of(1));
-        when(mContentIntent.getIntent()).thenReturn(mContentIntentInner);
 
         NotificationTestHelper notificationTestHelper = new NotificationTestHelper(
                 mContext,
@@ -374,7 +370,6 @@
                 eq(entry.getKey()), any(NotificationVisibility.class));
 
         // The content intent should NOT be sent on click.
-        verify(mContentIntent).getIntent();
         verify(mContentIntent).isActivity();
         verifyNoMoreInteractions(mContentIntent);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 4c8599d..ceaceb4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -111,6 +111,10 @@
     @Mock
     private NotificationPanelViewController mNotificationPanelViewController;
     @Mock
+    private StatusBarIconController.DarkIconManager.Factory mIconManagerFactory;
+    @Mock
+    private StatusBarIconController.DarkIconManager mIconManager;
+    @Mock
     private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock
     private DumpManager mDumpManager;
@@ -424,6 +428,7 @@
         mOperatorNameViewControllerFactory = mock(OperatorNameViewController.Factory.class);
         when(mOperatorNameViewControllerFactory.create(any()))
                 .thenReturn(mOperatorNameViewController);
+        when(mIconManagerFactory.create(any())).thenReturn(mIconManager);
         mSecureSettings = mock(SecureSettings.class);
 
         setUpNotificationIconAreaController();
@@ -436,6 +441,7 @@
                 new PanelExpansionStateManager(),
                 mock(FeatureFlags.class),
                 mStatusBarIconController,
+                mIconManagerFactory,
                 mStatusBarHideIconsForBouncerManager,
                 mKeyguardStateController,
                 mNotificationPanelViewController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt
deleted file mode 100644
index 7b492cb..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline
-
-import android.net.NetworkCapabilities
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.InternalCoroutinesApi
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.yield
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.`when` as whenever
-
-@OptIn(InternalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class ConnectivityInfoProcessorTest : SysuiTestCase() {
-
-    private val statusBarPipelineFlags = mock<StatusBarPipelineFlags>()
-
-    @Before
-    fun setUp() {
-        whenever(statusBarPipelineFlags.isNewPipelineEnabled()).thenReturn(true)
-    }
-
-    @Test
-    fun collectorInfoUpdated_processedInfoAlsoUpdated() = runBlocking {
-        // GIVEN a processor hooked up to a collector
-        val scope = CoroutineScope(Dispatchers.Unconfined)
-        val collector = FakeConnectivityInfoCollector()
-        val processor = ConnectivityInfoProcessor(
-                collector,
-                context,
-                scope,
-                statusBarPipelineFlags,
-                mock(),
-        )
-
-        var mostRecentValue: ProcessedConnectivityInfo? = null
-        val job = launch(start = CoroutineStart.UNDISPATCHED) {
-            processor.processedInfoFlow.collect {
-                mostRecentValue = it
-            }
-        }
-
-        // WHEN the collector emits a value
-        val networkCapabilityInfo = mapOf(
-                10 to NetworkCapabilityInfo(mock(), NetworkCapabilities.Builder().build())
-        )
-        collector.emitValue(RawConnectivityInfo(networkCapabilityInfo))
-        // Because our job uses [CoroutineStart.UNDISPATCHED], it executes in the same thread as
-        // this test. So, our test needs to yield to let the job run.
-        // Note: Once we upgrade our Kotlin coroutines testing library, we won't need this.
-        yield()
-
-        // THEN the processor receives it
-        assertThat(mostRecentValue?.networkCapabilityInfo).isEqualTo(networkCapabilityInfo)
-
-        job.cancel()
-        scope.cancel()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/FakeConnectivityInfoCollector.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/FakeConnectivityInfoCollector.kt
deleted file mode 100644
index 710e5f6..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/FakeConnectivityInfoCollector.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline
-
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-/**
- * A test-friendly implementation of [ConnectivityInfoCollector] that just emits whatever value it
- * receives in [emitValue].
- */
-class FakeConnectivityInfoCollector : ConnectivityInfoCollector {
-    private val _rawConnectivityInfoFlow = MutableStateFlow(RawConnectivityInfo())
-    override val rawConnectivityInfoFlow = _rawConnectivityInfoFlow.asStateFlow()
-
-    suspend fun emitValue(value: RawConnectivityInfo) {
-        _rawConnectivityInfoFlow.emit(value)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index df389bc..6b8d4aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -17,21 +17,22 @@
 package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Fake implementation of [WifiRepository] exposing set methods for all the flows. */
 class FakeWifiRepository : WifiRepository {
-    private val _wifiModel: MutableStateFlow<WifiModel?> = MutableStateFlow(null)
-    override val wifiModel: Flow<WifiModel?> = _wifiModel
+    private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> =
+        MutableStateFlow(WifiNetworkModel.Inactive)
+    override val wifiNetwork: Flow<WifiNetworkModel> = _wifiNetwork
 
     private val _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT)
     override val wifiActivity: Flow<WifiActivityModel> = _wifiActivity
 
-    fun setWifiModel(wifiModel: WifiModel?) {
-        _wifiModel.value = wifiModel
+    fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) {
+        _wifiNetwork.value = wifiNetworkModel
     }
 
     fun setWifiActivity(activity: WifiActivityModel) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt
deleted file mode 100644
index 6edf76c..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.wifi.data.repository
-
-import android.net.ConnectivityManager
-import android.net.ConnectivityManager.NetworkCallback
-import android.net.Network
-import android.net.NetworkCapabilities
-import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
-import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
-import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
-import android.net.NetworkCapabilities.TRANSPORT_WIFI
-import android.net.NetworkRequest
-import android.test.suitebuilder.annotation.SmallTest
-import android.testing.AndroidTestingRunner
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.withArgCaptor
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-// TODO(b/240619365): Update this test to use `runTest` when we update the testing library
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class NetworkCapabilitiesRepoTest : SysuiTestCase() {
-    @Mock private lateinit var connectivityManager: ConnectivityManager
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-    }
-
-    @Test
-    fun testOnCapabilitiesChanged_oneNewNetwork_networkStored() = runBlocking {
-        // GIVEN a repo hooked up to [ConnectivityManager]
-        val scope = CoroutineScope(Dispatchers.Unconfined)
-        val repo = NetworkCapabilitiesRepo(
-            connectivityManager = connectivityManager,
-            scope = scope,
-            logger = logger,
-        )
-
-        val job = launch(start = CoroutineStart.UNDISPATCHED) {
-            repo.dataStream.collect {
-            }
-        }
-
-        val callback: NetworkCallback = withArgCaptor {
-            verify(connectivityManager)
-                .registerNetworkCallback(any(NetworkRequest::class.java), capture())
-        }
-
-        // WHEN a new network is added
-        callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
-
-        val currentMap = repo.dataStream.value
-
-        // THEN it is emitted from the flow
-        assertThat(currentMap[NET_1_ID]?.network).isEqualTo(NET_1)
-        assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_1_CAPS)
-
-        job.cancel()
-        scope.cancel()
-    }
-
-    @Test
-    fun testOnCapabilitiesChanged_twoNewNetworks_bothStored() = runBlocking {
-        // GIVEN a repo hooked up to [ConnectivityManager]
-        val scope = CoroutineScope(Dispatchers.Unconfined)
-        val repo = NetworkCapabilitiesRepo(
-            connectivityManager = connectivityManager,
-            scope = scope,
-            logger = logger,
-        )
-
-        val job = launch(start = CoroutineStart.UNDISPATCHED) {
-            repo.dataStream.collect {
-            }
-        }
-
-        val callback: NetworkCallback = withArgCaptor {
-            verify(connectivityManager)
-                .registerNetworkCallback(any(NetworkRequest::class.java), capture())
-        }
-
-        // WHEN two new networks are added
-        callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
-        callback.onCapabilitiesChanged(NET_2, NET_2_CAPS)
-
-        val currentMap = repo.dataStream.value
-
-        // THEN the current state of the flow reflects 2 networks
-        assertThat(currentMap[NET_1_ID]?.network).isEqualTo(NET_1)
-        assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_1_CAPS)
-        assertThat(currentMap[NET_2_ID]?.network).isEqualTo(NET_2)
-        assertThat(currentMap[NET_2_ID]?.capabilities).isEqualTo(NET_2_CAPS)
-
-        job.cancel()
-        scope.cancel()
-    }
-
-    @Test
-    fun testOnCapabilitesChanged_newCapabilitiesForExistingNetwork_areCaptured() = runBlocking {
-        // GIVEN a repo hooked up to [ConnectivityManager]
-        val scope = CoroutineScope(Dispatchers.Unconfined)
-        val repo = NetworkCapabilitiesRepo(
-            connectivityManager = connectivityManager,
-            scope = scope,
-            logger = logger,
-        )
-
-        val job = launch(start = CoroutineStart.UNDISPATCHED) {
-            repo.dataStream.collect {
-            }
-        }
-
-        val callback: NetworkCallback = withArgCaptor {
-            verify(connectivityManager)
-                .registerNetworkCallback(any(NetworkRequest::class.java), capture())
-        }
-
-        // WHEN a network is added, and then its capabilities are changed
-        callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
-        callback.onCapabilitiesChanged(NET_1, NET_2_CAPS)
-
-        val currentMap = repo.dataStream.value
-
-        // THEN the current state of the flow reflects the new capabilities
-        assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_2_CAPS)
-
-        job.cancel()
-        scope.cancel()
-    }
-
-    @Test
-    fun testOnLost_networkIsRemoved() = runBlocking {
-        // GIVEN a repo hooked up to [ConnectivityManager]
-        val scope = CoroutineScope(Dispatchers.Unconfined)
-        val repo = NetworkCapabilitiesRepo(
-            connectivityManager = connectivityManager,
-            scope = scope,
-            logger = logger,
-        )
-
-        val job = launch(start = CoroutineStart.UNDISPATCHED) {
-            repo.dataStream.collect {
-            }
-        }
-
-        val callback: NetworkCallback = withArgCaptor {
-            verify(connectivityManager)
-                .registerNetworkCallback(any(NetworkRequest::class.java), capture())
-        }
-
-        // WHEN two new networks are added, and one is removed
-        callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
-        callback.onCapabilitiesChanged(NET_2, NET_2_CAPS)
-        callback.onLost(NET_1)
-
-        val currentMap = repo.dataStream.value
-
-        // THEN the current state of the flow reflects only the remaining network
-        assertThat(currentMap[NET_1_ID]).isNull()
-        assertThat(currentMap[NET_2_ID]?.network).isEqualTo(NET_2)
-        assertThat(currentMap[NET_2_ID]?.capabilities).isEqualTo(NET_2_CAPS)
-
-        job.cancel()
-        scope.cancel()
-    }
-
-    @Test
-    fun testOnLost_noNetworks_doesNotCrash() = runBlocking {
-        // GIVEN a repo hooked up to [ConnectivityManager]
-        val scope = CoroutineScope(Dispatchers.Unconfined)
-        val repo = NetworkCapabilitiesRepo(
-            connectivityManager = connectivityManager,
-            scope = scope,
-            logger = logger,
-        )
-
-        val job = launch(start = CoroutineStart.UNDISPATCHED) {
-            repo.dataStream.collect {
-            }
-        }
-
-        val callback: NetworkCallback = withArgCaptor {
-            verify(connectivityManager)
-                .registerNetworkCallback(any(NetworkRequest::class.java), capture())
-        }
-
-        // WHEN no networks are added, and one is removed
-        callback.onLost(NET_1)
-
-        val currentMap = repo.dataStream.value
-
-        // THEN the current state of the flow shows no networks
-        assertThat(currentMap).isEmpty()
-
-        job.cancel()
-        scope.cancel()
-    }
-
-    private val NET_1_ID = 100
-    private val NET_1 = mock<Network>().also {
-        whenever(it.getNetId()).thenReturn(NET_1_ID)
-    }
-    private val NET_2_ID = 200
-    private val NET_2 = mock<Network>().also {
-        whenever(it.getNetId()).thenReturn(NET_2_ID)
-    }
-
-    private val NET_1_CAPS = NetworkCapabilities.Builder()
-        .addTransportType(TRANSPORT_CELLULAR)
-        .addCapability(NET_CAPABILITY_VALIDATED)
-        .build()
-
-    private val NET_2_CAPS = NetworkCapabilities.Builder()
-        .addTransportType(TRANSPORT_WIFI)
-        .addCapability(NET_CAPABILITY_NOT_METERED)
-        .addCapability(NET_CAPABILITY_VALIDATED)
-        .build()
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
index 8b61364..d0a3808 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -16,16 +16,26 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.vcn.VcnTransportInfo
+import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiManager.TrafficStateCallback
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
 import com.android.systemui.util.concurrency.FakeExecutor
 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.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executor
@@ -38,6 +48,7 @@
 import org.junit.Test
 import org.mockito.Mock
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -47,6 +58,7 @@
     private lateinit var underTest: WifiRepositoryImpl
 
     @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var wifiManager: WifiManager
     private lateinit var executor: Executor
 
@@ -54,11 +66,347 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         executor = FakeExecutor(FakeSystemClock())
+
+        underTest = WifiRepositoryImpl(
+            connectivityManager,
+            wifiManager,
+            executor,
+            logger,
+        )
+    }
+
+    @Test
+    fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        assertThat(latest).isEqualTo(WIFI_NETWORK_DEFAULT)
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        val wifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(SSID)
+            whenever(this.isPrimary).thenReturn(true)
+        }
+        val network = mock<Network>().apply {
+            whenever(this.getNetId()).thenReturn(NETWORK_ID)
+        }
+
+        getNetworkCallback().onCapabilitiesChanged(network, createWifiNetworkCapabilities(wifiInfo))
+
+        assertThat(latest is WifiNetworkModel.Active).isTrue()
+        val latestActive = latest as WifiNetworkModel.Active
+        assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+        assertThat(latestActive.ssid).isEqualTo(SSID)
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        val wifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(SSID)
+            whenever(this.isPrimary).thenReturn(false)
+        }
+
+        getNetworkCallback().onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+
+        assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_cellularVcnNetworkAdded_flowHasNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        val capabilities = mock<NetworkCapabilities>().apply {
+            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+            whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+        }
+
+        getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+        assertThat(latest is WifiNetworkModel.Active).isTrue()
+        val latestActive = latest as WifiNetworkModel.Active
+        assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+        assertThat(latestActive.ssid).isEqualTo(SSID)
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_nonPrimaryCellularVcnNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        val wifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(SSID)
+            whenever(this.isPrimary).thenReturn(false)
+        }
+        val capabilities = mock<NetworkCapabilities>().apply {
+            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+            whenever(this.transportInfo).thenReturn(VcnTransportInfo(wifiInfo))
+        }
+
+        getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+        assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_cellularNotVcnNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        val capabilities = mock<NetworkCapabilities>().apply {
+            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+            whenever(this.transportInfo).thenReturn(mock())
+        }
+
+        getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+        assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        // Start with the original network
+        getNetworkCallback()
+            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+
+        // WHEN we update to a new primary network
+        val newNetworkId = 456
+        val newNetwork = mock<Network>().apply {
+            whenever(this.getNetId()).thenReturn(newNetworkId)
+        }
+        val newSsid = "CD"
+        val newWifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(newSsid)
+            whenever(this.isPrimary).thenReturn(true)
+        }
+
+        getNetworkCallback().onCapabilitiesChanged(
+            newNetwork, createWifiNetworkCapabilities(newWifiInfo)
+        )
+
+        // THEN we use the new network
+        assertThat(latest is WifiNetworkModel.Active).isTrue()
+        val latestActive = latest as WifiNetworkModel.Active
+        assertThat(latestActive.networkId).isEqualTo(newNetworkId)
+        assertThat(latestActive.ssid).isEqualTo(newSsid)
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_newNonPrimaryWifiNetwork_flowHasOldNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        // Start with the original network
+        getNetworkCallback()
+            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+
+        // WHEN we notify of a new but non-primary network
+        val newNetworkId = 456
+        val newNetwork = mock<Network>().apply {
+            whenever(this.getNetId()).thenReturn(newNetworkId)
+        }
+        val newSsid = "EF"
+        val newWifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(newSsid)
+            whenever(this.isPrimary).thenReturn(false)
+        }
+
+        getNetworkCallback().onCapabilitiesChanged(
+            newNetwork, createWifiNetworkCapabilities(newWifiInfo)
+        )
+
+        // THEN we still use the original network
+        assertThat(latest is WifiNetworkModel.Active).isTrue()
+        val latestActive = latest as WifiNetworkModel.Active
+        assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+        assertThat(latestActive.ssid).isEqualTo(SSID)
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_newNetworkCapabilities_flowHasNewData() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        val wifiInfo = mock<WifiInfo>().apply {
+        whenever(this.ssid).thenReturn(SSID)
+        whenever(this.isPrimary).thenReturn(true)
+    }
+
+        // Start with the original network
+        getNetworkCallback()
+            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+
+        // WHEN we keep the same network ID but change the SSID
+        val newSsid = "CD"
+        val newWifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(newSsid)
+            whenever(this.isPrimary).thenReturn(true)
+        }
+
+        getNetworkCallback()
+            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(newWifiInfo))
+
+        // THEN we've updated to the new SSID
+        assertThat(latest is WifiNetworkModel.Active).isTrue()
+        val latestActive = latest as WifiNetworkModel.Active
+        assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+        assertThat(latestActive.ssid).isEqualTo(newSsid)
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        // WHEN we receive #onLost without any #onCapabilitiesChanged beforehand
+        getNetworkCallback().onLost(NETWORK)
+
+        // THEN there's no crash and we still have no network
+        assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_currentNetworkLost_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        getNetworkCallback()
+            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+        assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
+
+        // WHEN we lose our current network
+        getNetworkCallback().onLost(NETWORK)
+
+        // THEN we update to no network
+        assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_unknownNetworkLost_flowHasPreviousNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        getNetworkCallback()
+            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+        assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
+
+        // WHEN we lose an unknown network
+        val unknownNetwork = mock<Network>().apply {
+            whenever(this.getNetId()).thenReturn(543)
+        }
+        getNetworkCallback().onLost(unknownNetwork)
+
+        // THEN we still have our previous network
+        assertThat(latest is WifiNetworkModel.Active).isTrue()
+        val latestActive = latest as WifiNetworkModel.Active
+        assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+        assertThat(latestActive.ssid).isEqualTo(SSID)
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_notCurrentNetworkLost_flowHasCurrentNetwork() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        getNetworkCallback()
+            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+        assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(NETWORK_ID)
+
+        // WHEN we update to a new network...
+        val newNetworkId = 89
+        val newNetwork = mock<Network>().apply {
+            whenever(this.getNetId()).thenReturn(newNetworkId)
+        }
+        getNetworkCallback().onCapabilitiesChanged(
+            newNetwork, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)
+        )
+        // ...and lose the old network
+        getNetworkCallback().onLost(NETWORK)
+
+        // THEN we still have the new network
+        assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(newNetworkId)
+
+        job.cancel()
     }
 
     @Test
     fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) {
         underTest = WifiRepositoryImpl(
+                connectivityManager,
                 wifiManager = null,
                 executor,
                 logger,
@@ -77,12 +425,6 @@
 
     @Test
     fun wifiActivity_callbackGivesNone_activityFlowHasNone() = runBlocking(IMMEDIATE) {
-        underTest = WifiRepositoryImpl(
-                wifiManager,
-                executor,
-                logger,
-        )
-
         var latest: WifiActivityModel? = null
         val job = underTest
                 .wifiActivity
@@ -100,12 +442,6 @@
 
     @Test
     fun wifiActivity_callbackGivesIn_activityFlowHasIn() = runBlocking(IMMEDIATE) {
-        underTest = WifiRepositoryImpl(
-                wifiManager,
-                executor,
-                logger,
-        )
-
         var latest: WifiActivityModel? = null
         val job = underTest
                 .wifiActivity
@@ -123,12 +459,6 @@
 
     @Test
     fun wifiActivity_callbackGivesOut_activityFlowHasOut() = runBlocking(IMMEDIATE) {
-        underTest = WifiRepositoryImpl(
-                wifiManager,
-                executor,
-                logger,
-        )
-
         var latest: WifiActivityModel? = null
         val job = underTest
                 .wifiActivity
@@ -146,12 +476,6 @@
 
     @Test
     fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = runBlocking(IMMEDIATE) {
-        underTest = WifiRepositoryImpl(
-                wifiManager,
-                executor,
-                logger,
-        )
-
         var latest: WifiActivityModel? = null
         val job = underTest
                 .wifiActivity
@@ -170,6 +494,30 @@
         verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture())
         return callbackCaptor.value!!
     }
+
+    private fun getNetworkCallback(): ConnectivityManager.NetworkCallback {
+        val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
+        verify(connectivityManager).registerNetworkCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+
+    private fun createWifiNetworkCapabilities(wifiInfo: WifiInfo) =
+        mock<NetworkCapabilities>().apply {
+            whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+            whenever(this.transportInfo).thenReturn(wifiInfo)
+        }
+
+    private companion object {
+        const val NETWORK_ID = 45
+        val NETWORK = mock<Network>().apply {
+            whenever(this.getNetId()).thenReturn(NETWORK_ID)
+        }
+        const val SSID = "AB"
+        val PRIMARY_WIFI_INFO: WifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(SSID)
+            whenever(this.isPrimary).thenReturn(true)
+        }
+    }
 }
 
 private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
index c52f347..5f1b1db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
@@ -47,7 +47,7 @@
 
     @Test
     fun hasActivityIn_noInOrOut_outputsFalse() = runBlocking(IMMEDIATE) {
-        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
         repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = false))
 
         var latest: Boolean? = null
@@ -63,7 +63,7 @@
 
     @Test
     fun hasActivityIn_onlyOut_outputsFalse() = runBlocking(IMMEDIATE) {
-        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
         repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = true))
 
         var latest: Boolean? = null
@@ -79,7 +79,7 @@
 
     @Test
     fun hasActivityIn_onlyIn_outputsTrue() = runBlocking(IMMEDIATE) {
-        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
         repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
 
         var latest: Boolean? = null
@@ -95,7 +95,7 @@
 
     @Test
     fun hasActivityIn_inAndOut_outputsTrue() = runBlocking(IMMEDIATE) {
-        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
         repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
 
         var latest: Boolean? = null
@@ -111,7 +111,7 @@
 
     @Test
     fun hasActivityIn_ssidNull_outputsFalse() = runBlocking(IMMEDIATE) {
-        repository.setWifiModel(WifiModel(ssid = null))
+        repository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = null))
         repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
 
         var latest: Boolean? = null
@@ -127,7 +127,7 @@
 
     @Test
     fun hasActivityIn_multipleChanges_multipleOutputChanges() = runBlocking(IMMEDIATE) {
-        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
 
         var latest: Boolean? = null
         val job = underTest
@@ -158,6 +158,10 @@
 
         job.cancel()
     }
+
+    companion object {
+        val VALID_WIFI_NETWORK_MODEL = WifiNetworkModel.Active(networkId = 1, ssid = "AB")
+    }
 }
 
 private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
new file mode 100644
index 0000000..3c200a5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.ui.view
+
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.lifecycle.InstantTaskExecutorRule
+import com.android.systemui.util.Assert
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+@RunWithLooper
+class ModernStatusBarWifiViewTest : SysuiTestCase() {
+
+    @JvmField @Rule
+    val instantTaskExecutor = InstantTaskExecutorRule()
+
+    @Before
+    fun setUp() {
+        Assert.setTestThread(Thread.currentThread())
+    }
+
+    @Test
+    fun constructAndBind_hasCorrectSlot() {
+        val view = ModernStatusBarWifiView.constructAndBind(
+            context, "slotName", mock()
+        )
+
+        assertThat(view.slot).isEqualTo("slotName")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index e9259b0..c790734 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -18,9 +18,10 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
@@ -43,6 +44,7 @@
 
     private lateinit var underTest: WifiViewModel
 
+    @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
     @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var constants: WifiConstants
     private lateinit var repository: FakeWifiRepository
@@ -55,13 +57,14 @@
         interactor = WifiInteractor(repository)
 
         underTest = WifiViewModel(
-                constants,
-                logger,
-                interactor
+            statusBarPipelineFlags,
+            constants,
+            logger,
+            interactor
         )
 
         // Set up with a valid SSID
-        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = "AB"))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
new file mode 100644
index 0000000..15ba672
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import java.util.concurrent.atomic.AtomicLong
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class IpcSerializerTest : SysuiTestCase() {
+
+    private val serializer = IpcSerializer()
+
+    @Test
+    fun serializeManyIncomingIpcs(): Unit = runBlocking(Dispatchers.Main.immediate) {
+        val processor = launch(start = CoroutineStart.LAZY) { serializer.process() }
+        withContext(Dispatchers.IO) {
+            val lastEvaluatedTime = AtomicLong(System.currentTimeMillis())
+            // First, launch many serialization requests in parallel
+            repeat(100_000) {
+                launch(Dispatchers.Unconfined) {
+                    val enqueuedTime = System.currentTimeMillis()
+                    serializer.runSerialized {
+                        val last = lastEvaluatedTime.getAndSet(enqueuedTime)
+                        assertTrue(
+                            "expected $last less than or equal to $enqueuedTime ",
+                            last <= enqueuedTime,
+                        )
+                    }
+                }
+            }
+            // Then, process them all in the order they came in.
+            processor.start()
+        }
+        // All done, stop processing
+        processor.cancel()
+    }
+
+    @Test(timeout = 5000)
+    fun serializeOnOneThread_doesNotDeadlock() = runBlocking {
+        val job = launch { serializer.process() }
+        repeat(100) {
+            serializer.runSerializedBlocking { }
+        }
+        job.cancel()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 8f2b715..5d63632 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -135,6 +135,7 @@
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.onehanded.OneHandedController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
@@ -225,6 +226,8 @@
     @Mock
     private ShellInit mShellInit;
     @Mock
+    private ShellCommandHandler mShellCommandHandler;
+    @Mock
     private ShellController mShellController;
     @Mock
     private Bubbles.BubbleExpandListener mBubbleExpandListener;
@@ -349,6 +352,7 @@
         mBubbleController = new TestableBubbleController(
                 mContext,
                 mShellInit,
+                mShellCommandHandler,
                 mShellController,
                 mBubbleData,
                 mFloatingContentCoordinator,
@@ -389,7 +393,6 @@
                 mCommonNotifCollection,
                 mNotifPipeline,
                 mSysUiState,
-                mDumpManager,
                 syncExecutor);
         mBubblesManager.addNotifCallback(mNotifCallback);
 
@@ -1395,6 +1398,33 @@
         assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE);
     }
 
+    /**
+     * Test to verify behavior for following situation:
+     * <ul>
+     *     <li>status bar shade state is set to <code>false</code></li>
+     *     <li>there is a bubble pending to be expanded</li>
+     * </ul>
+     * Test that duplicate status bar state updates to <code>false</code> do not clear the
+     * pending bubble to be
+     * expanded.
+     */
+    @Test
+    public void testOnStatusBarStateChanged_statusBarChangeDoesNotClearExpandingBubble() {
+        mBubbleController.updateBubble(mBubbleEntry);
+        mBubbleController.onStatusBarStateChanged(false);
+        // Set the bubble to expand once status bar state changes
+        mBubbleController.expandStackAndSelectBubble(mBubbleEntry);
+        // Check that stack is currently collapsed
+        assertStackCollapsed();
+        // Post status bar state change update with the same value
+        mBubbleController.onStatusBarStateChanged(false);
+        // Stack should remain collapsedb
+        assertStackCollapsed();
+        // Post status bar state change which should trigger bubble to expand
+        mBubbleController.onStatusBarStateChanged(true);
+        assertStackExpanded();
+    }
+
     @Test
     public void testSetShouldAutoExpand_notifiesFlagChanged() {
         mBubbleController.updateBubble(mBubbleEntry);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 880ad187..6357a09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -38,6 +38,7 @@
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.onehanded.OneHandedController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
@@ -51,6 +52,7 @@
     // Let's assume surfaces can be synchronized immediately.
     TestableBubbleController(Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ShellController shellController,
             BubbleData data,
             FloatingContentCoordinator floatingContentCoordinator,
@@ -71,12 +73,12 @@
             Handler shellMainHandler,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
-        super(context, shellInit, shellController, data, Runnable::run, floatingContentCoordinator,
-                dataRepository, statusBarService, windowManager, windowManagerShellWrapper,
-                userManager, launcherApps, bubbleLogger, taskStackListener, shellTaskOrganizer,
-                positioner, displayController, oneHandedOptional, dragAndDropController,
-                shellMainExecutor, shellMainHandler, new SyncExecutor(), taskViewTransitions,
-                syncQueue);
+        super(context, shellInit, shellCommandHandler, shellController, data, Runnable::run,
+                floatingContentCoordinator, dataRepository, statusBarService, windowManager,
+                windowManagerShellWrapper, userManager, launcherApps, bubbleLogger,
+                taskStackListener, shellTaskOrganizer, positioner, displayController,
+                oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
+                new SyncExecutor(), taskViewTransitions, syncQueue);
         setInflateSynchronously(true);
         onInit();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index 9c21366..da33fa6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -28,10 +28,10 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.UserInfoController;
 import com.android.systemui.tracing.ProtoTracer;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.onehanded.OneHanded;
@@ -72,7 +72,7 @@
     @Mock OneHanded mOneHanded;
     @Mock WakefulnessLifecycle mWakefulnessLifecycle;
     @Mock ProtoTracer mProtoTracer;
-    @Mock UserInfoController mUserInfoController;
+    @Mock UserTracker mUserTracker;
     @Mock ShellExecutor mSysUiMainExecutor;
 
     @Before
@@ -83,7 +83,7 @@
                 Optional.of(mSplitScreen), Optional.of(mOneHanded), mCommandQueue,
                 mConfigurationController, mKeyguardStateController, mKeyguardUpdateMonitor,
                 mScreenLifecycle, mSysUiState, mProtoTracer, mWakefulnessLifecycle,
-                mUserInfoController, mSysUiMainExecutor);
+                mUserTracker, mSysUiMainExecutor);
     }
 
     @Test
diff --git a/services/OWNERS b/services/OWNERS
index 67cee55..495c0737 100644
--- a/services/OWNERS
+++ b/services/OWNERS
@@ -1,4 +1,4 @@
-per-file Android.bp = file:platform/build/soong:/OWNERS
+per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
 
 # art-team@ manages the system server profile
 per-file art-profile* = calin@google.com, ngeoffray@google.com, vmarko@google.com
diff --git a/services/api/OWNERS b/services/api/OWNERS
index a609390..e10440c 100644
--- a/services/api/OWNERS
+++ b/services/api/OWNERS
@@ -1,4 +1,4 @@
-per-file Android.bp = file:platform/build/soong:/OWNERS
+per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
 
 # API changes are managed via Prolog rules, not OWNERS
 *
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 0b858cf..eb1d2d7 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -2987,8 +2987,8 @@
                 pw.println("mBarringInfo=" + mBarringInfo.get(i));
                 pw.println("mCarrierNetworkChangeState=" + mCarrierNetworkChangeState[i]);
                 pw.println("mTelephonyDisplayInfo=" + mTelephonyDisplayInfos[i]);
-                pw.println("mIsDataEnabled=" + mIsDataEnabled);
-                pw.println("mDataEnabledReason=" + mDataEnabledReason);
+                pw.println("mIsDataEnabled=" + mIsDataEnabled[i]);
+                pw.println("mDataEnabledReason=" + mDataEnabledReason[i]);
                 pw.println("mAllowedNetworkTypeReason=" + mAllowedNetworkTypeReason[i]);
                 pw.println("mAllowedNetworkTypeValue=" + mAllowedNetworkTypeValue[i]);
                 pw.println("mPhysicalChannelConfigs=" + mPhysicalChannelConfigs.get(i));
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 297e6a2..734f455 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -454,6 +454,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -5501,7 +5502,7 @@
             IIntentSender pendingResult, int matchFlags) {
         enforceCallingPermission(Manifest.permission.GET_INTENT_SENDER_INTENT,
                 "queryIntentComponentsForIntentSender()");
-        Preconditions.checkNotNull(pendingResult);
+        Objects.requireNonNull(pendingResult);
         final PendingIntentRecord res;
         try {
             res = (PendingIntentRecord) pendingResult;
@@ -5513,17 +5514,19 @@
             return null;
         }
         final int userId = res.key.userId;
+        final int uid = res.uid;
+        final String resolvedType = res.key.requestResolvedType;
         switch (res.key.type) {
             case ActivityManager.INTENT_SENDER_ACTIVITY:
-                return new ParceledListSlice<>(mContext.getPackageManager()
-                        .queryIntentActivitiesAsUser(intent, matchFlags, userId));
+                return new ParceledListSlice<>(mPackageManagerInt.queryIntentActivities(
+                        intent, resolvedType, matchFlags, uid, userId));
             case ActivityManager.INTENT_SENDER_SERVICE:
             case ActivityManager.INTENT_SENDER_FOREGROUND_SERVICE:
-                return new ParceledListSlice<>(mContext.getPackageManager()
-                        .queryIntentServicesAsUser(intent, matchFlags, userId));
+                return new ParceledListSlice<>(mPackageManagerInt.queryIntentServices(
+                        intent, matchFlags, uid, userId));
             case ActivityManager.INTENT_SENDER_BROADCAST:
-                return new ParceledListSlice<>(mContext.getPackageManager()
-                        .queryBroadcastReceiversAsUser(intent, matchFlags, userId));
+                return new ParceledListSlice<>(mPackageManagerInt.queryIntentReceivers(
+                        intent, resolvedType, matchFlags, uid, userId, false));
             default: // ActivityManager.INTENT_SENDER_ACTIVITY_RESULT
                 throw new IllegalStateException("Unsupported intent sender type: " + res.key.type);
         }
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index aedbe4e..b7e817e 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -729,8 +729,11 @@
     }
 
     private boolean isDeviceCompatibleWithSpatializationModes(@NonNull AudioDeviceAttributes ada) {
+        // modeForDevice will be neither transaural or binaural for devices that do not support
+        // spatial audio. For instance mono devices like earpiece, speaker safe or sco must
+        // not be included.
         final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(ada.getType(),
-                /*default when type not found*/ SpatializationMode.SPATIALIZER_BINAURAL);
+                /*default when type not found*/ -1);
         if ((modeForDevice == SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported)
                 || (modeForDevice == SpatializationMode.SPATIALIZER_TRANSAURAL
                         && mTransauralSupported)) {
@@ -1538,8 +1541,8 @@
 
         @Override
         public String toString() {
-            return "type:" + mDeviceType + " addr:" + mDeviceAddress + " enabled:" + mEnabled
-                    + " HT:" + mHasHeadTracker + " HTenabled:" + mHeadTrackerEnabled;
+            return "type: " + mDeviceType + " addr: " + mDeviceAddress + " enabled: " + mEnabled
+                    + " HT: " + mHasHeadTracker + " HTenabled: " + mHeadTrackerEnabled;
         }
 
         String toPersistableString() {
diff --git a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
index bdc5711..5e0a180 100644
--- a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
+++ b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
@@ -131,6 +131,17 @@
         }
     }
 
+    // For tests: just do the setting of various local variables without actually doing work
+    @VisibleForTesting
+    protected void initForTests(Context context, NotificationUsageStats usageStats,
+            LruCache peopleCache) {
+        mUserToContextMap = new ArrayMap<>();
+        mBaseContext = context;
+        mUsageStats = usageStats;
+        mPeopleCache = peopleCache;
+        mEnabled = true;
+    }
+
     public RankingReconsideration process(NotificationRecord record) {
         if (!mEnabled) {
             if (VERBOSE) Slog.i(TAG, "disabled");
@@ -179,7 +190,7 @@
             return NONE;
         }
         final PeopleRankingReconsideration prr =
-                validatePeople(context, key, extras, null, affinityOut);
+                validatePeople(context, key, extras, null, affinityOut, null);
         float affinity = affinityOut[0];
 
         if (prr != null) {
@@ -224,15 +235,21 @@
         return context;
     }
 
-    private RankingReconsideration validatePeople(Context context,
+    @VisibleForTesting
+    protected RankingReconsideration validatePeople(Context context,
             final NotificationRecord record) {
         final String key = record.getKey();
         final Bundle extras = record.getNotification().extras;
         final float[] affinityOut = new float[1];
+        ArraySet<String> phoneNumbersOut = new ArraySet<>();
         final PeopleRankingReconsideration rr =
-                validatePeople(context, key, extras, record.getPeopleOverride(), affinityOut);
+                validatePeople(context, key, extras, record.getPeopleOverride(), affinityOut,
+                        phoneNumbersOut);
         final float affinity = affinityOut[0];
         record.setContactAffinity(affinity);
+        if (phoneNumbersOut.size() > 0) {
+            record.mergePhoneNumbers(phoneNumbersOut);
+        }
         if (rr == null) {
             mUsageStats.registerPeopleAffinity(record, affinity > NONE, affinity == STARRED_CONTACT,
                     true /* cached */);
@@ -243,7 +260,7 @@
     }
 
     private PeopleRankingReconsideration validatePeople(Context context, String key, Bundle extras,
-            List<String> peopleOverride, float[] affinityOut) {
+            List<String> peopleOverride, float[] affinityOut, ArraySet<String> phoneNumbersOut) {
         float affinity = NONE;
         if (extras == null) {
             return null;
@@ -270,6 +287,15 @@
                 }
                 if (lookupResult != null) {
                     affinity = Math.max(affinity, lookupResult.getAffinity());
+
+                    // add all phone numbers associated with this lookup result, if they exist
+                    // and if requested
+                    if (phoneNumbersOut != null) {
+                        ArraySet<String> phoneNumbers = lookupResult.getPhoneNumbers();
+                        if (phoneNumbers != null && phoneNumbers.size() > 0) {
+                            phoneNumbersOut.addAll(phoneNumbers);
+                        }
+                    }
                 }
             }
             if (++personIdx == MAX_PEOPLE) {
@@ -289,7 +315,8 @@
         return new PeopleRankingReconsideration(context, key, pendingLookups);
     }
 
-    private String getCacheKey(int userId, String handle) {
+    @VisibleForTesting
+    protected static String getCacheKey(int userId, String handle) {
         return Integer.toString(userId) + ":" + handle;
     }
 
@@ -485,7 +512,8 @@
         }
     }
 
-    private static class LookupResult {
+    @VisibleForTesting
+    protected static class LookupResult {
         private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000;  // 1hr
 
         private final long mExpireMillis;
@@ -574,7 +602,8 @@
             return mPhoneNumbers;
         }
 
-        private boolean isExpired() {
+        @VisibleForTesting
+        protected boolean isExpired() {
             return mExpireMillis < System.currentTimeMillis();
         }
 
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 51b36dd..7159673 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -39,7 +39,6 @@
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
-import android.app.ActivityManagerInternal;
 import android.app.IActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -1417,18 +1416,16 @@
 
     private static void broadcastActionOverlayChanged(@NonNull final Set<String> targetPackages,
             final int userId) {
-        final PackageManagerInternal pmInternal =
-                LocalServices.getService(PackageManagerInternal.class);
-        final ActivityManagerInternal amInternal =
-                LocalServices.getService(ActivityManagerInternal.class);
         CollectionUtils.forEach(targetPackages, target -> {
             final Intent intent = new Intent(ACTION_OVERLAY_CHANGED,
                     Uri.fromParts("package", target, null));
             intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-            final int[] allowList = pmInternal.getVisibilityAllowList(target, userId);
-            amInternal.broadcastIntent(intent, null /* resultTo */, null /* requiredPermissions */,
-                    false /* serialized */, userId, allowList, null /* filterExtrasForReceiver */,
-                    null /* bOptions */);
+            try {
+                ActivityManager.getService().broadcastIntent(null, intent, null, null, 0, null,
+                        null, null, android.app.AppOpsManager.OP_NONE, null, false, false, userId);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "broadcastActionOverlayChanged remote exception", e);
+            }
         });
     }
 
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index b3c7cc0..ab99860 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -113,6 +113,8 @@
             @PackageManagerInternal.PrivateResolveFlags long privateResolveFlags,
             int filterCallingUid, int userId, boolean resolveForStart, boolean allowDynamicSplits);
     @NonNull List<ResolveInfo> queryIntentActivitiesInternal(Intent intent, String resolvedType,
+            long flags, int filterCallingUid, int userId);
+    @NonNull List<ResolveInfo> queryIntentActivitiesInternal(Intent intent, String resolvedType,
             long flags, int userId);
     @NonNull List<ResolveInfo> queryIntentServicesInternal(Intent intent, String resolvedType,
             long flags, int userId, int callingUid, boolean includeInstantApps);
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 7dc648c..7c17778 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -608,6 +608,15 @@
                 resolveForStart, userId, intent);
     }
 
+    @NonNull
+    @Override
+    public final List<ResolveInfo> queryIntentActivitiesInternal(Intent intent, String resolvedType,
+            @PackageManager.ResolveInfoFlagsBits long flags, int filterCallingUid, int userId) {
+        return queryIntentActivitiesInternal(
+                intent, resolvedType, flags, 0 /*privateResolveFlags*/, filterCallingUid,
+                userId, false /*resolveForStart*/, true /*allowDynamicSplits*/);
+    }
+
     public final @NonNull List<ResolveInfo> queryIntentActivitiesInternal(Intent intent,
             String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags, int userId) {
         return queryIntentActivitiesInternal(
diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
index c8db297..e969d93 100644
--- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
+++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
@@ -309,7 +309,8 @@
     public final List<ResolveInfo> queryIntentActivities(
             Intent intent, String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags,
             int filterCallingUid, int userId) {
-        return snapshot().queryIntentActivitiesInternal(intent, resolvedType, flags, userId);
+        return snapshot().queryIntentActivitiesInternal(intent, resolvedType, flags,
+                filterCallingUid, userId);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 7dcfd8b..937a789 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3258,8 +3258,7 @@
 
     @Override
     public void onKeyguardOccludedChangedLw(boolean occluded) {
-        if (mKeyguardDelegate != null && mKeyguardDelegate.isShowing()
-                && !WindowManagerService.sEnableShellTransitions) {
+        if (mKeyguardDelegate != null && mKeyguardDelegate.isShowing()) {
             mPendingKeyguardOccluded = occluded;
             mKeyguardOccludedChanged = true;
         } else {
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index c04b195..4153ea5 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -1655,7 +1655,8 @@
             }
 
             // If the window is completely covered by other windows - ignore.
-            if (unaccountedSpace.quickReject(regionInScreen)) {
+            Region intersectionWindow = mTempRegion1;
+            if (!intersectionWindow.op(unaccountedSpace, regionInScreen, Region.Op.INTERSECT)) {
                 return false;
             }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 7e94743..765e7f0 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3245,7 +3245,7 @@
         rootTask.moveToFront(reason, task);
         // Report top activity change to tracking services and WM
         if (mRootWindowContainer.getTopResumedActivity() == this) {
-            mAtmService.setResumedActivityUncheckLocked(this, reason);
+            mAtmService.setLastResumedActivityUncheckLocked(this, reason);
         }
         return true;
     }
@@ -5887,7 +5887,8 @@
             try {
                 mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                         PauseActivityItem.obtain(finishing, false /* userLeaving */,
-                                configChangeFlags, false /* dontReport */));
+                                configChangeFlags, false /* dontReport */,
+                                false /* autoEnteringPip */));
             } catch (Exception e) {
                 Slog.w(TAG, "Exception thrown sending pause: " + intent.getComponent(), e);
             }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index de3b2a6..4003eeb 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3563,7 +3563,7 @@
                 // Continue the pausing process after entering pip.
                 if (r.isState(PAUSING)) {
                     r.getTask().schedulePauseActivity(r, false /* userLeaving */,
-                            false /* pauseImmediately */, "auto-pip");
+                            false /* pauseImmediately */, true /* autoEnteringPip */, "auto-pip");
                 }
             }
         };
@@ -4624,7 +4624,7 @@
     }
 
     /** Update AMS states when an activity is resumed. */
-    void setResumedActivityUncheckLocked(ActivityRecord r, String reason) {
+    void setLastResumedActivityUncheckLocked(ActivityRecord r, String reason) {
         final Task task = r.getTask();
         if (task.isActivityTypeStandard()) {
             if (mCurAppTimeTracker != r.appTimeTracker) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 20032d6..dc91c15 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2083,7 +2083,7 @@
      * activity releases the top state and reports back, message about acquiring top state will be
      * sent to the new top resumed activity.
      */
-    void updateTopResumedActivityIfNeeded() {
+    void updateTopResumedActivityIfNeeded(String reason) {
         final ActivityRecord prevTopActivity = mTopResumedActivity;
         final Task topRootTask = mRootWindowContainer.getTopDisplayFocusedRootTask();
         if (topRootTask == null || topRootTask.getTopResumedActivity() == prevTopActivity) {
@@ -2119,6 +2119,12 @@
             }
             mService.updateOomAdj();
         }
+        // Update the last resumed activity and focused app when the top resumed activity changed
+        // because the new top resumed activity might be already resumed and thus won't have
+        // activity state change to update the records to AMS.
+        if (mTopResumedActivity != null) {
+            mService.setLastResumedActivityUncheckLocked(mTopResumedActivity, reason);
+        }
         scheduleTopResumedActivityStateIfNeeded();
 
         mService.updateTopApp(mTopResumedActivity);
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 552c6a5..077f8b5 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -512,7 +512,7 @@
     void onChildPositionChanged(WindowContainer child) {
         mWmService.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,
                 !mWmService.mPerDisplayFocusEnabled /* updateInputWindows */);
-        mTaskSupervisor.updateTopResumedActivityIfNeeded();
+        mTaskSupervisor.updateTopResumedActivityIfNeeded("onChildPositionChanged");
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 0332935..384a4d1 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -920,7 +920,7 @@
                 // If the original state is resumed, there is no state change to update focused app.
                 // So here makes sure the activity focus is set if it is the top.
                 if (r.isState(RESUMED) && r == mRootWindowContainer.getTopResumedActivity()) {
-                    mAtmService.setResumedActivityUncheckLocked(r, reason);
+                    mAtmService.setLastResumedActivityUncheckLocked(r, reason);
                 }
             }
             if (!animate) {
@@ -2433,11 +2433,7 @@
         focusableTask.moveToFront(myReason);
         // Top display focused root task is changed, update top resumed activity if needed.
         if (rootTask.getTopResumedActivity() != null) {
-            mTaskSupervisor.updateTopResumedActivityIfNeeded();
-            // Set focused app directly because if the next focused activity is already resumed
-            // (e.g. the next top activity is on a different display), there won't have activity
-            // state change to update it.
-            mAtmService.setResumedActivityUncheckLocked(rootTask.getTopResumedActivity(), reason);
+            mTaskSupervisor.updateTopResumedActivityIfNeeded(reason);
         }
         return rootTask;
     }
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 52bf220..4063cae4 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -323,6 +323,10 @@
             // Clear preferred top because the adding focusable task has a higher z-order.
             mPreferredTopFocusableRootTask = null;
         }
+
+        // Update the top resumed activity because the preferred top focusable task may be changed.
+        mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded("addChildTask");
+
         mAtmService.updateSleepIfNeededLocked();
         onRootTaskOrderChanged(task);
     }
@@ -416,12 +420,7 @@
         }
 
         // Update the top resumed activity because the preferred top focusable task may be changed.
-        mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded();
-
-        final ActivityRecord r = child.getTopResumedActivity();
-        if (r != null && r == mRootWindowContainer.getTopResumedActivity()) {
-            mAtmService.setResumedActivityUncheckLocked(r, "positionChildAt");
-        }
+        mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded("positionChildTaskAt");
 
         if (mChildren.indexOf(child) != oldPosition) {
             onRootTaskOrderChanged(child);
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 44b5b88..1b7c6d2 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -460,7 +460,7 @@
 
         final ActivityRecord prevR = mResumedActivity;
         mResumedActivity = r;
-        mTaskSupervisor.updateTopResumedActivityIfNeeded();
+        mTaskSupervisor.updateTopResumedActivityIfNeeded(reason);
         if (r == null && prevR.mDisplayContent != null
                 && prevR.mDisplayContent.getFocusedRootTask() == null) {
             // Only need to notify DWPC when no activity will resume.
@@ -773,9 +773,6 @@
                 Slog.v(TAG, "set resumed activity to:" + record + " reason:" + reason);
             }
             setResumedActivity(record, reason + " - onActivityStateChanged");
-            if (record == mRootWindowContainer.getTopResumedActivity()) {
-                mAtmService.setResumedActivityUncheckLocked(record, reason);
-            }
             mTaskSupervisor.mRecentTasks.add(record.getTask());
         }
     }
@@ -1621,7 +1618,8 @@
                 ProtoLog.d(WM_DEBUG_STATES, "Auto-PIP allowed, entering PIP mode "
                         + "directly: %s, didAutoPip: %b", prev, didAutoPip);
             } else {
-                schedulePauseActivity(prev, userLeaving, pauseImmediately, reason);
+                schedulePauseActivity(prev, userLeaving, pauseImmediately,
+                        false /* autoEnteringPip */, reason);
             }
         } else {
             mPausingActivity = null;
@@ -1675,7 +1673,7 @@
     }
 
     void schedulePauseActivity(ActivityRecord prev, boolean userLeaving,
-            boolean pauseImmediately, String reason) {
+            boolean pauseImmediately, boolean autoEnteringPip, String reason) {
         ProtoLog.v(WM_DEBUG_STATES, "Enqueueing pending pause: %s", prev);
         try {
             EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev),
@@ -1683,7 +1681,7 @@
 
             mAtmService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
                     prev.token, PauseActivityItem.obtain(prev.finishing, userLeaving,
-                            prev.configChangeFlags, pauseImmediately));
+                            prev.configChangeFlags, pauseImmediately, autoEnteringPip));
         } catch (Exception e) {
             // Ignore exception, if process died other code will cleanup.
             Slog.w(TAG, "Exception thrown during pause", e);
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 88059e1..d615583 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -49,7 +49,9 @@
 import android.window.ITaskFragmentOrganizerController;
 import android.window.TaskFragmentInfo;
 import android.window.TaskFragmentTransaction;
+import android.window.WindowContainerTransaction;
 
+import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.common.ProtoLog;
 
 import java.lang.annotation.Retention;
@@ -68,6 +70,8 @@
 
     private final ActivityTaskManagerService mAtmService;
     private final WindowManagerGlobalLock mGlobalLock;
+    private final WindowOrganizerController mWindowOrganizerController;
+
     /**
      * A Map which manages the relationship between
      * {@link ITaskFragmentOrganizer} and {@link TaskFragmentOrganizerState}
@@ -82,9 +86,11 @@
 
     private final ArraySet<Task> mTmpTaskSet = new ArraySet<>();
 
-    TaskFragmentOrganizerController(ActivityTaskManagerService atm) {
-        mAtmService = atm;
+    TaskFragmentOrganizerController(@NonNull ActivityTaskManagerService atm,
+            @NonNull WindowOrganizerController windowOrganizerController) {
+        mAtmService = requireNonNull(atm);
         mGlobalLock = atm.mGlobalLock;
+        mWindowOrganizerController = requireNonNull(windowOrganizerController);
     }
 
     /**
@@ -131,6 +137,14 @@
         private final SparseArray<RemoteAnimationDefinition> mRemoteAnimationDefinitions =
                 new SparseArray<>();
 
+        /**
+         * List of {@link TaskFragmentTransaction#getTransactionToken()} that have been sent to the
+         * organizer. If the transaction is sent during a transition, the
+         * {@link TransitionController} will wait until the transaction is finished.
+         * @see #onTransactionFinished(IBinder)
+         */
+        private final List<IBinder> mRunningTransactions = new ArrayList<>();
+
         TaskFragmentOrganizerState(ITaskFragmentOrganizer organizer, int pid, int uid) {
             mOrganizer = organizer;
             mOrganizerPid = pid;
@@ -176,6 +190,10 @@
                 taskFragment.removeImmediately();
                 mOrganizedTaskFragments.remove(taskFragment);
             }
+            for (int i = mRunningTransactions.size() - 1; i >= 0; i--) {
+                // Cleanup any running transaction to unblock the current transition.
+                onTransactionFinished(mRunningTransactions.get(i));
+            }
             mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/);
         }
 
@@ -320,6 +338,40 @@
                     .setActivityIntent(activity.intent)
                     .setActivityToken(activityToken);
         }
+
+        void dispatchTransaction(@NonNull TaskFragmentTransaction transaction) {
+            if (transaction.isEmpty()) {
+                return;
+            }
+            try {
+                mOrganizer.onTransactionReady(transaction);
+            } catch (RemoteException e) {
+                Slog.d(TAG, "Exception sending TaskFragmentTransaction", e);
+                return;
+            }
+            onTransactionStarted(transaction.getTransactionToken());
+        }
+
+        /** Called when the transaction is sent to the organizer. */
+        void onTransactionStarted(@NonNull IBinder transactionToken) {
+            if (!mWindowOrganizerController.getTransitionController().isCollecting()) {
+                return;
+            }
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+                    "Defer transition ready for TaskFragmentTransaction=%s", transactionToken);
+            mRunningTransactions.add(transactionToken);
+            mWindowOrganizerController.getTransitionController().deferTransitionReady();
+        }
+
+        /** Called when the transaction is finished. */
+        void onTransactionFinished(@NonNull IBinder transactionToken) {
+            if (!mRunningTransactions.remove(transactionToken)) {
+                return;
+            }
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+                    "Continue transition ready for TaskFragmentTransaction=%s", transactionToken);
+            mWindowOrganizerController.getTransitionController().continueTransitionReady();
+        }
     }
 
     @Nullable
@@ -336,7 +388,7 @@
     }
 
     @Override
-    public void registerOrganizer(ITaskFragmentOrganizer organizer) {
+    public void registerOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
         final int pid = Binder.getCallingPid();
         final int uid = Binder.getCallingUid();
         synchronized (mGlobalLock) {
@@ -354,7 +406,7 @@
     }
 
     @Override
-    public void unregisterOrganizer(ITaskFragmentOrganizer organizer) {
+    public void unregisterOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
         validateAndGetState(organizer);
         final int pid = Binder.getCallingPid();
         final long uid = Binder.getCallingUid();
@@ -372,8 +424,8 @@
     }
 
     @Override
-    public void registerRemoteAnimations(ITaskFragmentOrganizer organizer, int taskId,
-            RemoteAnimationDefinition definition) {
+    public void registerRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer, int taskId,
+            @NonNull RemoteAnimationDefinition definition) {
         final int pid = Binder.getCallingPid();
         final int uid = Binder.getCallingUid();
         synchronized (mGlobalLock) {
@@ -398,7 +450,7 @@
     }
 
     @Override
-    public void unregisterRemoteAnimations(ITaskFragmentOrganizer organizer, int taskId) {
+    public void unregisterRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer, int taskId) {
         final int pid = Binder.getCallingPid();
         final long uid = Binder.getCallingUid();
         synchronized (mGlobalLock) {
@@ -416,6 +468,17 @@
         }
     }
 
+    @Override
+    public void onTransactionHandled(@NonNull ITaskFragmentOrganizer organizer,
+            @NonNull IBinder transactionToken, @NonNull WindowContainerTransaction wct) {
+        synchronized (mGlobalLock) {
+            // Keep the calling identity to avoid unsecure change.
+            mWindowOrganizerController.applyTransaction(wct);
+            final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+            state.onTransactionFinished(transactionToken);
+        }
+    }
+
     /**
      * Gets the {@link RemoteAnimationDefinition} set on the given organizer if exists. Returns
      * {@code null} if it doesn't, or if the organizer has activity(ies) embedded in untrusted mode.
@@ -775,13 +838,13 @@
         }
         final int organizerNum = mPendingTaskFragmentEvents.size();
         for (int i = 0; i < organizerNum; i++) {
-            final ITaskFragmentOrganizer organizer = mTaskFragmentOrganizerState.get(
-                    mPendingTaskFragmentEvents.keyAt(i)).mOrganizer;
-            dispatchPendingEvents(organizer, mPendingTaskFragmentEvents.valueAt(i));
+            final TaskFragmentOrganizerState state =
+                    mTaskFragmentOrganizerState.get(mPendingTaskFragmentEvents.keyAt(i));
+            dispatchPendingEvents(state, mPendingTaskFragmentEvents.valueAt(i));
         }
     }
 
-    void dispatchPendingEvents(@NonNull ITaskFragmentOrganizer organizer,
+    void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
             @NonNull List<PendingTaskFragmentEvent> pendingEvents) {
         if (pendingEvents.isEmpty()) {
             return;
@@ -817,7 +880,7 @@
                 if (mTmpTaskSet.add(task)) {
                     // Make sure the organizer know about the Task config.
                     transaction.addChange(prepareChange(new PendingTaskFragmentEvent.Builder(
-                            PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED, organizer)
+                            PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED, state.mOrganizer)
                             .setTask(task)
                             .build()));
                 }
@@ -825,7 +888,7 @@
             transaction.addChange(prepareChange(event));
         }
         mTmpTaskSet.clear();
-        dispatchTransactionInfo(organizer, transaction);
+        state.dispatchTransaction(transaction);
         pendingEvents.removeAll(candidateEvents);
     }
 
@@ -855,6 +918,7 @@
         }
 
         final ITaskFragmentOrganizer organizer = taskFragment.getTaskFragmentOrganizer();
+        final TaskFragmentOrganizerState state = validateAndGetState(organizer);
         final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
         // Make sure the organizer know about the Task config.
         transaction.addChange(prepareChange(new PendingTaskFragmentEvent.Builder(
@@ -862,22 +926,10 @@
                 .setTask(taskFragment.getTask())
                 .build()));
         transaction.addChange(prepareChange(event));
-        dispatchTransactionInfo(event.mTaskFragmentOrg, transaction);
+        state.dispatchTransaction(transaction);
         mPendingTaskFragmentEvents.get(organizer.asBinder()).remove(event);
     }
 
-    private void dispatchTransactionInfo(@NonNull ITaskFragmentOrganizer organizer,
-            @NonNull TaskFragmentTransaction transaction) {
-        if (transaction.isEmpty()) {
-            return;
-        }
-        try {
-            organizer.onTransactionReady(transaction);
-        } catch (RemoteException e) {
-            Slog.d(TAG, "Exception sending TaskFragmentTransaction", e);
-        }
-    }
-
     @Nullable
     private TaskFragmentTransaction.Change prepareChange(
             @NonNull PendingTaskFragmentEvent event) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 803890b..2d3e437 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1815,6 +1815,8 @@
     /** This undoes one call to {@link #deferTransitionReady}. */
     void continueTransitionReady() {
         --mReadyTracker.mDeferReadyDepth;
+        // Apply ready in case it is waiting for the previous defer call.
+        applyReady();
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 4f03264..68b1d35 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -147,7 +147,7 @@
         mGlobalLock = atm.mGlobalLock;
         mTaskOrganizerController = new TaskOrganizerController(mService);
         mDisplayAreaOrganizerController = new DisplayAreaOrganizerController(mService);
-        mTaskFragmentOrganizerController = new TaskFragmentOrganizerController(atm);
+        mTaskFragmentOrganizerController = new TaskFragmentOrganizerController(atm, this);
     }
 
     void setWindowManager(WindowManagerService wms) {
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index 8567110..9abf107 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -11,7 +11,7 @@
 # BatteryStats
 per-file com_android_server_am_BatteryStatsService.cpp = file:/BATTERY_STATS_OWNERS
 
-per-file Android.bp = file:platform/build/soong:/OWNERS
+per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
 per-file com_android_server_Usb* = file:/services/usb/OWNERS
 per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS
 per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 0018a52..4f67f1d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -5786,29 +5786,8 @@
     @VisibleForTesting
     public void enforceCallerCanRequestDeviceIdAttestation(CallerIdentity caller)
             throws SecurityException {
-        /**
-         *  First check if there's a profile owner because the device could be in COMP mode (where
-         *  there's a device owner and profile owner on the same device).
-         *  If the caller is from the work profile, then it must be the PO or the delegate, and
-         *  it must have the right permission to access device identifiers.
-         */
-        int callerUserId = caller.getUserId();
-        if (hasProfileOwner(callerUserId)) {
-            // Make sure that the caller is the profile owner or delegate.
-            Preconditions.checkCallAuthorization(canInstallCertificates(caller));
-            // Verify that the managed profile is on an organization-owned device (or is affiliated
-            // with the device owner user) and as such the profile owner can access Device IDs.
-            if (isProfileOwnerOfOrganizationOwnedDevice(callerUserId)
-                    || isUserAffiliatedWithDevice(callerUserId)) {
-                return;
-            }
-            throw new SecurityException(
-                    "Profile Owner is not allowed to access Device IDs.");
-        }
-
-        // If not, fall back to the device owner check.
-        Preconditions.checkCallAuthorization(
-                isDefaultDeviceOwner(caller) || isCallerDelegate(caller, DELEGATION_CERT_INSTALL));
+        Preconditions.checkCallAuthorization(hasDeviceIdAccessUnchecked(caller.getPackageName(),
+                caller.getUid()));
     }
 
     @VisibleForTesting
@@ -5856,7 +5835,6 @@
         final boolean deviceIdAttestationRequired = attestationUtilsFlags != null;
         KeyGenParameterSpec keySpec = parcelableKeySpec.getSpec();
         final String alias = keySpec.getKeystoreAlias();
-
         Preconditions.checkStringNotEmpty(alias, "Empty alias provided");
         Preconditions.checkArgument(
                 !deviceIdAttestationRequired || keySpec.getAttestationChallenge() != null,
@@ -9393,26 +9371,33 @@
         if (!hasPermission(permission.READ_PHONE_STATE, pid, uid)) {
             return false;
         }
+        return hasDeviceIdAccessUnchecked(packageName, uid);
+    }
 
-        // Allow access to the device owner or delegate cert installer or profile owner of an
-        // affiliated user
+    /**
+     * Check if caller is device owner, delegate cert installer or profile owner of
+     * affiliated user. Or if caller is profile owner for a specified user or delegate cert
+     * installer on an organization-owned device.
+     */
+    private boolean hasDeviceIdAccessUnchecked(String packageName, int uid) {
+        // Is the caller a  device owner, delegate cert installer or profile owner of an
+        // affiliated user.
         ComponentName deviceOwner = getDeviceOwnerComponent(true);
         if (deviceOwner != null && (deviceOwner.getPackageName().equals(packageName)
                 || isCallerDelegate(packageName, uid, DELEGATION_CERT_INSTALL))) {
             return true;
         }
         final int userId = UserHandle.getUserId(uid);
-        // Allow access to the profile owner for the specified user, or delegate cert installer
-        // But only if this is an organization-owned device.
+        // Is the caller the profile owner for the specified user, or delegate cert installer on an
+        // organization-owned device.
         ComponentName profileOwner = getProfileOwnerAsUser(userId);
         final boolean isCallerProfileOwnerOrDelegate = profileOwner != null
                 && (profileOwner.getPackageName().equals(packageName)
-                        || isCallerDelegate(packageName, uid, DELEGATION_CERT_INSTALL));
+                || isCallerDelegate(packageName, uid, DELEGATION_CERT_INSTALL));
         if (isCallerProfileOwnerOrDelegate && (isProfileOwnerOfOrganizationOwnedDevice(userId)
                 || isUserAffiliatedWithDevice(userId))) {
             return true;
         }
-
         return false;
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
index c12f0a9..d72cfc7 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
@@ -17,7 +17,9 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.contains;
 import static org.mockito.ArgumentMatchers.eq;
@@ -30,6 +32,7 @@
 import static org.mockito.Mockito.when;
 
 import android.app.Notification;
+import android.app.NotificationChannel;
 import android.app.Person;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
@@ -39,8 +42,11 @@
 import android.os.Bundle;
 import android.os.UserManager;
 import android.provider.ContactsContract;
+import android.service.notification.StatusBarNotification;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.text.SpannableString;
+import android.util.ArraySet;
+import android.util.LruCache;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -323,6 +329,69 @@
                 isNull());  // sort order
     }
 
+    @Test
+    public void testValidatePeople_needsLookupWhenNoCache() {
+        final Context mockContext = mock(Context.class);
+        final ContentResolver mockContentResolver = mock(ContentResolver.class);
+        when(mockContext.getContentResolver()).thenReturn(mockContentResolver);
+        final NotificationUsageStats mockNotificationUsageStats =
+                mock(NotificationUsageStats.class);
+
+        // Create validator with empty cache
+        ValidateNotificationPeople vnp = new ValidateNotificationPeople();
+        LruCache cache = new LruCache<String, ValidateNotificationPeople.LookupResult>(5);
+        vnp.initForTests(mockContext, mockNotificationUsageStats, cache);
+
+        NotificationRecord record = getNotificationRecord();
+        String[] callNumber = new String[]{"tel:12345678910"};
+        setNotificationPeople(record, callNumber);
+
+        // Returned ranking reconsideration not null indicates that there is a lookup to be done
+        RankingReconsideration rr = vnp.validatePeople(mockContext, record);
+        assertNotNull(rr);
+    }
+
+    @Test
+    public void testValidatePeople_noLookupWhenCached_andPopulatesContactInfo() {
+        final Context mockContext = mock(Context.class);
+        final ContentResolver mockContentResolver = mock(ContentResolver.class);
+        when(mockContext.getContentResolver()).thenReturn(mockContentResolver);
+        when(mockContext.getUserId()).thenReturn(1);
+        final NotificationUsageStats mockNotificationUsageStats =
+                mock(NotificationUsageStats.class);
+
+        // Information to be passed in & returned from the lookup result
+        String lookup = "lookup:contactinfohere";
+        String lookupTel = "16175551234";
+        float affinity = 0.7f;
+
+        // Create a fake LookupResult for the data we'll pass in
+        LruCache cache = new LruCache<String, ValidateNotificationPeople.LookupResult>(5);
+        ValidateNotificationPeople.LookupResult lr =
+                mock(ValidateNotificationPeople.LookupResult.class);
+        when(lr.getAffinity()).thenReturn(affinity);
+        when(lr.getPhoneNumbers()).thenReturn(new ArraySet<>(new String[]{lookupTel}));
+        when(lr.isExpired()).thenReturn(false);
+        cache.put(ValidateNotificationPeople.getCacheKey(1, lookup), lr);
+
+        // Create validator with the established cache
+        ValidateNotificationPeople vnp = new ValidateNotificationPeople();
+        vnp.initForTests(mockContext, mockNotificationUsageStats, cache);
+
+        NotificationRecord record = getNotificationRecord();
+        String[] peopleInfo = new String[]{lookup};
+        setNotificationPeople(record, peopleInfo);
+
+        // Returned ranking reconsideration null indicates that there is no pending work to be done
+        RankingReconsideration rr = vnp.validatePeople(mockContext, record);
+        assertNull(rr);
+
+        // Confirm that the affinity & phone number made it into our record
+        assertEquals(affinity, record.getContactAffinity(), 1e-8);
+        assertNotNull(record.getPhoneNumbers());
+        assertTrue(record.getPhoneNumbers().contains(lookupTel));
+    }
+
     // Creates a cursor that points to one item of Contacts data with the specified
     // columns.
     private Cursor makeMockCursor(int id, String lookupKey, int starred, int hasPhone) {
@@ -365,4 +434,17 @@
         String resultString = Arrays.toString(result);
         assertEquals(message + ": arrays differ", expectedString, resultString);
     }
+
+    private NotificationRecord getNotificationRecord() {
+        StatusBarNotification sbn = mock(StatusBarNotification.class);
+        Notification notification = mock(Notification.class);
+        when(sbn.getNotification()).thenReturn(notification);
+        return new NotificationRecord(mContext, sbn, mock(NotificationChannel.class));
+    }
+
+    private void setNotificationPeople(NotificationRecord r, String[] people) {
+        Bundle extras = new Bundle();
+        extras.putObject(Notification.EXTRA_PEOPLE_LIST, people);
+        r.getSbn().getNotification().extras = extras;
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index 75ecfd8..d5e336b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -228,7 +228,7 @@
                 mAtm.getTaskChangeNotificationController();
         spyOn(taskChangeNotifier);
 
-        mAtm.setResumedActivityUncheckLocked(fullScreenActivityA, "resumeA");
+        mAtm.setLastResumedActivityUncheckLocked(fullScreenActivityA, "resumeA");
         verify(taskChangeNotifier).notifyTaskFocusChanged(eq(taskA.mTaskId) /* taskId */,
                 eq(true) /* focused */);
         reset(taskChangeNotifier);
@@ -237,7 +237,7 @@
                 .build();
         final Task taskB = fullScreenActivityB.getTask();
 
-        mAtm.setResumedActivityUncheckLocked(fullScreenActivityB, "resumeB");
+        mAtm.setLastResumedActivityUncheckLocked(fullScreenActivityB, "resumeB");
         verify(taskChangeNotifier).notifyTaskFocusChanged(eq(taskA.mTaskId) /* taskId */,
                 eq(false) /* focused */);
         verify(taskChangeNotifier).notifyTaskFocusChanged(eq(taskB.mTaskId) /* taskId */,
@@ -295,6 +295,7 @@
         activity1.moveFocusableActivityToTop("test");
         assertEquals(activity1.getUid(), pendingTopUid[0]);
         verify(mAtm).updateOomAdj();
+        verify(mAtm).setLastResumedActivityUncheckLocked(any(), eq("test"));
     }
 
     /**
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 da72030..9274eb3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -25,6 +25,7 @@
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
@@ -46,7 +47,6 @@
 import static org.mockito.Mockito.clearInvocations;
 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;
 
@@ -65,6 +65,7 @@
 import android.window.TaskFragmentInfo;
 import android.window.TaskFragmentOrganizer;
 import android.window.TaskFragmentOrganizerToken;
+import android.window.TaskFragmentTransaction;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 import android.window.WindowContainerTransactionCallback;
@@ -90,6 +91,7 @@
 
     private TaskFragmentOrganizerController mController;
     private WindowOrganizerController mWindowOrganizerController;
+    private TransitionController mTransitionController;
     private TaskFragmentOrganizer mOrganizer;
     private TaskFragmentOrganizerToken mOrganizerToken;
     private ITaskFragmentOrganizer mIOrganizer;
@@ -107,9 +109,10 @@
     private Task mTask;
 
     @Before
-    public void setup() {
+    public void setup() throws RemoteException {
         MockitoAnnotations.initMocks(this);
         mWindowOrganizerController = mAtm.mWindowOrganizerController;
+        mTransitionController = mWindowOrganizerController.mTransitionController;
         mController = mWindowOrganizerController.mTaskFragmentOrganizerController;
         mOrganizer = new TaskFragmentOrganizer(Runnable::run);
         mOrganizerToken = mOrganizer.getOrganizerToken();
@@ -128,11 +131,16 @@
         spyOn(mController);
         spyOn(mOrganizer);
         spyOn(mTaskFragment);
+        spyOn(mWindowOrganizerController);
+        spyOn(mTransitionController);
         doReturn(mIOrganizer).when(mTaskFragment).getTaskFragmentOrganizer();
         doReturn(mTaskFragmentInfo).when(mTaskFragment).getTaskFragmentInfo();
         doReturn(new SurfaceControl()).when(mTaskFragment).getSurfaceControl();
         doReturn(mFragmentToken).when(mTaskFragment).getFragmentToken();
         doReturn(new Configuration()).when(mTaskFragmentInfo).getConfiguration();
+
+        // To prevent it from calling the real server.
+        doNothing().when(mOrganizer).onTransactionHandled(any(), any());
     }
 
     @Test
@@ -866,7 +874,7 @@
         assertFalse(parentTask.shouldBeVisible(null));
 
         // Verify the info changed callback still occurred despite the task being invisible
-        reset(mOrganizer);
+        clearInvocations(mOrganizer);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
@@ -899,7 +907,7 @@
         verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
 
         // Verify the info changed callback is not called when the task is invisible
-        reset(mOrganizer);
+        clearInvocations(mOrganizer);
         doReturn(false).when(task).shouldBeVisible(any());
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
@@ -1092,6 +1100,40 @@
                 .that(mTaskFragment.getBounds()).isEqualTo(task.getBounds());
     }
 
+    @Test
+    public void testOnTransactionReady_invokeOnTransactionHandled() {
+        mController.registerOrganizer(mIOrganizer);
+        final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
+        mOrganizer.onTransactionReady(transaction);
+
+        // Organizer should always trigger #onTransactionHandled when receives #onTransactionReady
+        verify(mOrganizer).onTransactionHandled(eq(transaction.getTransactionToken()), any());
+        verify(mOrganizer, never()).applyTransaction(any());
+    }
+
+    @Test
+    public void testDispatchTransaction_deferTransitionReady() {
+        mController.registerOrganizer(mIOrganizer);
+        setupMockParent(mTaskFragment, mTask);
+        final ArgumentCaptor<IBinder> tokenCaptor = ArgumentCaptor.forClass(IBinder.class);
+        final ArgumentCaptor<WindowContainerTransaction> wctCaptor =
+                ArgumentCaptor.forClass(WindowContainerTransaction.class);
+        doReturn(true).when(mTransitionController).isCollecting();
+
+        mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
+        mController.dispatchPendingEvents();
+
+        // Defer transition when send TaskFragment transaction during transition collection.
+        verify(mTransitionController).deferTransitionReady();
+        verify(mOrganizer).onTransactionHandled(tokenCaptor.capture(), wctCaptor.capture());
+
+        mController.onTransactionHandled(mIOrganizer, tokenCaptor.getValue(), wctCaptor.getValue());
+
+        // Apply the organizer change and continue transition.
+        verify(mWindowOrganizerController).applyTransaction(wctCaptor.getValue());
+        verify(mTransitionController).continueTransitionReady();
+    }
+
     /**
      * Creates a {@link TaskFragment} with the {@link WindowContainerTransaction}. Calls
      * {@link WindowOrganizerController#applyTransaction} to apply the transaction,