Merge "Improve app name handling" into tm-qpr-dev
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index da5b88b..19de1a5 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3349,12 +3349,17 @@
     ctor public TaskFragmentOrganizer(@NonNull java.util.concurrent.Executor);
     method @NonNull public java.util.concurrent.Executor getExecutor();
     method @NonNull public android.window.TaskFragmentOrganizerToken getOrganizerToken();
-    method public void onTaskFragmentAppeared(@NonNull android.window.TaskFragmentInfo);
+    method public void onActivityReparentedToTask(@NonNull android.window.WindowContainerTransaction, int, @NonNull android.content.Intent, @NonNull android.os.IBinder);
+    method @Deprecated public void onTaskFragmentAppeared(@NonNull android.window.TaskFragmentInfo);
+    method public void onTaskFragmentAppeared(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.TaskFragmentInfo);
     method @Deprecated public void onTaskFragmentError(@NonNull android.os.IBinder, @NonNull Throwable);
-    method public void onTaskFragmentError(@NonNull android.os.IBinder, @Nullable android.window.TaskFragmentInfo, int, @NonNull Throwable);
-    method public void onTaskFragmentInfoChanged(@NonNull android.window.TaskFragmentInfo);
-    method public void onTaskFragmentParentInfoChanged(@NonNull android.os.IBinder, @NonNull android.content.res.Configuration);
-    method public void onTaskFragmentVanished(@NonNull android.window.TaskFragmentInfo);
+    method public void onTaskFragmentError(@NonNull android.window.WindowContainerTransaction, @NonNull android.os.IBinder, @Nullable android.window.TaskFragmentInfo, int, @NonNull Throwable);
+    method @Deprecated public void onTaskFragmentInfoChanged(@NonNull android.window.TaskFragmentInfo);
+    method public void onTaskFragmentInfoChanged(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.TaskFragmentInfo);
+    method @Deprecated public void onTaskFragmentParentInfoChanged(@NonNull android.os.IBinder, @NonNull android.content.res.Configuration);
+    method public void onTaskFragmentParentInfoChanged(@NonNull android.window.WindowContainerTransaction, int, @NonNull android.content.res.Configuration);
+    method @Deprecated public void onTaskFragmentVanished(@NonNull android.window.TaskFragmentInfo);
+    method public void onTaskFragmentVanished(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.TaskFragmentInfo);
     method @CallSuper public void registerOrganizer();
     method @CallSuper public void unregisterOrganizer();
   }
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index c9cc1a1..05c9fca 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -124,8 +124,13 @@
     /**
      * The maximum length for text fields in a NotificationChannel. Fields will be truncated at this
      * limit.
+     * @hide
      */
-    private static final int MAX_TEXT_LENGTH = 1000;
+    public static final int MAX_TEXT_LENGTH = 1000;
+    /**
+     * @hide
+     */
+    public static final int MAX_VIBRATION_LENGTH = 1000;
 
     private static final String TAG_CHANNEL = "channel";
     private static final String ATT_NAME = "name";
@@ -283,17 +288,17 @@
      */
     protected NotificationChannel(Parcel in) {
         if (in.readByte() != 0) {
-            mId = in.readString();
+            mId = getTrimmedString(in.readString());
         } else {
             mId = null;
         }
         if (in.readByte() != 0) {
-            mName = in.readString();
+            mName = getTrimmedString(in.readString());
         } else {
             mName = null;
         }
         if (in.readByte() != 0) {
-            mDesc = in.readString();
+            mDesc = getTrimmedString(in.readString());
         } else {
             mDesc = null;
         }
@@ -302,18 +307,22 @@
         mLockscreenVisibility = in.readInt();
         if (in.readByte() != 0) {
             mSound = Uri.CREATOR.createFromParcel(in);
+            mSound = Uri.parse(getTrimmedString(mSound.toString()));
         } else {
             mSound = null;
         }
         mLights = in.readByte() != 0;
         mVibration = in.createLongArray();
+        if (mVibration != null && mVibration.length > MAX_VIBRATION_LENGTH) {
+            mVibration = Arrays.copyOf(mVibration, MAX_VIBRATION_LENGTH);
+        }
         mUserLockedFields = in.readInt();
         mFgServiceShown = in.readByte() != 0;
         mVibrationEnabled = in.readByte() != 0;
         mShowBadge = in.readByte() != 0;
         mDeleted = in.readByte() != 0;
         if (in.readByte() != 0) {
-            mGroup = in.readString();
+            mGroup = getTrimmedString(in.readString());
         } else {
             mGroup = null;
         }
@@ -322,8 +331,8 @@
         mBlockableSystem = in.readBoolean();
         mAllowBubbles = in.readInt();
         mOriginalImportance = in.readInt();
-        mParentId = in.readString();
-        mConversationId = in.readString();
+        mParentId = getTrimmedString(in.readString());
+        mConversationId = getTrimmedString(in.readString());
         mDemoted = in.readBoolean();
         mImportantConvo = in.readBoolean();
         mDeletedTime = in.readLong();
diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java
index f97415c..807bd57 100644
--- a/core/java/android/app/NotificationChannelGroup.java
+++ b/core/java/android/app/NotificationChannelGroup.java
@@ -20,6 +20,7 @@
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Intent;
+import android.content.pm.ParceledListSlice;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
@@ -43,8 +44,9 @@
     /**
      * The maximum length for text fields in a NotificationChannelGroup. Fields will be truncated at
      * this limit.
+     * @hide
      */
-    private static final int MAX_TEXT_LENGTH = 1000;
+    public static final int MAX_TEXT_LENGTH = 1000;
 
     private static final String TAG_GROUP = "channelGroup";
     private static final String ATT_NAME = "name";
@@ -66,7 +68,7 @@
     private CharSequence mName;
     private String mDescription;
     private boolean mBlocked;
-    private List<NotificationChannel> mChannels = new ArrayList<>();
+    private ParceledListSlice<NotificationChannel> mChannels;
     // Bitwise representation of fields that have been changed by the user
     private int mUserLockedFields;
 
@@ -90,17 +92,19 @@
      */
     protected NotificationChannelGroup(Parcel in) {
         if (in.readByte() != 0) {
-            mId = in.readString();
+            mId = getTrimmedString(in.readString());
         } else {
             mId = null;
         }
         mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+        mName = getTrimmedString(mName.toString());
         if (in.readByte() != 0) {
-            mDescription = in.readString();
+            mDescription = getTrimmedString(in.readString());
         } else {
             mDescription = null;
         }
-        in.readParcelableList(mChannels, NotificationChannel.class.getClassLoader(), android.app.NotificationChannel.class);
+        mChannels = in.readParcelable(
+                NotificationChannelGroup.class.getClassLoader(), ParceledListSlice.class);
         mBlocked = in.readBoolean();
         mUserLockedFields = in.readInt();
     }
@@ -120,14 +124,14 @@
         } else {
             dest.writeByte((byte) 0);
         }
-        TextUtils.writeToParcel(mName, dest, flags);
+        TextUtils.writeToParcel(mName.toString(), dest, flags);
         if (mDescription != null) {
             dest.writeByte((byte) 1);
             dest.writeString(mDescription);
         } else {
             dest.writeByte((byte) 0);
         }
-        dest.writeParcelableList(mChannels, flags);
+        dest.writeParcelable(mChannels, flags);
         dest.writeBoolean(mBlocked);
         dest.writeInt(mUserLockedFields);
     }
@@ -157,7 +161,7 @@
      * Returns the list of channels that belong to this group
      */
     public List<NotificationChannel> getChannels() {
-        return mChannels;
+        return mChannels == null ? new ArrayList<>() : mChannels.getList();
     }
 
     /**
@@ -191,15 +195,8 @@
     /**
      * @hide
      */
-    public void addChannel(NotificationChannel channel) {
-        mChannels.add(channel);
-    }
-
-    /**
-     * @hide
-     */
     public void setChannels(List<NotificationChannel> channels) {
-        mChannels = channels;
+        mChannels = new ParceledListSlice<>(channels);
     }
 
     /**
@@ -334,7 +331,7 @@
         proto.write(NotificationChannelGroupProto.NAME, mName.toString());
         proto.write(NotificationChannelGroupProto.DESCRIPTION, mDescription);
         proto.write(NotificationChannelGroupProto.IS_BLOCKED, mBlocked);
-        for (NotificationChannel channel : mChannels) {
+        for (NotificationChannel channel : mChannels.getList()) {
             channel.dumpDebug(proto, NotificationChannelGroupProto.CHANNELS);
         }
         proto.end(token);
diff --git a/core/java/android/attention/AttentionManagerInternal.java b/core/java/android/attention/AttentionManagerInternal.java
index 47bec61..24fe0db 100644
--- a/core/java/android/attention/AttentionManagerInternal.java
+++ b/core/java/android/attention/AttentionManagerInternal.java
@@ -83,11 +83,11 @@
     }
 
     /** Internal interface for proximity callback. */
-    public abstract static class ProximityUpdateCallbackInternal {
+    public interface ProximityUpdateCallbackInternal {
         /**
          * @param distance the estimated distance of the user (in meter)
          * The distance will be PROXIMITY_UNKNOWN if the proximity sensing was inconclusive.
          */
-        public abstract void onProximityUpdate(double distance);
+        void onProximityUpdate(double distance);
     }
 }
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index a9d665c8..621eab5 100644
--- a/core/java/android/hardware/soundtrigger/SoundTrigger.java
+++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java
@@ -1963,7 +1963,6 @@
     }
 
     private static Object mServiceLock = new Object();
-    private static ISoundTriggerMiddlewareService mService;
 
     /**
      * Translate an exception thrown from interaction with the underlying service to an error code.
@@ -2217,20 +2216,12 @@
                     binder =
                             ServiceManager.getServiceOrThrow(
                                     Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE);
-                    binder.linkToDeath(() -> {
-                        synchronized (mServiceLock) {
-                            mService = null;
-                        }
-                    }, 0);
-                    mService = ISoundTriggerMiddlewareService.Stub.asInterface(binder);
-                    break;
+                    return ISoundTriggerMiddlewareService.Stub.asInterface(binder);
                 } catch (Exception e) {
                     Log.e(TAG, "Failed to bind to soundtrigger service", e);
                 }
             }
-            return  mService;
         }
-
     }
 
     /**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index a76524a..592673e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10591,6 +10591,13 @@
         public static final String MEDIA_CONTROLS_RESUME = "qs_media_resumption";
 
         /**
+         * Whether to enable media controls on lock screen.
+         * When enabled, media controls will appear on lock screen.
+         * @hide
+         */
+        public static final String MEDIA_CONTROLS_LOCK_SCREEN = "media_controls_lock_screen";
+
+        /**
          * Controls whether contextual suggestions can be shown in the media controls.
          * @hide
          */
diff --git a/core/java/android/service/voice/HotwordDetectedResult.java b/core/java/android/service/voice/HotwordDetectedResult.java
index 7234145..ab71459 100644
--- a/core/java/android/service/voice/HotwordDetectedResult.java
+++ b/core/java/android/service/voice/HotwordDetectedResult.java
@@ -95,6 +95,16 @@
     /** Limits the max value for the triggered audio channel. */
     private static final int LIMIT_AUDIO_CHANNEL_MAX_VALUE = 63;
 
+    /**
+     * The bundle key for proximity value
+     *
+     * TODO(b/238896013): Move the proximity logic out of bundle to proper API.
+     *
+     * @hide
+     */
+    public static final String EXTRA_PROXIMITY_METERS =
+            "android.service.voice.extra.PROXIMITY_METERS";
+
     /** Confidence level in the trigger outcome. */
     @HotwordConfidenceLevelValue
     private final int mConfidenceLevel;
@@ -197,6 +207,14 @@
      * <p>The use of this method is discouraged, and support for it will be removed in future
      * versions of Android.
      *
+     * <p>After the trigger happens, a special case of proximity-related extra, with the key of
+     * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double),
+     * will be stored to enable proximity logic. The proximity meters is provided by the system,
+     * on devices that support detecting proximity of nearby users, to help disambiguate which
+     * nearby device should respond. When the proximity is unknown, the proximity value will not
+     * be stored. This mapping will be excluded from the max bundle size calculation because this
+     * mapping is included after the result is returned from the hotword detector service.
+     *
      * <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents
      * that can be used to communicate with other processes.
      */
@@ -315,8 +333,23 @@
                     "audioChannel");
         }
         if (!mExtras.isEmpty()) {
-            Preconditions.checkArgumentInRange(getParcelableSize(mExtras), 0, getMaxBundleSize(),
-                    "extras");
+            // Remove the proximity key from the bundle before checking the bundle size. The
+            // proximity value is added after the privileged module and can avoid the
+            // maxBundleSize limitation.
+            if (mExtras.containsKey(EXTRA_PROXIMITY_METERS)) {
+                double proximityMeters = mExtras.getDouble(EXTRA_PROXIMITY_METERS);
+                mExtras.remove(EXTRA_PROXIMITY_METERS);
+                // Skip checking parcelable size if the new bundle size is 0. Newly empty bundle
+                // has parcelable size of 4, but the default bundle has parcelable size of 0.
+                if (mExtras.size() > 0) {
+                    Preconditions.checkArgumentInRange(getParcelableSize(mExtras), 0,
+                            getMaxBundleSize(), "extras");
+                }
+                mExtras.putDouble(EXTRA_PROXIMITY_METERS, proximityMeters);
+            } else {
+                Preconditions.checkArgumentInRange(getParcelableSize(mExtras), 0,
+                        getMaxBundleSize(), "extras");
+            }
         }
     }
 
@@ -513,6 +546,14 @@
      * <p>The use of this method is discouraged, and support for it will be removed in future
      * versions of Android.
      *
+     * <p>After the trigger happens, a special case of proximity-related extra, with the key of
+     * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double),
+     * will be stored to enable proximity logic. The proximity meters is provided by the system,
+     * on devices that support detecting proximity of nearby users, to help disambiguate which
+     * nearby device should respond. When the proximity is unknown, the proximity value will not
+     * be stored. This mapping will be excluded from the max bundle size calculation because this
+     * mapping is included after the result is returned from the hotword detector service.
+     *
      * <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents
      * that can be used to communicate with other processes.
      */
@@ -813,6 +854,14 @@
          * <p>The use of this method is discouraged, and support for it will be removed in future
          * versions of Android.
          *
+         * <p>After the trigger happens, a special case of proximity-related extra, with the key of
+         * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double),
+         * will be stored to enable proximity logic. The proximity meters is provided by the system,
+         * on devices that support detecting proximity of nearby users, to help disambiguate which
+         * nearby device should respond. When the proximity is unknown, the proximity value will not
+         * be stored. This mapping will be excluded from the max bundle size calculation because this
+         * mapping is included after the result is returned from the hotword detector service.
+         *
          * <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents
          * that can be used to communicate with other processes.
          */
@@ -882,10 +931,10 @@
     }
 
     @DataClass.Generated(
-            time = 1625541522353L,
+            time = 1658357814396L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/service/voice/HotwordDetectedResult.java",
-            inputSignatures = "public static final  int CONFIDENCE_LEVEL_NONE\npublic static final  int CONFIDENCE_LEVEL_LOW\npublic static final  int CONFIDENCE_LEVEL_LOW_MEDIUM\npublic static final  int CONFIDENCE_LEVEL_MEDIUM\npublic static final  int CONFIDENCE_LEVEL_MEDIUM_HIGH\npublic static final  int CONFIDENCE_LEVEL_HIGH\npublic static final  int CONFIDENCE_LEVEL_VERY_HIGH\npublic static final  int HOTWORD_OFFSET_UNSET\npublic static final  int AUDIO_CHANNEL_UNSET\nprivate static final  int LIMIT_HOTWORD_OFFSET_MAX_VALUE\nprivate static final  int LIMIT_AUDIO_CHANNEL_MAX_VALUE\nprivate final @android.service.voice.HotwordDetectedResult.HotwordConfidenceLevelValue int mConfidenceLevel\nprivate @android.annotation.Nullable android.media.MediaSyncEvent mMediaSyncEvent\nprivate  int mHotwordOffsetMillis\nprivate  int mHotwordDurationMillis\nprivate  int mAudioChannel\nprivate  boolean mHotwordDetectionPersonalized\nprivate final  int mScore\nprivate final  int mPersonalizedScore\nprivate final  int mHotwordPhraseId\nprivate final @android.annotation.NonNull android.os.PersistableBundle mExtras\nprivate static  int sMaxBundleSize\nprivate static  int defaultConfidenceLevel()\nprivate static  int defaultScore()\nprivate static  int defaultPersonalizedScore()\npublic static  int getMaxScore()\nprivate static  int defaultHotwordPhraseId()\npublic static  int getMaxHotwordPhraseId()\nprivate static  android.os.PersistableBundle defaultExtras()\npublic static  int getMaxBundleSize()\npublic @android.annotation.Nullable android.media.MediaSyncEvent getMediaSyncEvent()\npublic static  int getParcelableSize(android.os.Parcelable)\npublic static  int getUsageSize(android.service.voice.HotwordDetectedResult)\nprivate static  int bitCount(long)\nprivate  void onConstructed()\nclass HotwordDetectedResult extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)")
+            inputSignatures = "public static final  int CONFIDENCE_LEVEL_NONE\npublic static final  int CONFIDENCE_LEVEL_LOW\npublic static final  int CONFIDENCE_LEVEL_LOW_MEDIUM\npublic static final  int CONFIDENCE_LEVEL_MEDIUM\npublic static final  int CONFIDENCE_LEVEL_MEDIUM_HIGH\npublic static final  int CONFIDENCE_LEVEL_HIGH\npublic static final  int CONFIDENCE_LEVEL_VERY_HIGH\npublic static final  int HOTWORD_OFFSET_UNSET\npublic static final  int AUDIO_CHANNEL_UNSET\nprivate static final  int LIMIT_HOTWORD_OFFSET_MAX_VALUE\nprivate static final  int LIMIT_AUDIO_CHANNEL_MAX_VALUE\npublic static final  java.lang.String EXTRA_PROXIMITY_METERS\nprivate final @android.service.voice.HotwordDetectedResult.HotwordConfidenceLevelValue int mConfidenceLevel\nprivate @android.annotation.Nullable android.media.MediaSyncEvent mMediaSyncEvent\nprivate  int mHotwordOffsetMillis\nprivate  int mHotwordDurationMillis\nprivate  int mAudioChannel\nprivate  boolean mHotwordDetectionPersonalized\nprivate final  int mScore\nprivate final  int mPersonalizedScore\nprivate final  int mHotwordPhraseId\nprivate final @android.annotation.NonNull android.os.PersistableBundle mExtras\nprivate static  int sMaxBundleSize\nprivate static  int defaultConfidenceLevel()\nprivate static  int defaultScore()\nprivate static  int defaultPersonalizedScore()\npublic static  int getMaxScore()\nprivate static  int defaultHotwordPhraseId()\npublic static  int getMaxHotwordPhraseId()\nprivate static  android.os.PersistableBundle defaultExtras()\npublic static  int getMaxBundleSize()\npublic @android.annotation.Nullable android.media.MediaSyncEvent getMediaSyncEvent()\npublic static  int getParcelableSize(android.os.Parcelable)\npublic static  int getUsageSize(android.service.voice.HotwordDetectedResult)\nprivate static  int bitCount(long)\nprivate  void onConstructed()\nclass HotwordDetectedResult extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index a2cb1d5..0e3bcd1 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -314,6 +314,9 @@
             (int) (startValue.right + fraction * (endValue.right - startValue.right)),
             (int) (startValue.bottom + fraction * (endValue.bottom - startValue.bottom)));
 
+    /** Logging listener. */
+    private WindowInsetsAnimationControlListener mLoggingListener;
+
     /**
      * The default implementation of listener, to be used by InsetsController and InsetsPolicy to
      * animate insets.
@@ -330,6 +333,7 @@
         private final long mDurationMs;
         private final boolean mDisable;
         private final int mFloatingImeBottomInset;
+        private final WindowInsetsAnimationControlListener mLoggingListener;
 
         private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
                 new ThreadLocal<AnimationHandler>() {
@@ -343,7 +347,7 @@
 
         public InternalAnimationControlListener(boolean show, boolean hasAnimationCallbacks,
                 @InsetsType int requestedTypes, @Behavior int behavior, boolean disable,
-                int floatingImeBottomInset) {
+                int floatingImeBottomInset, WindowInsetsAnimationControlListener loggingListener) {
             mShow = show;
             mHasAnimationCallbacks = hasAnimationCallbacks;
             mRequestedTypes = requestedTypes;
@@ -351,12 +355,16 @@
             mDurationMs = calculateDurationMs();
             mDisable = disable;
             mFloatingImeBottomInset = floatingImeBottomInset;
+            mLoggingListener = loggingListener;
         }
 
         @Override
         public void onReady(WindowInsetsAnimationController controller, int types) {
             mController = controller;
             if (DEBUG) Log.d(TAG, "default animation onReady types: " + types);
+            if (mLoggingListener != null) {
+                mLoggingListener.onReady(controller, types);
+            }
 
             if (mDisable) {
                 onAnimationFinish();
@@ -410,6 +418,9 @@
         public void onFinished(WindowInsetsAnimationController controller) {
             if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onFinished types:"
                     + Type.toString(mRequestedTypes));
+            if (mLoggingListener != null) {
+                mLoggingListener.onFinished(controller);
+            }
         }
 
         @Override
@@ -420,6 +431,9 @@
             }
             if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onCancelled types:"
                     + mRequestedTypes);
+            if (mLoggingListener != null) {
+                mLoggingListener.onCancelled(controller);
+            }
         }
 
         protected Interpolator getInsetsInterpolator() {
@@ -1147,6 +1161,13 @@
         updateRequestedVisibilities();
     }
 
+    // TODO(b/242962223): Make this setter restrictive.
+    @Override
+    public void setSystemDrivenInsetsAnimationLoggingListener(
+            @Nullable WindowInsetsAnimationControlListener listener) {
+        mLoggingListener = listener;
+    }
+
     /**
      * @return Pair of (types ready to animate, IME ready to animate).
      */
@@ -1460,7 +1481,8 @@
         boolean hasAnimationCallbacks = mHost.hasAnimationCallbacks();
         final InternalAnimationControlListener listener = new InternalAnimationControlListener(
                 show, hasAnimationCallbacks, types, mHost.getSystemBarsBehavior(),
-                skipAnim || mAnimationsDisabled, mHost.dipToPx(FLOATING_IME_BOTTOM_INSET_DP));
+                skipAnim || mAnimationsDisabled, mHost.dipToPx(FLOATING_IME_BOTTOM_INSET_DP),
+                mLoggingListener);
 
         // We are about to playing the default animation (show/hide). Passing a null frame indicates
         // the controlled types should be animated regardless of the frame.
diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java
index c61baf6..3fe9110 100644
--- a/core/java/android/view/PendingInsetsController.java
+++ b/core/java/android/view/PendingInsetsController.java
@@ -44,6 +44,7 @@
     private ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
             = new ArrayList<>();
     private int mCaptionInsetsHeight = 0;
+    private WindowInsetsAnimationControlListener mLoggingListener;
 
     @Override
     public void show(int types) {
@@ -176,6 +177,9 @@
             controller.addOnControllableInsetsChangedListener(
                     mControllableInsetsChangedListeners.get(i));
         }
+        if (mLoggingListener != null) {
+            controller.setSystemDrivenInsetsAnimationLoggingListener(mLoggingListener);
+        }
 
         // Reset all state so it doesn't get applied twice just in case
         mRequests.clear();
@@ -184,7 +188,7 @@
         mAppearance = 0;
         mAppearanceMask = 0;
         mAnimationsDisabled = false;
-
+        mLoggingListener = null;
         // After replaying, we forward everything directly to the replayed instance.
         mReplayedInsetsController = controller;
     }
@@ -198,6 +202,16 @@
     }
 
     @Override
+    public void setSystemDrivenInsetsAnimationLoggingListener(
+            @Nullable WindowInsetsAnimationControlListener listener) {
+        if (mReplayedInsetsController != null) {
+            mReplayedInsetsController.setSystemDrivenInsetsAnimationLoggingListener(listener);
+        } else {
+            mLoggingListener = listener;
+        }
+    }
+
+    @Override
     public void controlWindowInsetsAnimation(@InsetsType int types, long durationMillis,
             @Nullable Interpolator interpolator,
             CancellationSignal cancellationSignal,
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index 5721fa6..3acb053 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -28,8 +28,8 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.util.Log;
-import android.window.WindowTokenClient;
 import android.view.accessibility.IAccessibilityEmbeddedConnection;
+import android.window.WindowTokenClient;
 
 import java.util.Objects;
 
@@ -271,14 +271,8 @@
     /** @hide */
     public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d,
             @NonNull WindowlessWindowManager wwm) {
-        this(c, d, wwm, false /* useSfChoreographer */);
-    }
-
-    /** @hide */
-    public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d,
-            @NonNull WindowlessWindowManager wwm, boolean useSfChoreographer) {
         mWm = wwm;
-        mViewRoot = new ViewRootImpl(c, d, mWm, new WindowlessWindowLayout(), useSfChoreographer);
+        mViewRoot = new ViewRootImpl(c, d, mWm, new WindowlessWindowLayout());
         addConfigCallback(c, d);
 
         WindowManagerGlobal.getInstance().addWindowlessRoot(mViewRoot);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index af57f3b..ec6b4ac 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -905,17 +905,11 @@
     private String mTag = TAG;
 
     public ViewRootImpl(Context context, Display display) {
-        this(context, display, WindowManagerGlobal.getWindowSession(), new WindowLayout(),
-                false /* useSfChoreographer */);
+        this(context, display, WindowManagerGlobal.getWindowSession(), new WindowLayout());
     }
 
     public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
             WindowLayout windowLayout) {
-        this(context, display, session, windowLayout, false /* useSfChoreographer */);
-    }
-
-    public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
-            WindowLayout windowLayout, boolean useSfChoreographer) {
         mContext = context;
         mWindowSession = session;
         mWindowLayout = windowLayout;
@@ -947,8 +941,7 @@
         mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi;
         mFallbackEventHandler = new PhoneFallbackEventHandler(context);
         // TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions
-        mChoreographer = useSfChoreographer
-                ? Choreographer.getSfInstance() : Choreographer.getInstance();
+        mChoreographer = Choreographer.getInstance();
         mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
         mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this));
         mHandwritingInitiator = new HandwritingInitiator(mViewConfiguration,
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index 227b9f4..63f9e13 100644
--- a/core/java/android/view/WindowInsetsController.java
+++ b/core/java/android/view/WindowInsetsController.java
@@ -201,6 +201,21 @@
             @NonNull WindowInsetsAnimationControlListener listener);
 
     /**
+     * Lets the application add non-controllable listener object that can be called back
+     * when animation is invoked by the system by host calling methods such as {@link #show} or
+     * {@link #hide}.
+     *
+     * The listener is supposed to be used for logging only, using the control or
+     * relying on the timing of the callback in any other way is not supported.
+     *
+     * @param listener The {@link WindowInsetsAnimationControlListener} that gets called when
+     *                 the animation is driven by the system and not the host
+     * @hide
+     */
+    void setSystemDrivenInsetsAnimationLoggingListener(
+            @Nullable WindowInsetsAnimationControlListener listener);
+
+    /**
      * Controls the appearance of system bars.
      * <p>
      * For example, the following statement adds {@link #APPEARANCE_LIGHT_STATUS_BARS}:
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index 5372071..c43cf55 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -26,6 +26,7 @@
 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;
@@ -151,24 +152,72 @@
      * Called when a TaskFragment is created and organized by this organizer.
      *
      * @param taskFragmentInfo  Info of the TaskFragment that is created.
+     * @deprecated Use {@link #onTaskFragmentAppeared(WindowContainerTransaction, TaskFragmentInfo)}
+     *             instead.
      */
+    @Deprecated
     public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {}
 
     /**
+     * Called when a TaskFragment is created and organized by this organizer.
+     *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
+     * @param taskFragmentInfo  Info of the TaskFragment that is created.
+     */
+    public void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
+        // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next release.
+        onTaskFragmentAppeared(taskFragmentInfo);
+    }
+
+    /**
      * Called when the status of an organized TaskFragment is changed.
      *
      * @param taskFragmentInfo  Info of the TaskFragment that is changed.
+     * @deprecated Use {@link #onTaskFragmentInfoChanged(WindowContainerTransaction,
+     *             TaskFragmentInfo)} instead.
      */
+    @Deprecated
     public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {}
 
     /**
+     * Called when the status of an organized TaskFragment is changed.
+     *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
+     * @param taskFragmentInfo  Info of the TaskFragment that is changed.
+     */
+    public void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
+        // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next release.
+        onTaskFragmentInfoChanged(taskFragmentInfo);
+    }
+
+    /**
      * Called when an organized TaskFragment is removed.
      *
      * @param taskFragmentInfo  Info of the TaskFragment that is removed.
+     *  @deprecated Use {@link #onTaskFragmentVanished(WindowContainerTransaction,
+     *              TaskFragmentInfo)} instead.
      */
+    @Deprecated
     public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {}
 
     /**
+     * Called when an organized TaskFragment is removed.
+     *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
+     * @param taskFragmentInfo  Info of the TaskFragment that is removed.
+     */
+    public void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
+        // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next release.
+        onTaskFragmentVanished(taskFragmentInfo);
+    }
+
+    /**
      * Called when the parent leaf Task of organized TaskFragments is changed.
      * When the leaf Task is changed, the organizer may want to update the TaskFragments in one
      * transaction.
@@ -176,7 +225,13 @@
      * For case like screen size change, it will trigger onTaskFragmentParentInfoChanged with new
      * Task bounds, but may not trigger onTaskFragmentInfoChanged because there can be an override
      * bounds.
+     *
+     * @param fragmentToken The parent Task this TaskFragment is changed.
+     * @param parentConfig  Config of the parent Task.
+     * @deprecated Use {@link #onTaskFragmentParentInfoChanged(WindowContainerTransaction, int,
+     *             Configuration)} instead.
      */
+    @Deprecated
     public void onTaskFragmentParentInfoChanged(
             @NonNull IBinder fragmentToken, @NonNull Configuration parentConfig) {}
 
@@ -189,11 +244,13 @@
      * Task bounds, but may not trigger onTaskFragmentInfoChanged because there can be an override
      * bounds.
      *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
      * @param taskId    Id of the parent Task that is changed.
      * @param parentConfig  Config of the parent Task.
-     * @hide
      */
-    public void onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig) {
+    public void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct, int taskId,
+            @NonNull Configuration parentConfig) {
         // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next release.
         final List<IBinder> tokens = mTaskIdToFragmentTokens.get(taskId);
         if (tokens == null || tokens.isEmpty()) {
@@ -211,9 +268,8 @@
      * @param errorCallbackToken    token set in
      *                             {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)}
      * @param exception             exception from the server side.
-     *
-     * @deprecated Use {@link #onTaskFragmentError(IBinder, TaskFragmentInfo, int, Throwable)}
-     * instead.
+     * @deprecated Use {@link #onTaskFragmentError(WindowContainerTransaction, IBinder,
+     *             TaskFragmentInfo, int, Throwable)} instead.
      */
     @Deprecated
     public void onTaskFragmentError(
@@ -223,6 +279,8 @@
      * Called when the {@link WindowContainerTransaction} created with
      * {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} failed on the server side.
      *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
      * @param errorCallbackToken    token set in
      *                             {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)}
      * @param taskFragmentInfo  The {@link TaskFragmentInfo}. This could be {@code null} if no
@@ -231,7 +289,7 @@
      *                          transaction operation.
      * @param exception             exception from the server side.
      */
-    public void onTaskFragmentError(
+    public void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
             @NonNull IBinder errorCallbackToken, @Nullable TaskFragmentInfo taskFragmentInfo,
             int opType, @NonNull Throwable exception) {
         // Doing so to keep compatibility. This will be removed in the next release.
@@ -244,6 +302,8 @@
      * original Task. In this case, we need to notify the organizer so that it can check if the
      * Activity matches any split rule.
      *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
      * @param taskId            The Task that the activity is reparented to.
      * @param activityIntent    The intent that the activity is original launched with.
      * @param activityToken     If the activity belongs to the same process as the organizer, this
@@ -251,20 +311,23 @@
      *                          different process, the server will generate a temporary token that
      *                          the organizer can use to reparent the activity through
      *                          {@link WindowContainerTransaction} if needed.
-     * @hide
      */
-    public void onActivityReparentedToTask(int taskId, @NonNull Intent activityIntent,
-            @NonNull IBinder activityToken) {}
+    public void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) {}
 
     /**
      * 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();
         for (TaskFragmentTransaction.Change change : changes) {
-            // TODO(b/240519866): apply all changes in one WCT.
             final int taskId = change.getTaskId();
             switch (change.getType()) {
                 case TYPE_TASK_FRAGMENT_APPEARED:
@@ -277,10 +340,10 @@
                     onTaskFragmentParentInfoChanged(change.getTaskFragmentToken(),
                             mTaskIdToConfigurations.get(taskId));
 
-                    onTaskFragmentAppeared(change.getTaskFragmentInfo());
+                    onTaskFragmentAppeared(wct, change.getTaskFragmentInfo());
                     break;
                 case TYPE_TASK_FRAGMENT_INFO_CHANGED:
-                    onTaskFragmentInfoChanged(change.getTaskFragmentInfo());
+                    onTaskFragmentInfoChanged(wct, change.getTaskFragmentInfo());
                     break;
                 case TYPE_TASK_FRAGMENT_VANISHED:
                     // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next
@@ -294,18 +357,19 @@
                         }
                     }
 
-                    onTaskFragmentVanished(change.getTaskFragmentInfo());
+                    onTaskFragmentVanished(wct, change.getTaskFragmentInfo());
                     break;
                 case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED:
                     // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next
                     // release.
                     mTaskIdToConfigurations.put(taskId, change.getTaskConfiguration());
 
-                    onTaskFragmentParentInfoChanged(taskId, change.getTaskConfiguration());
+                    onTaskFragmentParentInfoChanged(wct, taskId, change.getTaskConfiguration());
                     break;
                 case TYPE_TASK_FRAGMENT_ERROR:
                     final Bundle errorBundle = change.getErrorBundle();
                     onTaskFragmentError(
+                            wct,
                             change.getErrorCallbackToken(),
                             errorBundle.getParcelable(
                                     KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO, TaskFragmentInfo.class),
@@ -315,6 +379,7 @@
                     break;
                 case TYPE_ACTIVITY_REPARENTED_TO_TASK:
                     onActivityReparentedToTask(
+                            wct,
                             change.getTaskId(),
                             change.getActivityIntent(),
                             change.getActivityToken());
@@ -324,6 +389,8 @@
                             "Unknown TaskFragmentEvent=" + change.getType());
             }
         }
+        // TODO(b/240519866): notify TaskFragmentOrganizerController that the transition is done.
+        applyTransaction(wct);
     }
 
     @Override
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index e3b7100..08e0225 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6323,6 +6323,8 @@
     <string name="vdm_camera_access_denied" product="default">Can’t access the phone’s camera from your <xliff:g id="device" example="Chromebook">%1$s</xliff:g></string>
     <!-- Error message indicating the camera cannot be accessed when running on a virtual device. [CHAR LIMIT=NONE] -->
     <string name="vdm_camera_access_denied" product="tablet">Can’t access the tablet’s camera from your <xliff:g id="device" example="Chromebook">%1$s</xliff:g></string>
+    <!-- Error message indicating the user cannot access secure content when running on a virtual device. [CHAR LIMIT=NONE] -->
+    <string name="vdm_secure_window">This can’t be accessed while streaming. Try on your phone instead.</string>
 
     <!-- Title for preference of the system default locale. [CHAR LIMIT=50]-->
     <string name="system_locale_title">System default</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 205681c..923de6e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4795,6 +4795,7 @@
 
   <!-- For VirtualDeviceManager -->
   <java-symbol type="string" name="vdm_camera_access_denied" />
+  <java-symbol type="string" name="vdm_secure_window" />
 
   <java-symbol type="color" name="camera_privacy_light_day"/>
   <java-symbol type="color" name="camera_privacy_light_night"/>
diff --git a/core/tests/coretests/src/android/app/NotificationChannelGroupTest.java b/core/tests/coretests/src/android/app/NotificationChannelGroupTest.java
new file mode 100644
index 0000000..2a3da05
--- /dev/null
+++ b/core/tests/coretests/src/android/app/NotificationChannelGroupTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.app;
+
+import static junit.framework.TestCase.assertEquals;
+
+import android.os.Parcel;
+import android.test.AndroidTestCase;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.base.Strings;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NotificationChannelGroupTest {
+    private final String CLASS = "android.app.NotificationChannelGroup";
+
+    @Test
+    public void testLongStringFields() {
+        NotificationChannelGroup group = new NotificationChannelGroup("my_group_01", "groupName");
+
+        try {
+            String longString = Strings.repeat("A", 65536);
+            Field mName = Class.forName(CLASS).getDeclaredField("mName");
+            mName.setAccessible(true);
+            mName.set(group, longString);
+            Field mId = Class.forName(CLASS).getDeclaredField("mId");
+            mId.setAccessible(true);
+            mId.set(group, longString);
+            Field mDescription = Class.forName(CLASS).getDeclaredField("mDescription");
+            mDescription.setAccessible(true);
+            mDescription.set(group, longString);
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        } catch (ClassNotFoundException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+
+        Parcel parcel = Parcel.obtain();
+        group.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        NotificationChannelGroup fromParcel =
+                NotificationChannelGroup.CREATOR.createFromParcel(parcel);
+        assertEquals(NotificationChannelGroup.MAX_TEXT_LENGTH, fromParcel.getId().length());
+        assertEquals(NotificationChannelGroup.MAX_TEXT_LENGTH, fromParcel.getName().length());
+        assertEquals(NotificationChannelGroup.MAX_TEXT_LENGTH,
+                fromParcel.getDescription().length());
+    }
+}
diff --git a/core/tests/coretests/src/android/app/NotificationChannelTest.java b/core/tests/coretests/src/android/app/NotificationChannelTest.java
new file mode 100644
index 0000000..647bfe8
--- /dev/null
+++ b/core/tests/coretests/src/android/app/NotificationChannelTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static junit.framework.TestCase.assertEquals;
+
+import android.net.Uri;
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.base.Strings;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NotificationChannelTest {
+    private final String CLASS = "android.app.NotificationChannel";
+
+    @Test
+    public void testLongStringFields() {
+        NotificationChannel channel = new NotificationChannel("id", "name", 3);
+
+        try {
+            String longString = Strings.repeat("A", 65536);
+            Field mName = Class.forName(CLASS).getDeclaredField("mName");
+            mName.setAccessible(true);
+            mName.set(channel, longString);
+            Field mId = Class.forName(CLASS).getDeclaredField("mId");
+            mId.setAccessible(true);
+            mId.set(channel, longString);
+            Field mDesc = Class.forName(CLASS).getDeclaredField("mDesc");
+            mDesc.setAccessible(true);
+            mDesc.set(channel, longString);
+            Field mParentId = Class.forName(CLASS).getDeclaredField("mParentId");
+            mParentId.setAccessible(true);
+            mParentId.set(channel, longString);
+            Field mGroup = Class.forName(CLASS).getDeclaredField("mGroup");
+            mGroup.setAccessible(true);
+            mGroup.set(channel, longString);
+            Field mConversationId = Class.forName(CLASS).getDeclaredField("mConversationId");
+            mConversationId.setAccessible(true);
+            mConversationId.set(channel, longString);
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        } catch (ClassNotFoundException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+
+        Parcel parcel = Parcel.obtain();
+        channel.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        NotificationChannel fromParcel = NotificationChannel.CREATOR.createFromParcel(parcel);
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH, fromParcel.getId().length());
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH, fromParcel.getName().length());
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
+                fromParcel.getDescription().length());
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
+                fromParcel.getParentChannelId().length());
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
+                fromParcel.getGroup().length());
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
+                fromParcel.getConversationId().length());
+    }
+
+    @Test
+    public void testLongAlertFields() {
+        NotificationChannel channel = new NotificationChannel("id", "name", 3);
+
+        channel.setSound(Uri.parse("content://" + Strings.repeat("A",65536)),
+                Notification.AUDIO_ATTRIBUTES_DEFAULT);
+        channel.setVibrationPattern(new long[65550/2]);
+
+        Parcel parcel = Parcel.obtain();
+        channel.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        NotificationChannel fromParcel = NotificationChannel.CREATOR.createFromParcel(parcel);
+        assertEquals(NotificationChannel.MAX_VIBRATION_LENGTH,
+                fromParcel.getVibrationPattern().length);
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
+                fromParcel.getSound().toString().length());
+    }
+}
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index ed6a649..6e59b83 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -236,6 +236,21 @@
     }
 
     @Test
+    public void testSystemDrivenInsetsAnimationLoggingListener_onReady() {
+        prepareControls();
+        // only the original thread that created view hierarchy can touch its views
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            WindowInsetsAnimationControlListener loggingListener =
+                    mock(WindowInsetsAnimationControlListener.class);
+            mController.setSystemDrivenInsetsAnimationLoggingListener(loggingListener);
+            mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
+            // since there is no focused view, forcefully make IME visible.
+            mController.show(Type.ime(), true /* fromIme */);
+            verify(loggingListener).onReady(notNull(), anyInt());
+        });
+    }
+
+    @Test
     public void testAnimationEndState() {
         InsetsSourceControl[] controls = prepareControls();
         InsetsSourceControl navBar = controls[0];
diff --git a/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java b/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java
index 03c8b1b..690b3587 100644
--- a/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java
@@ -213,6 +213,25 @@
     }
 
     @Test
+    public void testSystemDrivenInsetsAnimationLoggingListener() {
+        WindowInsetsAnimationControlListener listener =
+                mock(WindowInsetsAnimationControlListener.class);
+        mPendingInsetsController.setSystemDrivenInsetsAnimationLoggingListener(listener);
+        mPendingInsetsController.replayAndAttach(mReplayedController);
+        verify(mReplayedController).setSystemDrivenInsetsAnimationLoggingListener(eq(listener));
+    }
+
+    @Test
+    public void testSystemDrivenInsetsAnimationLoggingListener_direct() {
+        mPendingInsetsController.replayAndAttach(mReplayedController);
+        WindowInsetsAnimationControlListener listener =
+                mock(WindowInsetsAnimationControlListener.class);
+        mPendingInsetsController.setSystemDrivenInsetsAnimationLoggingListener(listener);
+        verify(mReplayedController).setSystemDrivenInsetsAnimationLoggingListener(
+                eq(listener));
+    }
+
+    @Test
     public void testDetachReattach() {
         mPendingInsetsController.show(systemBars());
         mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 9fb7d19..1335e5e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -271,56 +271,49 @@
     }
 
     @Override
-    public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
+    public void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
         mFragmentInfos.put(fragmentToken, taskFragmentInfo);
         mCallback.onTaskFragmentAppeared(wct, taskFragmentInfo);
-        applyTransaction(wct);
     }
 
     @Override
-    public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
+    public void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
         mFragmentInfos.put(fragmentToken, taskFragmentInfo);
         mCallback.onTaskFragmentInfoChanged(wct, taskFragmentInfo);
-        applyTransaction(wct);
     }
 
     @Override
-    public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
+    public void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         mFragmentInfos.remove(taskFragmentInfo.getFragmentToken());
         mCallback.onTaskFragmentVanished(wct, taskFragmentInfo);
-        applyTransaction(wct);
     }
 
     @Override
-    public void onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
+    public void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Configuration parentConfig) {
         mCallback.onTaskFragmentParentInfoChanged(wct, taskId, parentConfig);
-        applyTransaction(wct);
     }
 
     @Override
-    public void onActivityReparentedToTask(int taskId, @NonNull Intent activityIntent,
-            @NonNull IBinder activityToken) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
+    public void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) {
         mCallback.onActivityReparentedToTask(wct, taskId, activityIntent, activityToken);
-        applyTransaction(wct);
     }
 
     @Override
-    public void onTaskFragmentError(@NonNull IBinder errorCallbackToken,
+    public void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder errorCallbackToken,
             @Nullable TaskFragmentInfo taskFragmentInfo,
             int opType, @NonNull Throwable exception) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (taskFragmentInfo != null) {
             final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
             mFragmentInfos.put(fragmentToken, taskFragmentInfo);
         }
         mCallback.onTaskFragmentError(wct, taskFragmentInfo, opType);
-        applyTransaction(wct);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
index 764e650..b085b73 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
@@ -16,14 +16,20 @@
 
 package com.android.wm.shell;
 
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+
+import android.app.WindowConfiguration;
 import android.util.SparseArray;
 import android.view.SurfaceControl;
 import android.window.DisplayAreaAppearedInfo;
 import android.window.DisplayAreaInfo;
 import android.window.DisplayAreaOrganizer;
+import android.window.WindowContainerTransaction;
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.protolog.common.ProtoLog;
+
 import java.io.PrintWriter;
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -102,10 +108,44 @@
         mDisplayAreasInfo.put(displayId, displayAreaInfo);
     }
 
+    /**
+     * Create a {@link WindowContainerTransaction} to update display windowing mode.
+     *
+     * @param displayId display id to update windowing mode for
+     * @param windowingMode target {@link WindowConfiguration.WindowingMode}
+     * @return {@link WindowContainerTransaction} with pending operation to set windowing mode
+     */
+    public WindowContainerTransaction prepareWindowingModeChange(int displayId,
+            @WindowConfiguration.WindowingMode int windowingMode) {
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId);
+        if (displayAreaInfo == null) {
+            ProtoLog.e(WM_SHELL_DESKTOP_MODE,
+                    "unable to update windowing mode for display %d display not found", displayId);
+            return wct;
+        }
+
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId,
+                displayAreaInfo.configuration.windowConfiguration.getWindowingMode(),
+                windowingMode);
+
+        wct.setWindowingMode(displayAreaInfo.token, windowingMode);
+        return wct;
+    }
+
     public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
         final String childPrefix = innerPrefix + "  ";
         pw.println(prefix + this);
+
+        for (int i = 0; i < mDisplayAreasInfo.size(); i++) {
+            int displayId = mDisplayAreasInfo.keyAt(i);
+            DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId);
+            int windowingMode =
+                    displayAreaInfo.configuration.windowConfiguration.getWindowingMode();
+            pw.println(innerPrefix + "# displayId=" + displayId + " wmMode=" + windowingMode);
+        }
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 6ae0f9b..d5d4935 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -22,6 +22,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
@@ -46,6 +47,7 @@
 import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
 import android.window.TaskOrganizer;
+import android.window.WindowContainerTransaction;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
@@ -690,6 +692,49 @@
         taskListener.reparentChildSurfaceToTask(taskId, sc, t);
     }
 
+    /**
+     * Create a {@link WindowContainerTransaction} to clear task bounds.
+     *
+     * @param displayId display id for tasks that will have bounds cleared
+     * @return {@link WindowContainerTransaction} with pending operations to clear bounds
+     */
+    public WindowContainerTransaction prepareClearBoundsForTasks(int displayId) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks: displayId=%d", displayId);
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        for (int i = 0; i < mTasks.size(); i++) {
+            RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
+            if (taskInfo.displayId == displayId) {
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s",
+                        taskInfo.token, taskInfo);
+                wct.setBounds(taskInfo.token, null);
+            }
+        }
+        return wct;
+    }
+
+    /**
+     * Create a {@link WindowContainerTransaction} to clear task level freeform setting.
+     *
+     * @param displayId display id for tasks that will have windowing mode reset to {@link
+     *                  WindowConfiguration#WINDOWING_MODE_UNDEFINED}
+     * @return {@link WindowContainerTransaction} with pending operations to clear windowing mode
+     */
+    public WindowContainerTransaction prepareClearFreeformForTasks(int displayId) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks: displayId=%d", displayId);
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        for (int i = 0; i < mTasks.size(); i++) {
+            RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
+            if (taskInfo.displayId == displayId
+                    && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                        "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token,
+                        taskInfo);
+                wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+            }
+        }
+        return wct;
+    }
+
     private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info,
             int event) {
         ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo;
@@ -816,7 +861,14 @@
                 final int key = mTasks.keyAt(i);
                 final TaskAppearedInfo info = mTasks.valueAt(i);
                 final TaskListener listener = getTaskListener(info.getTaskInfo());
-                pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener);
+                final int windowingMode = info.getTaskInfo().getWindowingMode();
+                String pkg = "";
+                if (info.getTaskInfo().baseActivity != null) {
+                    pkg = info.getTaskInfo().baseActivity.getPackageName();
+                }
+                Rect bounds = info.getTaskInfo().getConfiguration().windowConfiguration.getBounds();
+                pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener
+                        + " wmMode=" + windowingMode + " pkg=" + pkg + " bounds=" + bounds);
             }
 
             pw.println();
@@ -826,6 +878,7 @@
                 final TaskListener listener = mLaunchCookieToListener.valueAt(i);
                 pw.println(innerPrefix + "#" + i + " cookie=" + key + " listener=" + listener);
             }
+
         }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index d5875c0..e270edb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -221,8 +221,7 @@
             }
             final Display display = mDisplayController.getDisplay(mDisplayId);
             SurfaceControlViewHost viewRoot =
-                    new SurfaceControlViewHost(
-                            view.getContext(), display, wwm, true /* useSfChoreographer */);
+                    new SurfaceControlViewHost(view.getContext(), display, wwm);
             attrs.flags |= FLAG_HARDWARE_ACCELERATED;
             viewRoot.setView(view, attrs);
             mViewRoots.put(view, viewRoot);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
index 35a309a..0cc545a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
@@ -20,7 +20,6 @@
 import static android.os.Process.THREAD_PRIORITY_DISPLAY;
 import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;
 
-import android.animation.AnimationHandler;
 import android.content.Context;
 import android.os.Build;
 import android.os.Handler;
@@ -31,11 +30,9 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.HandlerExecutor;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ChoreographerSfVsync;
 import com.android.wm.shell.common.annotations.ExternalMainThread;
 import com.android.wm.shell.common.annotations.ShellAnimationThread;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
@@ -195,30 +192,6 @@
     }
 
     /**
-     * Provide a Shell main-thread AnimationHandler.  The AnimationHandler can be set on
-     * {@link android.animation.ValueAnimator}s and will ensure that the animation will run on
-     * the Shell main-thread with the SF vsync.
-     */
-    @WMSingleton
-    @Provides
-    @ChoreographerSfVsync
-    public static AnimationHandler provideShellMainExecutorSfVsyncAnimationHandler(
-            @ShellMainThread ShellExecutor mainExecutor) {
-        try {
-            AnimationHandler handler = new AnimationHandler();
-            mainExecutor.executeBlocking(() -> {
-                // This is called on the animation thread since it calls
-                // Choreographer.getSfInstance() which returns a thread-local Choreographer instance
-                // that uses the SF vsync
-                handler.setProvider(new SfVsyncFrameCallbackProvider());
-            });
-            return handler;
-        } catch (InterruptedException e) {
-            throw new RuntimeException("Failed to initialize SfVsync animation handler in 1s", e);
-        }
-    }
-
-    /**
      * Provides a Shell background thread Handler for low priority background tasks.
      */
     @WMSingleton
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 2ca9c3b..2bcc134 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
@@ -27,6 +27,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.TaskViewTransitions;
@@ -48,6 +49,8 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.desktopmode.DesktopModeConstants;
+import com.android.wm.shell.desktopmode.DesktopModeController;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.freeform.FreeformComponents;
 import com.android.wm.shell.freeform.FreeformTaskListener;
@@ -574,6 +577,27 @@
     }
 
     //
+    // Desktop mode (optional feature)
+    //
+
+    @WMSingleton
+    @Provides
+    static Optional<DesktopModeController> provideDesktopModeController(
+            Context context, ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            RootDisplayAreaOrganizer rootDisplayAreaOrganizer,
+            @ShellMainThread Handler mainHandler
+    ) {
+        if (DesktopModeConstants.IS_FEATURE_ENABLED) {
+            return Optional.of(new DesktopModeController(context, shellInit, shellTaskOrganizer,
+                    rootDisplayAreaOrganizer,
+                    mainHandler));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    //
     // Misc
     //
 
@@ -583,7 +607,8 @@
     @ShellCreateTriggerOverride
     @Provides
     static Object provideIndependentShellComponentsToCreate(
-            SplitscreenPipMixedHandler splitscreenPipMixedHandler) {
+            SplitscreenPipMixedHandler splitscreenPipMixedHandler,
+            Optional<DesktopModeController> desktopModeController) {
         return new Object();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeConstants.java
new file mode 100644
index 0000000..e62a63a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeConstants.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode;
+
+import android.os.SystemProperties;
+
+/**
+ * Constants for desktop mode feature
+ */
+public class DesktopModeConstants {
+
+    /**
+     * Flag to indicate whether desktop mode is available on the device
+     */
+    public static final boolean IS_FEATURE_ENABLED = SystemProperties.getBoolean(
+            "persist.wm.debug.desktop_mode", false);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
new file mode 100644
index 0000000..5849e16
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -0,0 +1,137 @@
+/*
+ * 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.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
+
+/**
+ * Handles windowing changes when desktop mode system setting changes
+ */
+public class DesktopModeController {
+
+    private final Context mContext;
+    private final ShellTaskOrganizer mShellTaskOrganizer;
+    private final RootDisplayAreaOrganizer mRootDisplayAreaOrganizer;
+    private final SettingsObserver mSettingsObserver;
+
+    public DesktopModeController(Context context, ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            RootDisplayAreaOrganizer rootDisplayAreaOrganizer,
+            @ShellMainThread Handler mainHandler) {
+        mContext = context;
+        mShellTaskOrganizer = shellTaskOrganizer;
+        mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer;
+        mSettingsObserver = new SettingsObserver(mContext, mainHandler);
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController");
+        mSettingsObserver.observe();
+    }
+
+    @VisibleForTesting
+    void updateDesktopModeEnabled(boolean enabled) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeState: enabled=%s", enabled);
+
+        int displayId = mContext.getDisplayId();
+
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        // Reset freeform windowing mode that is set per task level (tasks should inherit
+        // container value)
+        wct.merge(mShellTaskOrganizer.prepareClearFreeformForTasks(displayId), true /* transfer */);
+        int targetWindowingMode;
+        if (enabled) {
+            targetWindowingMode = WINDOWING_MODE_FREEFORM;
+        } else {
+            targetWindowingMode = WINDOWING_MODE_FULLSCREEN;
+            // Clear any resized bounds
+            wct.merge(mShellTaskOrganizer.prepareClearBoundsForTasks(displayId),
+                    true /* transfer */);
+        }
+        wct.merge(mRootDisplayAreaOrganizer.prepareWindowingModeChange(displayId,
+                targetWindowingMode), true /* transfer */);
+        mRootDisplayAreaOrganizer.applyTransaction(wct);
+    }
+
+    /**
+     * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE}
+     */
+    private final class SettingsObserver extends ContentObserver {
+
+        private final Uri mDesktopModeSetting = Settings.System.getUriFor(
+                Settings.System.DESKTOP_MODE);
+
+        private final Context mContext;
+
+        SettingsObserver(Context context, Handler handler) {
+            super(handler);
+            mContext = context;
+        }
+
+        public void observe() {
+            // TODO(b/242867463): listen for setting change for all users
+            mContext.getContentResolver().registerContentObserver(mDesktopModeSetting,
+                    false /* notifyForDescendants */, this /* observer */, UserHandle.USER_CURRENT);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, @Nullable Uri uri) {
+            if (mDesktopModeSetting.equals(uri)) {
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Received update for desktop mode setting");
+                desktopModeSettingChanged();
+            }
+        }
+
+        private void desktopModeSettingChanged() {
+            boolean enabled = isDesktopModeEnabled();
+            updateDesktopModeEnabled(enabled);
+        }
+
+        private boolean isDesktopModeEnabled() {
+            try {
+                int result = Settings.System.getIntForUser(mContext.getContentResolver(),
+                        Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT);
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "isDesktopModeEnabled=%s", result);
+                return result != 0;
+            } catch (Settings.SettingNotFoundException e) {
+                ProtoLog.e(WM_SHELL_DESKTOP_MODE, "Failed to read DESKTOP_MODE setting %s", e);
+                return false;
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index 4942987..281ea53 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -31,8 +31,6 @@
 import android.util.Size;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
-import android.view.SyncRtSurfaceTransactionApplier;
-import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
 import android.view.WindowManagerGlobal;
 
 import com.android.internal.protolog.common.ProtoLog;
@@ -42,6 +40,7 @@
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipMediaController.ActionListener;
 import com.android.wm.shell.pip.PipMenuController;
+import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip.PipUiEventLogger;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -115,6 +114,10 @@
     private final ShellExecutor mMainExecutor;
     private final Handler mMainHandler;
 
+    private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+            mSurfaceControlTransactionFactory;
+    private final float[] mTmpTransform = new float[9];
+
     private final ArrayList<Listener> mListeners = new ArrayList<>();
     private final SystemWindows mSystemWindows;
     private final Optional<SplitScreenController> mSplitScreenController;
@@ -124,7 +127,6 @@
     private RemoteAction mCloseAction;
     private List<RemoteAction> mMediaActions;
 
-    private SyncRtSurfaceTransactionApplier mApplier;
     private int mMenuState;
 
     private PipMenuView mPipMenuView;
@@ -150,6 +152,9 @@
         mMainHandler = mainHandler;
         mSplitScreenController = splitScreenOptional;
         mPipUiEventLogger = pipUiEventLogger;
+
+        mSurfaceControlTransactionFactory =
+                new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
     }
 
     public boolean isMenuVisible() {
@@ -194,7 +199,6 @@
             return;
         }
 
-        mApplier = null;
         mSystemWindows.removeView(mPipMenuView);
         mPipMenuView = null;
     }
@@ -289,7 +293,7 @@
                     willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, "    "));
         }
 
-        if (!maybeCreateSyncApplier()) {
+        if (!checkPipMenuState()) {
             return;
         }
 
@@ -312,7 +316,7 @@
             return;
         }
 
-        if (!maybeCreateSyncApplier()) {
+        if (!checkPipMenuState()) {
             return;
         }
 
@@ -328,18 +332,15 @@
         mTmpSourceRectF.set(mTmpSourceBounds);
         mTmpDestinationRectF.set(destinationBounds);
         mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
-        SurfaceControl surfaceControl = getSurfaceControl();
-        SurfaceParams params = new SurfaceParams.Builder(surfaceControl)
-                .withMatrix(mMoveTransform)
-                .build();
+        final SurfaceControl surfaceControl = getSurfaceControl();
+        final SurfaceControl.Transaction menuTx =
+                mSurfaceControlTransactionFactory.getTransaction();
+        menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform);
         if (pipLeash != null && t != null) {
-            SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash)
-                    .withMergeTransaction(t)
-                    .build();
-            mApplier.scheduleApply(params, pipParams);
-        } else {
-            mApplier.scheduleApply(params);
+            // Merge the two transactions, vsyncId has been set on menuTx.
+            menuTx.merge(t);
         }
+        menuTx.apply();
     }
 
     /**
@@ -353,36 +354,29 @@
             return;
         }
 
-        if (!maybeCreateSyncApplier()) {
+        if (!checkPipMenuState()) {
             return;
         }
 
-        SurfaceControl surfaceControl = getSurfaceControl();
-        SurfaceParams params = new SurfaceParams.Builder(surfaceControl)
-                .withWindowCrop(destinationBounds)
-                .build();
+        final SurfaceControl surfaceControl = getSurfaceControl();
+        final SurfaceControl.Transaction menuTx =
+                mSurfaceControlTransactionFactory.getTransaction();
+        menuTx.setCrop(surfaceControl, destinationBounds);
         if (pipLeash != null && t != null) {
-            SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash)
-                    .withMergeTransaction(t)
-                    .build();
-            mApplier.scheduleApply(params, pipParams);
-        } else {
-            mApplier.scheduleApply(params);
+            // Merge the two transactions, vsyncId has been set on menuTx.
+            menuTx.merge(t);
         }
+        menuTx.apply();
     }
 
-    private boolean maybeCreateSyncApplier() {
+    private boolean checkPipMenuState() {
         if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: Not going to move PiP, either menu or its parent is not created.", TAG);
             return false;
         }
 
-        if (mApplier == null) {
-            mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView);
-        }
-
-        return mApplier != null;
+        return true;
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
index 0f3ff36..8e3376f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
@@ -146,11 +146,8 @@
                     "%s: Failed to create input consumer, %s", TAG, e);
         }
         mMainExecutor.execute(() -> {
-            // Choreographer.getSfInstance() must be called on the thread that the input event
-            // receiver should be receiving events
-            // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions
             mInputEventReceiver = new InputEventReceiver(inputChannel,
-                Looper.myLooper(), Choreographer.getSfInstance());
+                Looper.myLooper(), Choreographer.getInstance());
             if (mRegistrationListener != null) {
                 mRegistrationListener.onRegistrationChanged(true /* isRegistered */);
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
index abf1a95..89d85e4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
@@ -625,8 +625,7 @@
 
     class PipResizeInputEventReceiver extends BatchedInputEventReceiver {
         PipResizeInputEventReceiver(InputChannel channel, Looper looper) {
-            // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions
-            super(channel, looper, Choreographer.getSfInstance());
+            super(channel, looper, Choreographer.getInstance());
         }
 
         public void onInputEvent(InputEvent event) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index b296151..93c7529 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -46,6 +46,8 @@
             Consts.TAG_WM_SHELL),
     WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
+    WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+            Consts.TAG_WM_SHELL),
     TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");
 
     private final boolean mEnabled;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 98b5ee9..dc3deb1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -34,6 +34,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.DesktopModeConstants;
 
 /**
  * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
@@ -163,7 +164,13 @@
         View caption = mResult.mRootView.findViewById(R.id.caption);
         caption.setOnTouchListener(mOnCaptionTouchListener);
         View maximize = caption.findViewById(R.id.maximize_window);
-        maximize.setOnClickListener(mOnCaptionButtonClickListener);
+        if (DesktopModeConstants.IS_FEATURE_ENABLED) {
+            // Hide maximize button when desktop mode is available
+            maximize.setVisibility(View.GONE);
+        } else {
+            maximize.setVisibility(View.VISIBLE);
+            maximize.setOnClickListener(mOnCaptionButtonClickListener);
+        }
         View close = caption.findViewById(R.id.close_window);
         close.setOnClickListener(mOnCaptionButtonClickListener);
         View minimize = caption.findViewById(R.id.minimize_window);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 506a4c0..5e64a06 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -248,7 +248,7 @@
         lp.setTrustedOverlay();
         if (mViewHost == null) {
             mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay,
-                    mCaptionWindowManager, true);
+                    mCaptionWindowManager);
             mViewHost.setView(outResult.mRootView, lp);
         } else {
             mViewHost.relayout(lp);
@@ -345,9 +345,8 @@
     }
 
     interface SurfaceControlViewHostFactory {
-        default SurfaceControlViewHost create(
-                Context c, Display d, WindowlessWindowManager wmm, boolean useSfChoreographer) {
-            return new SurfaceControlViewHost(c, d, wmm, useSfChoreographer);
+        default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
+            return new SurfaceControlViewHost(c, d, wmm);
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index f865649..b29c436 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -16,9 +16,11 @@
 
 package com.android.wm.shell;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -30,6 +32,8 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
@@ -38,9 +42,11 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
+import android.app.WindowConfiguration;
 import android.content.LocusId;
 import android.content.pm.ParceledListSlice;
 import android.os.Binder;
@@ -53,6 +59,8 @@
 import android.window.ITaskOrganizerController;
 import android.window.TaskAppearedInfo;
 import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransaction.Change;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -628,6 +636,71 @@
         verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token);
     }
 
+    @Test
+    public void testPrepareClearBoundsForTasks() {
+        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED);
+        task1.displayId = 1;
+        MockToken token1 = new MockToken();
+        task1.token = token1.token();
+        mOrganizer.onTaskAppeared(task1, null);
+
+        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED);
+        task2.displayId = 1;
+        MockToken token2 = new MockToken();
+        task2.token = token2.token();
+        mOrganizer.onTaskAppeared(task2, null);
+
+        RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_UNDEFINED);
+        otherDisplayTask.displayId = 2;
+        MockToken otherDisplayToken = new MockToken();
+        otherDisplayTask.token = otherDisplayToken.token();
+        mOrganizer.onTaskAppeared(otherDisplayTask, null);
+
+        WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForTasks(1);
+
+        assertEquals(wct.getChanges().size(), 2);
+        Change boundsChange1 = wct.getChanges().get(token1.binder());
+        assertNotNull(boundsChange1);
+        assertNotEquals(
+                (boundsChange1.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
+        assertTrue(boundsChange1.getConfiguration().windowConfiguration.getBounds().isEmpty());
+
+        Change boundsChange2 = wct.getChanges().get(token2.binder());
+        assertNotNull(boundsChange2);
+        assertNotEquals(
+                (boundsChange2.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
+        assertTrue(boundsChange2.getConfiguration().windowConfiguration.getBounds().isEmpty());
+    }
+
+    @Test
+    public void testPrepareClearFreeformForTasks() {
+        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM);
+        task1.displayId = 1;
+        MockToken token1 = new MockToken();
+        task1.token = token1.token();
+        mOrganizer.onTaskAppeared(task1, null);
+
+        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
+        task2.displayId = 1;
+        MockToken token2 = new MockToken();
+        task2.token = token2.token();
+        mOrganizer.onTaskAppeared(task2, null);
+
+        RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_FREEFORM);
+        otherDisplayTask.displayId = 2;
+        MockToken otherDisplayToken = new MockToken();
+        otherDisplayTask.token = otherDisplayToken.token();
+        mOrganizer.onTaskAppeared(otherDisplayTask, null);
+
+        WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForTasks(1);
+
+        // Only task with freeform windowing mode and the right display should be updated
+        assertEquals(wct.getChanges().size(), 1);
+        Change wmModeChange1 = wct.getChanges().get(token1.binder());
+        assertNotNull(wmModeChange1);
+        assertEquals(wmModeChange1.getWindowingMode(), WINDOWING_MODE_UNDEFINED);
+    }
+
     private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
@@ -635,4 +708,22 @@
         return taskInfo;
     }
 
+    private static class MockToken {
+        private final WindowContainerToken mToken;
+        private final IBinder mBinder;
+
+        MockToken() {
+            mToken = mock(WindowContainerToken.class);
+            mBinder = mock(IBinder.class);
+            when(mToken.asBinder()).thenReturn(mBinder);
+        }
+
+        WindowContainerToken token() {
+            return mToken;
+        }
+
+        IBinder binder() {
+            return mBinder;
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
new file mode 100644
index 0000000..58f20da
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.WindowConfiguration;
+import android.os.Handler;
+import android.os.IBinder;
+import android.testing.AndroidTestingRunner;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransaction.Change;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.RootDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DesktopModeControllerTest extends ShellTestCase {
+
+    @Mock
+    private ShellTaskOrganizer mShellTaskOrganizer;
+    @Mock
+    private RootDisplayAreaOrganizer mRootDisplayAreaOrganizer;
+    @Mock
+    private ShellExecutor mTestExecutor;
+    @Mock
+    private Handler mMockHandler;
+
+    private DesktopModeController mController;
+    private ShellInit mShellInit;
+
+    @Before
+    public void setUp() {
+        mShellInit = Mockito.spy(new ShellInit(mTestExecutor));
+
+        mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer,
+                mRootDisplayAreaOrganizer, mMockHandler);
+
+        mShellInit.init();
+    }
+
+    @Test
+    public void instantiate_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
+    public void testDesktopModeEnabled_taskWmClearedDisplaySetToFreeform() {
+        // Create a fake WCT to simulate setting task windowing mode to undefined
+        WindowContainerTransaction taskWct = new WindowContainerTransaction();
+        MockToken taskMockToken = new MockToken();
+        taskWct.setWindowingMode(taskMockToken.token(), WINDOWING_MODE_UNDEFINED);
+        when(mShellTaskOrganizer.prepareClearFreeformForTasks(mContext.getDisplayId())).thenReturn(
+                taskWct);
+
+        // Create a fake WCT to simulate setting display windowing mode to freeform
+        WindowContainerTransaction displayWct = new WindowContainerTransaction();
+        MockToken displayMockToken = new MockToken();
+        displayWct.setWindowingMode(displayMockToken.token(), WINDOWING_MODE_FREEFORM);
+        when(mRootDisplayAreaOrganizer.prepareWindowingModeChange(mContext.getDisplayId(),
+                WINDOWING_MODE_FREEFORM)).thenReturn(displayWct);
+
+        // The test
+        mController.updateDesktopModeEnabled(true);
+
+        ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+                WindowContainerTransaction.class);
+        verify(mRootDisplayAreaOrganizer).applyTransaction(arg.capture());
+
+        // WCT should have 2 changes - clear task wm mode and set display wm mode
+        WindowContainerTransaction wct = arg.getValue();
+        assertThat(wct.getChanges()).hasSize(2);
+
+        // Verify executed WCT has a change for setting task windowing mode to undefined
+        Change taskWmModeChange = wct.getChanges().get(taskMockToken.binder());
+        assertThat(taskWmModeChange).isNotNull();
+        assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+
+        // Verify executed WCT has a change for setting display windowing mode to freeform
+        Change displayWmModeChange = wct.getChanges().get(displayMockToken.binder());
+        assertThat(displayWmModeChange).isNotNull();
+        assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM);
+    }
+
+    @Test
+    public void testDesktopModeDisabled_taskWmAndBoundsClearedDisplaySetToFullscreen() {
+        // Create a fake WCT to simulate setting task windowing mode to undefined
+        WindowContainerTransaction taskWmWct = new WindowContainerTransaction();
+        MockToken taskWmMockToken = new MockToken();
+        taskWmWct.setWindowingMode(taskWmMockToken.token(), WINDOWING_MODE_UNDEFINED);
+        when(mShellTaskOrganizer.prepareClearFreeformForTasks(mContext.getDisplayId())).thenReturn(
+                taskWmWct);
+
+        // Create a fake WCT to simulate clearing task bounds
+        WindowContainerTransaction taskBoundsWct = new WindowContainerTransaction();
+        MockToken taskBoundsMockToken = new MockToken();
+        taskBoundsWct.setBounds(taskBoundsMockToken.token(), null);
+        when(mShellTaskOrganizer.prepareClearBoundsForTasks(mContext.getDisplayId())).thenReturn(
+                taskBoundsWct);
+
+        // Create a fake WCT to simulate setting display windowing mode to fullscreen
+        WindowContainerTransaction displayWct = new WindowContainerTransaction();
+        MockToken displayMockToken = new MockToken();
+        displayWct.setWindowingMode(displayMockToken.token(), WINDOWING_MODE_FULLSCREEN);
+        when(mRootDisplayAreaOrganizer.prepareWindowingModeChange(mContext.getDisplayId(),
+                WINDOWING_MODE_FULLSCREEN)).thenReturn(displayWct);
+
+        // The test
+        mController.updateDesktopModeEnabled(false);
+
+        ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+                WindowContainerTransaction.class);
+        verify(mRootDisplayAreaOrganizer).applyTransaction(arg.capture());
+
+        // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode
+        WindowContainerTransaction wct = arg.getValue();
+        assertThat(wct.getChanges()).hasSize(3);
+
+        // Verify executed WCT has a change for setting task windowing mode to undefined
+        Change taskWmModeChange = wct.getChanges().get(taskWmMockToken.binder());
+        assertThat(taskWmModeChange).isNotNull();
+        assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+
+        // Verify executed WCT has a change for clearing task bounds
+        Change taskBoundsChange = wct.getChanges().get(taskBoundsMockToken.binder());
+        assertThat(taskBoundsChange).isNotNull();
+        assertThat(taskBoundsChange.getWindowSetMask()
+                & WindowConfiguration.WINDOW_CONFIG_BOUNDS).isNotEqualTo(0);
+        assertThat(taskBoundsChange.getConfiguration().windowConfiguration.getBounds().isEmpty())
+                .isTrue();
+
+        // Verify executed WCT has a change for setting display windowing mode to fullscreen
+        Change displayWmModeChange = wct.getChanges().get(displayMockToken.binder());
+        assertThat(displayWmModeChange).isNotNull();
+        assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
+    }
+
+    private static class MockToken {
+        private final WindowContainerToken mToken;
+        private final IBinder mBinder;
+
+        MockToken() {
+            mToken = mock(WindowContainerToken.class);
+            mBinder = mock(IBinder.class);
+            when(mToken.asBinder()).thenReturn(mBinder);
+        }
+
+        WindowContainerToken token() {
+            return mToken;
+        }
+
+        IBinder binder() {
+            return mBinder;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 226843e..e11be31 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -24,7 +24,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
@@ -107,7 +106,7 @@
         mMockSurfaceControlFinishT = createMockSurfaceControlTransaction();
 
         doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory)
-                .create(any(), any(), any(), anyBoolean());
+                .create(any(), any(), any());
     }
 
     @Test
@@ -148,8 +147,7 @@
 
         verify(decorContainerSurfaceBuilder, never()).build();
         verify(taskBackgroundSurfaceBuilder, never()).build();
-        verify(mMockSurfaceControlViewHostFactory, never())
-                .create(any(), any(), any(), anyBoolean());
+        verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any());
 
         verify(mMockSurfaceControlFinishT).hide(taskSurface);
 
@@ -207,8 +205,7 @@
         verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface, -1);
         verify(mMockSurfaceControlStartT).show(taskBackgroundSurface);
 
-        verify(mMockSurfaceControlViewHostFactory)
-                .create(any(), eq(defaultDisplay), any(), anyBoolean());
+        verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
         verify(mMockSurfaceControlViewHost)
                 .setView(same(mMockView),
                         argThat(lp -> lp.height == 64
@@ -326,8 +323,7 @@
         verify(mMockDisplayController).removeDisplayWindowListener(same(listener));
 
         assertThat(mRelayoutResult.mRootView).isSameInstanceAs(mMockView);
-        verify(mMockSurfaceControlViewHostFactory)
-                .create(any(), eq(mockDisplay), any(), anyBoolean());
+        verify(mMockSurfaceControlViewHostFactory).create(any(), eq(mockDisplay), any());
         verify(mMockSurfaceControlViewHost).setView(same(mMockView), any());
     }
 
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 11cb9c1..0e60caa 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1125,7 +1125,7 @@
     <!-- [CHAR_LIMIT=40] Label for battery level chart when charging with duration -->
     <string name="power_charging_duration"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="time">%2$s</xliff:g> left until full</string>
     <!-- [CHAR_LIMIT=80] Label for battery level chart when charge been limited -->
-    <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging temporarily limited</string>
+    <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging is paused</string>
 
     <!-- Battery Info screen. Value for a status item.  Used for diagnostic info screens, precise translation isn't needed -->
     <string name="battery_info_status_unknown">Unknown</string>
@@ -1138,7 +1138,7 @@
     <!-- [CHAR_LIMIT=20] Battery use screen.  Battery status shown in chart label when charging wirelessly.  -->
     <string name="battery_info_status_charging_wireless">Charging wirelessly</string>
     <!-- [CHAR_LIMIT=20] Battery use screen.  Battery status shown in chart label when the device is dock charging.  -->
-    <string name="battery_info_status_charging_dock">Charging Dock</string>
+    <string name="battery_info_status_charging_dock">Charging</string>
     <!-- Battery Info screen. Value for a status item.  Used for diagnostic info screens, precise translation isn't needed -->
     <string name="battery_info_status_discharging">Not charging</string>
     <!-- Battery Info screen. Value for a status item. A state which device is connected with any charger(e.g. USB, Adapter or Wireless) but not charging yet. Used for diagnostic info screens, precise translation isn't needed -->
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index d1f10a6..86d2eb8 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -182,6 +182,7 @@
         Settings.Secure.PEOPLE_STRIP,
         Settings.Secure.MEDIA_CONTROLS_RESUME,
         Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+        Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
         Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
         Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
         Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 4aadf72..6a70230 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -277,6 +277,7 @@
         VALIDATORS.put(Secure.PEOPLE_STRIP, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.MEDIA_CONTROLS_RESUME, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.MEDIA_CONTROLS_RECOMMENDATION, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.MEDIA_CONTROLS_LOCK_SCREEN, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
                 new InclusiveIntegerRangeValidator(
                         Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN,
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
index 40218de..325ede6 100644
--- a/packages/SystemUI/compose/features/Android.bp
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -30,6 +30,7 @@
     ],
 
     static_libs: [
+        "SystemUI-core",
         "SystemUIComposeCore",
 
         "androidx.compose.runtime_runtime",
diff --git a/packages/SystemUI/compose/features/AndroidManifest.xml b/packages/SystemUI/compose/features/AndroidManifest.xml
index 0aea99d..eada40e 100644
--- a/packages/SystemUI/compose/features/AndroidManifest.xml
+++ b/packages/SystemUI/compose/features/AndroidManifest.xml
@@ -16,7 +16,38 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+
     package="com.android.systemui.compose.features">
-
-
+    <application
+        android:name="android.app.Application"
+        android:appComponentFactory="androidx.core.app.AppComponentFactory"
+        tools:replace="android:name,android:appComponentFactory">
+        <!-- Disable providers from SystemUI -->
+        <provider android:name="com.android.systemui.keyguard.KeyguardSliceProvider"
+            android:authorities="com.android.systemui.test.keyguard.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove" />
+        <provider android:name="com.google.android.systemui.keyguard.KeyguardSliceProviderGoogle"
+            android:authorities="com.android.systemui.test.keyguard.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove" />
+        <provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
+            android:authorities="com.android.systemui.test.keyguard.clock.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove" />
+        <provider android:name="com.android.systemui.people.PeopleProvider"
+            android:authorities="com.android.systemui.test.people.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove" />
+        <provider android:name="androidx.core.content.FileProvider"
+            android:authorities="com.android.systemui.test.fileprovider.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove"/>
+    </application>
 </manifest>
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
new file mode 100644
index 0000000..2bf1937
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.people.ui.compose
+
+import android.annotation.StringRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import kotlinx.coroutines.flow.collect
+
+/**
+ * Compose the screen associated to a [PeopleViewModel].
+ *
+ * @param viewModel the [PeopleViewModel] that should be composed.
+ * @param onResult the callback called with the result of this screen. Callers should usually finish
+ * the Activity/Fragment/View hosting this Composable once a result is available.
+ */
+@Composable
+fun PeopleScreen(
+    viewModel: PeopleViewModel,
+    onResult: (PeopleViewModel.Result) -> Unit,
+) {
+    val priorityTiles by viewModel.priorityTiles.collectAsState()
+    val recentTiles by viewModel.recentTiles.collectAsState()
+
+    // Make sure to refresh the tiles/conversations when the lifecycle is resumed, so that it
+    // updates them when going back to the Activity after leaving it.
+    val lifecycleOwner = LocalLifecycleOwner.current
+    LaunchedEffect(lifecycleOwner, viewModel) {
+        lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+            viewModel.onTileRefreshRequested()
+        }
+    }
+
+    // Call [onResult] this activity when the ViewModel tells us so.
+    LaunchedEffect(viewModel.result) {
+        viewModel.result.collect { result ->
+            if (result != null) {
+                viewModel.clearResult()
+                onResult(result)
+            }
+        }
+    }
+
+    // Make sure to use the Android colors and not the default Material3 colors to have the exact
+    // same colors as the View implementation.
+    val androidColors = LocalAndroidColorScheme.current
+    Surface(
+        color = androidColors.colorBackground,
+        contentColor = androidColors.textColorPrimary,
+        modifier = Modifier.fillMaxSize(),
+    ) {
+        if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
+            PeopleScreenWithConversations(priorityTiles, recentTiles, viewModel::onTileClicked)
+        } else {
+            PeopleScreenEmpty(viewModel::onUserJourneyCancelled)
+        }
+    }
+}
+
+@Composable
+private fun PeopleScreenWithConversations(
+    priorityTiles: List<PeopleTileViewModel>,
+    recentTiles: List<PeopleTileViewModel>,
+    onTileClicked: (PeopleTileViewModel) -> Unit,
+) {
+    Column {
+        Column(
+            Modifier.fillMaxWidth().padding(PeopleSpacePadding),
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Text(
+                stringResource(R.string.select_conversation_title),
+                style = MaterialTheme.typography.headlineSmall,
+                textAlign = TextAlign.Center,
+            )
+
+            Spacer(Modifier.height(24.dp))
+
+            Text(
+                stringResource(R.string.select_conversation_text),
+                Modifier.padding(horizontal = 24.dp),
+                style = MaterialTheme.typography.bodyLarge,
+                textAlign = TextAlign.Center,
+            )
+        }
+
+        LazyColumn(
+            Modifier.fillMaxWidth(),
+            contentPadding =
+                PaddingValues(
+                    top = 16.dp,
+                    bottom = PeopleSpacePadding,
+                    start = 8.dp,
+                    end = 8.dp,
+                )
+        ) {
+            ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked)
+            item { Spacer(Modifier.height(35.dp)) }
+            ConversationList(R.string.recent_conversations, recentTiles, onTileClicked)
+        }
+    }
+}
+
+private fun LazyListScope.ConversationList(
+    @StringRes headerTextResource: Int,
+    tiles: List<PeopleTileViewModel>,
+    onTileClicked: (PeopleTileViewModel) -> Unit
+) {
+    item {
+        Text(
+            stringResource(headerTextResource),
+            Modifier.padding(start = 16.dp),
+            style = MaterialTheme.typography.labelLarge,
+            color = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
+        )
+
+        Spacer(Modifier.height(10.dp))
+    }
+
+    tiles.forEachIndexed { index, tile ->
+        if (index > 0) {
+            item {
+                Divider(
+                    color = LocalAndroidColorScheme.current.colorBackground,
+                    thickness = 2.dp,
+                )
+            }
+        }
+
+        item(tile.key.toString()) {
+            Tile(
+                tile,
+                onTileClicked,
+                withTopCornerRadius = index == 0,
+                withBottomCornerRadius = index == tiles.lastIndex,
+            )
+        }
+    }
+}
+
+@Composable
+private fun Tile(
+    tile: PeopleTileViewModel,
+    onTileClicked: (PeopleTileViewModel) -> Unit,
+    withTopCornerRadius: Boolean,
+    withBottomCornerRadius: Boolean,
+) {
+    val androidColors = LocalAndroidColorScheme.current
+    val cornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
+    val topCornerRadius = if (withTopCornerRadius) cornerRadius else 0.dp
+    val bottomCornerRadius = if (withBottomCornerRadius) cornerRadius else 0.dp
+
+    Surface(
+        color = androidColors.colorSurface,
+        contentColor = androidColors.textColorPrimary,
+        shape =
+            RoundedCornerShape(
+                topStart = topCornerRadius,
+                topEnd = topCornerRadius,
+                bottomStart = bottomCornerRadius,
+                bottomEnd = bottomCornerRadius,
+            ),
+    ) {
+        Row(
+            Modifier.fillMaxWidth().clickable { onTileClicked(tile) }.padding(12.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            Image(
+                tile.icon.asImageBitmap(),
+                // TODO(b/238993727): Add a content description.
+                contentDescription = null,
+                Modifier.size(dimensionResource(R.dimen.avatar_size_for_medium)),
+            )
+
+            Text(
+                tile.username ?: "",
+                Modifier.padding(horizontal = 16.dp),
+                style = MaterialTheme.typography.titleLarge,
+            )
+        }
+    }
+}
+
+/** The padding applied to the PeopleSpace screen. */
+internal val PeopleSpacePadding = 24.dp
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
new file mode 100644
index 0000000..5c9358f
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.people.ui.compose
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.android.systemui.R
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+
+@Composable
+internal fun PeopleScreenEmpty(
+    onGotItClicked: () -> Unit,
+) {
+    Column(
+        Modifier.fillMaxSize().padding(PeopleSpacePadding),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        Text(
+            stringResource(R.string.select_conversation_title),
+            style = MaterialTheme.typography.headlineSmall,
+            textAlign = TextAlign.Center,
+        )
+
+        Spacer(Modifier.height(50.dp))
+
+        Text(
+            stringResource(R.string.no_conversations_text),
+            style = MaterialTheme.typography.bodyLarge,
+            textAlign = TextAlign.Center,
+        )
+
+        Spacer(Modifier.weight(1f))
+        ExampleTile()
+        Spacer(Modifier.weight(1f))
+
+        val androidColors = LocalAndroidColorScheme.current
+        Button(
+            onGotItClicked,
+            Modifier.fillMaxWidth().defaultMinSize(minHeight = 56.dp),
+            colors =
+                ButtonDefaults.buttonColors(
+                    containerColor = androidColors.colorAccentPrimary,
+                    contentColor = androidColors.textColorOnAccent,
+                )
+        ) { Text(stringResource(R.string.got_it)) }
+    }
+}
+
+@Composable
+private fun ExampleTile() {
+    val androidColors = LocalAndroidColorScheme.current
+    Surface(
+        shape = RoundedCornerShape(28.dp),
+        color = androidColors.colorSurface,
+        contentColor = androidColors.textColorPrimary,
+    ) {
+        Row(
+            Modifier.padding(vertical = 20.dp, horizontal = 16.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            Column(horizontalAlignment = Alignment.CenterHorizontally) {
+                // TODO(b/238993727): Add a content description.
+                Image(
+                    painterResource(R.drawable.ic_avatar_with_badge),
+                    contentDescription = null,
+                    Modifier.size(40.dp),
+                )
+                Spacer(Modifier.height(2.dp))
+                Text(
+                    stringResource(R.string.empty_user_name),
+                    style = MaterialTheme.typography.labelMedium,
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis,
+                )
+            }
+
+            Spacer(Modifier.width(24.dp))
+
+            Text(
+                stringResource(R.string.empty_status),
+                style = MaterialTheme.typography.labelMedium,
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/gallery/Android.bp b/packages/SystemUI/compose/gallery/Android.bp
index 40504dc..b0f5cc1 100644
--- a/packages/SystemUI/compose/gallery/Android.bp
+++ b/packages/SystemUI/compose/gallery/Android.bp
@@ -27,6 +27,7 @@
 
     srcs: [
         "src/**/*.kt",
+        ":SystemUI-tests-utils",
     ],
 
     resource_dirs: [
@@ -45,6 +46,14 @@
         "androidx.navigation_navigation-compose",
 
         "androidx.appcompat_appcompat",
+
+        // TODO(b/240431193): Remove the dependencies and depend on
+        // SystemUI-test-utils directly.
+        "androidx.test.runner",
+        "mockito-target-extended-minus-junit4",
+        "testables",
+        "truth-prebuilt",
+        "androidx.test.uiautomator",
     ],
 
     kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
index c341867..bb98fb3 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
@@ -13,7 +13,7 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
@@ -33,6 +33,28 @@
     val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
     val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
 
+    val PeopleEmpty =
+        ChildScreen("people_empty") { navController ->
+            EmptyPeopleScreen(onResult = { navController.popBackStack() })
+        }
+    val PeopleFew =
+        ChildScreen("people_few") { navController ->
+            FewPeopleScreen(onResult = { navController.popBackStack() })
+        }
+    val PeopleFull =
+        ChildScreen("people_full") { navController ->
+            FullPeopleScreen(onResult = { navController.popBackStack() })
+        }
+    val People =
+        ParentScreen(
+            "people",
+            mapOf(
+                "Empty" to PeopleEmpty,
+                "Few" to PeopleFew,
+                "Full" to PeopleFull,
+            )
+        )
+
     val Home =
         ParentScreen(
             "home",
@@ -41,20 +63,21 @@
                 "Material colors" to MaterialColors,
                 "Android colors" to AndroidColors,
                 "Example feature" to ExampleFeature,
+                "People" to People,
             )
         )
 }
 
 /** The main content of the app, that shows [GalleryAppScreens.Home] by default. */
 @Composable
-private fun MainContent() {
+private fun MainContent(onControlToggleRequested: () -> Unit) {
     Box(Modifier.fillMaxSize()) {
         val navController = rememberNavController()
         NavHost(
             navController = navController,
             startDestination = GalleryAppScreens.Home.identifier,
         ) {
-            screen(GalleryAppScreens.Home, navController)
+            screen(GalleryAppScreens.Home, navController, onControlToggleRequested)
         }
     }
 }
@@ -69,7 +92,7 @@
     onChangeTheme: () -> Unit,
 ) {
     val systemFontScale = LocalDensity.current.fontScale
-    var fontScale: FontScale by remember {
+    var fontScale: FontScale by rememberSaveable {
         mutableStateOf(
             FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal
         )
@@ -87,7 +110,7 @@
     }
 
     val systemLayoutDirection = LocalLayoutDirection.current
-    var layoutDirection by remember { mutableStateOf(systemLayoutDirection) }
+    var layoutDirection by rememberSaveable { mutableStateOf(systemLayoutDirection) }
     val onChangeLayoutDirection = {
         layoutDirection =
             when (layoutDirection) {
@@ -105,19 +128,24 @@
                 Modifier.fillMaxSize(),
                 color = MaterialTheme.colorScheme.background,
             ) {
-                Column(Modifier.fillMaxSize().systemBarsPadding().padding(16.dp)) {
-                    ConfigurationControls(
-                        theme,
-                        fontScale,
-                        layoutDirection,
-                        onChangeTheme,
-                        onChangeLayoutDirection,
-                        onChangeFontScale,
-                    )
+                Column(Modifier.fillMaxSize().systemBarsPadding()) {
+                    var showControls by rememberSaveable { mutableStateOf(true) }
 
-                    Spacer(Modifier.height(4.dp))
+                    if (showControls) {
+                        ConfigurationControls(
+                            theme,
+                            fontScale,
+                            layoutDirection,
+                            onChangeTheme,
+                            onChangeLayoutDirection,
+                            onChangeFontScale,
+                            Modifier.padding(horizontal = 16.dp),
+                        )
 
-                    MainContent()
+                        Spacer(Modifier.height(4.dp))
+                    }
+
+                    MainContent(onControlToggleRequested = { showControls = !showControls })
                 }
             }
         }
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt
new file mode 100644
index 0000000..2f0df77
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.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.compose.gallery
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import com.android.systemui.people.emptyPeopleSpaceViewModel
+import com.android.systemui.people.fewPeopleSpaceViewModel
+import com.android.systemui.people.fullPeopleSpaceViewModel
+import com.android.systemui.people.ui.compose.PeopleScreen
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+@Composable
+fun EmptyPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
+    val context = LocalContext.current.applicationContext
+    val viewModel = emptyPeopleSpaceViewModel(context)
+    PeopleScreen(viewModel, onResult)
+}
+
+@Composable
+fun FewPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
+    val context = LocalContext.current.applicationContext
+    val viewModel = fewPeopleSpaceViewModel(context)
+    PeopleScreen(viewModel, onResult)
+}
+
+@Composable
+fun FullPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
+    val context = LocalContext.current.applicationContext
+    val viewModel = fullPeopleSpaceViewModel(context)
+    PeopleScreen(viewModel, onResult)
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
index 467dac04..d7d0d72 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
@@ -52,17 +52,29 @@
 ) : Screen(identifier)
 
 /** Create the navigation graph for [screen]. */
-fun NavGraphBuilder.screen(screen: Screen, navController: NavController) {
+fun NavGraphBuilder.screen(
+    screen: Screen,
+    navController: NavController,
+    onControlToggleRequested: () -> Unit,
+) {
     when (screen) {
         is ChildScreen -> composable(screen.identifier) { screen.content(navController) }
         is ParentScreen -> {
             val menuRoute = "${screen.identifier}_menu"
             navigation(startDestination = menuRoute, route = screen.identifier) {
                 // The menu to navigate to one of the children screens.
-                composable(menuRoute) { ScreenMenu(screen, navController) }
+                composable(menuRoute) {
+                    ScreenMenu(screen, navController, onControlToggleRequested)
+                }
 
                 // The content of the child screens.
-                screen.children.forEach { (_, child) -> screen(child, navController) }
+                screen.children.forEach { (_, child) ->
+                    screen(
+                        child,
+                        navController,
+                        onControlToggleRequested,
+                    )
+                }
             }
         }
     }
@@ -72,8 +84,27 @@
 private fun ScreenMenu(
     screen: ParentScreen,
     navController: NavController,
+    onControlToggleRequested: () -> Unit,
 ) {
-    LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+    LazyColumn(
+        Modifier.padding(horizontal = 16.dp),
+        verticalArrangement = Arrangement.spacedBy(8.dp),
+    ) {
+        item {
+            Surface(
+                Modifier.fillMaxWidth(),
+                color = MaterialTheme.colorScheme.tertiaryContainer,
+                shape = CircleShape,
+            ) {
+                Column(
+                    Modifier.clickable(onClick = onControlToggleRequested).padding(16.dp),
+                    horizontalAlignment = Alignment.CenterHorizontally,
+                ) {
+                    Text("Toggle controls")
+                }
+            }
+        }
+
         screen.children.forEach { (name, child) ->
             item {
                 Surface(
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt
new file mode 100644
index 0000000..0966c32
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.people
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.drawable.Icon
+import androidx.core.graphics.drawable.toIcon
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.people.data.model.PeopleTileModel
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.people.widget.PeopleTileKey
+
+/** A [PeopleViewModel] that does not have any conversations. */
+fun emptyPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
+    return fakePeopleSpaceViewModel(context, emptyList(), emptyList())
+}
+
+/** A [PeopleViewModel] that has a few conversations. */
+fun fewPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
+    return fakePeopleSpaceViewModel(
+        context,
+        priorityTiles =
+            listOf(
+                fakeTile(context, id = "0", Color.RED, "Priority"),
+                fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
+            ),
+        recentTiles =
+            listOf(
+                fakeTile(context, id = "2", Color.GREEN, "Recent Important", isImportant = true),
+                fakeTile(context, id = "3", Color.CYAN, "Recent DndBlocking", isDndBlocking = true),
+            ),
+    )
+}
+
+/** A [PeopleViewModel] that has a lot of conversations. */
+fun fullPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
+    return fakePeopleSpaceViewModel(
+        context,
+        priorityTiles =
+            listOf(
+                fakeTile(context, id = "0", Color.RED, "Priority"),
+                fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
+                fakeTile(context, id = "2", Color.GREEN, "Priority Important", isImportant = true),
+                fakeTile(
+                    context,
+                    id = "3",
+                    Color.CYAN,
+                    "Priority DndBlocking",
+                    isDndBlocking = true,
+                ),
+                fakeTile(
+                    context,
+                    id = "4",
+                    Color.MAGENTA,
+                    "Priority NewStory Important",
+                    hasNewStory = true,
+                    isImportant = true,
+                ),
+            ),
+        recentTiles =
+            listOf(
+                fakeTile(
+                    context,
+                    id = "5",
+                    Color.RED,
+                    "Recent NewStory DndBlocking",
+                    hasNewStory = true,
+                    isDndBlocking = true,
+                ),
+                fakeTile(
+                    context,
+                    id = "6",
+                    Color.BLUE,
+                    "Recent Important DndBlocking",
+                    isImportant = true,
+                    isDndBlocking = true,
+                ),
+                fakeTile(
+                    context,
+                    id = "7",
+                    Color.GREEN,
+                    "Recent NewStory Important DndBlocking",
+                    hasNewStory = true,
+                    isImportant = true,
+                    isDndBlocking = true,
+                ),
+                fakeTile(context, id = "8", Color.CYAN, "Recent"),
+                fakeTile(context, id = "9", Color.MAGENTA, "Recent"),
+            ),
+    )
+}
+
+private fun fakePeopleSpaceViewModel(
+    @Application context: Context,
+    priorityTiles: List<PeopleTileModel>,
+    recentTiles: List<PeopleTileModel>,
+): PeopleViewModel {
+    return PeopleViewModel(
+        context,
+        FakePeopleTileRepository(priorityTiles, recentTiles),
+        FakePeopleWidgetRepository(),
+    )
+}
+
+private fun fakeTile(
+    @Application context: Context,
+    id: String,
+    iconColor: Int,
+    username: String,
+    hasNewStory: Boolean = false,
+    isImportant: Boolean = false,
+    isDndBlocking: Boolean = false
+): PeopleTileModel {
+    return PeopleTileModel(
+        PeopleTileKey(id, /* userId= */ 0, /* packageName */ ""),
+        username,
+        fakeUserIcon(context, iconColor),
+        hasNewStory,
+        isImportant,
+        isDndBlocking,
+    )
+}
+
+private fun fakeUserIcon(@Application context: Context, color: Int): Icon {
+    val size = context.resources.getDimensionPixelSize(R.dimen.avatar_size_for_medium)
+    val bitmap =
+        Bitmap.createBitmap(
+            size,
+            size,
+            Bitmap.Config.ARGB_8888,
+        )
+    val canvas = Canvas(bitmap)
+    val paint = Paint().apply { this.color = color }
+    val radius = size / 2f
+    canvas.drawCircle(/* cx= */ radius, /* cy= */ radius, /* radius= */ radius, paint)
+    return bitmap.toIcon()
+}
diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index babe924..8fa2204 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -37,8 +37,8 @@
     <!-- When the lock screen is showing and the phone plugged in, and the battery is not fully charged, say that it's wirelessly charging. [CHAR LIMIT=50]  -->
     <string name="keyguard_plugged_in_wireless"><xliff:g id="percentage" example="20%">%s</xliff:g> • Charging wirelessly</string>
 
-    <!-- When the lock screen is showing and the phone plugged in, and the battery is not fully charged, say that it's dock charging. [CHAR LIMIT=50]  -->
-    <string name="keyguard_plugged_in_dock"><xliff:g id="percentage" example="20%">%s</xliff:g> • Charging Dock</string>
+    <!-- When the lock screen is showing and the phone plugged in, and the battery is not fully charged, say that it's charging. [CHAR LIMIT=50]  -->
+    <string name="keyguard_plugged_in_dock"><xliff:g id="percentage" example="20%">%s</xliff:g> • Charging</string>
 
     <!-- When the lock screen is showing and the phone plugged in, and the battery
          is not fully charged, say that it's charging.  -->
@@ -53,7 +53,7 @@
     <string name="keyguard_plugged_in_charging_slowly"><xliff:g id="percentage">%s</xliff:g> • Charging slowly</string>
 
     <!-- When the lock screen is showing and the phone plugged in, and the defend mode is triggered, say that charging is temporarily limited.  -->
-    <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging temporarily limited</string>
+    <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging is paused to protect battery</string>
 
     <!-- On the keyguard screen, when pattern lock is disabled, only tell them to press menu to unlock.  This is shown in small font at the bottom. -->
     <string name="keyguard_instructions_when_pattern_disabled">Press Menu to unlock.</string>
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
index 7a57293..3bf44a4 100644
--- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
@@ -20,10 +20,11 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:fontFamily="@font/clock"
-    android:includeFontPadding="false"
     android:textColor="@android:color/white"
     android:format12Hour="@string/dream_time_complication_12_hr_time_format"
     android:format24Hour="@string/dream_time_complication_24_hr_time_format"
     android:shadowColor="@color/keyguard_shadow_color"
     android:shadowRadius="?attr/shadowRadius"
+    android:fontFeatureSettings="pnum, lnum"
+    android:letterSpacing="0.02"
     android:textSize="@dimen/dream_overlay_complication_clock_time_text_size"/>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f344721..2298584 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1459,7 +1459,7 @@
     <dimen name="dream_overlay_status_bar_extra_margin">16dp</dimen>
 
     <!-- Dream overlay complications related dimensions -->
-    <dimen name="dream_overlay_complication_clock_time_text_size">100sp</dimen>
+    <dimen name="dream_overlay_complication_clock_time_text_size">86sp</dimen>
     <dimen name="dream_overlay_complication_clock_subtitle_text_size">24sp</dimen>
     <dimen name="dream_overlay_complication_preview_text_size">36sp</dimen>
     <dimen name="dream_overlay_complication_preview_icon_padding">28dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 7f3caec..e4fefc7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -884,7 +884,7 @@
     <string name="keyguard_indication_charging_time_slowly"><xliff:g id="percentage">%2$s</xliff:g> • Charging slowly • Full in <xliff:g id="charging_time_left" example="4 hr, 2 min">%1$s</xliff:g></string>
 
     <!-- Indication on the keyguard that is shown when the device is dock charging. [CHAR LIMIT=80]-->
-    <string name="keyguard_indication_charging_time_dock"><xliff:g id="percentage" example="20%">%2$s</xliff:g> • Charging Dock • Full in <xliff:g id="charging_time_left" example="4 hr, 2 min">%1$s</xliff:g></string>
+    <string name="keyguard_indication_charging_time_dock"><xliff:g id="percentage" example="20%">%2$s</xliff:g> • Charging • Full in <xliff:g id="charging_time_left" example="4 hr, 2 min">%1$s</xliff:g></string>
 
     <!-- Related to user switcher --><skip/>
 
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
index 60130e1..47e2d2c 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
@@ -44,9 +44,13 @@
 import platform.test.screenshot.MaterialYouColorsRule
 import platform.test.screenshot.ScreenshotTestRule
 import platform.test.screenshot.getEmulatedDevicePathConfig
+import platform.test.screenshot.matchers.BitmapMatcher
 
 /** A rule for View screenshot diff unit tests. */
-class ViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestRule {
+class ViewScreenshotTestRule(
+    emulationSpec: DeviceEmulationSpec,
+    private val matcher: BitmapMatcher = UnitTestBitmapMatcher
+) : TestRule {
     private val colorsRule = MaterialYouColorsRule()
     private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
     private val screenshotRule =
@@ -59,7 +63,6 @@
             .around(deviceEmulationRule)
             .around(screenshotRule)
             .around(activityRule)
-    private val matcher = UnitTestBitmapMatcher
 
     override fun apply(base: Statement, description: Description): Statement {
         return delegateRule.apply(base, description)
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 2739d59..4aee8aa 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -69,6 +69,8 @@
     private var textAnimator: TextAnimator? = null
     private var onTextAnimatorInitialized: Runnable? = null
 
+    var timeOverrideInMillis: Long? = null
+
     val dozingWeight: Int
         get() = if (useBoldedVersion()) dozingWeightInternal + 100 else dozingWeightInternal
 
@@ -125,7 +127,7 @@
     }
 
     fun refreshTime() {
-        time.timeInMillis = System.currentTimeMillis()
+        time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
         contentDescription = DateFormat.format(descFormat, time)
         val formattedText = DateFormat.format(format, time)
         // Setting text actually triggers a layout pass (because the text view is set to
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 206b8be..9164108 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -88,6 +88,7 @@
 
     private int mClockSwitchYAmount;
     @VisibleForTesting boolean mChildrenAreLaidOut = false;
+    @VisibleForTesting boolean mAnimateOnLayout = true;
 
     public KeyguardClockSwitch(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -305,7 +306,7 @@
         super.onLayout(changed, l, t, r, b);
 
         if (mDisplayedClockSize != null && !mChildrenAreLaidOut) {
-            post(() -> updateClockViews(mDisplayedClockSize == LARGE, /* animate */ true));
+            post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout));
         }
 
         mChildrenAreLaidOut = true;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index cf50f7f..d1589b2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -517,8 +517,6 @@
                                     scaledMajor);
                             Log.v(TAG, "onTouch | finger down: " + touchInfo);
                             mTouchLogTime = mSystemClock.elapsedRealtime();
-                            mPowerManager.userActivity(mSystemClock.uptimeMillis(),
-                                    PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0);
                             handled = true;
                         } else if (sinceLastLog >= MIN_TOUCH_LOG_INTERVAL) {
                             Log.v(TAG, "onTouch | finger move: " + touchInfo);
@@ -846,6 +844,9 @@
             return;
         }
         mLatencyTracker.onActionStart(LatencyTracker.ACTION_UDFPS_ILLUMINATE);
+        // Refresh screen timeout and boost process priority if possible.
+        mPowerManager.userActivity(mSystemClock.uptimeMillis(),
+                PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0);
 
         if (!mOnFingerDown) {
             playStartHaptic();
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java
index 5250d44..7d9f105 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java
@@ -37,7 +37,7 @@
 public interface DreamClockTimeComplicationModule {
     String DREAM_CLOCK_TIME_COMPLICATION_VIEW = "clock_time_complication_view";
     String TAG_WEIGHT = "'wght' ";
-    int WEIGHT = 200;
+    int WEIGHT = 400;
 
     /**
      * Provides the complication view.
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 7299aa5..5849d6a 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -179,6 +179,7 @@
     public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901);
     public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903);
     public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904);
+    public static final UnreleasedFlag MEDIA_DREAM_COMPLICATION = new UnreleasedFlag(905);
 
     // 1000 - dock
     public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING =
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index ca65d12..da5819a 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -90,6 +90,8 @@
 import android.widget.LinearLayout;
 import android.widget.ListPopupWindow;
 import android.widget.TextView;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
 import androidx.lifecycle.Lifecycle;
@@ -155,6 +157,8 @@
     public static final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions";
     public static final String SYSTEM_DIALOG_REASON_DREAM = "dream";
 
+    private static final boolean DEBUG = false;
+
     private static final String TAG = "GlobalActionsDialogLite";
 
     private static final String INTERACTION_JANK_TAG = "global_actions";
@@ -2177,6 +2181,11 @@
 
         protected ViewGroup mContainer;
 
+        private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
+            logOnBackInvocation();
+            dismiss();
+        };
+
         @VisibleForTesting
         protected GestureDetector.SimpleOnGestureListener mGestureListener =
                 new GestureDetector.SimpleOnGestureListener() {
@@ -2221,6 +2230,16 @@
                     }
                 };
 
+
+        // this exists so that we can point it to a mock during Unit Testing
+        private OnBackInvokedDispatcher mOverriddenBackDispatcher;
+
+        // the following method exists so that a Unit Test can supply a `OnBackInvokedDispatcher`
+        @VisibleForTesting
+        void setBackDispatcherOverride(OnBackInvokedDispatcher mockDispatcher) {
+            mOverriddenBackDispatcher = mockDispatcher;
+        }
+
         ActionsDialogLite(Context context, int themeRes, MyAdapter adapter,
                 MyOverflowAdapter overflowAdapter,
                 SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService,
@@ -2254,6 +2273,22 @@
             super.onCreate(savedInstanceState);
             initializeLayout();
             mWindowDimAmount = getWindow().getAttributes().dimAmount;
+            getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                    OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback);
+            if (DEBUG) Log.d(TAG, "OnBackInvokedCallback handler registered");
+        }
+
+        @VisibleForTesting
+        @Override
+        public OnBackInvokedDispatcher getOnBackInvokedDispatcher() {
+            if (mOverriddenBackDispatcher != null) return mOverriddenBackDispatcher;
+            else return super.getOnBackInvokedDispatcher();
+        }
+
+        @Override
+        public void onDetachedFromWindow() {
+            getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mOnBackInvokedCallback);
+            if (DEBUG) Log.d(TAG, "OnBackInvokedCallback handler unregistered");
         }
 
         @Override
@@ -2453,7 +2488,12 @@
         @Override
         public void onBackPressed() {
             super.onBackPressed();
+            logOnBackInvocation();
+        }
+
+        private void logOnBackInvocation() {
             mUiEventLogger.log(GlobalActionsEvent.GA_CLOSE_BACK);
+            if (DEBUG) Log.d(TAG, "onBack invoked");
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index c858bc3..c2a8764 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -84,6 +84,14 @@
         return factory.create("LSShadeTransitionLog", 50);
     }
 
+    /** Provides a logging buffer for Shade messages. */
+    @Provides
+    @SysUISingleton
+    @ShadeLog
+    public static LogBuffer provideShadeLogBuffer(LogBufferFactory factory) {
+        return factory.create("ShadeLog", 500, false);
+    }
+
     /** Provides a logging buffer for all logs related to managing notification sections. */
     @Provides
     @SysUISingleton
@@ -262,7 +270,7 @@
     @SysUISingleton
     @StatusBarConnectivityLog
     public static LogBuffer provideStatusBarConnectivityBuffer(LogBufferFactory factory) {
-        return factory.create("StatusBarConnectivityLog", 64);
+        return factory.create("SbConnectivity", 64);
     }
 
     /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
new file mode 100644
index 0000000..bd0d298
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/** A {@link LogBuffer} for Shade touch handling messages. */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface ShadeLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
index 237b505..32600fb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
@@ -18,19 +18,25 @@
 
 import android.content.Context
 import android.content.res.Configuration
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
 import android.view.View
 import android.view.ViewGroup
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.dagger.MediaModule.KEYGUARD
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.stack.MediaContainerView
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.LargeScreenUtils
+import com.android.systemui.util.settings.SecureSettings
 import javax.inject.Inject
 import javax.inject.Named
 
@@ -43,9 +49,10 @@
     @param:Named(KEYGUARD) private val mediaHost: MediaHost,
     private val bypassController: KeyguardBypassController,
     private val statusBarStateController: SysuiStatusBarStateController,
-    private val notifLockscreenUserManager: NotificationLockscreenUserManager,
     private val context: Context,
-    configurationController: ConfigurationController
+    private val secureSettings: SecureSettings,
+    @Main private val handler: Handler,
+    configurationController: ConfigurationController,
 ) {
 
     init {
@@ -60,6 +67,24 @@
             }
         })
 
+        val settingsObserver: ContentObserver = object : ContentObserver(handler) {
+            override fun onChange(selfChange: Boolean, uri: Uri?) {
+                if (uri == lockScreenMediaPlayerUri) {
+                    allowMediaPlayerOnLockScreen =
+                            secureSettings.getBoolForUser(
+                                    Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                                    true,
+                                    UserHandle.USER_CURRENT
+                            )
+                    refreshMediaPosition()
+                }
+            }
+        }
+        secureSettings.registerContentObserverForUser(
+                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                settingsObserver,
+                UserHandle.USER_ALL)
+
         // First let's set the desired state that we want for this host
         mediaHost.expansion = MediaHostState.EXPANDED
         mediaHost.showsOnlyActiveMedia = true
@@ -101,6 +126,13 @@
     private var splitShadeContainer: ViewGroup? = null
 
     /**
+     * Track the media player setting status on lock screen.
+     */
+    private var allowMediaPlayerOnLockScreen: Boolean = true
+    private val lockScreenMediaPlayerUri =
+            secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
+
+    /**
      * Attaches media container in single pane mode, situated at the top of the notifications list
      */
     fun attachSinglePaneContainer(mediaView: MediaContainerView?) {
@@ -164,7 +196,7 @@
         visible = mediaHost.visible &&
                 !bypassController.bypassEnabled &&
                 keyguardOrUserSwitcher &&
-                notifLockscreenUserManager.shouldShowLockscreenNotifications()
+                allowMediaPlayerOnLockScreen
         if (visible) {
             showMediaPlayer()
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index 9260020..ae4c7c7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -22,7 +22,12 @@
 import android.annotation.IntDef
 import android.content.Context
 import android.content.res.Configuration
+import android.database.ContentObserver
 import android.graphics.Rect
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
 import android.util.Log
 import android.util.MathUtils
 import android.view.View
@@ -33,12 +38,12 @@
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dreams.DreamOverlayStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.NotifPanelEvents
 import com.android.systemui.statusbar.CrossFadeHelper
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
@@ -47,6 +52,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.LargeScreenUtils
 import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.settings.SecureSettings
 import com.android.systemui.util.traceSection
 import javax.inject.Inject
 
@@ -85,15 +91,23 @@
     private val keyguardStateController: KeyguardStateController,
     private val bypassController: KeyguardBypassController,
     private val mediaCarouselController: MediaCarouselController,
-    private val notifLockscreenUserManager: NotificationLockscreenUserManager,
     private val keyguardViewController: KeyguardViewController,
     private val dreamOverlayStateController: DreamOverlayStateController,
     configurationController: ConfigurationController,
     wakefulnessLifecycle: WakefulnessLifecycle,
     panelEventsEvents: NotifPanelEvents,
+    private val secureSettings: SecureSettings,
+    @Main private val handler: Handler,
 ) {
 
     /**
+     * Track the media player setting status on lock screen.
+     */
+    private var allowMediaPlayerOnLockScreen: Boolean = true
+    private val lockScreenMediaPlayerUri =
+            secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
+
+    /**
      * Whether we "skip" QQS during panel expansion.
      *
      * This means that when expanding the panel we go directly to QS. Also when we are on QS and
@@ -521,6 +535,23 @@
                 updateDesiredLocation()
             }
         })
+
+        val settingsObserver: ContentObserver = object : ContentObserver(handler) {
+            override fun onChange(selfChange: Boolean, uri: Uri?) {
+                if (uri == lockScreenMediaPlayerUri) {
+                    allowMediaPlayerOnLockScreen =
+                            secureSettings.getBoolForUser(
+                                    Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                                    true,
+                                    UserHandle.USER_CURRENT
+                            )
+                }
+            }
+        }
+        secureSettings.registerContentObserverForUser(
+                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                settingsObserver,
+                UserHandle.USER_ALL)
     }
 
     private fun updateConfiguration() {
@@ -1036,7 +1067,6 @@
         }
         val onLockscreen = (!bypassController.bypassEnabled &&
             (statusbarState == StatusBarState.KEYGUARD))
-        val allowedOnLockscreen = notifLockscreenUserManager.shouldShowLockscreenNotifications()
         val location = when {
             dreamOverlayActive -> LOCATION_DREAM_OVERLAY
             (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
@@ -1044,7 +1074,7 @@
             !hasActiveMedia -> LOCATION_QS
             onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
             onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
-            onLockscreen && allowedOnLockscreen -> LOCATION_LOCKSCREEN
+            onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
             else -> LOCATION_QQS
         }
         // When we're on lock screen and the player is not active, we should keep it in QS.
@@ -1116,7 +1146,7 @@
         return !statusBarStateController.isDozing &&
                 !keyguardViewController.isBouncerShowing &&
                 statusBarStateController.state == StatusBarState.KEYGUARD &&
-                notifLockscreenUserManager.shouldShowLockscreenNotifications() &&
+                allowMediaPlayerOnLockScreen &&
                 statusBarStateController.isExpanded &&
                 !qsExpanded
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
index e077fed..c544871 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.media.dream;
 
+import static com.android.systemui.flags.Flags.MEDIA_DREAM_COMPLICATION;
+
 import android.content.Context;
 
 import androidx.annotation.NonNull;
@@ -23,6 +25,7 @@
 
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.MediaData;
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.media.SmartspaceMediaData;
@@ -34,7 +37,7 @@
  * the media complication as appropriate
  */
 public class MediaDreamSentinel extends CoreStartable {
-    private MediaDataManager.Listener mListener = new MediaDataManager.Listener() {
+    private final MediaDataManager.Listener mListener = new MediaDataManager.Listener() {
         private boolean mAdded;
         @Override
         public void onSmartspaceMediaDataRemoved(@NonNull String key, boolean immediately) {
@@ -63,6 +66,10 @@
         public void onMediaDataLoaded(@NonNull String key, @Nullable String oldKey,
                 @NonNull MediaData data, boolean immediately, int receivedSmartspaceCardLatency,
                 boolean isSsReactivated) {
+            if (!mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)) {
+                return;
+            }
+
             if (mAdded) {
                 return;
             }
@@ -79,15 +86,18 @@
     private final MediaDataManager mMediaDataManager;
     private final DreamOverlayStateController mDreamOverlayStateController;
     private final MediaDreamComplication mComplication;
+    private final FeatureFlags mFeatureFlags;
 
     @Inject
     public MediaDreamSentinel(Context context, MediaDataManager mediaDataManager,
             DreamOverlayStateController dreamOverlayStateController,
-            MediaDreamComplication complication) {
+            MediaDreamComplication complication,
+            FeatureFlags featureFlags) {
         super(context);
         mMediaDataManager = mediaDataManager;
         mDreamOverlayStateController = dreamOverlayStateController;
         mComplication = complication;
+        mFeatureFlags = featureFlags;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
index 0834a5a..e27bfb3 100644
--- a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
@@ -31,7 +31,6 @@
 import com.android.systemui.people.data.repository.PeopleTileRepository
 import com.android.systemui.people.data.repository.PeopleWidgetRepository
 import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -52,7 +51,7 @@
      * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
      */
     private val _priorityTiles = MutableStateFlow(priorityTiles())
-    val priorityTiles: Flow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
+    val priorityTiles: StateFlow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
 
     /**
      * The list of the priority tiles/conversations.
@@ -61,7 +60,7 @@
      * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
      */
     private val _recentTiles = MutableStateFlow(recentTiles())
-    val recentTiles: Flow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
+    val recentTiles: StateFlow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
 
     /** The ID of the widget currently being edited/added. */
     private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index cf416c5..f283b95 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -448,7 +448,6 @@
      */
     private boolean mQsAnimatorExpand;
     private boolean mIsLaunchTransitionFinished;
-    private boolean mOnlyAffordanceInThisMotion;
     private ValueAnimator mQsSizeChangeAnimator;
 
     private boolean mQsScrimEnabled = true;
@@ -726,6 +725,7 @@
             AccessibilityManager accessibilityManager, @DisplayId int displayId,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             MetricsLogger metricsLogger,
+            ShadeLogger shadeLogger,
             ConfigurationController configurationController,
             Provider<FlingAnimationUtils.Builder> flingAnimationUtilsBuilder,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
@@ -792,6 +792,7 @@
                 panelExpansionStateManager,
                 ambientState,
                 interactionJankMonitor,
+                shadeLogger,
                 systemClock);
         mView = view;
         mVibratorHelper = vibratorHelper;
@@ -1887,6 +1888,8 @@
                 }
                 if (mQsExpansionAnimator != null) {
                     mInitialHeightOnTouch = mQsExpansionHeight;
+                    mShadeLog.logMotionEvent(event,
+                            "onQsIntercept: down action, QS tracking enabled");
                     mQsTracking = true;
                     traceQsJank(true /* startTracing */, false /* wasCancelled */);
                     mNotificationStackScrollLayoutController.cancelLongPress();
@@ -1914,12 +1917,16 @@
                     setQsExpansion(h + mInitialHeightOnTouch);
                     trackMovement(event);
                     return true;
+                } else {
+                    mShadeLog.logMotionEvent(event,
+                            "onQsIntercept: move ignored because qs tracking disabled");
                 }
                 if ((h > getTouchSlop(event) || (h < -getTouchSlop(event) && mQsExpanded))
                         && Math.abs(h) > Math.abs(x - mInitialTouchX)
                         && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
                     if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept - start tracking expansion");
                     mView.getParent().requestDisallowInterceptTouchEvent(true);
+                    mShadeLog.onQsInterceptMoveQsTrackingEnabled(h);
                     mQsTracking = true;
                     traceQsJank(true /* startTracing */, false /* wasCancelled */);
                     onQsExpansionStarted();
@@ -1935,6 +1942,7 @@
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
                 trackMovement(event);
+                mShadeLog.logMotionEvent(event, "onQsIntercept: up action, QS tracking disabled");
                 mQsTracking = false;
                 break;
         }
@@ -1972,7 +1980,6 @@
 
     private void initDownStates(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
-            mOnlyAffordanceInThisMotion = false;
             mQsTouchAboveFalsingThreshold = mQsFullyExpanded;
             mDozingOnDown = isDozing();
             mDownX = event.getX();
@@ -2111,6 +2118,7 @@
                 && collapsedQs && isQsExpansionEnabled();
         if (action == MotionEvent.ACTION_DOWN && expandedShadeCollapsedQs) {
             // Down in the empty area while fully expanded - go to QS.
+            mShadeLog.logMotionEvent(event, "handleQsTouch: down action, QS tracking enabled");
             mQsTracking = true;
             traceQsJank(true /* startTracing */, false /* wasCancelled */);
             mConflictingQsExpansionGesture = true;
@@ -2125,6 +2133,8 @@
         if (!mQsExpandImmediate && mQsTracking) {
             onQsTouch(event);
             if (!mConflictingQsExpansionGesture && !mSplitShadeEnabled) {
+                mShadeLog.logMotionEvent(event,
+                        "handleQsTouch: not immediate expand or conflicting gesture");
                 return true;
             }
         }
@@ -2192,6 +2202,7 @@
                 event.getX(), event.getY(), -1)) {
             if (DEBUG_LOGCAT) Log.d(TAG, "handleQsDown");
             mFalsingCollector.onQsDown();
+            mShadeLog.logMotionEvent(event, "handleQsDown: down action, QS tracking enabled");
             mQsTracking = true;
             onQsExpansionStarted();
             mInitialHeightOnTouch = mQsExpansionHeight;
@@ -2274,6 +2285,7 @@
 
         switch (event.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
+                mShadeLog.logMotionEvent(event, "onQsTouch: down action, QS tracking enabled");
                 mQsTracking = true;
                 traceQsJank(true /* startTracing */, false /* wasCancelled */);
                 mInitialTouchY = y;
@@ -2300,6 +2312,7 @@
 
             case MotionEvent.ACTION_MOVE:
                 if (DEBUG_LOGCAT) Log.d(TAG, "onQSTouch move");
+                mShadeLog.logMotionEvent(event, "onQsTouch: move action, setting QS expansion");
                 setQsExpansion(h + mInitialHeightOnTouch);
                 if (h >= getFalsingThreshold()) {
                     mQsTouchAboveFalsingThreshold = true;
@@ -2309,6 +2322,8 @@
 
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
+                mShadeLog.logMotionEvent(event,
+                        "onQsTouch: up/cancel action, QS tracking disabled");
                 mQsTracking = false;
                 mTrackingPointer = -1;
                 trackMovement(event);
@@ -3131,8 +3146,8 @@
                 positionClockAndNotifications();
             }
         }
-        if (mQsExpandImmediate || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
-                && !mQsExpansionFromOverscroll) {
+        if (mQsExpandImmediate || (mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
+                && !mQsExpansionFromOverscroll)) {
             float t;
             if (mKeyguardShowing) {
 
@@ -4224,6 +4239,7 @@
                         || mPulseExpansionHandler.isExpanding();
                 if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) {
                     // We're expanding all the other ones shouldn't get this anymore
+                    mShadeLog.logMotionEvent(event, "onTouch: PulseExpansionHandler handled event");
                     return true;
                 }
                 if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
@@ -4231,14 +4247,10 @@
                         && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
                     mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
                 }
-                boolean handled = false;
-                if (mOnlyAffordanceInThisMotion) {
-                    return true;
-                }
-                handled |= mHeadsUpTouchHelper.onTouchEvent(event);
+                boolean handled = mHeadsUpTouchHelper.onTouchEvent(event);
 
                 if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
-                    if (DEBUG_LOGCAT) Log.d(TAG, "handleQsTouch true");
+                    mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
                     return true;
                 }
                 if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
@@ -4810,6 +4822,8 @@
                 }
             } else if (!mQsExpanded && mQsExpansionAnimator == null) {
                 setQsExpansion(mQsMinExpansionHeight + mLastOverscroll);
+            } else {
+                mShadeLog.v("onLayoutChange: qs expansion not set");
             }
             updateExpandedHeight(getExpandedHeight());
             updateHeader();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
index 4aad245..73eaa85 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
@@ -202,6 +202,8 @@
     private final InteractionJankMonitor mInteractionJankMonitor;
     protected final SystemClock mSystemClock;
 
+    protected final ShadeLogger mShadeLog;
+
     protected abstract void onExpandingFinished();
 
     protected void onExpandingStarted() {
@@ -242,6 +244,7 @@
             PanelExpansionStateManager panelExpansionStateManager,
             AmbientState ambientState,
             InteractionJankMonitor interactionJankMonitor,
+            ShadeLogger shadeLogger,
             SystemClock systemClock) {
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
@@ -254,6 +257,7 @@
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mLockscreenGestureLogger = lockscreenGestureLogger;
         mPanelExpansionStateManager = panelExpansionStateManager;
+        mShadeLog = shadeLogger;
         TouchHandler touchHandler = createTouchHandler();
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
@@ -1275,9 +1279,16 @@
 
         @Override
         public boolean onTouch(View v, MotionEvent event) {
-            if (mInstantExpanding || (mTouchDisabled
-                    && event.getActionMasked() != MotionEvent.ACTION_CANCEL) || (mMotionAborted
-                    && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+            if (mInstantExpanding) {
+                mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding");
+                return false;
+            }
+            if (mTouchDisabled  && event.getActionMasked() != MotionEvent.ACTION_CANCEL) {
+                mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled");
+                return false;
+            }
+            if (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+                mShadeLog.logMotionEvent(event, "onTouch: non-down action, motion was aborted");
                 return false;
             }
 
@@ -1287,6 +1298,7 @@
                     // Turn off tracking if it's on or the shade can get stuck in the down position.
                     onTrackingStopped(true /* expand */);
                 }
+                mShadeLog.logMotionEvent(event, "onTouch: drag not enabled");
                 return false;
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
new file mode 100644
index 0000000..f1e44ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -0,0 +1,48 @@
+package com.android.systemui.shade
+
+import android.view.MotionEvent
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.LogMessage
+import com.android.systemui.log.dagger.ShadeLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "systemui.shade"
+
+/** Lightweight logging utility for the Shade. */
+class ShadeLogger @Inject constructor(
+    @ShadeLog
+    private val buffer: LogBuffer
+) {
+    fun v(@CompileTimeConstant msg: String) {
+        buffer.log(TAG, LogLevel.VERBOSE, msg)
+    }
+
+    private inline fun log(
+        logLevel: LogLevel,
+        initializer: LogMessage.() -> Unit,
+        noinline printer: LogMessage.() -> String
+    ) {
+        buffer.log(TAG, logLevel, initializer, printer)
+    }
+
+    fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
+        log(LogLevel.VERBOSE,
+            { double1 = h.toDouble() },
+            { "onQsIn[tercept: move action, QS tracking enabled. h = $double1" })
+    }
+
+    fun logMotionEvent(event: MotionEvent, message: String) {
+        log(LogLevel.VERBOSE, {
+            str1 = message
+            long1 = event.eventTime
+            long2 = event.downTime
+            int1 = event.action
+            int2 = event.classification
+            double1 = event.y.toDouble()
+        }, {
+            "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,classification=$int2"
+        })
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt
index 6c02b0d..780a02d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline
 
-import com.android.systemui.statusbar.pipeline.repository.NetworkCapabilityInfo
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo
 import kotlinx.coroutines.flow.StateFlow
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt
index 8d69422..99798f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt
@@ -18,9 +18,9 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.repository.NetworkCapabilitiesRepo
-import kotlinx.coroutines.CoroutineScope
+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
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 1aae250..64c47f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
@@ -20,14 +20,18 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.repository.NetworkCapabilityInfo
+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
@@ -42,12 +46,16 @@
 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.
         @Application private val scope: CoroutineScope,
-        statusBarPipelineFlags: StatusBarPipelineFlags,
+        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()
@@ -60,6 +68,14 @@
                     )
 
     override fun start() {
+        if (!statusBarPipelineFlags.isNewPipelineEnabled()) {
+            return
+        }
+        // TODO(b/238425913): The view binder should do this instead. For now, do it here so we can
+        // see the logs.
+        scope.launch {
+            wifiViewModelProvider.get().isActivityInVisible.collect { }
+        }
     }
 
     private fun RawConnectivityInfo.process(): ProcessedConnectivityInfo {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLogger.kt
deleted file mode 100644
index f88e9d6..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLogger.kt
+++ /dev/null
@@ -1,59 +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.Network
-import android.net.NetworkCapabilities
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.StatusBarConnectivityLog
-import javax.inject.Inject
-
-@SysUISingleton
-class ConnectivityPipelineLogger @Inject constructor(
-    @StatusBarConnectivityLog private val buffer: LogBuffer,
-) {
-    fun logOnCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
-        buffer.log(
-            TAG,
-            LogLevel.INFO,
-            {
-                int1 = network.getNetId()
-                str1 = networkCapabilities.toString()
-            },
-            {
-                "onCapabilitiesChanged: net=$int1 capabilities=$str1"
-            }
-        )
-    }
-
-    fun logOnLost(network: Network) {
-        buffer.log(
-            TAG,
-            LogLevel.INFO,
-            {
-                int1 = network.getNetId()
-            },
-            {
-                "onLost: net=$int1"
-            }
-        )
-    }
-}
-
-private const val TAG = "SbConnectivityPipeline"
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 c4e2b73..7abe19e7b 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
@@ -20,6 +20,8 @@
 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
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.ClassKey
@@ -37,4 +39,7 @@
     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
new file mode 100644
index 0000000..a5fff5e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared
+
+import android.net.Network
+import android.net.NetworkCapabilities
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.StatusBarConnectivityLog
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onEach
+
+@SysUISingleton
+class ConnectivityPipelineLogger @Inject constructor(
+    @StatusBarConnectivityLog private val buffer: LogBuffer,
+) {
+    fun logInputChange(callbackName: String, changeInfo: String) {
+        buffer.log(
+                SB_LOGGING_TAG,
+                LogLevel.INFO,
+                {
+                    str1 = callbackName
+                    str2 = changeInfo
+                },
+                {
+                    "Input: $str1: $str2"
+                }
+        )
+    }
+
+    fun logOutputChange(outputParamName: String, changeInfo: String) {
+        buffer.log(
+                SB_LOGGING_TAG,
+                LogLevel.INFO,
+                {
+                    str1 = outputParamName
+                    str2 = changeInfo
+                },
+                {
+                    "Output: $str1: $str2"
+                }
+        )
+    }
+
+    fun logOnCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+        buffer.log(
+            SB_LOGGING_TAG,
+            LogLevel.INFO,
+            {
+                int1 = network.getNetId()
+                str1 = networkCapabilities.toString()
+            },
+            {
+                "onCapabilitiesChanged: net=$int1 capabilities=$str1"
+            }
+        )
+    }
+
+    fun logOnLost(network: Network) {
+        buffer.log(
+            SB_LOGGING_TAG,
+            LogLevel.INFO,
+            {
+                int1 = network.getNetId()
+            },
+            {
+                "onLost: net=$int1"
+            }
+        )
+    }
+
+    companion object {
+        const val SB_LOGGING_TAG = "SbConnectivity"
+
+        /**
+         * Log a change in one of the **outputs** to the connectivity pipeline.
+         *
+         * @param prettyPrint an optional function to transform the value into a readable string.
+         *   [toString] is used if no custom function is provided.
+         */
+        fun <T : Any> Flow<T>.logOutputChange(
+                logger: ConnectivityPipelineLogger,
+                outputParamName: String,
+                prettyPrint: (T) -> String = { it.toString() }
+        ): Flow<T> {
+            return this.onEach { logger.logOutputChange(outputParamName, prettyPrint(it)) }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
new file mode 100644
index 0000000..44c0496
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
@@ -0,0 +1,27 @@
+/*
+ * 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 on the current wifi activity.
+ */
+data class WifiActivityModel(
+    /** True if the wifi has activity in (download). */
+    val hasActivityIn: Boolean,
+    /** True if the wifi has activity out (upload). */
+    val hasActivityOut: Boolean,
+)
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
new file mode 100644
index 0000000..1b73322
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt
@@ -0,0 +1,29 @@
+/*
+ * 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/repository/NetworkCapabilitiesRepo.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt
similarity index 91%
rename from packages/SystemUI/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepo.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt
index e5980c3..6c0a445 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepo.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt
@@ -16,7 +16,7 @@
 
 @file:OptIn(ExperimentalCoroutinesApi::class)
 
-package com.android.systemui.statusbar.pipeline.repository
+package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
 import android.annotation.SuppressLint
 import android.net.ConnectivityManager
@@ -25,7 +25,7 @@
 import android.net.NetworkRequest
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -35,7 +35,11 @@
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.stateIn
 
-/** Repository that contains all relevant [NetworkCapabilites] for the current networks */
+/**
+ * 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,
@@ -88,5 +92,3 @@
     val network: Network,
     val capabilities: NetworkCapabilities,
 )
-
-private const val TAG = "ConnectivityRepository"
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
new file mode 100644
index 0000000..012dde5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.wifi.WifiManager
+import android.net.wifi.WifiManager.TrafficStateCallback
+import android.util.Log
+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 java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/**
+ * Provides data related to the wifi state.
+ */
+interface WifiRepository {
+    /**
+     * Observable for the current state of wifi; `null` when there is no active wifi.
+     */
+    val wifiModel: Flow<WifiModel?>
+
+    /**
+     * Observable for the current wifi network activity.
+     */
+    val wifiActivity: Flow<WifiActivityModel>
+}
+
+/** Real implementation of [WifiRepository]. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class WifiRepositoryImpl @Inject constructor(
+        wifiManager: WifiManager?,
+        @Main mainExecutor: Executor,
+        logger: ConnectivityPipelineLogger,
+) : WifiRepository {
+
+    // TODO(b/238425913): Actually implement the wifiModel flow.
+    override val wifiModel: Flow<WifiModel?> = flowOf(WifiModel(ssid = "AB"))
+
+    override val wifiActivity: Flow<WifiActivityModel> =
+            if (wifiManager == null) {
+                Log.w(SB_LOGGING_TAG, "Null WifiManager; skipping activity callback")
+                flowOf(ACTIVITY_DEFAULT)
+            } else {
+                conflatedCallbackFlow {
+                    val callback = TrafficStateCallback { state ->
+                        logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
+                        trySend(trafficStateToWifiActivityModel(state))
+                    }
+
+                    wifiManager.registerTrafficStateCallback(mainExecutor, callback)
+                    trySend(ACTIVITY_DEFAULT)
+
+                    awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
+                }
+            }
+
+    companion object {
+        val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+
+        private fun trafficStateToWifiActivityModel(state: Int): WifiActivityModel {
+            return WifiActivityModel(
+                hasActivityIn = state == TrafficStateCallback.DATA_ACTIVITY_IN ||
+                    state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
+                hasActivityOut = state == TrafficStateCallback.DATA_ACTIVITY_OUT ||
+                    state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
+            )
+        }
+
+        private fun prettyPrintActivity(activity: Int): String {
+            return when (activity) {
+                TrafficStateCallback.DATA_ACTIVITY_NONE -> "NONE"
+                TrafficStateCallback.DATA_ACTIVITY_IN -> "IN"
+                TrafficStateCallback.DATA_ACTIVITY_OUT -> "OUT"
+                TrafficStateCallback.DATA_ACTIVITY_INOUT -> "INOUT"
+                else -> "INVALID"
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000..f705399
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.domain.interactor
+
+import android.net.wifi.WifiManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+
+/**
+ * The business logic layer for the wifi icon.
+ *
+ * This interactor processes information from our data layer into information that the UI layer can
+ * use.
+ */
+@SysUISingleton
+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
+        }
+    }
+
+    val hasActivityIn: Flow<Boolean> = combine(repository.wifiActivity, ssid) { activity, ssid ->
+            activity.hasActivityIn && ssid != null
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
new file mode 100644
index 0000000..a19d1bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.shared
+
+import android.content.Context
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * An object storing constants that we use for calculating the wifi icon. Stored in a class for
+ * logging purposes.
+ */
+@SysUISingleton
+class WifiConstants @Inject constructor(
+        context: Context,
+        dumpManager: DumpManager,
+) : Dumpable {
+    init {
+        dumpManager.registerDumpable("$SB_LOGGING_TAG:WifiConstants", this)
+    }
+
+    /** True if we should show the activityIn/activityOut icons and false otherwise. */
+    val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity)
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply {
+            println("shouldShowActivityConfig=$shouldShowActivityConfig")
+        }
+    }
+}
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
new file mode 100644
index 0000000..b990eb7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.viewmodel
+
+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.flowOf
+
+/**
+ * Models the UI state for the status bar wifi icon.
+ *
+ * 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,
+) {
+    val isActivityInVisible: Flow<Boolean>
+        get() =
+            if (!constants.shouldShowActivityConfig) {
+                flowOf(false)
+            } else {
+                interactor.hasActivityIn
+            }
+                .logOutputChange(logger, "activityInVisible")
+}
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 094490b..adef182 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -244,7 +244,8 @@
         final int currentUser = mUserTracker.getUserId();
         final boolean hadWallpaperColors = mCurrentColors.get(userId) != null;
         int latestWallpaperType = getLatestWallpaperType(userId);
-        if ((flags & latestWallpaperType) != 0) {
+        boolean eventForLatestWallpaper = (flags & latestWallpaperType) != 0;
+        if (eventForLatestWallpaper) {
             mCurrentColors.put(userId, wallpaperColors);
             if (DEBUG) Log.d(TAG, "got new colors: " + wallpaperColors + " where: " + flags);
         }
@@ -280,14 +281,19 @@
                 currentUser);
         boolean isDestinationBoth = (flags == (WallpaperManager.FLAG_SYSTEM
                 | WallpaperManager.FLAG_LOCK));
+        boolean isDestinationHomeOnly = (flags == WallpaperManager.FLAG_SYSTEM);
         try {
             JSONObject jsonObject = (overlayPackageJson == null) ? new JSONObject()
                     : new JSONObject(overlayPackageJson);
             // The latest applied wallpaper should be the source of system colors when:
             // There is not preset color applied and the incoming wallpaper color is not applied
-            if (!COLOR_SOURCE_PRESET.equals(jsonObject.optString(OVERLAY_COLOR_SOURCE))
-                    && ((flags & latestWallpaperType) != 0 && !isSeedColorSet(jsonObject,
-                    wallpaperColors))) {
+            String wallpaperPickerColorSource = jsonObject.optString(OVERLAY_COLOR_SOURCE);
+            boolean userChosePresetColor = COLOR_SOURCE_PRESET.equals(wallpaperPickerColorSource);
+            boolean userChoseLockScreenColor = COLOR_SOURCE_LOCK.equals(wallpaperPickerColorSource);
+            boolean preserveLockScreenColor = isDestinationHomeOnly && userChoseLockScreenColor;
+
+            if (!userChosePresetColor && !preserveLockScreenColor && eventForLatestWallpaper
+                    && !isSeedColorSet(jsonObject, wallpaperColors)) {
                 mSkipSettingChange = true;
                 if (jsonObject.has(OVERLAY_CATEGORY_ACCENT_COLOR) || jsonObject.has(
                         OVERLAY_CATEGORY_SYSTEM_PALETTE)) {
@@ -642,7 +648,7 @@
         }
         if (mNeedsOverlayCreation) {
             mNeedsOverlayCreation = false;
-            mThemeManager.applyCurrentUserOverlays(categoryToPackage, new FabricatedOverlay[] {
+            mThemeManager.applyCurrentUserOverlays(categoryToPackage, new FabricatedOverlay[]{
                     mSecondaryOverlay, mNeutralOverlay
             }, currentUser, managedProfiles);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 8f2a432..fc20ac2 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -138,7 +138,7 @@
 
         ensureOverlayRemoved()
 
-        val newRoot = SurfaceControlViewHost(context, context.display!!, wwm, false)
+        val newRoot = SurfaceControlViewHost(context, context.display!!, wwm)
         val newView =
             LightRevealScrim(context, null).apply {
                 revealEffect = createLightRevealEffect()
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index 763a5cb..ba28045 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -53,7 +53,8 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.REGISTER_WINDOW_MANAGER_LISTENERS" />
 
-    <application android:debuggable="true" android:largeHeap="true">
+    <application android:debuggable="true" android:largeHeap="true"
+            android:enableOnBackInvokedCallback="true" >
         <uses-library android:name="android.test.runner" />
 
         <receiver android:name="com.android.systemui.SliceBroadcastRelayHandlerTest$Receiver"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index 141a213..b42b769 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -42,8 +42,11 @@
 import android.testing.TestableLooper;
 import android.view.GestureDetector;
 import android.view.IWindowManager;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.WindowManagerPolicyConstants;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import androidx.test.filters.SmallTest;
 
@@ -73,6 +76,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -117,6 +122,8 @@
     @Mock private CentralSurfaces mCentralSurfaces;
     @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Mock private DialogLaunchAnimator mDialogLaunchAnimator;
+    @Mock private OnBackInvokedDispatcher mOnBackInvokedDispatcher;
+    @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
 
     private TestableLooper mTestableLooper;
 
@@ -203,6 +210,58 @@
     }
 
     @Test
+    public void testPredictiveBackCallbackRegisteredAndUnregistered() {
+        mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+        doReturn(4).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
+        doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any());
+        doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
+        String[] actions = {
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
+        };
+        doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
+
+        GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog();
+        dialog.setBackDispatcherOverride(mOnBackInvokedDispatcher);
+        dialog.create();
+        mTestableLooper.processAllMessages();
+        verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
+                eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), any());
+        dialog.onDetachedFromWindow();
+        mTestableLooper.processAllMessages();
+        verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback(any());
+    }
+
+    @Test
+    public void testPredictiveBackInvocationDismissesDialog() {
+        mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+        doReturn(4).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
+        doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any());
+        doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
+        String[] actions = {
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
+        };
+        doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
+
+        GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog();
+        dialog.create();
+        dialog.show();
+        mTestableLooper.processAllMessages();
+        dialog.getWindow().injectInputEvent(
+                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK));
+        dialog.getWindow().injectInputEvent(
+                new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK));
+        mTestableLooper.processAllMessages();
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_CLOSE_BACK);
+        assertThat(dialog.isShowing()).isFalse();
+    }
+
+    @Test
     public void testSingleTap_logAndDismiss() {
         mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
         doReturn(4).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
index fcfef4a4..c41fac7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
@@ -16,19 +16,22 @@
 
 package com.android.systemui.media
 
+import android.provider.Settings
 import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
 import android.view.View.GONE
 import android.view.View.VISIBLE
 import android.widget.FrameLayout
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.stack.MediaContainerView
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.utils.os.FakeHandler
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertTrue
 import org.junit.Before
@@ -37,11 +40,12 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
 class KeyguardMediaControllerTest : SysuiTestCase() {
 
     @Mock
@@ -53,31 +57,33 @@
     @Mock
     private lateinit var configurationController: ConfigurationController
 
-    @Mock
-    private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
     @JvmField @Rule
     val mockito = MockitoJUnit.rule()
 
     private val mediaContainerView: MediaContainerView = MediaContainerView(context, null)
     private val hostView = UniqueObjectHostView(context)
+    private val settings = FakeSettings()
     private lateinit var keyguardMediaController: KeyguardMediaController
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var fakeHandler: FakeHandler
 
     @Before
     fun setup() {
         // default state is positive, media should show up
         whenever(mediaHost.visible).thenReturn(true)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
-        whenever(notificationLockscreenUserManager.shouldShowLockscreenNotifications())
-                .thenReturn(true)
         whenever(mediaHost.hostView).thenReturn(hostView)
         hostView.layoutParams = FrameLayout.LayoutParams(100, 100)
+        testableLooper = TestableLooper.get(this)
+        fakeHandler = FakeHandler(testableLooper.looper)
         keyguardMediaController = KeyguardMediaController(
             mediaHost,
             bypassController,
             statusBarStateController,
-            notificationLockscreenUserManager,
             context,
-            configurationController
+            settings,
+            fakeHandler,
+            configurationController,
         )
         keyguardMediaController.attachSinglePaneContainer(mediaContainerView)
         keyguardMediaController.useSplitShade = false
@@ -106,9 +112,8 @@
     }
 
     @Test
-    fun testHiddenOnKeyguard_whenNotificationsAreHidden() {
-        whenever(notificationLockscreenUserManager.shouldShowLockscreenNotifications())
-                .thenReturn(false)
+    fun testHiddenOnKeyguard_whenMediaOnLockScreenDisabled() {
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 0)
 
         keyguardMediaController.refreshMediaPosition()
 
@@ -116,6 +121,15 @@
     }
 
     @Test
+    fun testAvailableOnKeyguard_whenMediaOnLockScreenEnabled() {
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1)
+
+        keyguardMediaController.refreshMediaPosition()
+
+        assertThat(mediaContainerView.visibility).isEqualTo(VISIBLE)
+    }
+
+    @Test
     fun testActivatesSplitShadeContainerInSplitShadeMode() {
         val splitShadeContainer = FrameLayout(context)
         keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
index 369913d..18bfd04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media
 
 import android.graphics.Rect
+import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.ViewGroup
@@ -30,7 +31,6 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.testing.FakeNotifPanelEvents
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.KeyguardBypassController
@@ -38,6 +38,8 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.animation.UniqueObjectHostView
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.utils.os.FakeHandler
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertNotNull
 import org.junit.Before
@@ -53,7 +55,6 @@
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
@@ -68,7 +69,6 @@
     @Mock private lateinit var bypassController: KeyguardBypassController
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
-    @Mock private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
     @Mock private lateinit var mediaCarouselController: MediaCarouselController
     @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
     @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
@@ -82,37 +82,42 @@
     @JvmField
     @Rule
     val mockito = MockitoJUnit.rule()
-    private lateinit var mediaHiearchyManager: MediaHierarchyManager
+    private lateinit var mediaHierarchyManager: MediaHierarchyManager
     private lateinit var mediaFrame: ViewGroup
     private val configurationController = FakeConfigurationController()
     private val notifPanelEvents = FakeNotifPanelEvents()
+    private val settings = FakeSettings()
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var fakeHandler: FakeHandler
 
     @Before
     fun setup() {
         context.getOrCreateTestableResources().addOverride(
                 R.bool.config_use_split_notification_shade, false)
         mediaFrame = FrameLayout(context)
-        `when`(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
-        mediaHiearchyManager = MediaHierarchyManager(
+        testableLooper = TestableLooper.get(this)
+        fakeHandler = FakeHandler(testableLooper.looper)
+        whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
+        mediaHierarchyManager = MediaHierarchyManager(
                 context,
                 statusBarStateController,
                 keyguardStateController,
                 bypassController,
                 mediaCarouselController,
-                notificationLockscreenUserManager,
                 keyguardViewController,
                 dreamOverlayStateController,
                 configurationController,
                 wakefulnessLifecycle,
                 notifPanelEvents,
-        )
+                settings,
+                fakeHandler,)
         verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
         verify(statusBarStateController).addCallback(statusBarCallback.capture())
         setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN, LOCKSCREEN_TOP)
         setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP)
         setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP)
-        `when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
-        `when`(mediaCarouselController.mediaCarouselScrollHandler)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+        whenever(mediaCarouselController.mediaCarouselScrollHandler)
                 .thenReturn(mediaCarouselScrollHandler)
         val observer = wakefullnessObserver.value
         assertNotNull("lifecycle observer wasn't registered", observer)
@@ -122,30 +127,30 @@
     }
 
     private fun setupHost(host: MediaHost, location: Int, top: Int) {
-        `when`(host.location).thenReturn(location)
-        `when`(host.currentBounds).thenReturn(Rect(0, top, 0, top))
-        `when`(host.hostView).thenReturn(uniqueObjectHostView)
-        `when`(host.visible).thenReturn(true)
-        mediaHiearchyManager.register(host)
+        whenever(host.location).thenReturn(location)
+        whenever(host.currentBounds).thenReturn(Rect(0, top, 0, top))
+        whenever(host.hostView).thenReturn(uniqueObjectHostView)
+        whenever(host.visible).thenReturn(true)
+        mediaHierarchyManager.register(host)
     }
 
     @Test
     fun testHostViewSetOnRegister() {
-        val host = mediaHiearchyManager.register(lockHost)
+        val host = mediaHierarchyManager.register(lockHost)
         verify(lockHost).hostView = eq(host)
     }
 
     @Test
     fun testBlockedWhenScreenTurningOff() {
         // Let's set it onto QS:
-        mediaHiearchyManager.qsExpansion = 1.0f
+        mediaHierarchyManager.qsExpansion = 1.0f
         verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
                 any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
         val observer = wakefullnessObserver.value
         assertNotNull("lifecycle observer wasn't registered", observer)
         observer.onStartedGoingToSleep()
         clearInvocations(mediaCarouselController)
-        mediaHiearchyManager.qsExpansion = 0.0f
+        mediaHierarchyManager.qsExpansion = 0.0f
         verify(mediaCarouselController, times(0))
                 .onDesiredLocationChanged(ArgumentMatchers.anyInt(),
                 any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
@@ -154,13 +159,13 @@
     @Test
     fun testAllowedWhenNotTurningOff() {
         // Let's set it onto QS:
-        mediaHiearchyManager.qsExpansion = 1.0f
+        mediaHierarchyManager.qsExpansion = 1.0f
         verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
                 any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
         val observer = wakefullnessObserver.value
         assertNotNull("lifecycle observer wasn't registered", observer)
         clearInvocations(mediaCarouselController)
-        mediaHiearchyManager.qsExpansion = 0.0f
+        mediaHierarchyManager.qsExpansion = 0.0f
         verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
                 any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
     }
@@ -170,7 +175,7 @@
         goToLockscreen()
 
         // Let's transition all the way to full shade
-        mediaHiearchyManager.setTransitionToFullShadeAmount(100000f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(100000f)
         verify(mediaCarouselController).onDesiredLocationChanged(
             eq(MediaHierarchyManager.LOCATION_QQS),
             any(MediaHostState::class.java),
@@ -180,7 +185,7 @@
         clearInvocations(mediaCarouselController)
 
         // Let's go back to the lock screen
-        mediaHiearchyManager.setTransitionToFullShadeAmount(0.0f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f)
         verify(mediaCarouselController).onDesiredLocationChanged(
             eq(MediaHierarchyManager.LOCATION_LOCKSCREEN),
             any(MediaHostState::class.java),
@@ -189,7 +194,7 @@
             anyLong())
 
         // Let's make sure alpha is set
-        mediaHiearchyManager.setTransitionToFullShadeAmount(2.0f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f)
         assertThat(mediaFrame.alpha).isNotEqualTo(1.0f)
     }
 
@@ -198,7 +203,7 @@
         goToLockscreen()
         expandQS()
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
     }
 
@@ -206,7 +211,7 @@
     fun calculateTransformationType_notOnLockscreen_returnsTransition() {
         expandQS()
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
 
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
     }
@@ -216,7 +221,7 @@
         goToLockscreen()
         expandQS()
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
 
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
     }
@@ -226,9 +231,9 @@
         enableSplitShade()
         goToLockscreen()
         expandQS()
-        mediaHiearchyManager.setTransitionToFullShadeAmount(10000f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(10000f)
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
     }
 
@@ -238,9 +243,9 @@
         goToLockscreen()
         expandQS()
         whenever(lockHost.visible).thenReturn(false)
-        mediaHiearchyManager.setTransitionToFullShadeAmount(10000f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(10000f)
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
     }
 
@@ -250,9 +255,9 @@
         goToLockscreen()
         goToLockedShade()
         expandQS()
-        mediaHiearchyManager.setTransitionToFullShadeAmount(0f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(0f)
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
     }
 
@@ -261,13 +266,13 @@
         goToLockscreen()
         goToLockedShade()
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
     }
 
     @Test
     fun testCloseGutsRelayToCarousel() {
-        mediaHiearchyManager.closeGuts()
+        mediaHierarchyManager.closeGuts()
 
         verify(mediaCarouselController).closeGuts()
     }
@@ -281,7 +286,7 @@
 
     @Test
     fun getGuidedTransformationTranslationY_notInGuidedTransformation_returnsNegativeNumber() {
-        assertThat(mediaHiearchyManager.getGuidedTransformationTranslationY()).isLessThan(0)
+        assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()).isLessThan(0)
     }
 
     @Test
@@ -289,7 +294,7 @@
         enterGuidedTransformation()
 
         val expectedTranslation = LOCKSCREEN_TOP - QS_TOP
-        assertThat(mediaHiearchyManager.getGuidedTransformationTranslationY())
+        assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY())
                 .isEqualTo(expectedTranslation)
     }
 
@@ -301,7 +306,7 @@
         whenever(qsHost.visible).thenReturn(true)
         whenever(qqsHost.visible).thenReturn(true)
 
-        assertThat(mediaHiearchyManager.isCurrentlyInGuidedTransformation()).isTrue()
+        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue()
     }
 
     @Test
@@ -313,7 +318,7 @@
         whenever(qsHost.visible).thenReturn(true)
         whenever(qqsHost.visible).thenReturn(true)
 
-        assertThat(mediaHiearchyManager.isCurrentlyInGuidedTransformation()).isFalse()
+        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
     }
 
     @Test
@@ -324,7 +329,7 @@
         whenever(qsHost.visible).thenReturn(true)
         whenever(qqsHost.visible).thenReturn(true)
 
-        assertThat(mediaHiearchyManager.isCurrentlyInGuidedTransformation()).isFalse()
+        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
     }
 
     private fun enableSplitShade() {
@@ -336,9 +341,7 @@
 
     private fun goToLockscreen() {
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
-        whenever(notificationLockscreenUserManager.shouldShowLockscreenNotifications()).thenReturn(
-            true
-        )
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1)
         statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD)
         clearInvocations(mediaCarouselController)
     }
@@ -352,13 +355,13 @@
     }
 
     private fun expandQS() {
-        mediaHiearchyManager.qsExpansion = 1.0f
+        mediaHierarchyManager.qsExpansion = 1.0f
     }
 
     private fun enterGuidedTransformation() {
-        mediaHiearchyManager.qsExpansion = 1.0f
+        mediaHierarchyManager.qsExpansion = 1.0f
         goToLockscreen()
-        mediaHiearchyManager.setTransitionToFullShadeAmount(123f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(123f)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
index 247316a..c101b9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.media.dream;
 
+import static com.android.systemui.flags.Flags.MEDIA_DREAM_COMPLICATION;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
@@ -28,6 +30,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.MediaData;
 import com.android.systemui.media.MediaDataManager;
 
@@ -50,6 +53,9 @@
     @Mock
     MediaDreamComplication mComplication;
 
+    @Mock
+    FeatureFlags mFeatureFlags;
+
     final String mKey = "key";
     final String mOldKey = "old_key";
 
@@ -59,21 +65,18 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+
+        when(mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)).thenReturn(true);
     }
 
     @Test
     public void testComplicationAddition() {
         final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
-                mDreamOverlayStateController, mComplication);
+                mDreamOverlayStateController, mComplication, mFeatureFlags);
 
         sentinel.start();
 
-        ArgumentCaptor<MediaDataManager.Listener> listenerCaptor =
-                ArgumentCaptor.forClass(MediaDataManager.Listener.class);
-        verify(mMediaDataManager).addListener(listenerCaptor.capture());
-
-        final MediaDataManager.Listener listener = listenerCaptor.getValue();
-
+        final MediaDataManager.Listener listener = captureMediaDataListener();
         when(mMediaDataManager.hasActiveMedia()).thenReturn(false);
         listener.onMediaDataLoaded(mKey, mOldKey, mData, /* immediately= */ true,
                 /* receivedSmartspaceCardLatency= */ 0, /* isSsReactived= */ false);
@@ -92,4 +95,27 @@
         verify(mDreamOverlayStateController).removeComplication(eq(mComplication));
     }
 
+    @Test
+    public void testMediaDreamSentinel_mediaComplicationDisabled_doNotAddComplication() {
+        when(mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)).thenReturn(false);
+
+        final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
+                mDreamOverlayStateController, mComplication, mFeatureFlags);
+
+        sentinel.start();
+
+        final MediaDataManager.Listener listener = captureMediaDataListener();
+        when(mMediaDataManager.hasActiveMedia()).thenReturn(true);
+        listener.onMediaDataLoaded(mKey, mOldKey, mData, /* immediately= */true,
+                /* receivedSmartspaceCardLatency= */0, /* isSsReactived= */ false);
+        verify(mDreamOverlayStateController, never()).addComplication(any());
+    }
+
+    private MediaDataManager.Listener captureMediaDataListener() {
+        final ArgumentCaptor<MediaDataManager.Listener> listenerCaptor =
+                ArgumentCaptor.forClass(MediaDataManager.Listener.class);
+        verify(mMediaDataManager).addListener(listenerCaptor.capture());
+
+        return listenerCaptor.getValue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 30ab424..a524842 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -238,6 +238,8 @@
     @Mock
     private DozeLog mDozeLog;
     @Mock
+    private ShadeLogger mShadeLog;
+    @Mock
     private CommandQueue mCommandQueue;
     @Mock
     private VibratorHelper mVibratorHelper;
@@ -524,7 +526,9 @@
                 mNotificationShadeWindowController,
                 mDozeLog, mDozeParameters, mCommandQueue, mVibratorHelper,
                 mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor,
-                mMetricsLogger, mConfigurationController,
+                mMetricsLogger,
+                mShadeLog,
+                mConfigurationController,
                 () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
                 mConversationNotificationManager, mMediaHiearchyManager,
                 mStatusBarKeyguardViewManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
index 64d0256..214ba16 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
@@ -98,7 +98,7 @@
 
     @Test
     fun testPrepareDialogForApp_onlyDefaultChannel() {
-        group.addChannel(channelDefault)
+        group.channels = listOf(channelDefault)
 
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
                 setOf(channelDefault), appIcon, clickListener)
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
index 515a7c9..7b492cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt
@@ -20,7 +20,7 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.repository.NetworkCapabilityInfo
+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
@@ -59,6 +59,7 @@
                 context,
                 scope,
                 statusBarPipelineFlags,
+                mock(),
         )
 
         var mostRecentValue: ProcessedConnectivityInfo? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLoggerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
index 2915ae8..36be1be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.pipeline
+package com.android.systemui.statusbar.pipeline.shared
 
 import android.net.Network
 import android.net.NetworkCapabilities
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
new file mode 100644
index 0000000..df389bc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -0,0 +1,40 @@
+/*
+ * 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 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.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 _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT)
+    override val wifiActivity: Flow<WifiActivityModel> = _wifiActivity
+
+    fun setWifiModel(wifiModel: WifiModel?) {
+        _wifiModel.value = wifiModel
+    }
+
+    fun setWifiActivity(activity: WifiActivityModel) {
+        _wifiActivity.value = activity
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepoTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt
index 40f8fbf..6edf76c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.pipeline.repository
+package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
 import android.net.ConnectivityManager
 import android.net.ConnectivityManager.NetworkCallback
@@ -28,7 +28,7 @@
 import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.ConnectivityPipelineLogger
+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
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
new file mode 100644
index 0000000..8b61364
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -0,0 +1,175 @@
+/*
+ * 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.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.repository.WifiRepositoryImpl.Companion.ACTIVITY_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.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class WifiRepositoryImplTest : SysuiTestCase() {
+
+    private lateinit var underTest: WifiRepositoryImpl
+
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var wifiManager: WifiManager
+    private lateinit var executor: Executor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        executor = FakeExecutor(FakeSystemClock())
+    }
+
+    @Test
+    fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) {
+        underTest = WifiRepositoryImpl(
+                wifiManager = null,
+                executor,
+                logger,
+        )
+
+        var latest: WifiActivityModel? = null
+        val job = underTest
+                .wifiActivity
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isEqualTo(ACTIVITY_DEFAULT)
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiActivity_callbackGivesNone_activityFlowHasNone() = runBlocking(IMMEDIATE) {
+        underTest = WifiRepositoryImpl(
+                wifiManager,
+                executor,
+                logger,
+        )
+
+        var latest: WifiActivityModel? = null
+        val job = underTest
+                .wifiActivity
+                .onEach { latest = it }
+                .launchIn(this)
+
+        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_NONE)
+
+        assertThat(latest).isEqualTo(
+            WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiActivity_callbackGivesIn_activityFlowHasIn() = runBlocking(IMMEDIATE) {
+        underTest = WifiRepositoryImpl(
+                wifiManager,
+                executor,
+                logger,
+        )
+
+        var latest: WifiActivityModel? = null
+        val job = underTest
+                .wifiActivity
+                .onEach { latest = it }
+                .launchIn(this)
+
+        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_IN)
+
+        assertThat(latest).isEqualTo(
+            WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiActivity_callbackGivesOut_activityFlowHasOut() = runBlocking(IMMEDIATE) {
+        underTest = WifiRepositoryImpl(
+                wifiManager,
+                executor,
+                logger,
+        )
+
+        var latest: WifiActivityModel? = null
+        val job = underTest
+                .wifiActivity
+                .onEach { latest = it }
+                .launchIn(this)
+
+        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_OUT)
+
+        assertThat(latest).isEqualTo(
+            WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = runBlocking(IMMEDIATE) {
+        underTest = WifiRepositoryImpl(
+                wifiManager,
+                executor,
+                logger,
+        )
+
+        var latest: WifiActivityModel? = null
+        val job = underTest
+                .wifiActivity
+                .onEach { latest = it }
+                .launchIn(this)
+
+        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_INOUT)
+
+        assertThat(latest).isEqualTo(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
+
+        job.cancel()
+    }
+
+    private fun getTrafficStateCallback(): TrafficStateCallback {
+        val callbackCaptor = argumentCaptor<TrafficStateCallback>()
+        verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+}
+
+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
new file mode 100644
index 0000000..c52f347
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.domain.interactor
+
+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.repository.FakeWifiRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class WifiInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: WifiInteractor
+
+    private lateinit var repository: FakeWifiRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeWifiRepository()
+        underTest = WifiInteractor(repository)
+    }
+
+    @Test
+    fun hasActivityIn_noInOrOut_outputsFalse() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun hasActivityIn_onlyOut_outputsFalse() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = true))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun hasActivityIn_onlyIn_outputsTrue() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun hasActivityIn_inAndOut_outputsTrue() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun hasActivityIn_ssidNull_outputsFalse() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = null))
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun hasActivityIn_multipleChanges_multipleOutputChanges() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        // Conduct a series of changes and verify we catch each of them in succession
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
+        yield()
+        assertThat(latest).isTrue()
+
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = true))
+        yield()
+        assertThat(latest).isFalse()
+
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
+        yield()
+        assertThat(latest).isTrue()
+
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
+        yield()
+        assertThat(latest).isTrue()
+
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = false))
+        yield()
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
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
new file mode 100644
index 0000000..e9259b0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.viewmodel
+
+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.WifiModel
+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
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class WifiViewModelTest : SysuiTestCase() {
+
+    private lateinit var underTest: WifiViewModel
+
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var constants: WifiConstants
+    private lateinit var repository: FakeWifiRepository
+    private lateinit var interactor: WifiInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        repository = FakeWifiRepository()
+        interactor = WifiInteractor(repository)
+
+        underTest = WifiViewModel(
+                constants,
+                logger,
+                interactor
+        )
+
+        // Set up with a valid SSID
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+    }
+
+    @Test
+    fun activityInVisible_showActivityConfigFalse_receivesFalse() = runBlocking(IMMEDIATE) {
+        whenever(constants.shouldShowActivityConfig).thenReturn(false)
+
+        var latest: Boolean? = null
+        val job = underTest
+                .isActivityInVisible
+                .onEach { latest = it }
+                .launchIn(this)
+
+        // Verify that on launch, we receive a false.
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun activityInVisible_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) {
+        whenever(constants.shouldShowActivityConfig).thenReturn(false)
+
+        var latest: Boolean? = null
+        val job = underTest
+                .isActivityInVisible
+                .onEach { latest = it }
+                .launchIn(this)
+
+        // Update the repo to have activityIn
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
+        yield()
+
+        // Verify that we didn't update to activityIn=true (because our config is false)
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun activityInVisible_showActivityConfigTrue_receivesUpdate() = runBlocking(IMMEDIATE) {
+        whenever(constants.shouldShowActivityConfig).thenReturn(true)
+
+        var latest: Boolean? = null
+        val job = underTest
+                .isActivityInVisible
+                .onEach { latest = it }
+                .launchIn(this)
+
+        // Update the repo to have activityIn
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
+        yield()
+
+        // Verify that we updated to activityIn=true
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index b7f38f1..50259b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -310,7 +310,7 @@
     }
 
     @Test
-    public void onWallpaperColorsChanged_ResetThemeWithNewHomeWallpapers() {
+    public void onWallpaperColorsChanged_resetThemeWithNewHomeWallpapers() {
         // Should ask for a new theme when wallpaper colors change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
@@ -345,6 +345,61 @@
     }
 
     @Test
+    public void onWallpaperColorsChanged_keepsThemeWhenSetFromLockScreen() {
+        // Should ask for a new theme when wallpaper colors change
+        WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
+                Color.valueOf(Color.BLUE), null);
+        String jsonString =
+                "{\"android.theme.customization.color_source\":\"lock_wallpaper\","
+                        + "\"android.theme.customization.system_palette\":\"A16B00\","
+                        + "\"android.theme.customization.accent_color\":\"A16B00\","
+                        + "\"android.theme.customization.color_index\":\"2\"}";
+        when(mSecureSettings.getStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
+                .thenReturn(jsonString);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_LOCK, USER_SYSTEM))
+                .thenReturn(20);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_SYSTEM, USER_SYSTEM))
+                .thenReturn(21);
+        mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
+                USER_SYSTEM);
+        verify(mSecureSettings, never()).putStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), any(), anyInt());
+    }
+
+    @Test
+    public void onWallpaperColorsChanged_resetLockScreenThemeWhenBothSet() {
+        // Should ask for a new theme when wallpaper colors change
+        WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
+                Color.valueOf(Color.BLUE), null);
+        String jsonString =
+                "{\"android.theme.customization.color_source\":\"lock_wallpaper\","
+                        + "\"android.theme.customization.system_palette\":\"A16B00\","
+                        + "\"android.theme.customization.accent_color\":\"A16B00\","
+                        + "\"android.theme.customization.color_index\":\"2\"}";
+        when(mSecureSettings.getStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
+                .thenReturn(jsonString);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_LOCK, USER_SYSTEM))
+                .thenReturn(20);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_SYSTEM, USER_SYSTEM))
+                .thenReturn(21);
+
+        mColorsListener.getValue().onColorsChanged(mainColors,
+                WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK,
+                USER_SYSTEM);
+
+        ArgumentCaptor<String> updatedSetting = ArgumentCaptor.forClass(String.class);
+        verify(mSecureSettings).putStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), updatedSetting.capture(),
+                anyInt());
+        assertThat(updatedSetting.getValue().contains(
+                "android.theme.customization.color_both\":\"1")).isTrue();
+        verify(mThemeOverlayApplier)
+                .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+    }
+
+    @Test
     public void onSettingChanged_honorThemeStyle() {
         when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true);
         List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT,
@@ -381,7 +436,7 @@
     }
 
     @Test
-    public void onWallpaperColorsChanged_ResetThemeWithNewHomeAndLockWallpaper() {
+    public void onWallpaperColorsChanged_resetThemeWithNewHomeAndLockWallpaper() {
         // Should ask for a new theme when wallpaper colors change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
@@ -450,7 +505,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
         String jsonString =
-                "{\"android.theme.customization.color_source\":\"lock_wallpaper\","
+                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
                         + "\"android.theme.customization.system_palette\":\"A16B00\","
                         + "\"android.theme.customization.accent_color\":\"A16B00\","
                         + "\"android.theme.customization.color_index\":\"2\"}";
@@ -476,7 +531,7 @@
     }
 
     @Test
-    public void onWallpaperColorsChanged_ResetThemeWhenFromLatestWallpaper() {
+    public void onWallpaperColorsChanged_resetThemeWhenFromLatestWallpaper() {
         // Should ask for a new theme when the colors of the last applied wallpaper change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index 0a4ecb2..5154a2d 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -298,6 +298,10 @@
     // Notify the user to setup their dock
     NOTE_SETUP_DOCK = 72;
 
+    // Inform the user of bluetooth apm state changes.
+    // Package: android
+    NOTE_BT_APM_NOTIFICATION = 74;
+
     // ADD_NEW_IDS_ABOVE_THIS_LINE
     // Legacy IDs with arbitrary values appear below
     // Legacy IDs existed as stable non-conflicting constants prior to the O release
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index 43e2b88..593a63c 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -77,6 +77,14 @@
             new ComponentName("android", BlockedAppStreamingActivity.class.getName());
 
     /**
+     * For communicating when a secure window shows on the virtual display.
+     */
+    public interface SecureWindowCallback {
+        /** Called when a secure window shows on the virtual display. */
+        void onSecureWindowShown(int displayId, int uid);
+    }
+
+    /**
      * If required, allow the secure activity to display on remote device since
      * {@link android.os.Build.VERSION_CODES#TIRAMISU}.
      */
@@ -108,6 +116,7 @@
             new ArraySet<>();
     @Nullable
     private final @AssociationRequest.DeviceProfile String mDeviceProfile;
+    @Nullable private final SecureWindowCallback mSecureWindowCallback;
 
     /**
      * Creates a window policy controller that is generic to the different use cases of virtual
@@ -131,6 +140,8 @@
      * @param activityListener Activity listener to listen for activity changes.
      * @param activityBlockedCallback Callback that is called when an activity is blocked from
      *   launching.
+     * @param secureWindowCallback Callback that is called when a secure window shows on the
+     *   virtual display.
      * @param deviceProfile The {@link AssociationRequest.DeviceProfile} of this virtual device.
      */
     public GenericWindowPolicyController(int windowFlags, int systemWindowFlags,
@@ -142,6 +153,7 @@
             @ActivityPolicy int defaultActivityPolicy,
             @NonNull ActivityListener activityListener,
             @NonNull ActivityBlockedCallback activityBlockedCallback,
+            @NonNull SecureWindowCallback secureWindowCallback,
             @AssociationRequest.DeviceProfile String deviceProfile) {
         super();
         mAllowedUsers = allowedUsers;
@@ -154,6 +166,7 @@
         setInterestedWindowFlags(windowFlags, systemWindowFlags);
         mActivityListener = activityListener;
         mDeviceProfile = deviceProfile;
+        mSecureWindowCallback = secureWindowCallback;
     }
 
     /**
@@ -234,6 +247,12 @@
     @Override
     public boolean keepActivityOnWindowFlagsChanged(ActivityInfo activityInfo, int windowFlags,
             int systemWindowFlags) {
+        // The callback is fired only when windowFlags are changed. To let VirtualDevice owner
+        // aware that the virtual display has a secure window on top.
+        if ((windowFlags & FLAG_SECURE) != 0) {
+            mSecureWindowCallback.onSecureWindowShown(mDisplayId, activityInfo.applicationInfo.uid);
+        }
+
         if (!canContainActivity(activityInfo, windowFlags, systemWindowFlags)) {
             mActivityBlockedCallback.onActivityBlocked(mDisplayId, activityInfo);
             return false;
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 5f337ab..cca3212 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -52,6 +52,7 @@
 import android.hardware.input.VirtualTouchEvent;
 import android.os.Binder;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -542,6 +543,7 @@
                             mParams.getDefaultActivityPolicy(),
                             createListenerAdapter(),
                             this::onActivityBlocked,
+                            this::onSecureWindowShown,
                             mAssociationInfo.getDeviceProfile());
             gwpc.registerRunningAppsChangedListener(/* listener= */ this);
             return gwpc;
@@ -591,6 +593,21 @@
                 mContext.getUser());
     }
 
+    private void onSecureWindowShown(int displayId, int uid) {
+        if (!mVirtualDisplayIds.contains(displayId)) {
+            return;
+        }
+
+        // If a virtual display isn't secure, the screen can't be captured. Show a warning toast
+        // if the secure window is shown on a non-secure virtual display.
+        DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+        Display display = displayManager.getDisplay(displayId);
+        if ((display.getFlags() & FLAG_SECURE) == 0) {
+            showToastWhereUidIsRunning(uid, com.android.internal.R.string.vdm_secure_window,
+                    Toast.LENGTH_LONG, mContext.getMainLooper());
+        }
+    }
+
     private ArraySet<UserHandle> getAllowedUserHandles() {
         ArraySet<UserHandle> result = new ArraySet<>();
         DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
@@ -650,14 +667,16 @@
     /**
      * Shows a toast on virtual displays owned by this device which have a given uid running.
      */
-    void showToastWhereUidIsRunning(int uid, @StringRes int resId, @Toast.Duration int duration) {
-        showToastWhereUidIsRunning(uid, mContext.getString(resId), duration);
+    void showToastWhereUidIsRunning(int uid, @StringRes int resId, @Toast.Duration int duration,
+            Looper looper) {
+        showToastWhereUidIsRunning(uid, mContext.getString(resId), duration, looper);
     }
 
     /**
      * Shows a toast on virtual displays owned by this device which have a given uid running.
      */
-    void showToastWhereUidIsRunning(int uid, String text, @Toast.Duration int duration) {
+    void showToastWhereUidIsRunning(int uid, String text, @Toast.Duration int duration,
+            Looper looper) {
         synchronized (mVirtualDeviceLock) {
             DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
             final int size = mWindowPolicyControllers.size();
@@ -666,7 +685,7 @@
                     int displayId = mWindowPolicyControllers.keyAt(i);
                     Display display = displayManager.getDisplay(displayId);
                     if (display != null && display.isValid()) {
-                        Toast.makeText(mContext.createDisplayContext(display), text,
+                        Toast.makeText(mContext.createDisplayContext(display), looper, text,
                                 duration).show();
                     }
                 }
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 35e9060..41b6fad 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -202,7 +202,7 @@
                         getContext().getString(
                             com.android.internal.R.string.vdm_camera_access_denied,
                             deviceName),
-                        Toast.LENGTH_LONG);
+                        Toast.LENGTH_LONG, Looper.myLooper());
             }
         }
     }
diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java
index 262436d..eb1fd3a 100644
--- a/services/core/java/com/android/server/am/ProcessStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessStateRecord.java
@@ -497,6 +497,7 @@
     @GuardedBy({"mService", "mProcLock"})
     void setCurAdj(int curAdj) {
         mCurAdj = curAdj;
+        mApp.getWindowProcessController().setCurrentAdj(curAdj);
     }
 
     @GuardedBy(anyOf = {"mService", "mProcLock"})
diff --git a/services/core/java/com/android/server/attention/AttentionManagerService.java b/services/core/java/com/android/server/attention/AttentionManagerService.java
index d16fe12..d4ef638 100644
--- a/services/core/java/com/android/server/attention/AttentionManagerService.java
+++ b/services/core/java/com/android/server/attention/AttentionManagerService.java
@@ -343,6 +343,9 @@
      *
      * Calling this multiple times for duplicate requests will be no-ops, returning true.
      *
+     * TODO(b/239130847): Maintain the proximity state in AttentionManagerService and change this
+     * to a polling API.
+     *
      * @return {@code true} if the framework was able to dispatch the request
      */
     @VisibleForTesting
@@ -853,9 +856,6 @@
     @GuardedBy("mLock")
     private void cancelAndUnbindLocked() {
         synchronized (mLock) {
-            if (mCurrentAttentionCheck == null && mCurrentProximityUpdate == null) {
-                return;
-            }
             if (mCurrentAttentionCheck != null) {
                 cancel();
             }
@@ -937,7 +937,7 @@
             }
         }
 
-        class TestableProximityUpdateCallbackInternal extends ProximityUpdateCallbackInternal {
+        class TestableProximityUpdateCallbackInternal implements ProximityUpdateCallbackInternal {
             private double mLastCallbackCode = PROXIMITY_UNKNOWN;
 
             @Override
@@ -1069,6 +1069,7 @@
         private void resetStates() {
             synchronized (mLock) {
                 mCurrentProximityUpdate = null;
+                cancelAndUnbindLocked();
             }
             mComponentName = resolveAttentionService(mContext);
         }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index c00c298..0a081bf 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1614,6 +1614,7 @@
         }
 
         synchronized (mAudioPolicies) {
+            ArrayList<AudioPolicyProxy> invalidProxies = new ArrayList<>();
             for (AudioPolicyProxy policy : mAudioPolicies.values()) {
                 final int status = policy.connectMixes();
                 if (status != AudioSystem.SUCCESS) {
@@ -1621,7 +1622,7 @@
                     Log.e(TAG, "onAudioServerDied: error "
                             + AudioSystem.audioSystemErrorToString(status)
                             + " when connecting mixes for policy " + policy.toLogFriendlyString());
-                    policy.release();
+                    invalidProxies.add(policy);
                 } else {
                     final int deviceAffinitiesStatus = policy.setupDeviceAffinities();
                     if (deviceAffinitiesStatus != AudioSystem.SUCCESS) {
@@ -1629,10 +1630,12 @@
                                 + AudioSystem.audioSystemErrorToString(deviceAffinitiesStatus)
                                 + " when connecting device affinities for policy "
                                 + policy.toLogFriendlyString());
-                        policy.release();
+                        invalidProxies.add(policy);
                     }
                 }
             }
+            invalidProxies.forEach((policy) -> policy.release());
+
         }
 
         // Restore capture policies
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 7c0f65f..2f34ccd 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -7848,7 +7848,8 @@
                 && (record.getSuppressedVisualEffects() & SUPPRESSED_EFFECT_STATUS_BAR) != 0;
         if (!record.isUpdate
                 && record.getImportance() > IMPORTANCE_MIN
-                && !suppressedByDnd) {
+                && !suppressedByDnd
+                && isNotificationForCurrentUser(record)) {
             sendAccessibilityEvent(record);
             sentAccessibilityEvent = true;
         }
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 477b8da..729c521 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -86,6 +86,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -1327,16 +1328,17 @@
                 return null;
             }
             NotificationChannelGroup group = r.groups.get(groupId).clone();
-            group.setChannels(new ArrayList<>());
+            ArrayList channels = new ArrayList();
             int N = r.channels.size();
             for (int i = 0; i < N; i++) {
                 final NotificationChannel nc = r.channels.valueAt(i);
                 if (includeDeleted || !nc.isDeleted()) {
                     if (groupId.equals(nc.getGroup())) {
-                        group.addChannel(nc);
+                        channels.add(nc);
                     }
                 }
             }
+            group.setChannels(channels);
             return group;
         }
     }
@@ -1349,7 +1351,10 @@
             if (r == null) {
                 return null;
             }
-            return r.groups.get(groupId);
+            if (r.groups.get(groupId) != null) {
+                 return r.groups.get(groupId).clone();
+            }
+            return null;
         }
     }
 
@@ -1357,44 +1362,48 @@
     public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
             int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty) {
         Objects.requireNonNull(pkg);
-        Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
+        List<NotificationChannelGroup> groups = new ArrayList<>();
         synchronized (mPackagePreferences) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return ParceledListSlice.emptyList();
             }
-            NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
+            Map<String, ArrayList<NotificationChannel>> groupedChannels = new HashMap();
             int N = r.channels.size();
             for (int i = 0; i < N; i++) {
                 final NotificationChannel nc = r.channels.valueAt(i);
                 if (includeDeleted || !nc.isDeleted()) {
                     if (nc.getGroup() != null) {
                         if (r.groups.get(nc.getGroup()) != null) {
-                            NotificationChannelGroup ncg = groups.get(nc.getGroup());
-                            if (ncg == null) {
-                                ncg = r.groups.get(nc.getGroup()).clone();
-                                ncg.setChannels(new ArrayList<>());
-                                groups.put(nc.getGroup(), ncg);
-
-                            }
-                            ncg.addChannel(nc);
+                            ArrayList<NotificationChannel> channels = groupedChannels.getOrDefault(
+                                    nc.getGroup(), new ArrayList<>());
+                            channels.add(nc);
+                            groupedChannels.put(nc.getGroup(), channels);
                         }
                     } else {
-                        nonGrouped.addChannel(nc);
+                        ArrayList<NotificationChannel> channels = groupedChannels.getOrDefault(
+                            null, new ArrayList<>());
+                        channels.add(nc);
+                        groupedChannels.put(null, channels);
                     }
                 }
             }
-            if (includeNonGrouped && nonGrouped.getChannels().size() > 0) {
-                groups.put(null, nonGrouped);
-            }
-            if (includeEmpty) {
-                for (NotificationChannelGroup group : r.groups.values()) {
-                    if (!groups.containsKey(group.getId())) {
-                        groups.put(group.getId(), group);
-                    }
+            for (NotificationChannelGroup group : r.groups.values()) {
+                ArrayList<NotificationChannel> channels =
+                        groupedChannels.getOrDefault(group.getId(), new ArrayList<>());
+                if (includeEmpty || !channels.isEmpty()) {
+                    NotificationChannelGroup clone = group.clone();
+                    clone.setChannels(channels);
+                    groups.add(clone);
                 }
             }
-            return new ParceledListSlice<>(new ArrayList<>(groups.values()));
+
+            if (includeNonGrouped && groupedChannels.containsKey(null)) {
+                NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
+                nonGrouped.setChannels(groupedChannels.get(null));
+                groups.add(nonGrouped);
+            }
+            return new ParceledListSlice<>(groups);
         }
     }
 
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index d645bb2..8731ac0 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -5107,18 +5107,10 @@
 
     /** {@inheritDoc} */
     @Override
-    public void userActivity() {
-        // ***************************************
-        // NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE
-        // ***************************************
-        // THIS IS CALLED FROM DEEP IN THE POWER MANAGER
-        // WITH ITS LOCKS HELD.
-        //
-        // This code must be VERY careful about the locks
-        // it acquires.
-        // In fact, the current code acquires way too many,
-        // and probably has lurking deadlocks.
-
+    public void userActivity(int displayGroupId, int event) {
+        if (displayGroupId == DEFAULT_DISPLAY && event == PowerManager.USER_ACTIVITY_EVENT_TOUCH) {
+            mDefaultDisplayPolicy.onUserActivityEventTouch();
+        }
         synchronized (mScreenLockTimeout) {
             if (mLockScreenTimerActive) {
                 // reset the timer
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index e8a3dcd..4f00992 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -1006,7 +1006,7 @@
      * Called when userActivity is signalled in the power manager.
      * This is safe to call from any thread, with any window manager locks held or not.
      */
-    public void userActivity();
+    void userActivity(int displayGroupId, int event);
 
     /**
      * Called when we have finished booting and can now display the home
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 685b744..5a2fb18 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -712,7 +712,7 @@
         }
         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
         tm.notifyUserActivity();
-        mPolicy.userActivity();
+        mPolicy.userActivity(displayGroupId, event);
         mFaceDownDetector.userActivity(event);
         mScreenUndimDetector.userActivity(displayGroupId);
     }
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index e7c4ba5..d2413f0 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -70,6 +70,7 @@
 import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__CLICKED_REVERT_TREATMENT;
 import static com.android.server.am.MemoryStatUtil.MemoryStat;
 import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem;
+import static com.android.server.am.ProcessList.INVALID_ADJ;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_METRICS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -277,6 +278,8 @@
         final boolean mProcessSwitch;
         /** The process state of the launching activity prior to the launch */
         final int mProcessState;
+        /** The oom adj score of the launching activity prior to the launch */
+        final int mProcessOomAdj;
         /** Whether the last launched activity has reported drawn. */
         boolean mIsDrawn;
         /** The latest activity to have been launched. */
@@ -312,7 +315,7 @@
         @Nullable
         static TransitionInfo create(@NonNull ActivityRecord r,
                 @NonNull LaunchingState launchingState, @Nullable ActivityOptions options,
-                boolean processRunning, boolean processSwitch, int processState,
+                boolean processRunning, boolean processSwitch, int processState, int processOomAdj,
                 boolean newActivityCreated, int startResult) {
             if (startResult != START_SUCCESS && startResult != START_TASK_TO_FRONT) {
                 return null;
@@ -328,19 +331,20 @@
                 transitionType = TYPE_TRANSITION_COLD_LAUNCH;
             }
             return new TransitionInfo(r, launchingState, options, transitionType, processRunning,
-                    processSwitch, processState);
+                    processSwitch, processState, processOomAdj);
         }
 
         /** Use {@link TransitionInfo#create} instead to ensure the transition type is valid. */
         private TransitionInfo(ActivityRecord r, LaunchingState launchingState,
                 ActivityOptions options, int transitionType, boolean processRunning,
-                boolean processSwitch, int processState) {
+                boolean processSwitch, int processState, int processOomAdj) {
             mLaunchingState = launchingState;
             mTransitionStartTimeNs = launchingState.mCurrentTransitionStartTimeNs;
             mTransitionType = transitionType;
             mProcessRunning = processRunning;
             mProcessSwitch = processSwitch;
             mProcessState = processState;
+            mProcessOomAdj = processOomAdj;
             mTransitionDeviceUptimeMs = launchingState.mCurrentUpTimeMs;
             setLatestLaunchedActivity(r);
             // The launching state can be reused by consecutive launch. Its original association
@@ -644,9 +648,15 @@
         // interesting.
         final boolean processSwitch = !processRunning
                 || !processRecord.hasStartedActivity(launchedActivity);
-        final int processState = processRunning
-                ? processRecord.getCurrentProcState()
-                : PROCESS_STATE_NONEXISTENT;
+        final int processState;
+        final int processOomAdj;
+        if (processRunning) {
+            processState = processRecord.getCurrentProcState();
+            processOomAdj = processRecord.getCurrentAdj();
+        } else {
+            processState = PROCESS_STATE_NONEXISTENT;
+            processOomAdj = INVALID_ADJ;
+        }
 
         final TransitionInfo info = launchingState.mAssociatedTransitionInfo;
         if (DEBUG_METRICS) {
@@ -654,6 +664,7 @@
                     + " launchedActivity=" + launchedActivity + " processRunning=" + processRunning
                     + " processSwitch=" + processSwitch
                     + " processState=" + processState
+                    + " processOomAdj=" + processOomAdj
                     + " newActivityCreated=" + newActivityCreated + " info=" + info);
         }
 
@@ -689,8 +700,8 @@
         }
 
         final TransitionInfo newInfo = TransitionInfo.create(launchedActivity, launchingState,
-                options, processRunning, processSwitch, processState, newActivityCreated,
-                resultCode);
+                options, processRunning, processSwitch, processState, processOomAdj,
+                newActivityCreated, resultCode);
         if (newInfo == null) {
             abort(launchingState, "unrecognized launch");
             return;
@@ -1005,8 +1016,10 @@
             final long uptime = info.mTransitionDeviceUptimeMs;
             final int transitionDelay = info.mCurrentTransitionDelayMs;
             final int processState = info.mProcessState;
+            final int processOomAdj = info.mProcessOomAdj;
             mLoggerHandler.post(() -> logAppTransition(
-                    timestamp, uptime, transitionDelay, infoSnapshot, isHibernating, processState));
+                    timestamp, uptime, transitionDelay, infoSnapshot, isHibernating,
+                    processState, processOomAdj));
         }
         mLoggerHandler.post(() -> logAppDisplayed(infoSnapshot));
         if (info.mPendingFullyDrawn != null) {
@@ -1019,7 +1032,7 @@
     // This gets called on another thread without holding the activity manager lock.
     private void logAppTransition(long transitionStartTimeNs, long transitionDeviceUptimeMs,
             int currentTransitionDelayMs, TransitionInfoSnapshot info, boolean isHibernating,
-            int processState) {
+            int processState, int processOomAdj) {
         final LogMaker builder = new LogMaker(APP_TRANSITION);
         builder.setPackageName(info.packageName);
         builder.setType(info.type);
@@ -1086,7 +1099,8 @@
                 isLoading,
                 info.launchedActivityName.hashCode(),
                 TimeUnit.NANOSECONDS.toMillis(transitionStartTimeNs),
-                processState);
+                processState,
+                processOomAdj);
 
         if (DEBUG_METRICS) {
             Slog.i(TAG, String.format("APP_START_OCCURRED(%s, %s, %s, %s, %s)",
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index bd88c41..2e30962 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -660,6 +660,9 @@
 
     boolean mUseTransferredAnimation;
 
+    /** Whether we need to setup the animation to animate only within the letterbox. */
+    private boolean mNeedsLetterboxedAnimation;
+
     /**
      * @see #currentLaunchCanTurnScreenOn()
      */
@@ -1762,8 +1765,12 @@
         mLetterboxUiController.layoutLetterbox(winHint);
     }
 
-    boolean hasWallpaperBackgroudForLetterbox() {
-        return mLetterboxUiController.hasWallpaperBackgroudForLetterbox();
+    boolean hasWallpaperBackgroundForLetterbox() {
+        return mLetterboxUiController.hasWallpaperBackgroundForLetterbox();
+    }
+
+    void updateLetterboxSurface(WindowState winHint, Transaction t) {
+        mLetterboxUiController.updateLetterboxSurface(winHint, t);
     }
 
     void updateLetterboxSurface(WindowState winHint) {
@@ -5334,6 +5341,18 @@
         commitVisibility(visible, performLayout, false /* fromTransition */);
     }
 
+    void setNeedsLetterboxedAnimation(boolean needsLetterboxedAnimation) {
+        mNeedsLetterboxedAnimation = needsLetterboxedAnimation;
+    }
+
+    boolean isNeedsLetterboxedAnimation() {
+        return mNeedsLetterboxedAnimation;
+    }
+
+    boolean isInLetterboxAnimation() {
+        return mNeedsLetterboxedAnimation && isAnimating();
+    }
+
     /**
      * Post process after applying an app transition animation.
      *
@@ -7261,6 +7280,10 @@
                 .setParent(getAnimationLeashParent())
                 .setName(getSurfaceControl() + " - animation-bounds")
                 .setCallsite("ActivityRecord.createAnimationBoundsLayer");
+        if (mNeedsLetterboxedAnimation) {
+            // Needs to be an effect layer to support rounded corners
+            builder.setEffectLayer();
+        }
         final SurfaceControl boundsLayer = builder.build();
         t.show(boundsLayer);
         return boundsLayer;
@@ -7298,6 +7321,11 @@
             mAnimatingActivityRegistry.notifyStarting(this);
         }
 
+        if (mNeedsLetterboxedAnimation) {
+            updateLetterboxSurface(findMainWindow(), t);
+            mNeedsAnimationBoundsLayer = true;
+        }
+
         // If the animation needs to be cropped then an animation bounds layer is created as a
         // child of the root pinned task or animation layer. The leash is then reparented to this
         // new layer.
@@ -7320,6 +7348,17 @@
             t.setLayer(leash, 0);
             t.setLayer(mAnimationBoundsLayer, getLastLayer());
 
+            if (mNeedsLetterboxedAnimation) {
+                final int cornerRadius = mLetterboxUiController
+                        .getRoundedCornersRadius(findMainWindow());
+
+                final Rect letterboxInnerBounds = new Rect();
+                getLetterboxInnerBounds(letterboxInnerBounds);
+
+                t.setCornerRadius(mAnimationBoundsLayer, cornerRadius)
+                        .setCrop(mAnimationBoundsLayer, letterboxInnerBounds);
+            }
+
             // Reparent leash to animation bounds layer.
             t.reparent(leash, mAnimationBoundsLayer);
         }
@@ -7433,6 +7472,12 @@
             mAnimationBoundsLayer = null;
         }
 
+        mNeedsAnimationBoundsLayer = false;
+        if (mNeedsLetterboxedAnimation) {
+            mNeedsLetterboxedAnimation = false;
+            updateLetterboxSurface(findMainWindow(), t);
+        }
+
         if (mAnimatingActivityRegistry != null) {
             mAnimatingActivityRegistry.notifyFinished(this);
         }
@@ -7445,7 +7490,6 @@
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "AR#onAnimationFinished");
         mTransit = TRANSIT_OLD_UNSET;
         mTransitFlags = 0;
-        mNeedsAnimationBoundsLayer = false;
 
         setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM | FINISH_LAYOUT_REDO_WALLPAPER,
                 "ActivityRecord");
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 7323718..7d814c7 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -311,7 +311,7 @@
 
     /**
      * The duration to keep a process in animating state (top scheduling group) when the
-     * wakefulness is changing from awake to doze or sleep.
+     * wakefulness is dozing (unlocking) or changing from awake to doze or sleep (locking).
      */
     private static final long DOZE_ANIMATING_STATE_RETAIN_TIME_MS = 2000;
 
@@ -2927,12 +2927,14 @@
             mDemoteTopAppReasons &= ~DEMOTE_TOP_REASON_DURING_UNLOCKING;
             final WindowState notificationShade = mRootWindowContainer.getDefaultDisplay()
                     .getDisplayPolicy().getNotificationShade();
-            proc = notificationShade != null
-                    ? mProcessMap.getProcess(notificationShade.mSession.mPid) : null;
+            proc = notificationShade != null ? notificationShade.getProcess() : null;
         }
-        if (proc == null) {
-            return;
-        }
+        setProcessAnimatingWhileDozing(proc);
+    }
+
+    // The caller MUST NOT hold the global lock because it calls AM method directly.
+    void setProcessAnimatingWhileDozing(WindowProcessController proc) {
+        if (proc == null) return;
         // Set to activity manager directly to make sure the state can be seen by the subsequent
         // update of scheduling group.
         proc.setRunningAnimationUnsafe();
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index fb9d7e6..5ac5f2e 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -81,9 +81,11 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.Rect;
 import android.os.Trace;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Pair;
 import android.util.Slog;
 import android.view.Display;
 import android.view.RemoteAnimationAdapter;
@@ -1028,6 +1030,32 @@
             return;
         }
 
+        if (AppTransition.isActivityTransitOld(transit)) {
+            final ArrayList<Pair<ActivityRecord, Rect>> closingLetterboxes = new ArrayList();
+            for (int i = 0; i < closingApps.size(); ++i) {
+                ActivityRecord closingApp = closingApps.valueAt(i);
+                if (closingApp.areBoundsLetterboxed()) {
+                    final Rect insets = closingApp.getLetterboxInsets();
+                    closingLetterboxes.add(new Pair(closingApp, insets));
+                }
+            }
+
+            for (int i = 0; i < openingApps.size(); ++i) {
+                ActivityRecord openingApp = openingApps.valueAt(i);
+                if (openingApp.areBoundsLetterboxed()) {
+                    final Rect openingInsets = openingApp.getLetterboxInsets();
+                    for (Pair<ActivityRecord, Rect> closingLetterbox : closingLetterboxes) {
+                        final Rect closingInsets = closingLetterbox.second;
+                        if (openingInsets.equals(closingInsets)) {
+                            ActivityRecord closingApp = closingLetterbox.first;
+                            openingApp.setNeedsLetterboxedAnimation(true);
+                            closingApp.setNeedsLetterboxedAnimation(true);
+                        }
+                    }
+                }
+            }
+        }
+
         final ArraySet<WindowContainer> openingWcs = getAnimationTargets(
                 openingApps, closingApps, true /* visible */);
         final ArraySet<WindowContainer> closingWcs = getAnimationTargets(
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index 0422906..b033dca 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -504,6 +504,7 @@
 
     @Override
     public void onConfigurationChanged(Configuration newParentConfig) {
+        mTransitionController.collectForDisplayAreaChange(this);
         mTmpConfiguration.setTo(getConfiguration());
         super.onConfigurationChanged(newParentConfig);
 
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ecb9fe3..b645254 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -946,7 +946,7 @@
 
                 final int preferredModeId = getDisplayPolicy().getRefreshRatePolicy()
                         .getPreferredModeId(w);
-                if (mTmpApplySurfaceChangesTransactionState.preferredModeId == 0
+                if (w.isFocused() && mTmpApplySurfaceChangesTransactionState.preferredModeId == 0
                         && preferredModeId != 0) {
                     mTmpApplySurfaceChangesTransactionState.preferredModeId = preferredModeId;
                 }
@@ -5935,6 +5935,9 @@
         if (changes != 0) {
             Slog.i(TAG, "Override config changes=" + Integer.toHexString(changes) + " "
                     + mTempConfig + " for displayId=" + mDisplayId);
+            if (isReady() && mTransitionController.isShellTransitionsEnabled()) {
+                requestChangeTransitionIfNeeded(changes, null /* displayChange */);
+            }
             onRequestedOverrideConfigurationChanged(mTempConfig);
 
             final boolean isDensityChange = (changes & ActivityInfo.CONFIG_DENSITY) != 0;
@@ -5951,9 +5954,6 @@
             }
             mWmService.mDisplayNotificationController.dispatchDisplayChanged(
                     this, getConfiguration());
-            if (isReady() && mTransitionController.isShellTransitionsEnabled()) {
-                requestChangeTransitionIfNeeded(changes, null /* displayChange */);
-            }
         }
         return changes;
     }
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 096ebe2..bfe4c8b 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -258,7 +258,7 @@
     private volatile boolean mWindowManagerDrawComplete;
 
     private WindowState mStatusBar = null;
-    private WindowState mNotificationShade = null;
+    private volatile WindowState mNotificationShade;
     private final int[] mStatusBarHeightForRotation = new int[4];
     private WindowState mNavigationBar = null;
     @NavigationBarPosition
@@ -2746,6 +2746,19 @@
         mImmersiveModeConfirmation.onLockTaskModeChangedLw(lockTaskState);
     }
 
+    /** Called when a {@link android.os.PowerManager#USER_ACTIVITY_EVENT_TOUCH} is sent. */
+    public void onUserActivityEventTouch() {
+        // If there is keyguard, it may use INPUT_FEATURE_DISABLE_USER_ACTIVITY (InputDispatcher
+        // won't trigger user activity for touch). So while the device is not interactive, the user
+        // event is only sent explicitly from SystemUI.
+        if (mAwake) return;
+        // If the event is triggered while the display is not awake, the screen may be showing
+        // dozing UI such as AOD or overlay UI of under display fingerprint. Then set the animating
+        // state temporarily to make the process more responsive.
+        final WindowState w = mNotificationShade;
+        mService.mAtmService.setProcessAnimatingWhileDozing(w != null ? w.getProcess() : null);
+    }
+
     boolean onSystemUiSettingsChanged() {
         return mImmersiveModeConfirmation.onSettingChanged(mService.mCurrentUserId);
     }
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 8dd5850..97609a7 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -513,19 +513,6 @@
         return true;
     }
 
-    /**
-     * Utility to get a rotating displaycontent from a Transition.
-     * @return null if the transition doesn't contain a rotating display.
-     */
-    static DisplayContent getDisplayFromTransition(Transition transition) {
-        for (int i = transition.mParticipants.size() - 1; i >= 0; --i) {
-            final WindowContainer wc = transition.mParticipants.valueAt(i);
-            if (!(wc instanceof DisplayContent)) continue;
-            return (DisplayContent) wc;
-        }
-        return null;
-    }
-
     private void startRemoteRotation(int fromRotation, int toRotation) {
         mDisplayContent.mRemoteDisplayChangeController.performRemoteDisplayChange(
                 fromRotation, toRotation, null /* newDisplayAreaInfo */,
@@ -545,11 +532,6 @@
                 throw new IllegalStateException("Trying to rotate outside a transition");
             }
             mDisplayContent.mTransitionController.collect(mDisplayContent);
-            // Go through all tasks and collect them before the rotation
-            // TODO(shell-transitions): move collect() to onConfigurationChange once wallpaper
-            //       handling is synchronized.
-            mDisplayContent.mTransitionController.collectForDisplayAreaChange(mDisplayContent,
-                    null /* use collecting transition */);
         }
         mService.mAtmService.deferWindowLayout();
         try {
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 3e2d7c9..506396a 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -759,7 +759,7 @@
 
         InsetsPolicyAnimationControlListener(boolean show, Runnable finishCallback, int types) {
             super(show, false /* hasCallbacks */, types, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
-                    false /* disable */, 0 /* floatingImeBottomInsets */);
+                    false /* disable */, 0 /* floatingImeBottomInsets */, null);
             mFinishCallback = finishCallback;
             mControlCallbacks = new InsetsPolicyAnimationControlCallbacks(this);
         }
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index df3109a..27550d9 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -56,6 +56,7 @@
     private final Supplier<Boolean> mHasWallpaperBackgroundSupplier;
     private final Supplier<Integer> mBlurRadiusSupplier;
     private final Supplier<Float> mDarkScrimAlphaSupplier;
+    private final Supplier<SurfaceControl> mParentSurfaceSupplier;
 
     private final Rect mOuter = new Rect();
     private final Rect mInner = new Rect();
@@ -87,7 +88,8 @@
             Supplier<Integer> blurRadiusSupplier,
             Supplier<Float> darkScrimAlphaSupplier,
             IntConsumer doubleTapCallbackX,
-            IntConsumer doubleTapCallbackY) {
+            IntConsumer doubleTapCallbackY,
+            Supplier<SurfaceControl> parentSurface) {
         mSurfaceControlFactory = surfaceControlFactory;
         mTransactionFactory = transactionFactory;
         mAreCornersRounded = areCornersRounded;
@@ -97,6 +99,7 @@
         mDarkScrimAlphaSupplier = darkScrimAlphaSupplier;
         mDoubleTapCallbackX = doubleTapCallbackX;
         mDoubleTapCallbackY = doubleTapCallbackY;
+        mParentSurfaceSupplier = parentSurface;
     }
 
     /**
@@ -121,7 +124,6 @@
         mFullWindowSurface.layout(outer.left, outer.top, outer.right, outer.bottom, surfaceOrigin);
     }
 
-
     /**
      * Gets the insets between the outer and inner rects.
      */
@@ -333,6 +335,7 @@
         private SurfaceControl mSurface;
         private Color mColor;
         private boolean mHasWallpaperBackground;
+        private SurfaceControl mParentSurface;
 
         private final Rect mSurfaceFrameRelative = new Rect();
         private final Rect mLayoutFrameGlobal = new Rect();
@@ -403,10 +406,12 @@
                 }
 
                 mColor = mColorSupplier.get();
+                mParentSurface = mParentSurfaceSupplier.get();
                 t.setColor(mSurface, getRgbColorArray());
                 t.setPosition(mSurface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top);
                 t.setWindowCrop(mSurface, mSurfaceFrameRelative.width(),
                         mSurfaceFrameRelative.height());
+                t.reparent(mSurface, mParentSurface);
 
                 mHasWallpaperBackground = mHasWallpaperBackgroundSupplier.get();
                 updateAlphaAndBlur(t);
@@ -452,12 +457,13 @@
 
         public boolean needsApplySurfaceChanges() {
             return !mSurfaceFrameRelative.equals(mLayoutFrameRelative)
-                    // If mSurfaceFrameRelative is empty then mHasWallpaperBackground and mColor
-                    // may never be updated in applySurfaceChanges but this doesn't mean that
-                    // update is needed.
+                    // If mSurfaceFrameRelative is empty then mHasWallpaperBackground, mColor,
+                    // and mParentSurface may never be updated in applySurfaceChanges but this
+                    // doesn't mean that update is needed.
                     || !mSurfaceFrameRelative.isEmpty()
                     && (mHasWallpaperBackgroundSupplier.get() != mHasWallpaperBackground
-                    || !mColorSupplier.get().equals(mColor));
+                    || !mColorSupplier.get().equals(mColor)
+                    || mParentSurfaceSupplier.get() != mParentSurface);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 57c60f4..a469c6b 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -265,7 +265,7 @@
     }
 
     /**
-     * Overrides corners raidus for activities presented in the letterbox mode. If given value < 0,
+     * Overrides corners radius for activities presented in the letterbox mode. If given value < 0,
      * both it and a value of {@link
      * com.android.internal.R.integer.config_letterboxActivityCornersRadius} will be ignored and
      * corners of the activity won't be rounded.
@@ -275,7 +275,7 @@
     }
 
     /**
-     * Resets corners raidus for activities presented in the letterbox mode to {@link
+     * Resets corners radius for activities presented in the letterbox mode to {@link
      * com.android.internal.R.integer.config_letterboxActivityCornersRadius}.
      */
     void resetLetterboxActivityCornersRadius() {
@@ -291,7 +291,7 @@
     }
 
     /**
-     * Gets corners raidus for activities presented in the letterbox mode.
+     * Gets corners radius for activities presented in the letterbox mode.
      */
     int getLetterboxActivityCornersRadius() {
         return mLetterboxActivityCornersRadius;
@@ -318,7 +318,7 @@
     /**
      * Sets color of letterbox background which is used when {@link
      * #getLetterboxBackgroundType()} is {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} or as
-     * fallback for other backfround types.
+     * fallback for other background types.
      */
     void setLetterboxBackgroundColor(Color color) {
         mLetterboxBackgroundColorOverride = color;
@@ -327,7 +327,7 @@
     /**
      * Sets color ID of letterbox background which is used when {@link
      * #getLetterboxBackgroundType()} is {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} or as
-     * fallback for other backfround types.
+     * fallback for other background types.
      */
     void setLetterboxBackgroundColorResourceId(int colorId) {
         mLetterboxBackgroundColorResourceIdOverride = colorId;
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index c8ed602..317c93e 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -89,7 +89,7 @@
 
     // Taskbar expanded height. Used to determine whether to crop an app window to display rounded
     // corners above the taskbar.
-    private float mExpandedTaskBarHeight;
+    private final float mExpandedTaskBarHeight;
 
     private boolean mShowWallpaperForLetterboxBackground;
 
@@ -120,7 +120,7 @@
         }
     }
 
-    boolean hasWallpaperBackgroudForLetterbox() {
+    boolean hasWallpaperBackgroundForLetterbox() {
         return mShowWallpaperForLetterboxBackground;
     }
 
@@ -137,6 +137,11 @@
     void getLetterboxInnerBounds(Rect outBounds) {
         if (mLetterbox != null) {
             outBounds.set(mLetterbox.getInnerFrame());
+            final WindowState w = mActivityRecord.findMainWindow();
+            if (w == null) {
+                return;
+            }
+            adjustBoundsForTaskbar(w, outBounds);
         } else {
             outBounds.setEmpty();
         }
@@ -160,13 +165,17 @@
     }
 
     void updateLetterboxSurface(WindowState winHint) {
+        updateLetterboxSurface(winHint, mActivityRecord.getSyncTransaction());
+    }
+
+    void updateLetterboxSurface(WindowState winHint, Transaction t) {
         final WindowState w = mActivityRecord.findMainWindow();
         if (w != winHint && winHint != null && w != null) {
             return;
         }
         layoutLetterbox(winHint);
         if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) {
-            mLetterbox.applySurfaceChanges(mActivityRecord.getSyncTransaction());
+            mLetterbox.applySurfaceChanges(t);
         }
     }
 
@@ -191,14 +200,22 @@
                         mActivityRecord.mWmService.mTransactionFactory,
                         this::shouldLetterboxHaveRoundedCorners,
                         this::getLetterboxBackgroundColor,
-                        this::hasWallpaperBackgroudForLetterbox,
+                        this::hasWallpaperBackgroundForLetterbox,
                         this::getLetterboxWallpaperBlurRadius,
                         this::getLetterboxWallpaperDarkScrimAlpha,
                         this::handleHorizontalDoubleTap,
-                        this::handleVerticalDoubleTap);
+                        this::handleVerticalDoubleTap,
+                        this::getLetterboxParentSurface);
                 mLetterbox.attachInput(w);
             }
-            mActivityRecord.getPosition(mTmpPoint);
+
+            if (mActivityRecord.isInLetterboxAnimation()) {
+                // In this case we attach the letterbox to the task instead of the activity.
+                mActivityRecord.getTask().getPosition(mTmpPoint);
+            } else {
+                mActivityRecord.getPosition(mTmpPoint);
+            }
+
             // Get the bounds of the "space-to-fill". The transformed bounds have the highest
             // priority because the activity is launched in a rotated environment. In multi-window
             // mode, the task-level represents this. In fullscreen-mode, the task container does
@@ -215,6 +232,13 @@
         }
     }
 
+    SurfaceControl getLetterboxParentSurface() {
+        if (mActivityRecord.isInLetterboxAnimation()) {
+            return mActivityRecord.getTask().getSurfaceControl();
+        }
+        return mActivityRecord.getSurfaceControl();
+    }
+
     private boolean shouldLetterboxHaveRoundedCorners() {
         // TODO(b/214030873): remove once background is drawn for transparent activities
         // Letterbox shouldn't have rounded corners if the activity is transparent
@@ -436,7 +460,7 @@
                 }
                 break;
             case LETTERBOX_BACKGROUND_WALLPAPER:
-                if (hasWallpaperBackgroudForLetterbox()) {
+                if (hasWallpaperBackgroundForLetterbox()) {
                     // Color is used for translucent scrim that dims wallpaper.
                     return Color.valueOf(Color.BLACK);
                 }
@@ -459,15 +483,14 @@
     private void updateRoundedCorners(WindowState mainWindow) {
         final SurfaceControl windowSurface = mainWindow.getClientViewRootSurface();
         if (windowSurface != null && windowSurface.isValid()) {
-            Transaction transaction = mActivityRecord.getSyncTransaction();
+            final Transaction transaction = mActivityRecord.getSyncTransaction();
 
-            final InsetsState insetsState = mainWindow.getInsetsState();
-            final InsetsSource taskbarInsetsSource =
-                    insetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
-
-            if (!isLetterboxedNotForDisplayCutout(mainWindow)
-                    || !mLetterboxConfiguration.isLetterboxActivityCornersRounded()
-                    || taskbarInsetsSource == null) {
+            if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
+                // We don't want corner radius on the window.
+                // In the case the ActivityRecord requires a letterboxed animation we never want
+                // rounded corners on the window because rounded corners are applied at the
+                // animation-bounds surface level and rounded corners on the window would interfere
+                // with that leading to unexpected rounded corner positioning during the animation.
                 transaction
                         .setWindowCrop(windowSurface, null)
                         .setCornerRadius(windowSurface, 0);
@@ -476,48 +499,89 @@
 
             Rect cropBounds = null;
 
-            // Rounded corners should be displayed above the taskbar. When taskbar is hidden,
-            // an insets frame is equal to a navigation bar which shouldn't affect position of
-            // rounded corners since apps are expected to handle navigation bar inset.
-            // This condition checks whether the taskbar is visible.
-            // Do not crop the taskbar inset if the window is in immersive mode - the user can
-            // swipe to show/hide the taskbar as an overlay.
-            if (taskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight
-                    && taskbarInsetsSource.isVisible()) {
+            if (hasVisibleTaskbar(mainWindow)) {
                 cropBounds = new Rect(mActivityRecord.getBounds());
                 // Activity bounds are in screen coordinates while (0,0) for activity's surface
                 // control is at the top left corner of an app window so offsetting bounds
                 // accordingly.
                 cropBounds.offsetTo(0, 0);
-                // Rounded cornerners should be displayed above the taskbar.
-                cropBounds.bottom =
-                        Math.min(cropBounds.bottom, taskbarInsetsSource.getFrame().top);
-                if (mActivityRecord.inSizeCompatMode()
-                        && mActivityRecord.getSizeCompatScale() < 1.0f) {
-                    cropBounds.scale(1.0f / mActivityRecord.getSizeCompatScale());
-                }
+                // Rounded corners should be displayed above the taskbar.
+                adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds);
             }
 
             transaction
                     .setWindowCrop(windowSurface, cropBounds)
-                    .setCornerRadius(windowSurface, getRoundedCorners(insetsState));
+                    .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow));
         }
     }
 
-    // Returns rounded corners radius based on override in
+    private boolean requiresRoundedCorners(WindowState mainWindow) {
+        final InsetsSource taskbarInsetsSource = getTaskbarInsetsSource(mainWindow);
+
+        return isLetterboxedNotForDisplayCutout(mainWindow)
+                && mLetterboxConfiguration.isLetterboxActivityCornersRounded()
+                && taskbarInsetsSource != null;
+    }
+
+    // Returns rounded corners radius the letterboxed activity should have based on override in
     // R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii.
     // Device corners can be different on the right and left sides but we use the same radius
     // for all corners for consistency and pick a minimal bottom one for consistency with a
     // taskbar rounded corners.
-    private int getRoundedCorners(InsetsState insetsState) {
+    int getRoundedCornersRadius(WindowState mainWindow) {
+        if (!requiresRoundedCorners(mainWindow)) {
+            return 0;
+        }
+
         if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) {
             return mLetterboxConfiguration.getLetterboxActivityCornersRadius();
         }
+
+        final InsetsState insetsState = mainWindow.getInsetsState();
         return Math.min(
                 getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT),
                 getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT));
     }
 
+    /**
+     * Returns whether the taskbar is visible. Returns false if the window is in immersive mode,
+     * since the user can swipe to show/hide the taskbar as an overlay.
+     */
+    private boolean hasVisibleTaskbar(WindowState mainWindow) {
+        final InsetsSource taskbarInsetsSource = getTaskbarInsetsSource(mainWindow);
+
+        return taskbarInsetsSource != null
+                && taskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight
+                && taskbarInsetsSource.isVisible();
+    }
+
+    private InsetsSource getTaskbarInsetsSource(WindowState mainWindow) {
+        final InsetsState insetsState = mainWindow.getInsetsState();
+        return insetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+    }
+
+    private void adjustBoundsForTaskbar(WindowState mainWindow, Rect bounds) {
+        // Rounded corners should be displayed above the taskbar. When taskbar is hidden,
+        // an insets frame is equal to a navigation bar which shouldn't affect position of
+        // rounded corners since apps are expected to handle navigation bar inset.
+        // This condition checks whether the taskbar is visible.
+        // Do not crop the taskbar inset if the window is in immersive mode - the user can
+        // swipe to show/hide the taskbar as an overlay.
+        if (hasVisibleTaskbar(mainWindow)) {
+            adjustBoundsForTaskbarUnchecked(mainWindow, bounds);
+        }
+    }
+
+    private void adjustBoundsForTaskbarUnchecked(WindowState mainWindow, Rect bounds) {
+        // Rounded corners should be displayed above the taskbar.
+        bounds.bottom =
+                Math.min(bounds.bottom, getTaskbarInsetsSource(mainWindow).getFrame().top);
+        if (mActivityRecord.inSizeCompatMode()
+                && mActivityRecord.getSizeCompatScale() < 1.0f) {
+            bounds.scale(1.0f / mActivityRecord.getSizeCompatScale());
+        }
+    }
+
     private int getInsetsStateCornerRadius(
                 InsetsState insetsState, @RoundedCorner.Position int position) {
         RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position);
@@ -592,7 +656,7 @@
                 + letterboxBackgroundTypeToString(
                         mLetterboxConfiguration.getLetterboxBackgroundType()));
         pw.println(prefix + "  letterboxCornerRadius="
-                + getRoundedCorners(mainWin.getInsetsState()));
+                + getRoundedCornersRadius(mainWin));
         if (mLetterboxConfiguration.getLetterboxBackgroundType()
                 == LETTERBOX_BACKGROUND_WALLPAPER) {
             pw.println(prefix + "  isLetterboxWallpaperBlurSupported="
diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
index 64749cf..a89894d 100644
--- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
+++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
@@ -101,7 +101,6 @@
 
         if (t != null) {
             mDisplayContent.mAtmService.startLaunchPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY);
-            mTransitionController.collectForDisplayAreaChange(mDisplayContent, t);
             mTransition = t;
         }
     }
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java b/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java
index c7f8a1e..f3670e4 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java
@@ -27,7 +27,10 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.Nullable;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
 import android.graphics.Insets;
+import android.graphics.Paint;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.hardware.power.Boost;
@@ -374,43 +377,45 @@
         final int targetSurfaceWidth = bounds.width();
 
         if (maxExtensionInsets.left < 0) {
-            final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
+            final Rect edgeBounds = new Rect(bounds.left, bounds.top, bounds.left + 1,
+                    bounds.bottom);
             final Rect extensionRect = new Rect(0, 0,
                     -maxExtensionInsets.left, targetSurfaceHeight);
-            final int xPos = maxExtensionInsets.left;
-            final int yPos = 0;
+            final int xPos = bounds.left + maxExtensionInsets.left;
+            final int yPos = bounds.top;
             createExtensionSurface(leash, edgeBounds,
                     extensionRect, xPos, yPos, "Left Edge Extension", transaction);
         }
 
         if (maxExtensionInsets.top < 0) {
-            final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
+            final Rect edgeBounds = new Rect(bounds.left, bounds.top, targetSurfaceWidth,
+                    bounds.top + 1);
             final Rect extensionRect = new Rect(0, 0,
                     targetSurfaceWidth, -maxExtensionInsets.top);
-            final int xPos = 0;
-            final int yPos = maxExtensionInsets.top;
+            final int xPos = bounds.left;
+            final int yPos = bounds.top + maxExtensionInsets.top;
             createExtensionSurface(leash, edgeBounds,
                     extensionRect, xPos, yPos, "Top Edge Extension", transaction);
         }
 
         if (maxExtensionInsets.right < 0) {
-            final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
-                    targetSurfaceWidth, targetSurfaceHeight);
+            final Rect edgeBounds = new Rect(bounds.right - 1, bounds.top, bounds.right,
+                    bounds.bottom);
             final Rect extensionRect = new Rect(0, 0,
                     -maxExtensionInsets.right, targetSurfaceHeight);
-            final int xPos = targetSurfaceWidth;
-            final int yPos = 0;
+            final int xPos = bounds.right;
+            final int yPos = bounds.top;
             createExtensionSurface(leash, edgeBounds,
                     extensionRect, xPos, yPos, "Right Edge Extension", transaction);
         }
 
         if (maxExtensionInsets.bottom < 0) {
-            final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
-                    targetSurfaceWidth, targetSurfaceHeight);
+            final Rect edgeBounds = new Rect(bounds.left, bounds.bottom - 1,
+                    bounds.right, bounds.bottom);
             final Rect extensionRect = new Rect(0, 0,
                     targetSurfaceWidth, -maxExtensionInsets.bottom);
-            final int xPos = maxExtensionInsets.left;
-            final int yPos = targetSurfaceHeight;
+            final int xPos = bounds.left;
+            final int yPos = bounds.bottom;
             createExtensionSurface(leash, edgeBounds,
                     extensionRect, xPos, yPos, "Bottom Edge Extension", transaction);
         }
@@ -453,16 +458,20 @@
                 .setHidden(true)
                 .setCallsite("DefaultTransitionHandler#startAnimation")
                 .setOpaque(true)
-                .setBufferSize(edgeBounds.width(), edgeBounds.height())
+                .setBufferSize(extensionRect.width(), extensionRect.height())
                 .build();
 
-        final Surface surface = new Surface(edgeExtensionLayer);
-        surface.attachAndQueueBufferWithColorSpace(edgeBuffer.getHardwareBuffer(),
-                edgeBuffer.getColorSpace());
-        surface.release();
+        BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(),
+                android.graphics.Shader.TileMode.CLAMP,
+                android.graphics.Shader.TileMode.CLAMP);
+        final Paint paint = new Paint();
+        paint.setShader(shader);
 
-        final float scaleX = getScaleXForExtensionSurface(edgeBounds, extensionRect);
-        final float scaleY = getScaleYForExtensionSurface(edgeBounds, extensionRect);
+        final Surface surface = new Surface(edgeExtensionLayer);
+        Canvas c = surface.lockHardwareCanvas();
+        c.drawRect(extensionRect, paint);
+        surface.unlockCanvasAndPost(c);
+        surface.release();
 
         synchronized (mEdgeExtensionLock) {
             if (!mEdgeExtensions.containsKey(leash)) {
@@ -472,7 +481,6 @@
                 return;
             }
 
-            startTransaction.setScale(edgeExtensionLayer, scaleX, scaleY);
             startTransaction.reparent(edgeExtensionLayer, leash);
             startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
             startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
@@ -508,8 +516,6 @@
         throw new RuntimeException("Unexpected edgeBounds and extensionRect heights");
     }
 
-
-
     private static final class RunningAnimation {
         final AnimationSpec mAnimSpec;
         final SurfaceControl mLeash;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index fa2ab31..803890b 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1641,6 +1641,19 @@
         return mainWin.getAttrs().rotationAnimation;
     }
 
+    /** Applies the new configuration and returns {@code true} if there is a display change. */
+    boolean applyDisplayChangeIfNeeded() {
+        boolean changed = false;
+        for (int i = mParticipants.size() - 1; i >= 0; --i) {
+            final WindowContainer<?> wc = mParticipants.valueAt(i);
+            final DisplayContent dc = wc.asDisplayContent();
+            if (dc == null || !mChanges.get(dc).hasChanged(dc)) continue;
+            dc.sendNewConfiguration();
+            changed = true;
+        }
+        return changed;
+    }
+
     boolean getLegacyIsReady() {
         return isCollecting() && mSyncId >= 0;
     }
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 4f324f2..846aa3e 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -463,13 +463,12 @@
     }
 
     /**
-     * Collects the window containers which need to be synced with the changing display (e.g.
-     * rotating) to the given transition or the current collecting transition.
+     * Collects the window containers which need to be synced with the changing display area into
+     * the current collecting transition.
      */
-    void collectForDisplayAreaChange(@NonNull DisplayArea<?> wc, @Nullable Transition incoming) {
-        if (incoming == null) incoming = mCollectingTransition;
-        if (incoming == null) return;
-        final Transition transition = incoming;
+    void collectForDisplayAreaChange(@NonNull DisplayArea<?> wc) {
+        final Transition transition = mCollectingTransition;
+        if (transition == null || !transition.mParticipants.contains(wc)) return;
         // Collect all visible tasks.
         wc.forAllLeafTasks(task -> {
             if (task.isVisible()) {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 25193d0..7979048 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3015,6 +3015,10 @@
                 final float windowCornerRadius = !inMultiWindowMode()
                         ? getDisplayContent().getWindowCornerRadius()
                         : 0;
+                if (asActivityRecord() != null
+                        && asActivityRecord().isNeedsLetterboxedAnimation()) {
+                    asActivityRecord().getLetterboxInnerBounds(mTmpRect);
+                }
                 AnimationAdapter adapter = new LocalAnimationAdapter(
                         new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
                                 getDisplayContent().mAppTransition.canSkipFirstFrame(),
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 3b9cd36..4f03264 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -397,17 +397,8 @@
         mService.deferWindowLayout();
         mService.mTaskSupervisor.setDeferRootVisibilityUpdate(true /* deferUpdate */);
         try {
-            if (transition != null) {
-                // First check if we have a display rotation transition and if so, update it.
-                final DisplayContent dc = DisplayRotation.getDisplayFromTransition(transition);
-                if (dc != null && transition.mChanges.get(dc).hasChanged(dc)) {
-                    // Go through all tasks and collect them before the rotation
-                    // TODO(shell-transitions): move collect() to onConfigurationChange once
-                    //       wallpaper handling is synchronized.
-                    dc.mTransitionController.collectForDisplayAreaChange(dc, transition);
-                    dc.sendNewConfiguration();
-                    effects |= TRANSACT_EFFECTS_LIFECYCLE;
-                }
+            if (transition != null && transition.applyDisplayChangeIfNeeded()) {
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
             }
             final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps();
             final int hopSize = hops.size();
@@ -428,15 +419,6 @@
                     addToSyncSet(syncId, wc);
                 }
                 if (transition != null) transition.collect(wc);
-                final DisplayArea da = wc.asDisplayArea();
-                // Only check DisplayArea here as a similar thing is done for DisplayContent above.
-                if (da != null && wc.asDisplayContent() == null
-                        && entry.getValue().getWindowingMode() != da.getWindowingMode()) {
-                    // Go through all tasks and collect them before changing the windowing mode of a
-                    // display-level container.
-                    // TODO(shell-transitions): handle this more elegantly.
-                    da.mTransitionController.collectForDisplayAreaChange(da, transition);
-                }
 
                 if ((entry.getValue().getChangeMask()
                         & WindowContainerTransaction.Change.CHANGE_FORCE_NO_PIP) != 0) {
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 87b0c8b..202fe55 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -25,6 +25,7 @@
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.server.am.ProcessList.INVALID_ADJ;
 import static com.android.server.wm.ActivityRecord.State.DESTROYED;
 import static com.android.server.wm.ActivityRecord.State.DESTROYING;
 import static com.android.server.wm.ActivityRecord.State.PAUSED;
@@ -123,6 +124,8 @@
     private volatile int mCurProcState = PROCESS_STATE_NONEXISTENT;
     // Last reported process state;
     private volatile int mRepProcState = PROCESS_STATE_NONEXISTENT;
+    // Currently computed oom adj score
+    private volatile int mCurAdj = INVALID_ADJ;
     // are we in the process of crashing?
     private volatile boolean mCrashing;
     // does the app have a not responding dialog?
@@ -317,6 +320,14 @@
         return mCurProcState;
     }
 
+    public void setCurrentAdj(int curAdj) {
+        mCurAdj = curAdj;
+    }
+
+    int getCurrentAdj() {
+        return mCurAdj;
+    }
+
     /**
      * Sets the computed process state from the oom adjustment calculation. This is frequently
      * called in activity manager's lock, so don't use window manager lock here.
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 41bcbf6..2432afb 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3803,6 +3803,10 @@
         return wpc != null && wpc.registeredForDisplayAreaConfigChanges();
     }
 
+    WindowProcessController getProcess() {
+        return mWpcForDisplayAreaConfigChanges;
+    }
+
     /**
      * Fills the given window frames and merged configuration for the client.
      *
@@ -6049,7 +6053,7 @@
     }
 
     boolean hasWallpaperForLetterboxBackground() {
-        return mActivityRecord != null && mActivityRecord.hasWallpaperBackgroudForLetterbox();
+        return mActivityRecord != null && mActivityRecord.hasWallpaperBackgroundForLetterbox();
     }
 
     /**
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index e2c3a94..4c939f0 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -83,6 +83,7 @@
                 VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED,
                 /* activityListener= */ null,
                 /* activityBlockedCallback= */ null,
+                /* secureWindowCallback= */ null,
                 /* deviceProfile= */ DEVICE_PROFILE_APP_STREAMING);
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
index 911fb6a..08c2c6e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
@@ -1301,6 +1301,21 @@
     }
 
     @Test
+    public void testA11yCrossUserEventNotSent() throws Exception {
+        final Notification n = new Builder(getContext(), "test")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+        int userId = mUser.getIdentifier() + 1;
+        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid,
+                mPid, n, UserHandle.of(userId), null, System.currentTimeMillis());
+        NotificationRecord r = new NotificationRecord(getContext(), sbn,
+                new NotificationChannel("test", "test", IMPORTANCE_HIGH));
+
+        mService.buzzBeepBlinkLocked(r);
+
+        verify(mAccessibilityService, never()).sendAccessibilityEvent(any(), anyInt());
+    }
+
+    @Test
     public void testLightsScreenOn() {
         mService.mScreenOn = true;
         NotificationRecord r = getLightsNotification();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 598a22b..d62ac99 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -93,6 +93,7 @@
 import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Parcel;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -2447,6 +2448,35 @@
     }
 
     @Test
+    public void testGetNotificationChannelGroup() throws Exception {
+        NotificationChannelGroup notDeleted = new NotificationChannelGroup("not", "deleted");
+        NotificationChannel base =
+                new NotificationChannel("not deleted", "belongs to notDeleted", IMPORTANCE_DEFAULT);
+        base.setGroup("not");
+        NotificationChannel convo =
+                new NotificationChannel("convo", "belongs to notDeleted", IMPORTANCE_DEFAULT);
+        convo.setGroup("not");
+        convo.setConversationId("not deleted", "banana");
+
+        mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, base, true, false);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, convo, true, false);
+        mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true);
+
+        NotificationChannelGroup g
+                = mHelper.getNotificationChannelGroup(notDeleted.getId(), PKG_N_MR1, UID_N_MR1);
+        Parcel parcel = Parcel.obtain();
+        g.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        NotificationChannelGroup g2
+                = mHelper.getNotificationChannelGroup(notDeleted.getId(), PKG_N_MR1, UID_N_MR1);
+        Parcel parcel2 = Parcel.obtain();
+        g2.writeToParcel(parcel2, 0);
+        parcel2.setDataPosition(0);
+    }
+
+    @Test
     public void testOnUserRemoved() throws Exception {
         int[] user0Uids = {98, 235, 16, 3782};
         int[] user1Uids = new int[user0Uids.length];
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 600881e..be266c9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1904,10 +1904,10 @@
         testPlayer.start();
         assertNotEquals(origRot, dc.getConfiguration().windowConfiguration.getRotation());
         assertNotNull(testPlayer.mLastReady);
-        assertEquals(dc, DisplayRotation.getDisplayFromTransition(testPlayer.mLastTransit));
         WindowContainerToken dcToken = dc.mRemoteToken.toWindowContainerToken();
         assertNotEquals(testPlayer.mLastReady.getChange(dcToken).getEndRotation(),
                 testPlayer.mLastReady.getChange(dcToken).getStartRotation());
+        assertTrue(testPlayer.mLastTransit.applyDisplayChangeIfNeeded());
         testPlayer.finish();
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
index e502f2f..d400a4c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
@@ -56,6 +56,7 @@
     private boolean mHasWallpaperBackground = false;
     private int mBlurRadius = 0;
     private float mDarkScrimAlpha = 0.5f;
+    private SurfaceControl mParentSurface = mock(SurfaceControl.class);
 
     @Before
     public void setUp() throws Exception {
@@ -63,7 +64,8 @@
         mLetterbox = new Letterbox(mSurfaces, StubTransaction::new,
                 () -> mAreCornersRounded, () -> Color.valueOf(mColor),
                 () -> mHasWallpaperBackground, () -> mBlurRadius, () -> mDarkScrimAlpha,
-                /* doubleTapCallbackX= */ x -> {}, /* doubleTapCallbackY= */ y -> {});
+                /* doubleTapCallbackX= */ x -> {}, /* doubleTapCallbackY= */ y -> {},
+                () -> mParentSurface);
         mTransaction = spy(StubTransaction.class);
     }
 
@@ -205,6 +207,22 @@
     }
 
     @Test
+    public void testNeedsApplySurfaceChanges_setParentSurface() {
+        mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000));
+        mLetterbox.applySurfaceChanges(mTransaction);
+
+        verify(mTransaction).reparent(mSurfaces.top, mParentSurface);
+        assertFalse(mLetterbox.needsApplySurfaceChanges());
+
+        mParentSurface = mock(SurfaceControl.class);
+
+        assertTrue(mLetterbox.needsApplySurfaceChanges());
+
+        mLetterbox.applySurfaceChanges(mTransaction);
+        verify(mTransaction).reparent(mSurfaces.top, mParentSurface);
+    }
+
+    @Test
     public void testApplySurfaceChanges_cornersNotRounded_surfaceFullWindowSurfaceNotCreated() {
         mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000));
         mLetterbox.applySurfaceChanges(mTransaction);
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 8332cb4..da72030 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -158,7 +158,7 @@
         mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentAppeared(any());
+        verify(mOrganizer, never()).onTaskFragmentAppeared(any(), any());
 
         // Send callback when the TaskFragment is attached.
         setupMockParent(mTaskFragment, mTask);
@@ -166,7 +166,7 @@
         mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentAppeared(any());
+        verify(mOrganizer).onTaskFragmentAppeared(any(), any());
     }
 
     @Test
@@ -179,13 +179,13 @@
                 mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
 
         // Call onTaskFragmentAppeared first.
         mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentAppeared(any());
+        verify(mOrganizer).onTaskFragmentAppeared(any(), any());
 
         // No callback if the info is not changed.
         doReturn(true).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any());
@@ -195,7 +195,7 @@
                 mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
 
         // Trigger callback if the info is changed.
         doReturn(false).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any());
@@ -204,7 +204,7 @@
                 mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentInfoChanged(eq(mTaskFragmentInfo));
+        verify(mOrganizer).onTaskFragmentInfoChanged(any(), eq(mTaskFragmentInfo));
     }
 
     @Test
@@ -215,7 +215,7 @@
         mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentVanished(any());
+        verify(mOrganizer).onTaskFragmentVanished(any(), any());
     }
 
     @Test
@@ -228,10 +228,10 @@
         mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentAppeared(any());
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
-        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(anyInt(), any());
-        verify(mOrganizer).onTaskFragmentVanished(eq(mTaskFragmentInfo));
+        verify(mOrganizer, never()).onTaskFragmentAppeared(any(), any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
+        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(any(), anyInt(), any());
+        verify(mOrganizer).onTaskFragmentVanished(any(), eq(mTaskFragmentInfo));
 
         // Not trigger onTaskFragmentInfoChanged.
         // Call onTaskFragmentAppeared before calling onTaskFragmentInfoChanged.
@@ -244,10 +244,10 @@
         mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentAppeared(any());
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
-        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(anyInt(), any());
-        verify(mOrganizer).onTaskFragmentVanished(eq(mTaskFragmentInfo));
+        verify(mOrganizer, never()).onTaskFragmentAppeared(any(), any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
+        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(any(), anyInt(), any());
+        verify(mOrganizer).onTaskFragmentVanished(any(), eq(mTaskFragmentInfo));
     }
 
     @Test
@@ -260,7 +260,7 @@
                 mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentParentInfoChanged(eq(mTask.mTaskId), any());
+        verify(mOrganizer).onTaskFragmentParentInfoChanged(any(), eq(mTask.mTaskId), any());
 
         // No extra callback if the info is not changed.
         clearInvocations(mOrganizer);
@@ -269,7 +269,7 @@
                 mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(anyInt(), any());
+        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(any(), anyInt(), any());
 
         // Trigger callback if the size is changed.
         mTask.getConfiguration().smallestScreenWidthDp = 100;
@@ -277,7 +277,7 @@
                 mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentParentInfoChanged(eq(mTask.mTaskId), any());
+        verify(mOrganizer).onTaskFragmentParentInfoChanged(any(), eq(mTask.mTaskId), any());
 
         // Trigger callback if the windowing mode is changed.
         clearInvocations(mOrganizer);
@@ -286,7 +286,7 @@
                 mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentParentInfoChanged(eq(mTask.mTaskId), any());
+        verify(mOrganizer).onTaskFragmentParentInfoChanged(any(), eq(mTask.mTaskId), any());
     }
 
     @Test
@@ -298,7 +298,7 @@
                 mErrorToken, null /* taskFragment */, -1 /* opType */, exception);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), eq(null), eq(-1),
+        verify(mOrganizer).onTaskFragmentError(any(), eq(mErrorToken), eq(null), eq(-1),
                 eq(exception));
     }
 
@@ -318,14 +318,14 @@
         mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onActivityReparentedToTask(anyInt(), any(), any());
+        verify(mOrganizer, never()).onActivityReparentedToTask(any(), anyInt(), any(), any());
 
         // Notify organizer if it was embedded before entered Pip.
         activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer;
         mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onActivityReparentedToTask(eq(task.mTaskId), eq(activity.intent),
+        verify(mOrganizer).onActivityReparentedToTask(any(), eq(task.mTaskId), eq(activity.intent),
                 eq(activity.token));
 
         // Notify organizer if there is any embedded in the Task.
@@ -341,7 +341,7 @@
         mController.dispatchPendingEvents();
 
         verify(mOrganizer, times(2))
-                .onActivityReparentedToTask(eq(task.mTaskId), eq(activity.intent),
+                .onActivityReparentedToTask(any(), eq(task.mTaskId), eq(activity.intent),
                         eq(activity.token));
     }
 
@@ -371,7 +371,7 @@
         mController.dispatchPendingEvents();
 
         // Allow organizer to reparent activity in other process using the temporary token.
-        verify(mOrganizer).onActivityReparentedToTask(eq(task.mTaskId), eq(activity.intent),
+        verify(mOrganizer).onActivityReparentedToTask(any(), eq(task.mTaskId), eq(activity.intent),
                 token.capture());
         final IBinder temporaryToken = token.getValue();
         assertNotEquals(activity.token, temporaryToken);
@@ -801,7 +801,7 @@
         mController.dispatchPendingEvents();
 
         // Verifies that event was not sent
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
     }
 
     @Test
@@ -827,7 +827,7 @@
         mController.dispatchPendingEvents();
 
         // Verifies that event was not sent
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
 
         // Mock the task becomes visible, and activity resumed
         doReturn(true).when(task).shouldBeVisible(any());
@@ -835,7 +835,7 @@
 
         // Verifies that event is sent.
         mController.dispatchPendingEvents();
-        verify(mOrganizer).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
     }
 
     /**
@@ -869,7 +869,7 @@
         reset(mOrganizer);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-        verify(mOrganizer).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
     }
 
     /**
@@ -887,8 +887,8 @@
                 .createActivityCount(1)
                 .build();
         final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity();
-        // Add another activity in the Task so that it always contains a non-finishing activitiy.
-        final ActivityRecord nonEmbeddedActivity = createActivityRecord(task);
+        // Add another activity in the Task so that it always contains a non-finishing activity.
+        createActivityRecord(task);
         assertTrue(task.shouldBeVisible(null));
 
         // Dispatch pending info changed event from creating the activity
@@ -896,21 +896,21 @@
         taskFragment.mTaskFragmentAppearedSent = true;
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-        verify(mOrganizer).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
 
         // Verify the info changed callback is not called when the task is invisible
         reset(mOrganizer);
         doReturn(false).when(task).shouldBeVisible(any());
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
 
         // Finish the embedded activity, and verify the info changed callback is called because the
         // TaskFragment is becoming empty.
         embeddedActivity.finishing = true;
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-        verify(mOrganizer).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
     }
 
     /**
@@ -1020,7 +1020,7 @@
         // The pending event will be dispatched on the handler (from requestTraversal).
         waitHandlerIdle(mWm.mAnimationHandler);
 
-        verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), any(),
+        verify(mOrganizer).onTaskFragmentError(any(), eq(mErrorToken), any(),
                 eq(HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT),
                 any(SecurityException.class));
     }
@@ -1059,7 +1059,7 @@
         // The pending event will be dispatched on the handler (from requestTraversal).
         waitHandlerIdle(mWm.mAnimationHandler);
 
-        verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), any(),
+        verify(mOrganizer).onTaskFragmentError(any(), eq(mErrorToken), any(),
                 eq(HIERARCHY_OP_TYPE_REPARENT_CHILDREN), any(SecurityException.class));
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index 851be9d..d2cb7ba 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -245,7 +245,7 @@
     }
 
     @Override
-    public void userActivity() {
+    public void userActivity(int displayGroupId, int event) {
     }
 
     @Override
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 25db81f..bde9c3d 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -18,6 +18,8 @@
 
 import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
 import static android.Manifest.permission.RECORD_AUDIO;
+import static android.service.attention.AttentionService.PROXIMITY_UNKNOWN;
+import static android.service.voice.HotwordDetectedResult.EXTRA_PROXIMITY_METERS;
 import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_EXTERNAL;
 import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_MICROPHONE;
 import static android.service.voice.HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS;
@@ -56,6 +58,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.attention.AttentionManagerInternal;
 import android.content.ComponentName;
 import android.content.ContentCaptureOptions;
 import android.content.Context;
@@ -182,6 +185,12 @@
     final int mUser;
     final Context mContext;
 
+    @Nullable final AttentionManagerInternal mAttentionManagerInternal;
+
+    final AttentionManagerInternal.ProximityUpdateCallbackInternal mProximityCallbackInternal =
+            this::setProximityMeters;
+
+
     volatile HotwordDetectionServiceIdentity mIdentity;
     private IMicrophoneHotwordDetectionVoiceInteractionCallback mSoftwareCallback;
     private Instant mLastRestartInstant;
@@ -202,6 +211,8 @@
     private @NonNull ServiceConnection mRemoteHotwordDetectionService;
     private IBinder mAudioFlinger;
     private boolean mDebugHotwordLogging = false;
+    @GuardedBy("mLock")
+    private double mProximityMeters = PROXIMITY_UNKNOWN;
 
     HotwordDetectionConnection(Object lock, Context context, int voiceInteractionServiceUid,
             Identity voiceInteractorIdentity, ComponentName serviceName, int userId,
@@ -229,6 +240,10 @@
         mServiceConnectionFactory = new ServiceConnectionFactory(intent, bindInstantServiceAllowed);
 
         mRemoteHotwordDetectionService = mServiceConnectionFactory.createLocked();
+        mAttentionManagerInternal = LocalServices.getService(AttentionManagerInternal.class);
+        if (mAttentionManagerInternal != null) {
+            mAttentionManagerInternal.onStartProximityUpdates(mProximityCallbackInternal);
+        }
 
         mLastRestartInstant = Instant.now();
         updateStateAfterProcessStart(options, sharedMemory);
@@ -393,6 +408,9 @@
         if (mAudioFlinger != null) {
             mAudioFlinger.unlinkToDeath(mAudioServerDeathRecipient, /* flags= */ 0);
         }
+        if (mAttentionManagerInternal != null) {
+            mAttentionManagerInternal.onStopProximityUpdates(mProximityCallbackInternal);
+        }
     }
 
     void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory) {
@@ -460,6 +478,7 @@
                         mSoftwareCallback.onError();
                         return;
                     }
+                    saveProximityMetersToBundle(result);
                     mSoftwareCallback.onDetected(result, null, null);
                     if (result != null) {
                         Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result)
@@ -564,6 +583,7 @@
                         externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION);
                         return;
                     }
+                    saveProximityMetersToBundle(result);
                     externalCallback.onKeyphraseDetected(recognitionEvent, result);
                     if (result != null) {
                         Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result)
@@ -643,6 +663,7 @@
                         externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION);
                         return;
                     }
+                    saveProximityMetersToBundle(result);
                     externalCallback.onKeyphraseDetected(recognitionEvent, result);
                     if (result != null) {
                         Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result)
@@ -1191,6 +1212,20 @@
         });
     }
 
+    private void saveProximityMetersToBundle(HotwordDetectedResult result) {
+        synchronized (mLock) {
+            if (result != null && mProximityMeters != PROXIMITY_UNKNOWN) {
+                result.getExtras().putDouble(EXTRA_PROXIMITY_METERS, mProximityMeters);
+            }
+        }
+    }
+
+    private void setProximityMeters(double proximityMeters) {
+        synchronized (mLock) {
+            mProximityMeters = proximityMeters;
+        }
+    }
+
     private static void bestEffortClose(Closeable... closeables) {
         for (Closeable closeable : closeables) {
             bestEffortClose(closeable);