Merge "Fix some OWNERS files for System UI" into tm-qpr-dev
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index da6a551..edf96f7 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -159,6 +159,7 @@
     void clearRequestedListenerHints(in INotificationListener token);
     void requestHintsFromListener(in INotificationListener token, int hints);
     int getHintsFromListener(in INotificationListener token);
+    int getHintsFromListenerNoToken();
     void requestInterruptionFilterFromListener(in INotificationListener token, int interruptionFilter);
     int getInterruptionFilterFromListener(in INotificationListener token);
     void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim);
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 392f52a..f6d27ad 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -66,9 +66,9 @@
 
 /**
  * Class to notify the user of events that happen.  This is how you tell
- * the user that something has happened in the background. {@more}
+ * the user that something has happened in the background.
  *
- * Notifications can take different forms:
+ * <p>Notifications can take different forms:
  * <ul>
  *      <li>A persistent icon that goes in the status bar and is accessible
  *          through the launcher, (when the user selects it, a designated Intent
diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java
index 0a6a405..16f9a12 100644
--- a/core/java/android/preference/SeekBarVolumizer.java
+++ b/core/java/android/preference/SeekBarVolumizer.java
@@ -115,6 +115,7 @@
     private final int mMaxStreamVolume;
     private boolean mAffectedByRingerMode;
     private boolean mNotificationOrRing;
+    private final boolean mNotifAliasRing;
     private final Receiver mReceiver = new Receiver();
 
     private Handler mHandler;
@@ -179,6 +180,8 @@
         if (mNotificationOrRing) {
             mRingerMode = mAudioManager.getRingerModeInternal();
         }
+        mNotifAliasRing = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_alias_ring_notif_stream_types);
         mZenMode = mNotificationManager.getZenMode();
 
         if (hasAudioProductStrategies()) {
@@ -280,7 +283,15 @@
         if (zenMuted) {
             mSeekBar.setProgress(mLastAudibleStreamVolume, true);
         } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
-            mSeekBar.setProgress(0, true);
+            /**
+             * the first variable above is preserved and the conditions below are made explicit
+             * so that when user attempts to slide the notification seekbar out of vibrate the
+             * seekbar doesn't wrongly snap back to 0 when the streams aren't aliased
+             */
+            if (mNotifAliasRing || mStreamType == AudioManager.STREAM_RING
+                    || (mStreamType == AudioManager.STREAM_NOTIFICATION && mMuted)) {
+                mSeekBar.setProgress(0, true);
+            }
         } else if (mMuted) {
             mSeekBar.setProgress(0, true);
         } else {
@@ -354,6 +365,7 @@
         // set the time of stop volume
         if ((mStreamType == AudioManager.STREAM_VOICE_CALL
                 || mStreamType == AudioManager.STREAM_RING
+                || (!mNotifAliasRing && mStreamType == AudioManager.STREAM_NOTIFICATION)
                 || mStreamType == AudioManager.STREAM_ALARM)) {
             sStopVolumeTime = java.lang.System.currentTimeMillis();
         }
@@ -631,8 +643,8 @@
         }
 
         private void updateVolumeSlider(int streamType, int streamValue) {
-            final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType)
-                    : (streamType == mStreamType);
+            final boolean streamMatch = mNotifAliasRing && mNotificationOrRing
+                    ? isNotificationOrRing(streamType) : streamType == mStreamType;
             if (mSeekBar != null && streamMatch && streamValue != -1) {
                 final boolean muted = mAudioManager.isStreamMute(mStreamType)
                         || streamValue == 0;
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 4324442..aa45c20 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -42,8 +42,11 @@
     private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
         @Override
         public void startDream(WindowManager.LayoutParams layoutParams,
-                IDreamOverlayCallback callback) {
+                IDreamOverlayCallback callback, String dreamComponent,
+                boolean shouldShowComplications) {
             mDreamOverlayCallback = callback;
+            mDreamComponent = ComponentName.unflattenFromString(dreamComponent);
+            mShowComplications = shouldShowComplications;
             onStartDream(layoutParams);
         }
     };
@@ -56,10 +59,6 @@
     @Nullable
     @Override
     public final IBinder onBind(@NonNull Intent intent) {
-        mShowComplications = intent.getBooleanExtra(DreamService.EXTRA_SHOW_COMPLICATIONS,
-                DreamService.DEFAULT_SHOW_COMPLICATIONS);
-        mDreamComponent = intent.getParcelableExtra(DreamService.EXTRA_DREAM_COMPONENT,
-                ComponentName.class);
         return mDreamOverlay.asBinder();
     }
 
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 3b7698e3..3c1fef0 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -214,19 +214,6 @@
     private static final String DREAM_META_DATA_ROOT_TAG = "dream";
 
     /**
-     * Extra containing a boolean for whether to show complications on the overlay.
-     * @hide
-     */
-    public static final String EXTRA_SHOW_COMPLICATIONS =
-            "android.service.dreams.SHOW_COMPLICATIONS";
-
-    /**
-     * Extra containing the component name for the active dream.
-     * @hide
-     */
-    public static final String EXTRA_DREAM_COMPONENT = "android.service.dreams.DREAM_COMPONENT";
-
-    /**
      * The default value for whether to show complications on the overlay.
      *
      * @hide
@@ -252,6 +239,9 @@
 
     private boolean mDebug = false;
 
+    private ComponentName mDreamComponent;
+    private boolean mShouldShowComplications;
+
     private DreamServiceWrapper mDreamServiceWrapper;
     private Runnable mDispatchAfterOnAttachedToWindow;
 
@@ -947,6 +937,11 @@
     @Override
     public void onCreate() {
         if (mDebug) Slog.v(mTag, "onCreate()");
+
+        mDreamComponent = new ComponentName(this, getClass());
+        mShouldShowComplications = fetchShouldShowComplications(this /*context*/,
+                fetchServiceInfo(this /*context*/, mDreamComponent));
+
         super.onCreate();
     }
 
@@ -994,14 +989,7 @@
         // Connect to the overlay service if present.
         if (!mWindowless && overlayComponent != null) {
             final Resources resources = getResources();
-            final ComponentName dreamService = new ComponentName(this, getClass());
-
-            final ServiceInfo serviceInfo = fetchServiceInfo(this, dreamService);
-            final Intent overlayIntent = new Intent()
-                    .setComponent(overlayComponent)
-                    .putExtra(EXTRA_SHOW_COMPLICATIONS,
-                            fetchShouldShowComplications(this, serviceInfo))
-                    .putExtra(EXTRA_DREAM_COMPONENT, dreamService);
+            final Intent overlayIntent = new Intent().setComponent(overlayComponent);
 
             mOverlayConnection = new OverlayConnection(
                     /* context= */ this,
@@ -1364,7 +1352,9 @@
                             // parameters once the window has been attached.
                             mDreamStartOverlayConsumer = overlay -> {
                                 try {
-                                    overlay.startDream(mWindow.getAttributes(), mOverlayCallback);
+                                    overlay.startDream(mWindow.getAttributes(), mOverlayCallback,
+                                            mDreamComponent.flattenToString(),
+                                            mShouldShowComplications);
                                 } catch (RemoteException e) {
                                     Log.e(mTag, "could not send window attributes:" + e);
                                 }
diff --git a/core/java/android/service/dreams/IDreamOverlay.aidl b/core/java/android/service/dreams/IDreamOverlay.aidl
index 2b6633d..05ebbfe 100644
--- a/core/java/android/service/dreams/IDreamOverlay.aidl
+++ b/core/java/android/service/dreams/IDreamOverlay.aidl
@@ -31,7 +31,11 @@
     * @param params The {@link LayoutParams} for the associated DreamWindow, including the window
                     token of the Dream Activity.
     * @param callback The {@link IDreamOverlayCallback} for requesting actions such as exiting the
-    *                 dream.
+    *                dream.
+    * @param dreamComponent The component name of the dream service requesting overlay.
+    * @param shouldShowComplications Whether the dream overlay should show complications, e.g. clock
+    *                and weather.
     */
-    void startDream(in LayoutParams params, in IDreamOverlayCallback callback);
+    void startDream(in LayoutParams params, in IDreamOverlayCallback callback,
+        in String dreamComponent, in boolean shouldShowComplications);
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 4522c0d..8f4a836 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -284,7 +284,7 @@
      * @hide
      */
     public static final boolean LOCAL_LAYOUT =
-            SystemProperties.getBoolean("persist.debug.local_layout", false);
+            SystemProperties.getBoolean("persist.debug.local_layout", true);
 
     /**
      * Set this system property to true to force the view hierarchy to render
diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java
index 6bf2474..514df59 100644
--- a/core/java/android/view/translation/UiTranslationController.java
+++ b/core/java/android/view/translation/UiTranslationController.java
@@ -175,10 +175,7 @@
      */
     public void onActivityDestroyed() {
         synchronized (mLock) {
-            if (DEBUG) {
-                Log.i(TAG,
-                        "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState));
-            }
+            Log.i(TAG, "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState));
             if (mCurrentState != STATE_UI_TRANSLATION_FINISHED) {
                 notifyTranslationFinished(/* activityDestroyed= */ true);
             }
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index fbdd325..8815ab3 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -135,8 +135,11 @@
     /** This change happened underneath something else. */
     public static final int FLAG_IS_OCCLUDED = 1 << 15;
 
+    /** The container is a system window, excluding wallpaper and input-method. */
+    public static final int FLAG_IS_SYSTEM_WINDOW = 1 << 16;
+
     /** The first unused bit. This can be used by remotes to attach custom flags to this change. */
-    public static final int FLAG_FIRST_CUSTOM = 1 << 16;
+    public static final int FLAG_FIRST_CUSTOM = 1 << 17;
 
     /** @hide */
     @IntDef(prefix = { "FLAG_" }, value = {
@@ -157,6 +160,7 @@
             FLAG_CROSS_PROFILE_WORK_THUMBNAIL,
             FLAG_IS_BEHIND_STARTING_WINDOW,
             FLAG_IS_OCCLUDED,
+            FLAG_IS_SYSTEM_WINDOW,
             FLAG_FIRST_CUSTOM
     })
     public @interface ChangeFlags {}
@@ -369,6 +373,9 @@
         if ((flags & FLAG_IS_OCCLUDED) != 0) {
             sb.append(sb.length() == 0 ? "" : "|").append("IS_OCCLUDED");
         }
+        if ((flags & FLAG_IS_SYSTEM_WINDOW) != 0) {
+            sb.append(sb.length() == 0 ? "" : "|").append("FLAG_IS_SYSTEM_WINDOW");
+        }
         if ((flags & FLAG_FIRST_CUSTOM) != 0) {
             sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM");
         }
@@ -407,6 +414,7 @@
     public static final class Change implements Parcelable {
         private final WindowContainerToken mContainer;
         private WindowContainerToken mParent;
+        private WindowContainerToken mLastParent;
         private final SurfaceControl mLeash;
         private @TransitionMode int mMode = TRANSIT_NONE;
         private @ChangeFlags int mFlags = FLAG_NONE;
@@ -435,6 +443,7 @@
         private Change(Parcel in) {
             mContainer = in.readTypedObject(WindowContainerToken.CREATOR);
             mParent = in.readTypedObject(WindowContainerToken.CREATOR);
+            mLastParent = in.readTypedObject(WindowContainerToken.CREATOR);
             mLeash = new SurfaceControl();
             mLeash.readFromParcel(in);
             mMode = in.readInt();
@@ -458,6 +467,14 @@
             mParent = parent;
         }
 
+        /**
+         * Sets the parent of this change's container before the transition if this change's
+         * container is reparented in the transition.
+         */
+        public void setLastParent(@Nullable WindowContainerToken lastParent) {
+            mLastParent = lastParent;
+        }
+
         /** Sets the transition mode for this change */
         public void setMode(@TransitionMode int mode) {
             mMode = mode;
@@ -541,6 +558,17 @@
             return mParent;
         }
 
+        /**
+         * @return the parent of the changing container before the transition if it is reparented
+         * in the transition. The parent window may not be collected in the transition as a
+         * participant, and it may have been detached from the display. {@code null} if the changing
+         * container has not been reparented in the transition, or if the parent is not organizable.
+         */
+        @Nullable
+        public WindowContainerToken getLastParent() {
+            return mLastParent;
+        }
+
         /** @return which action this change represents. */
         public @TransitionMode int getMode() {
             return mMode;
@@ -640,6 +668,7 @@
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             dest.writeTypedObject(mContainer, flags);
             dest.writeTypedObject(mParent, flags);
+            dest.writeTypedObject(mLastParent, flags);
             mLeash.writeToParcel(dest, flags);
             dest.writeInt(mMode);
             dest.writeInt(mFlags);
@@ -679,13 +708,37 @@
 
         @Override
         public String toString() {
-            String out = "{" + mContainer + "(" + mParent + ") leash=" + mLeash
-                    + " m=" + modeToString(mMode) + " f=" + flagsToString(mFlags) + " sb="
-                    + mStartAbsBounds + " eb=" + mEndAbsBounds + " eo=" + mEndRelOffset + " r="
-                    + mStartRotation + "->" + mEndRotation + ":" + mRotationAnimation
-                    + " endFixedRotation=" + mEndFixedRotation;
-            if (mSnapshot != null) out += " snapshot=" + mSnapshot;
-            return out + "}";
+            final StringBuilder sb = new StringBuilder();
+            sb.append('{'); sb.append(mContainer);
+            sb.append(" m="); sb.append(modeToString(mMode));
+            sb.append(" f="); sb.append(flagsToString(mFlags));
+            if (mParent != null) {
+                sb.append(" p="); sb.append(mParent);
+            }
+            if (mLeash != null) {
+                sb.append(" leash="); sb.append(mLeash);
+            }
+            sb.append(" sb="); sb.append(mStartAbsBounds);
+            sb.append(" eb="); sb.append(mEndAbsBounds);
+            if (mEndRelOffset.x != 0 || mEndRelOffset.y != 0) {
+                sb.append(" eo="); sb.append(mEndRelOffset);
+            }
+            if (mStartRotation != mEndRotation) {
+                sb.append(" r="); sb.append(mStartRotation);
+                sb.append("->"); sb.append(mEndRotation);
+                sb.append(':'); sb.append(mRotationAnimation);
+            }
+            if (mEndFixedRotation != ROTATION_UNDEFINED) {
+                sb.append(" endFixedRotation="); sb.append(mEndFixedRotation);
+            }
+            if (mSnapshot != null) {
+                sb.append(" snapshot="); sb.append(mSnapshot);
+            }
+            if (mLastParent != null) {
+                sb.append(" lastParent="); sb.append(mLastParent);
+            }
+            sb.append('}');
+            return sb.toString();
         }
     }
 
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 4f74ca7..2ae2c09 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -43,6 +43,7 @@
 import android.widget.TextView;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
 import com.android.internal.app.chooser.ChooserTargetInfo;
 import com.android.internal.app.chooser.DisplayResolveInfo;
@@ -86,6 +87,7 @@
     private final ChooserActivityLogger mChooserActivityLogger;
 
     private int mNumShortcutResults = 0;
+    private final Map<SelectableTargetInfo, LoadDirectShareIconTask> mIconLoaders = new HashMap<>();
     private boolean mApplySharingAppLimits;
 
     // Reserve spots for incoming direct share targets by adding placeholders
@@ -239,7 +241,6 @@
         mListViewDataChanged = false;
     }
 
-
     private void createPlaceHolders() {
         mNumShortcutResults = 0;
         mServiceTargets.clear();
@@ -268,12 +269,16 @@
         holder.bindIcon(info);
         if (info instanceof SelectableTargetInfo) {
             // direct share targets should append the application name for a better readout
-            DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
+            SelectableTargetInfo sti = (SelectableTargetInfo) info;
+            DisplayResolveInfo rInfo = sti.getDisplayResolveInfo();
             CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
             CharSequence extendedInfo = info.getExtendedInfo();
             String contentDescription = String.join(" ", info.getDisplayLabel(),
                     extendedInfo != null ? extendedInfo : "", appName);
             holder.updateContentDescription(contentDescription);
+            if (!sti.hasDisplayIcon()) {
+                loadDirectShareIcon(sti);
+            }
         } else if (info instanceof DisplayResolveInfo) {
             DisplayResolveInfo dri = (DisplayResolveInfo) info;
             if (!dri.hasDisplayIcon()) {
@@ -318,6 +323,20 @@
         }
     }
 
+    private void loadDirectShareIcon(SelectableTargetInfo info) {
+        LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
+        if (task == null) {
+            task = createLoadDirectShareIconTask(info);
+            mIconLoaders.put(info, task);
+            task.loadIcon();
+        }
+    }
+
+    @VisibleForTesting
+    protected LoadDirectShareIconTask createLoadDirectShareIconTask(SelectableTargetInfo info) {
+        return new LoadDirectShareIconTask(info);
+    }
+
     void updateAlphabeticalList() {
         new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
             @Override
@@ -332,7 +351,7 @@
                 Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
                 for (DisplayResolveInfo info : allTargets) {
                     String resolvedTarget = info.getResolvedComponentName().getPackageName()
-                        + '#' + info.getDisplayLabel();
+                            + '#' + info.getDisplayLabel();
                     DisplayResolveInfo multiDri = consolidated.get(resolvedTarget);
                     if (multiDri == null) {
                         consolidated.put(resolvedTarget, info);
@@ -341,7 +360,7 @@
                     } else {
                         // create consolidated target from the single DisplayResolveInfo
                         MultiDisplayResolveInfo multiDisplayResolveInfo =
-                            new MultiDisplayResolveInfo(resolvedTarget, multiDri);
+                                new MultiDisplayResolveInfo(resolvedTarget, multiDri);
                         multiDisplayResolveInfo.addTarget(info);
                         consolidated.put(resolvedTarget, multiDisplayResolveInfo);
                     }
@@ -731,7 +750,8 @@
      * Necessary methods to communicate between {@link ChooserListAdapter}
      * and {@link ChooserActivity}.
      */
-    interface ChooserListCommunicator extends ResolverListCommunicator {
+    @VisibleForTesting
+    public interface ChooserListCommunicator extends ResolverListCommunicator {
 
         int getMaxRankedTargets();
 
@@ -739,4 +759,35 @@
 
         boolean isSendAction(Intent targetIntent);
     }
+
+    /**
+     * Loads direct share targets icons.
+     */
+    @VisibleForTesting
+    public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Boolean> {
+        private final SelectableTargetInfo mTargetInfo;
+
+        private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) {
+            mTargetInfo = targetInfo;
+        }
+
+        @Override
+        protected Boolean doInBackground(Void... voids) {
+            return mTargetInfo.loadIcon();
+        }
+
+        @Override
+        protected void onPostExecute(Boolean isLoaded) {
+            if (isLoaded) {
+                notifyDataSetChanged();
+            }
+        }
+
+        /**
+         * An alias for execute to use with unit tests.
+         */
+        public void loadIcon() {
+            execute();
+        }
+    }
 }
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index f6075b0..4a1f7eb 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -870,7 +870,12 @@
         void onHandlePackagesChanged(ResolverListAdapter listAdapter);
     }
 
-    static class ViewHolder {
+    /**
+     * A view holder keeps a reference to a list view and provides functionality for managing its
+     * state.
+     */
+    @VisibleForTesting
+    public static class ViewHolder {
         public View itemView;
         public Drawable defaultItemViewBackground;
 
@@ -878,7 +883,8 @@
         public TextView text2;
         public ImageView icon;
 
-        ViewHolder(View view) {
+        @VisibleForTesting
+        public ViewHolder(View view) {
             itemView = view;
             defaultItemViewBackground = view.getBackground();
             text = (TextView) view.findViewById(com.android.internal.R.id.text1);
diff --git a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
index 4b9b7cb..d7f3a76 100644
--- a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
+++ b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
@@ -37,6 +37,7 @@
 import android.text.SpannableStringBuilder;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.ChooserActivity;
 import com.android.internal.app.ResolverActivity;
 import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter;
@@ -59,8 +60,11 @@
     private final String mDisplayLabel;
     private final PackageManager mPm;
     private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator;
+    @GuardedBy("this")
+    private ShortcutInfo mShortcutInfo;
     private Drawable mBadgeIcon = null;
     private CharSequence mBadgeContentDescription;
+    @GuardedBy("this")
     private Drawable mDisplayIcon;
     private final Intent mFillInIntent;
     private final int mFillInFlags;
@@ -78,6 +82,7 @@
         mModifiedScore = modifiedScore;
         mPm = mContext.getPackageManager();
         mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
+        mShortcutInfo = shortcutInfo;
         mIsPinned = shortcutInfo != null && shortcutInfo.isPinned();
         if (sourceInfo != null) {
             final ResolveInfo ri = sourceInfo.getResolveInfo();
@@ -92,8 +97,6 @@
                 }
             }
         }
-        // TODO(b/121287224): do this in the background thread, and only for selected targets
-        mDisplayIcon = getChooserTargetIconDrawable(chooserTarget, shortcutInfo);
 
         if (sourceInfo != null) {
             mBackupResolveInfo = null;
@@ -118,7 +121,10 @@
         mChooserTarget = other.mChooserTarget;
         mBadgeIcon = other.mBadgeIcon;
         mBadgeContentDescription = other.mBadgeContentDescription;
-        mDisplayIcon = other.mDisplayIcon;
+        synchronized (other) {
+            mShortcutInfo = other.mShortcutInfo;
+            mDisplayIcon = other.mDisplayIcon;
+        }
         mFillInIntent = fillInIntent;
         mFillInFlags = flags;
         mModifiedScore = other.mModifiedScore;
@@ -141,6 +147,27 @@
         return mSourceInfo;
     }
 
+    /**
+     * Load display icon, if needed.
+     */
+    public boolean loadIcon() {
+        ShortcutInfo shortcutInfo;
+        Drawable icon;
+        synchronized (this) {
+            shortcutInfo = mShortcutInfo;
+            icon = mDisplayIcon;
+        }
+        boolean shouldLoadIcon = icon == null && shortcutInfo != null;
+        if (shouldLoadIcon) {
+            icon = getChooserTargetIconDrawable(mChooserTarget, shortcutInfo);
+            synchronized (this) {
+                mDisplayIcon = icon;
+                mShortcutInfo = null;
+            }
+        }
+        return shouldLoadIcon;
+    }
+
     private Drawable getChooserTargetIconDrawable(ChooserTarget target,
             @Nullable ShortcutInfo shortcutInfo) {
         Drawable directShareIcon = null;
@@ -271,10 +298,17 @@
     }
 
     @Override
-    public Drawable getDisplayIcon(Context context) {
+    public synchronized Drawable getDisplayIcon(Context context) {
         return mDisplayIcon;
     }
 
+    /**
+     * @return true if display icon is available
+     */
+    public synchronized boolean hasDisplayIcon() {
+        return mDisplayIcon != null;
+    }
+
     public ChooserTarget getChooserTarget() {
         return mChooserTarget;
     }
diff --git a/core/res/Android.bp b/core/res/Android.bp
index c42517d..179eff8 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -130,6 +130,10 @@
 
         // Allow overlay to add resource
         "--auto-add-overlay",
+
+        // Framework resources benefit tremendously from enabling sparse encoding, saving tens
+        // of MBs in size and RAM use.
+        "--enable-sparse-encoding",
     ],
 
     resource_zips: [
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 5ae133b..964fe2d 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1035,25 +1035,38 @@
         android:priority="900" />
 
     <!-- Allows an application to read from external storage.
-      <p>Any app that declares the {@link #WRITE_EXTERNAL_STORAGE} permission is implicitly
-      granted this permission.</p>
+      <p class="note"><strong>Note: </strong>Starting in API level 33, this permission has no
+      effect. If your app accesses other apps' media files, request one or more of these permissions
+      instead: <a href="#READ_MEDIA_IMAGES"><code>READ_MEDIA_IMAGES</code></a>,
+      <a href="#READ_MEDIA_VIDEO"><code>READ_MEDIA_VIDEO</code></a>,
+      <a href="#READ_MEDIA_AUDIO"><code>READ_MEDIA_AUDIO</code></a>. Learn more about the
+      <a href="{@docRoot}training/data-storage/shared/media#storage-permission">storage
+      permissions</a> that are associated with media files.</p>
+
       <p>This permission is enforced starting in API level 19.  Before API level 19, this
       permission is not enforced and all apps still have access to read from external storage.
       You can test your app with the permission enforced by enabling <em>Protect USB
-      storage</em> under Developer options in the Settings app on a device running Android 4.1 or
-      higher.</p>
+      storage</em> under <b>Developer options</b> in the Settings app on a device running Android
+      4.1 or higher.</p>
       <p>Also starting in API level 19, this permission is <em>not</em> required to
-      read/write files in your application-specific directories returned by
+      read or write files in your application-specific directories returned by
       {@link android.content.Context#getExternalFilesDir} and
-      {@link android.content.Context#getExternalCacheDir}.
-      <p class="note"><strong>Note:</strong> If <em>both</em> your <a
+      {@link android.content.Context#getExternalCacheDir}.</p>
+      <p>Starting in API level 29, apps don't need to request this permission to access files in
+      their app-specific directory on external storage, or their own files in the
+      <a href="{@docRoot}reference/android/provider/MediaStore"><code>MediaStore</code></a>. Apps
+      shouldn't request this permission unless they need to access other apps' files in the
+      <code>MediaStore</code>. Read more about these changes in the
+      <a href="{@docRoot}training/data-storage#scoped-storage">scoped storage</a> section of the
+      developer documentation.</p>
+      <p>If <em>both</em> your <a
       href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code
       minSdkVersion}</a> and <a
       href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
       targetSdkVersion}</a> values are set to 3 or lower, the system implicitly
       grants your app this permission. If you don't need this permission, be sure your <a
       href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
-      targetSdkVersion}</a> is 4 or higher.
+      targetSdkVersion}</a> is 4 or higher.</p>
 
       <p> This is a soft restricted permission which cannot be held by an app it its
       full form until the installer on record allowlists the permission.
diff --git a/core/res/res/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml
index ded23fe..38a71f0 100644
--- a/core/res/res/layout/miniresolver.xml
+++ b/core/res/res/layout/miniresolver.xml
@@ -65,8 +65,7 @@
         android:paddingTop="32dp"
         android:paddingBottom="@dimen/resolver_button_bar_spacing"
         android:orientation="vertical"
-        android:background="?attr/colorBackground"
-        android:layout_ignoreOffset="true">
+        android:background="?attr/colorBackground">
         <RelativeLayout
             style="?attr/buttonBarStyle"
             android:layout_width="match_parent"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 3408f75..8428060 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -658,6 +658,20 @@
         -->
     </integer-array>
 
+    <!-- The device states (supplied by DeviceStateManager) that should be treated as half-folded by
+     the display fold controller. Default is empty. -->
+    <integer-array name="config_halfFoldedDeviceStates">
+        <!-- Example:
+        <item>0</item>
+        <item>1</item>
+        <item>2</item>
+        -->
+    </integer-array>
+
+    <!-- Indicates whether the window manager reacts to half-fold device states by overriding
+     rotation. -->
+    <bool name="config_windowManagerHalfFoldAutoRotateOverride">false</bool>
+
     <!-- When a device enters any of these states, it should be woken up. States are defined in
          device_state_configuration.xml. -->
     <integer-array name="config_deviceStatesOnWhichToWakeUp">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 16798b8..97105d8 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4005,6 +4005,8 @@
 
   <!-- For Foldables -->
   <java-symbol type="array" name="config_foldedDeviceStates" />
+  <java-symbol type="array" name="config_halfFoldedDeviceStates" />
+  <java-symbol type="bool" name="config_windowManagerHalfFoldAutoRotateOverride" />
   <java-symbol type="array" name="config_deviceStatesOnWhichToWakeUp" />
   <java-symbol type="array" name="config_deviceStatesOnWhichToSleep" />
   <java-symbol type="string" name="config_foldedArea" />
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt
new file mode 100644
index 0000000..8218b98
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.internal.app
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Bundle
+import android.service.chooser.ChooserTarget
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.R
+import com.android.internal.app.ChooserListAdapter.LoadDirectShareIconTask
+import com.android.internal.app.chooser.SelectableTargetInfo
+import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator
+import com.android.internal.app.chooser.TargetInfo
+import com.android.server.testutils.any
+import com.android.server.testutils.mock
+import com.android.server.testutils.whenever
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ChooserListAdapterTest {
+    private val packageManager = mock<PackageManager> {
+        whenever(resolveActivity(any(), anyInt())).thenReturn(mock())
+    }
+    private val context = InstrumentationRegistry.getInstrumentation().getContext()
+    private val resolverListController = mock<ResolverListController>()
+    private val chooserListCommunicator = mock<ChooserListAdapter.ChooserListCommunicator> {
+        whenever(maxRankedTargets).thenReturn(0)
+    }
+    private val selectableTargetInfoCommunicator =
+        mock<SelectableTargetInfoCommunicator> {
+            whenever(targetIntent).thenReturn(mock())
+        }
+    private val chooserActivityLogger = mock<ChooserActivityLogger>()
+
+    private fun createChooserListAdapter(
+        taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask
+    ) =
+        ChooserListAdapterOverride(
+            context,
+            emptyList(),
+            emptyArray(),
+            emptyList(),
+            false,
+            resolverListController,
+            chooserListCommunicator,
+            selectableTargetInfoCommunicator,
+            packageManager,
+            chooserActivityLogger,
+            taskProvider
+        )
+
+    @Test
+    fun testDirectShareTargetLoadingIconIsStarted() {
+        val view = createView()
+        val viewHolder = ResolverListAdapter.ViewHolder(view)
+        view.tag = viewHolder
+        val targetInfo = createSelectableTargetInfo()
+        val iconTask = mock<LoadDirectShareIconTask>()
+        val testSubject = createChooserListAdapter { iconTask }
+        testSubject.testViewBind(view, targetInfo, 0)
+
+        verify(iconTask, times(1)).loadIcon()
+    }
+
+    @Test
+    fun testOnlyOneTaskPerTarget() {
+        val view = createView()
+        val viewHolderOne = ResolverListAdapter.ViewHolder(view)
+        view.tag = viewHolderOne
+        val targetInfo = createSelectableTargetInfo()
+        val iconTaskOne = mock<LoadDirectShareIconTask>()
+        val testTaskProvider = mock<() -> LoadDirectShareIconTask> {
+            whenever(invoke()).thenReturn(iconTaskOne)
+        }
+        val testSubject = createChooserListAdapter { testTaskProvider.invoke() }
+        testSubject.testViewBind(view, targetInfo, 0)
+
+        val viewHolderTwo = ResolverListAdapter.ViewHolder(view)
+        view.tag = viewHolderTwo
+        whenever(testTaskProvider()).thenReturn(mock())
+
+        testSubject.testViewBind(view, targetInfo, 0)
+
+        verify(iconTaskOne, times(1)).loadIcon()
+        verify(testTaskProvider, times(1)).invoke()
+    }
+
+    private fun createSelectableTargetInfo(): SelectableTargetInfo =
+        SelectableTargetInfo(
+            context,
+            null,
+            createChooserTarget(),
+            1f,
+            selectableTargetInfoCommunicator,
+            null
+        )
+
+    private fun createChooserTarget(): ChooserTarget =
+        ChooserTarget(
+            "Title",
+            null,
+            1f,
+            ComponentName("package", "package.Class"),
+            Bundle()
+        )
+
+    private fun createView(): View {
+        val view = FrameLayout(context)
+        TextView(context).apply {
+            id = R.id.text1
+            view.addView(this)
+        }
+        TextView(context).apply {
+            id = R.id.text2
+            view.addView(this)
+        }
+        ImageView(context).apply {
+            id = R.id.icon
+            view.addView(this)
+        }
+        return view
+    }
+}
+
+private class ChooserListAdapterOverride(
+    context: Context?,
+    payloadIntents: List<Intent>?,
+    initialIntents: Array<out Intent>?,
+    rList: List<ResolveInfo>?,
+    filterLastUsed: Boolean,
+    resolverListController: ResolverListController?,
+    chooserListCommunicator: ChooserListCommunicator?,
+    selectableTargetInfoCommunicator: SelectableTargetInfoCommunicator?,
+    packageManager: PackageManager?,
+    chooserActivityLogger: ChooserActivityLogger?,
+    private val taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask
+) : ChooserListAdapter(
+    context,
+    payloadIntents,
+    initialIntents,
+    rList,
+    filterLastUsed,
+    resolverListController,
+    chooserListCommunicator,
+    selectableTargetInfoCommunicator,
+    packageManager,
+    chooserActivityLogger
+) {
+    override fun createLoadDirectShareIconTask(
+        info: SelectableTargetInfo?
+    ): LoadDirectShareIconTask =
+        taskProvider.invoke(info)
+
+    fun testViewBind(view: View?, info: TargetInfo?, position: Int) {
+        onBindView(view, info, position)
+    }
+}
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index b1ecb43..31e2abe 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1087,6 +1087,12 @@
       "group": "WM_DEBUG_FOCUS",
       "at": "com\/android\/server\/wm\/WindowState.java"
     },
+    "-1043981272": {
+      "message": "Reverting orientation. Rotating to %s from %s rather than %s.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayRotation.java"
+    },
     "-1042574499": {
       "message": "Attempted to add Accessibility overlay window with unknown token %s.  Aborting.",
       "level": "WARN",
@@ -4285,6 +4291,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "2066210760": {
+      "message": "foldStateChanged: displayId %d, halfFoldStateChanged %s, saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, mLastOrientation: %d, mRotation: %d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayRotation.java"
+    },
     "2070726247": {
       "message": "InsetsSource updateVisibility for %s, serverVisible: %s clientVisible: %s",
       "level": "DEBUG",
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index fb0a9db..7e9c418 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -41,7 +41,7 @@
     // TODO(b/241126279) Introduce constants to better version functionality
     @Override
     public int getVendorApiLevel() {
-        return 1;
+        return 2;
     }
 
     /**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 0fb6ff8..b516e140 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -117,9 +117,7 @@
         if (mWindowLayoutChangeListeners.containsKey(context)
                 // In theory this method can be called on the same consumer with different context.
                 || mWindowLayoutChangeListeners.containsValue(consumer)) {
-            throw new IllegalArgumentException(
-                    "Context or Consumer has already been registered for WindowLayoutInfo"
-                            + " callback.");
+            return;
         }
         if (!context.isUiContext()) {
             throw new IllegalArgumentException("Context must be a UI Context, which should be"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 591e347..215308d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -130,6 +130,10 @@
         if (!cropRect.intersect(mWholeAnimationBounds)) {
             // Hide the surface when it is outside of the animation area.
             t.setAlpha(mLeash, 0);
+        } else if (mAnimation.hasExtension()) {
+            // Allow the surface to be shown in its original bounds in case we want to use edge
+            // extensions.
+            cropRect.union(mChange.getEndAbsBounds());
         }
 
         // cropRect is in absolute coordinate, so we need to translate it to surface top left.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index 756d802..490975c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -21,6 +21,7 @@
 import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
 
 import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
 
 import android.animation.Animator;
@@ -45,6 +46,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /** To run the ActivityEmbedding animations. */
 class ActivityEmbeddingAnimationRunner {
@@ -65,10 +67,31 @@
     void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction) {
+        // There may be some surface change that we want to apply after the start transaction is
+        // applied to make sure the surface is ready.
+        final List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks =
+                new ArrayList<>();
         final Animator animator = createAnimator(info, startTransaction, finishTransaction,
-                () -> mController.onAnimationFinished(transition));
-        startTransaction.apply();
-        animator.start();
+                () -> mController.onAnimationFinished(transition), postStartTransactionCallbacks);
+
+        // Start the animation.
+        if (!postStartTransactionCallbacks.isEmpty()) {
+            // postStartTransactionCallbacks require that the start transaction is already
+            // applied to run otherwise they may result in flickers and UI inconsistencies.
+            startTransaction.apply(true /* sync */);
+
+            // Run tasks that require startTransaction to already be applied
+            final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+            for (Consumer<SurfaceControl.Transaction> postStartTransactionCallback :
+                    postStartTransactionCallbacks) {
+                postStartTransactionCallback.accept(t);
+            }
+            t.apply();
+            animator.start();
+        } else {
+            startTransaction.apply();
+            animator.start();
+        }
     }
 
     /**
@@ -85,9 +108,13 @@
     Animator createAnimator(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Runnable animationFinishCallback) {
-        final List<ActivityEmbeddingAnimationAdapter> adapters =
-                createAnimationAdapters(info, startTransaction, finishTransaction);
+            @NonNull Runnable animationFinishCallback,
+            @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks) {
+        final List<ActivityEmbeddingAnimationAdapter> adapters = createAnimationAdapters(info,
+                startTransaction);
+        addEdgeExtensionIfNeeded(startTransaction, finishTransaction, postStartTransactionCallbacks,
+                adapters);
+        addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters);
         long duration = 0;
         for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
             duration = Math.max(duration, adapter.getDurationHint());
@@ -131,8 +158,7 @@
      */
     @NonNull
     private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters(
-            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction) {
+            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
         boolean isChangeTransition = false;
         for (TransitionInfo.Change change : info.getChanges()) {
             if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) {
@@ -148,25 +174,23 @@
             return createChangeAnimationAdapters(info, startTransaction);
         }
         if (Transitions.isClosingType(info.getType())) {
-            return createCloseAnimationAdapters(info, startTransaction, finishTransaction);
+            return createCloseAnimationAdapters(info);
         }
-        return createOpenAnimationAdapters(info, startTransaction, finishTransaction);
+        return createOpenAnimationAdapters(info);
     }
 
     @NonNull
     private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters(
-            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction) {
-        return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction,
-                true /* isOpening */, mAnimationSpec::loadOpenAnimation);
+            @NonNull TransitionInfo info) {
+        return createOpenCloseAnimationAdapters(info, true /* isOpening */,
+                mAnimationSpec::loadOpenAnimation);
     }
 
     @NonNull
     private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters(
-            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction) {
-        return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction,
-                false /* isOpening */, mAnimationSpec::loadCloseAnimation);
+            @NonNull TransitionInfo info) {
+        return createOpenCloseAnimationAdapters(info, false /* isOpening */,
+                mAnimationSpec::loadCloseAnimation);
     }
 
     /**
@@ -175,8 +199,7 @@
      */
     @NonNull
     private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters(
-            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction, boolean isOpening,
+            @NonNull TransitionInfo info, boolean isOpening,
             @NonNull AnimationProvider animationProvider) {
         // We need to know if the change window is only a partial of the whole animation screen.
         // If so, we will need to adjust it to make the whole animation screen looks like one.
@@ -200,8 +223,7 @@
         final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
         for (TransitionInfo.Change change : openingChanges) {
             final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
-                    info, change, startTransaction, finishTransaction, animationProvider,
-                    openingWholeScreenBounds);
+                    info, change, animationProvider, openingWholeScreenBounds);
             if (isOpening) {
                 adapter.overrideLayer(offsetLayer++);
             }
@@ -209,8 +231,7 @@
         }
         for (TransitionInfo.Change change : closingChanges) {
             final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
-                    info, change, startTransaction, finishTransaction, animationProvider,
-                    closingWholeScreenBounds);
+                    info, change, animationProvider, closingWholeScreenBounds);
             if (!isOpening) {
                 adapter.overrideLayer(offsetLayer++);
             }
@@ -219,20 +240,51 @@
         return adapters;
     }
 
+    /** Adds edge extension to the surfaces that have such an animation property. */
+    private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks,
+            @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
+        for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+            final Animation animation = adapter.mAnimation;
+            if (!animation.hasExtension()) {
+                continue;
+            }
+            final TransitionInfo.Change change = adapter.mChange;
+            if (Transitions.isOpeningType(adapter.mChange.getMode())) {
+                // Need to screenshot after startTransaction is applied otherwise activity
+                // may not be visible or ready yet.
+                postStartTransactionCallbacks.add(
+                        t -> edgeExtendWindow(change, animation, t, finishTransaction));
+            } else {
+                // Can screenshot now (before startTransaction is applied)
+                edgeExtendWindow(change, animation, startTransaction, finishTransaction);
+            }
+        }
+    }
+
+    /** Adds background color to the transition if any animation has such a property. */
+    private void addBackgroundColorIfNeeded(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
+        for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+            final int backgroundColor = getTransitionBackgroundColorIfSet(info, adapter.mChange,
+                    adapter.mAnimation, 0 /* defaultColor */);
+            if (backgroundColor != 0) {
+                // We only need to show one color.
+                addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction,
+                        finishTransaction);
+                return;
+            }
+        }
+    }
+
     @NonNull
     private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter(
             @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull AnimationProvider animationProvider, @NonNull Rect wholeAnimationBounds) {
         final Animation animation = animationProvider.get(info, change, wholeAnimationBounds);
-        // We may want to show a background color for open/close transition.
-        final int backgroundColor = getTransitionBackgroundColorIfSet(info, change, animation,
-                0 /* defaultColor */);
-        if (backgroundColor != 0) {
-            addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction,
-                    finishTransaction);
-        }
         return new ActivityEmbeddingAnimationAdapter(animation, change, change.getLeash(),
                 wholeAnimationBounds);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index eb6ac76..58b2366 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -181,15 +181,15 @@
             @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
         final boolean isEnter = Transitions.isOpeningType(change.getMode());
         final Animation animation;
-        // TODO(b/207070762): Implement edgeExtension version
         if (shouldShowBackdrop(info, change)) {
             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
                     ? com.android.internal.R.anim.task_fragment_clear_top_open_enter
                     : com.android.internal.R.anim.task_fragment_clear_top_open_exit);
         } else {
+            // Use the same edge extension animation as regular activity open.
             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
-                    ? com.android.internal.R.anim.task_fragment_open_enter
-                    : com.android.internal.R.anim.task_fragment_open_exit);
+                    ? com.android.internal.R.anim.activity_open_enter
+                    : com.android.internal.R.anim.activity_open_exit);
         }
         // Use the whole animation bounds instead of the change bounds, so that when multiple change
         // targets are opening at the same time, the animation applied to each will be the same.
@@ -205,15 +205,15 @@
             @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
         final boolean isEnter = Transitions.isOpeningType(change.getMode());
         final Animation animation;
-        // TODO(b/207070762): Implement edgeExtension version
         if (shouldShowBackdrop(info, change)) {
             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
                     ? com.android.internal.R.anim.task_fragment_clear_top_close_enter
                     : com.android.internal.R.anim.task_fragment_clear_top_close_exit);
         } else {
+            // Use the same edge extension animation as regular activity close.
             animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
-                    ? com.android.internal.R.anim.task_fragment_close_enter
-                    : com.android.internal.R.anim.task_fragment_close_exit);
+                    ? com.android.internal.R.anim.activity_close_enter
+                    : com.android.internal.R.anim.activity_close_exit);
         }
         // Use the whole animation bounds instead of the change bounds, so that when multiple change
         // targets are closing at the same time, the animation applied to each will be the same.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 33761d2..2b36b4c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -452,14 +452,17 @@
             @NonNull Transitions.TransitionFinishCallback finishCallback,
             @NonNull TaskInfo taskInfo, @Nullable TransitionInfo.Change pipTaskChange) {
         TransitionInfo.Change pipChange = pipTaskChange;
-        if (pipChange == null) {
+        if (mCurrentPipTaskToken == null) {
+            ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG);
+        } else if (pipChange == null) {
             // The pipTaskChange is null, this can happen if we are reparenting the PIP activity
             // back to its original Task. In that case, we should animate the activity leash
-            // instead, which should be the only non-task, independent, TRANSIT_CHANGE window.
+            // instead, which should be the change whose last parent is the recorded PiP Task.
             for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                 final TransitionInfo.Change change = info.getChanges().get(i);
-                if (change.getTaskInfo() == null && change.getMode() == TRANSIT_CHANGE
-                        && TransitionInfo.isIndependent(change, info)) {
+                if (mCurrentPipTaskToken.equals(change.getLastParent())) {
+                    // Find the activity that is exiting PiP.
                     pipChange = change;
                     break;
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 30124a5..616d447 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -745,6 +745,15 @@
                 // Directly move PiP to its final destination bounds without animation.
                 mPipTaskOrganizer.scheduleFinishResizePip(postChangeBounds);
             }
+
+            // if the pip window size is beyond allowed bounds user resize to normal bounds
+            if (mPipBoundsState.getBounds().width() < mPipBoundsState.getMinSize().x
+                    || mPipBoundsState.getBounds().width() > mPipBoundsState.getMaxSize().x
+                    || mPipBoundsState.getBounds().height() < mPipBoundsState.getMinSize().y
+                    || mPipBoundsState.getBounds().height() > mPipBoundsState.getMaxSize().y) {
+                mTouchHandler.userResizeTo(mPipBoundsState.getNormalBounds(), snapFraction);
+            }
+
         } else {
             updateDisplayLayout.run();
         }
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 89d85e4..41ff0b3 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
@@ -96,6 +96,7 @@
     private final Rect mDisplayBounds = new Rect();
     private final Function<Rect, Rect> mMovementBoundsSupplier;
     private final Runnable mUpdateMovementBoundsRunnable;
+    private final Consumer<Rect> mUpdateResizeBoundsCallback;
 
     private int mDelta;
     private float mTouchSlop;
@@ -137,6 +138,13 @@
         mPhonePipMenuController = menuActivityController;
         mPipUiEventLogger = pipUiEventLogger;
         mPinchResizingAlgorithm = new PipPinchResizingAlgorithm();
+
+        mUpdateResizeBoundsCallback = (rect) -> {
+            mUserResizeBounds.set(rect);
+            mMotionHelper.synchronizePinnedStackBounds();
+            mUpdateMovementBoundsRunnable.run();
+            resetState();
+        };
     }
 
     public void init() {
@@ -508,15 +516,50 @@
         }
     }
 
+    private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) {
+        final int leftEdge = bounds.left;
+
+
+        final int fromLeft = Math.abs(leftEdge - movementBounds.left);
+        final int fromRight = Math.abs(movementBounds.right - leftEdge);
+
+        // The PIP will be snapped to either the right or left edge, so calculate which one
+        // is closest to the current position.
+        final int newLeft = fromLeft < fromRight
+                ? movementBounds.left : movementBounds.right;
+
+        bounds.offsetTo(newLeft, mLastResizeBounds.top);
+    }
+
+    /**
+     * Resizes the pip window and updates user-resized bounds.
+     *
+     * @param bounds target bounds to resize to
+     * @param snapFraction snap fraction to apply after resizing
+     */
+    void userResizeTo(Rect bounds, float snapFraction) {
+        Rect finalBounds = new Rect(bounds);
+
+        // get the current movement bounds
+        final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds);
+
+        // snap the target bounds to the either left or right edge, by choosing the closer one
+        snapToMovementBoundsEdge(finalBounds, movementBounds);
+
+        // apply the requested snap fraction onto the target bounds
+        mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction);
+
+        // resize from current bounds to target bounds without animation
+        mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null);
+        // set the flag that pip has been resized
+        mPipBoundsState.setHasUserResizedPip(true);
+
+        // finish the resize operation and update the state of the bounds
+        mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback);
+    }
+
     private void finishResize() {
         if (!mLastResizeBounds.isEmpty()) {
-            final Consumer<Rect> callback = (rect) -> {
-                mUserResizeBounds.set(mLastResizeBounds);
-                mMotionHelper.synchronizePinnedStackBounds();
-                mUpdateMovementBoundsRunnable.run();
-                resetState();
-            };
-
             // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped
             // position correctly. Drag-resize does not need to move, so just finalize resize.
             if (mOngoingPinchToResize) {
@@ -526,24 +569,23 @@
                         || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) {
                     resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y);
                 }
-                final int leftEdge = mLastResizeBounds.left;
-                final Rect movementBounds =
-                        mPipBoundsAlgorithm.getMovementBounds(mLastResizeBounds);
-                final int fromLeft = Math.abs(leftEdge - movementBounds.left);
-                final int fromRight = Math.abs(movementBounds.right - leftEdge);
-                // The PIP will be snapped to either the right or left edge, so calculate which one
-                // is closest to the current position.
-                final int newLeft = fromLeft < fromRight
-                        ? movementBounds.left : movementBounds.right;
-                mLastResizeBounds.offsetTo(newLeft, mLastResizeBounds.top);
+
+                // get the current movement bounds
+                final Rect movementBounds = mPipBoundsAlgorithm
+                        .getMovementBounds(mLastResizeBounds);
+
+                // snap mLastResizeBounds to the correct edge based on movement bounds
+                snapToMovementBoundsEdge(mLastResizeBounds, movementBounds);
+
                 final float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
                         mLastResizeBounds, movementBounds);
                 mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction);
                 mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds,
-                        PINCH_RESIZE_SNAP_DURATION, mAngle, callback);
+                        PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback);
             } else {
                 mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds,
-                        PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, callback);
+                        PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE,
+                        mUpdateResizeBoundsCallback);
             }
             final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f;
             mPipDismissTargetHandler
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 1f3f31e..975d4bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -825,6 +825,16 @@
     }
 
     /**
+     * Resizes the pip window and updates user resized bounds
+     *
+     * @param bounds target bounds to resize to
+     * @param snapFraction snap fraction to apply after resizing
+     */
+    void userResizeTo(Rect bounds, float snapFraction) {
+        mPipResizeGestureHandler.userResizeTo(bounds, snapFraction);
+    }
+
+    /**
      * Gesture controlling normal movement of the PIP.
      */
     private class DefaultPipTouchGesture extends PipTouchGesture {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index dbb2948..9c2c2fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -59,6 +59,7 @@
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.sDisableCustomTaskAnimationProperty;
@@ -76,10 +77,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.graphics.Canvas;
 import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -89,7 +87,6 @@
 import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.view.Choreographer;
-import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.view.WindowManager;
@@ -525,123 +522,6 @@
         }
     }
 
-    private void edgeExtendWindow(TransitionInfo.Change change,
-            Animation a, SurfaceControl.Transaction startTransaction,
-            SurfaceControl.Transaction finishTransaction) {
-        // Do not create edge extension surface for transfer starting window change.
-        // The app surface could be empty thus nothing can draw on the hardware renderer, which will
-        // block this thread when calling Surface#unlockCanvasAndPost.
-        if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
-            return;
-        }
-        final Transformation transformationAtStart = new Transformation();
-        a.getTransformationAt(0, transformationAtStart);
-        final Transformation transformationAtEnd = new Transformation();
-        a.getTransformationAt(1, transformationAtEnd);
-
-        // We want to create an extension surface that is the maximal size and the animation will
-        // take care of cropping any part that overflows.
-        final Insets maxExtensionInsets = Insets.min(
-                transformationAtStart.getInsets(), transformationAtEnd.getInsets());
-
-        final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(),
-                change.getEndAbsBounds().height());
-        final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(),
-                change.getEndAbsBounds().width());
-        if (maxExtensionInsets.left < 0) {
-            final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
-            final Rect extensionRect = new Rect(0, 0,
-                    -maxExtensionInsets.left, targetSurfaceHeight);
-            final int xPos = maxExtensionInsets.left;
-            final int yPos = 0;
-            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
-                    "Left Edge Extension", startTransaction, finishTransaction);
-        }
-
-        if (maxExtensionInsets.top < 0) {
-            final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
-            final Rect extensionRect = new Rect(0, 0,
-                    targetSurfaceWidth, -maxExtensionInsets.top);
-            final int xPos = 0;
-            final int yPos = maxExtensionInsets.top;
-            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
-                    "Top Edge Extension", startTransaction, finishTransaction);
-        }
-
-        if (maxExtensionInsets.right < 0) {
-            final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
-                    targetSurfaceWidth, targetSurfaceHeight);
-            final Rect extensionRect = new Rect(0, 0,
-                    -maxExtensionInsets.right, targetSurfaceHeight);
-            final int xPos = targetSurfaceWidth;
-            final int yPos = 0;
-            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
-                    "Right Edge Extension", startTransaction, finishTransaction);
-        }
-
-        if (maxExtensionInsets.bottom < 0) {
-            final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
-                    targetSurfaceWidth, targetSurfaceHeight);
-            final Rect extensionRect = new Rect(0, 0,
-                    targetSurfaceWidth, -maxExtensionInsets.bottom);
-            final int xPos = maxExtensionInsets.left;
-            final int yPos = targetSurfaceHeight;
-            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
-                    "Bottom Edge Extension", startTransaction, finishTransaction);
-        }
-    }
-
-    private SurfaceControl createExtensionSurface(SurfaceControl surfaceToExtend, Rect edgeBounds,
-            Rect extensionRect, int xPos, int yPos, String layerName,
-            SurfaceControl.Transaction startTransaction,
-            SurfaceControl.Transaction finishTransaction) {
-        final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
-                .setName(layerName)
-                .setParent(surfaceToExtend)
-                .setHidden(true)
-                .setCallsite("DefaultTransitionHandler#startAnimation")
-                .setOpaque(true)
-                .setBufferSize(extensionRect.width(), extensionRect.height())
-                .build();
-
-        SurfaceControl.LayerCaptureArgs captureArgs =
-                new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend)
-                        .setSourceCrop(edgeBounds)
-                        .setFrameScale(1)
-                        .setPixelFormat(PixelFormat.RGBA_8888)
-                        .setChildrenOnly(true)
-                        .setAllowProtected(true)
-                        .build();
-        final SurfaceControl.ScreenshotHardwareBuffer edgeBuffer =
-                SurfaceControl.captureLayers(captureArgs);
-
-        if (edgeBuffer == null) {
-            ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
-                    "Failed to capture edge of window.");
-            return null;
-        }
-
-        android.graphics.BitmapShader shader =
-                new android.graphics.BitmapShader(edgeBuffer.asBitmap(),
-                        android.graphics.Shader.TileMode.CLAMP,
-                        android.graphics.Shader.TileMode.CLAMP);
-        final Paint paint = new Paint();
-        paint.setShader(shader);
-
-        final Surface surface = new Surface(edgeExtensionLayer);
-        Canvas c = surface.lockHardwareCanvas();
-        c.drawRect(extensionRect, paint);
-        surface.unlockCanvasAndPost(c);
-        surface.release();
-
-        startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
-        startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
-        startTransaction.setVisibility(edgeExtensionLayer, true);
-        finishTransaction.remove(edgeExtensionLayer);
-
-        return edgeExtensionLayer;
-    }
-
     @Nullable
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index efee6f40..b75c552 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -24,6 +24,7 @@
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.view.WindowManager.transitTypeToString;
+import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
@@ -34,10 +35,19 @@
 import android.annotation.ColorInt;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.Shader;
 import android.os.SystemProperties;
+import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.animation.Animation;
+import android.view.animation.Transformation;
 import android.window.TransitionInfo;
 
 import com.android.internal.R;
@@ -217,4 +227,126 @@
                 .show(animationBackgroundSurface);
         finishTransaction.remove(animationBackgroundSurface);
     }
+
+    /**
+     * Adds edge extension surface to the given {@code change} for edge extension animation.
+     */
+    public static void edgeExtendWindow(@NonNull TransitionInfo.Change change,
+            @NonNull Animation a, @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction) {
+        // Do not create edge extension surface for transfer starting window change.
+        // The app surface could be empty thus nothing can draw on the hardware renderer, which will
+        // block this thread when calling Surface#unlockCanvasAndPost.
+        if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
+            return;
+        }
+        final Transformation transformationAtStart = new Transformation();
+        a.getTransformationAt(0, transformationAtStart);
+        final Transformation transformationAtEnd = new Transformation();
+        a.getTransformationAt(1, transformationAtEnd);
+
+        // We want to create an extension surface that is the maximal size and the animation will
+        // take care of cropping any part that overflows.
+        final Insets maxExtensionInsets = Insets.min(
+                transformationAtStart.getInsets(), transformationAtEnd.getInsets());
+
+        final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(),
+                change.getEndAbsBounds().height());
+        final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(),
+                change.getEndAbsBounds().width());
+        if (maxExtensionInsets.left < 0) {
+            final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
+            final Rect extensionRect = new Rect(0, 0,
+                    -maxExtensionInsets.left, targetSurfaceHeight);
+            final int xPos = maxExtensionInsets.left;
+            final int yPos = 0;
+            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+                    "Left Edge Extension", startTransaction, finishTransaction);
+        }
+
+        if (maxExtensionInsets.top < 0) {
+            final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
+            final Rect extensionRect = new Rect(0, 0,
+                    targetSurfaceWidth, -maxExtensionInsets.top);
+            final int xPos = 0;
+            final int yPos = maxExtensionInsets.top;
+            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+                    "Top Edge Extension", startTransaction, finishTransaction);
+        }
+
+        if (maxExtensionInsets.right < 0) {
+            final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
+                    targetSurfaceWidth, targetSurfaceHeight);
+            final Rect extensionRect = new Rect(0, 0,
+                    -maxExtensionInsets.right, targetSurfaceHeight);
+            final int xPos = targetSurfaceWidth;
+            final int yPos = 0;
+            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+                    "Right Edge Extension", startTransaction, finishTransaction);
+        }
+
+        if (maxExtensionInsets.bottom < 0) {
+            final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
+                    targetSurfaceWidth, targetSurfaceHeight);
+            final Rect extensionRect = new Rect(0, 0,
+                    targetSurfaceWidth, -maxExtensionInsets.bottom);
+            final int xPos = maxExtensionInsets.left;
+            final int yPos = targetSurfaceHeight;
+            createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
+                    "Bottom Edge Extension", startTransaction, finishTransaction);
+        }
+    }
+
+    /**
+     * Takes a screenshot of {@code surfaceToExtend}'s edge and extends it for edge extension
+     * animation.
+     */
+    private static SurfaceControl createExtensionSurface(@NonNull SurfaceControl surfaceToExtend,
+            @NonNull Rect edgeBounds, @NonNull Rect extensionRect, int xPos, int yPos,
+            @NonNull String layerName, @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction) {
+        final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
+                .setName(layerName)
+                .setParent(surfaceToExtend)
+                .setHidden(true)
+                .setCallsite("TransitionAnimationHelper#createExtensionSurface")
+                .setOpaque(true)
+                .setBufferSize(extensionRect.width(), extensionRect.height())
+                .build();
+
+        final SurfaceControl.LayerCaptureArgs captureArgs =
+                new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend)
+                        .setSourceCrop(edgeBounds)
+                        .setFrameScale(1)
+                        .setPixelFormat(PixelFormat.RGBA_8888)
+                        .setChildrenOnly(true)
+                        .setAllowProtected(true)
+                        .build();
+        final SurfaceControl.ScreenshotHardwareBuffer edgeBuffer =
+                SurfaceControl.captureLayers(captureArgs);
+
+        if (edgeBuffer == null) {
+            ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                    "Failed to capture edge of window.");
+            return null;
+        }
+
+        final BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(),
+                Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+        final Paint paint = new Paint();
+        paint.setShader(shader);
+
+        final Surface surface = new Surface(edgeExtensionLayer);
+        final Canvas c = surface.lockHardwareCanvas();
+        c.drawRect(extensionRect, paint);
+        surface.unlockCanvasAndPost(c);
+        surface.release();
+
+        startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
+        startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
+        startTransaction.setVisibility(edgeExtensionLayer, true);
+        finishTransaction.remove(edgeExtensionLayer);
+
+        return edgeExtensionLayer;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index d1bc738..db1f19a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -322,6 +322,11 @@
         boolean isOpening = isOpeningType(info.getType());
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
+            if ((change.getFlags() & TransitionInfo.FLAG_IS_SYSTEM_WINDOW) != 0) {
+                // Currently system windows are controlled by WindowState, so don't change their
+                // surfaces. Otherwise their window tokens could be hidden unexpectedly.
+                continue;
+            }
             final SurfaceControl leash = change.getLeash();
             final int mode = info.getChanges().get(i).getMode();
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
index 98b5912..79070b1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -40,6 +40,8 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 
+import java.util.ArrayList;
+
 /**
  * Tests for {@link ActivityEmbeddingAnimationRunner}.
  *
@@ -62,13 +64,13 @@
         final TransitionInfo.Change embeddingChange = createChange();
         embeddingChange.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
         info.addChange(embeddingChange);
-        doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+        doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any());
 
         mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction);
 
         final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class);
         verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction),
-                finishCallback.capture());
+                finishCallback.capture(), any());
         verify(mStartTransaction).apply();
         verify(mAnimator).start();
         verifyNoMoreInteractions(mFinishTransaction);
@@ -88,7 +90,8 @@
         info.addChange(embeddingChange);
         final Animator animator = mAnimRunner.createAnimator(
                 info, mStartTransaction, mFinishTransaction,
-                () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */));
+                () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */),
+                new ArrayList());
 
         // The animation should be empty when it is behind starting window.
         assertEquals(0, animator.getDuration());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
index 3792e83..54a12ab 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
@@ -56,13 +56,12 @@
     @Mock
     SurfaceControl.Transaction mFinishTransaction;
     @Mock
-    Transitions.TransitionFinishCallback mFinishCallback;
-    @Mock
     Animator mAnimator;
 
     ActivityEmbeddingController mController;
     ActivityEmbeddingAnimationRunner mAnimRunner;
     ActivityEmbeddingAnimationSpec mAnimSpec;
+    Transitions.TransitionFinishCallback mFinishCallback;
 
     @CallSuper
     @Before
@@ -75,9 +74,11 @@
         assertNotNull(mAnimRunner);
         mAnimSpec = mAnimRunner.mAnimationSpec;
         assertNotNull(mAnimSpec);
+        mFinishCallback = (wct, wctCB) -> {};
         spyOn(mController);
         spyOn(mAnimRunner);
         spyOn(mAnimSpec);
+        spyOn(mFinishCallback);
     }
 
     /** Creates a mock {@link TransitionInfo.Change}. */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
index baecf6f..4d98b6b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -55,7 +55,7 @@
     @Before
     public void setup() {
         super.setUp();
-        doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+        doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any());
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index dba037d..3bd2ae7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.pip.phone;
 
+import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -55,6 +56,7 @@
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class PipResizeGestureHandlerTest extends ShellTestCase {
+    private static final float DEFAULT_SNAP_FRACTION = 2.0f;
     private static final int STEP_SIZE = 40;
     private final MotionEvent.PointerProperties[] mPp = new MotionEvent.PointerProperties[2];
 
@@ -196,6 +198,51 @@
                         < mPipBoundsState.getBounds().width());
     }
 
+    @Test
+    public void testUserResizeTo() {
+        // resizing the bounds to normal bounds at first
+        mPipResizeGestureHandler.userResizeTo(mPipBoundsState.getNormalBounds(),
+                DEFAULT_SNAP_FRACTION);
+
+        assertPipBoundsUserResizedTo(mPipBoundsState.getNormalBounds());
+
+        verify(mPipTaskOrganizer, times(1))
+                .scheduleUserResizePip(any(), any(), any());
+
+        verify(mPipTaskOrganizer, times(1))
+                .scheduleFinishResizePip(any(), any());
+
+        // bounds with max size
+        final Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x,
+                mPipBoundsState.getMaxSize().y);
+
+        // resizing the bounds to maximum bounds the second time
+        mPipResizeGestureHandler.userResizeTo(maxBounds, DEFAULT_SNAP_FRACTION);
+
+        assertPipBoundsUserResizedTo(maxBounds);
+
+        // another call to scheduleUserResizePip() and scheduleFinishResizePip() makes
+        // the total number of invocations 2 for each method
+        verify(mPipTaskOrganizer, times(2))
+                .scheduleUserResizePip(any(), any(), any());
+
+        verify(mPipTaskOrganizer, times(2))
+                .scheduleFinishResizePip(any(), any());
+    }
+
+    private void assertPipBoundsUserResizedTo(Rect bounds) {
+        // check user-resized bounds
+        assertEquals(mPipResizeGestureHandler.getUserResizeBounds().width(), bounds.width());
+        assertEquals(mPipResizeGestureHandler.getUserResizeBounds().height(), bounds.height());
+
+        // check if the bounds are the same
+        assertEquals(mPipBoundsState.getBounds().width(), bounds.width());
+        assertEquals(mPipBoundsState.getBounds().height(), bounds.height());
+
+        // a flag should be set to indicate pip has been resized by the user
+        assertTrue(mPipBoundsState.hasUserResizedPip());
+    }
+
     private MotionEvent obtainMotionEvent(int action, int topLeft, int bottomRight) {
         final MotionEvent.PointerCoords[] pc = new MotionEvent.PointerCoords[2];
         for (int i = 0; i < 2; i++) {
diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml
index f7150ab..2d82307a 100644
--- a/packages/SystemUI/animation/res/values/ids.xml
+++ b/packages/SystemUI/animation/res/values/ids.xml
@@ -16,7 +16,6 @@
 -->
 <resources>
     <!-- DialogLaunchAnimator -->
-    <item type="id" name="tag_launch_animation_running"/>
     <item type="id" name="tag_dialog_background"/>
 
     <!-- ViewBoundsAnimator -->
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index 9656b8a..23cee4d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -29,12 +29,12 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewRootImpl
 import android.view.WindowInsets
 import android.view.WindowManager
 import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
 import android.widget.FrameLayout
 import com.android.internal.jank.InteractionJankMonitor
-import com.android.internal.jank.InteractionJankMonitor.Configuration
 import com.android.internal.jank.InteractionJankMonitor.CujType
 import kotlin.math.roundToInt
 
@@ -46,6 +46,7 @@
  *
  * This animator also allows to easily animate a dialog into an activity.
  *
+ * @see show
  * @see showFromView
  * @see showFromDialog
  * @see createActivityLaunchController
@@ -67,8 +68,81 @@
             ActivityLaunchAnimator.INTERPOLATORS.copy(
                 positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
             )
+    }
 
-        private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.tag_launch_animation_running
+    /**
+     * A controller that takes care of applying the dialog launch and exit animations to the source
+     * that triggered the animation.
+     */
+    interface Controller {
+        /** The [ViewRootImpl] of this controller. */
+        val viewRoot: ViewRootImpl
+
+        /**
+         * The identity object of the source animated by this controller. This animator will ensure
+         * that 2 animations with the same source identity are not going to run at the same time, to
+         * avoid flickers when a dialog is shown from the same source more or less at the same time
+         * (for instance if the user clicks an expandable button twice).
+         */
+        val sourceIdentity: Any
+
+        /**
+         * Move the drawing of the source in the overlay of [viewGroup].
+         *
+         * Once this method is called, and until [stopDrawingInOverlay] is called, the source
+         * controlled by this Controller should be drawn in the overlay of [viewGroup] so that it is
+         * drawn above all other elements in the same [viewRoot].
+         */
+        fun startDrawingInOverlayOf(viewGroup: ViewGroup)
+
+        /**
+         * Move the drawing of the source back in its original location.
+         *
+         * @see startDrawingInOverlayOf
+         */
+        fun stopDrawingInOverlay()
+
+        /**
+         * Create the [LaunchAnimator.Controller] that will be called to animate the source
+         * controlled by this [Controller] during the dialog launch animation.
+         *
+         * At the end of this animation, the source should *not* be visible anymore (until the
+         * dialog is closed and is animated back into the source).
+         */
+        fun createLaunchController(): LaunchAnimator.Controller
+
+        /**
+         * Create the [LaunchAnimator.Controller] that will be called to animate the source
+         * controlled by this [Controller] during the dialog exit animation.
+         *
+         * At the end of this animation, the source should be visible again.
+         */
+        fun createExitController(): LaunchAnimator.Controller
+
+        /**
+         * Whether we should animate the dialog back into the source when it is dismissed. If this
+         * methods returns `false`, then the dialog will simply fade out and
+         * [onExitAnimationCancelled] will be called.
+         *
+         * Note that even when this returns `true`, the exit animation might still be cancelled (in
+         * which case [onExitAnimationCancelled] will also be called).
+         */
+        fun shouldAnimateExit(): Boolean
+
+        /**
+         * Called if we decided to *not* animate the dialog into the source for some reason. This
+         * means that [createExitController] will *not* be called and this implementation should
+         * make sure that the source is back in its original state, before it was animated into the
+         * dialog. In particular, the source should be visible again.
+         */
+        fun onExitAnimationCancelled()
+
+        /**
+         * Return the [InteractionJankMonitor.Configuration.Builder] to be used for animations
+         * controlled by this controller.
+         */
+        // TODO(b/252723237): Make this non-nullable
+        fun jankConfigurationBuilder(cuj: Int): InteractionJankMonitor.Configuration.Builder?
     }
 
     /**
@@ -96,7 +170,28 @@
         dialog: Dialog,
         view: View,
         cuj: DialogCuj? = null,
-        animateBackgroundBoundsChange: Boolean = false,
+        animateBackgroundBoundsChange: Boolean = false
+    ) {
+        show(dialog, createController(view), cuj, animateBackgroundBoundsChange)
+    }
+
+    /**
+     * Show [dialog] by expanding it from a source controlled by [controller].
+     *
+     * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be
+     * animated when the dialog bounds change.
+     *
+     * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
+     * animated.
+     *
+     * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
+     * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
+     */
+    fun show(
+        dialog: Dialog,
+        controller: Controller,
+        cuj: DialogCuj? = null,
+        animateBackgroundBoundsChange: Boolean = false
     ) {
         if (Looper.myLooper() != Looper.getMainLooper()) {
             throw IllegalStateException(
@@ -109,9 +204,10 @@
         // intent is to launch a dialog from another dialog.
         val animatedParent =
             openedDialogs.firstOrNull {
-                it.dialog.window.decorView.viewRootImpl == view.viewRootImpl
+                it.dialog.window.decorView.viewRootImpl == controller.viewRoot
             }
-        val animateFrom = animatedParent?.dialogContentWithBackground ?: view
+        val animateFrom =
+            animatedParent?.dialogContentWithBackground?.let { createController(it) } ?: controller
 
         if (animatedParent == null && animateFrom !is LaunchableView) {
             // Make sure the View we launch from implements LaunchableView to avoid visibility
@@ -126,15 +222,17 @@
             )
         }
 
-        // Make sure we don't run the launch animation from the same view twice at the same time.
-        if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) {
-            Log.e(TAG, "Not running dialog launch animation as there is already one running")
+        // Make sure we don't run the launch animation from the same source twice at the same time.
+        if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) {
+            Log.e(
+                TAG,
+                "Not running dialog launch animation from source as it is already expanded into a" +
+                    " dialog"
+            )
             dialog.show()
             return
         }
 
-        animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
-
         val animatedDialog =
             AnimatedDialog(
                 launchAnimator,
@@ -146,16 +244,99 @@
                 animateBackgroundBoundsChange,
                 animatedParent,
                 isForTesting,
-                cuj
+                cuj,
             )
 
         openedDialogs.add(animatedDialog)
         animatedDialog.start()
     }
 
+    /** Create a [Controller] that can animate [source] to & from a dialog. */
+    private fun createController(source: View): Controller {
+        return object : Controller {
+            override val viewRoot: ViewRootImpl
+                get() = source.viewRootImpl
+
+            override val sourceIdentity: Any = source
+
+            override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
+                // Create a temporary ghost of the source (which will make it invisible) and add it
+                // to the host dialog.
+                GhostView.addGhost(source, viewGroup)
+
+                // The ghost of the source was just created, so the source is currently invisible.
+                // We need to make sure that it stays invisible as long as the dialog is shown or
+                // animating.
+                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
+            }
+
+            override fun stopDrawingInOverlay() {
+                // Note: here we should remove the ghost from the overlay, but in practice this is
+                // already done by the launch controllers created below.
+
+                // Make sure we allow the source to change its visibility again.
+                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
+                source.visibility = View.VISIBLE
+            }
+
+            override fun createLaunchController(): LaunchAnimator.Controller {
+                val delegate = GhostedViewLaunchAnimatorController(source)
+                return object : LaunchAnimator.Controller by delegate {
+                    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+                        // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
+                        // ghost (that ghosts only the source content, and not its background) will
+                        // be added right after this by the delegate and will be animated.
+                        GhostView.removeGhost(source)
+                        delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+                    }
+
+                    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+                        delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+
+                        // We hide the source when the dialog is showing. We will make this view
+                        // visible again when dismissing the dialog. This does nothing if the source
+                        // implements [LaunchableView], as it's already INVISIBLE in that case.
+                        source.visibility = View.INVISIBLE
+                    }
+                }
+            }
+
+            override fun createExitController(): LaunchAnimator.Controller {
+                return GhostedViewLaunchAnimatorController(source)
+            }
+
+            override fun shouldAnimateExit(): Boolean {
+                // The source should be invisible by now, if it's not then something else changed
+                // its visibility and we probably don't want to run the animation.
+                if (source.visibility != View.INVISIBLE) {
+                    return false
+                }
+
+                return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true)
+            }
+
+            override fun onExitAnimationCancelled() {
+                // Make sure we allow the source to change its visibility again.
+                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
+
+                // If the view is invisible it's probably because of us, so we make it visible
+                // again.
+                if (source.visibility == View.INVISIBLE) {
+                    source.visibility = View.VISIBLE
+                }
+            }
+
+            override fun jankConfigurationBuilder(
+                cuj: Int
+            ): InteractionJankMonitor.Configuration.Builder? {
+                return InteractionJankMonitor.Configuration.Builder.withView(cuj, source)
+            }
+        }
+    }
+
     /**
-     * Launch [dialog] from [another dialog][animateFrom] that was shown using [showFromView]. This
-     * will allow for dismissing the whole stack.
+     * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will
+     * allow for dismissing the whole stack.
      *
      * @see dismissStack
      */
@@ -181,32 +362,55 @@
 
     /**
      * Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from the
-     * dialog that contains [View]. Note that the dialog must have been show using [showFromView]
-     * and be currently showing, otherwise this will return null.
+     * dialog that contains [View]. Note that the dialog must have been shown using this animator,
+     * otherwise this method will return null.
      *
      * The returned controller will take care of dismissing the dialog at the right time after the
      * activity started, when the dialog to app animation is done (or when it is cancelled). If this
      * method returns null, then the dialog won't be dismissed.
      *
-     * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
-     * animated.
-     *
      * @param view any view inside the dialog to animate.
      */
     @JvmOverloads
     fun createActivityLaunchController(
         view: View,
-        cujType: Int? = null
+        cujType: Int? = null,
     ): ActivityLaunchAnimator.Controller? {
         val animatedDialog =
             openedDialogs.firstOrNull {
                 it.dialog.window.decorView.viewRootImpl == view.viewRootImpl
             }
                 ?: return null
+        return createActivityLaunchController(animatedDialog, cujType)
+    }
 
+    /**
+     * Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from
+     * [dialog]. Note that the dialog must have been shown using this animator, otherwise this
+     * method will return null.
+     *
+     * The returned controller will take care of dismissing the dialog at the right time after the
+     * activity started, when the dialog to app animation is done (or when it is cancelled). If this
+     * method returns null, then the dialog won't be dismissed.
+     *
+     * @param dialog the dialog to animate.
+     */
+    @JvmOverloads
+    fun createActivityLaunchController(
+        dialog: Dialog,
+        cujType: Int? = null,
+    ): ActivityLaunchAnimator.Controller? {
+        val animatedDialog = openedDialogs.firstOrNull { it.dialog == dialog } ?: return null
+        return createActivityLaunchController(animatedDialog, cujType)
+    }
+
+    private fun createActivityLaunchController(
+        animatedDialog: AnimatedDialog,
+        cujType: Int? = null
+    ): ActivityLaunchAnimator.Controller? {
         // At this point, we know that the intent of the caller is to dismiss the dialog to show
-        // an app, so we disable the exit animation into the touch surface because we will never
-        // want to run it anyways.
+        // an app, so we disable the exit animation into the source because we will never want to
+        // run it anyways.
         animatedDialog.exitAnimationDisabled = true
 
         val dialog = animatedDialog.dialog
@@ -252,7 +456,7 @@
 
                 // If this dialog was shown from a cascade of other dialogs, make sure those ones
                 // are dismissed too.
-                animatedDialog.touchSurface = animatedDialog.prepareForStackDismiss()
+                animatedDialog.prepareForStackDismiss()
 
                 // Remove the dim.
                 dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
@@ -283,12 +487,11 @@
     }
 
     /**
-     * Ensure that all dialogs currently shown won't animate into their touch surface when
-     * dismissed.
+     * Ensure that all dialogs currently shown won't animate into their source when dismissed.
      *
      * This is a temporary API meant to be called right before we both dismiss a dialog and start an
-     * activity, which currently does not look good if we animate the dialog into the touch surface
-     * at the same time as the activity starts.
+     * activity, which currently does not look good if we animate the dialog into their source at
+     * the same time as the activity starts.
      *
      * TODO(b/193634619): Remove this function and animate dialog into opening activity instead.
      */
@@ -297,13 +500,11 @@
     }
 
     /**
-     * Dismiss [dialog]. If it was launched from another dialog using [showFromView], also dismiss
-     * the stack of dialogs, animating back to the original touchSurface.
+     * Dismiss [dialog]. If it was launched from another dialog using this animator, also dismiss
+     * the stack of dialogs and simply fade out [dialog].
      */
     fun dismissStack(dialog: Dialog) {
-        openedDialogs
-            .firstOrNull { it.dialog == dialog }
-            ?.let { it.touchSurface = it.prepareForStackDismiss() }
+        openedDialogs.firstOrNull { it.dialog == dialog }?.prepareForStackDismiss()
         dialog.dismiss()
     }
 
@@ -337,8 +538,11 @@
     private val callback: DialogLaunchAnimator.Callback,
     private val interactionJankMonitor: InteractionJankMonitor,
 
-    /** The view that triggered the dialog after being tapped. */
-    var touchSurface: View,
+    /**
+     * The controller of the source that triggered the dialog and that will animate into/from the
+     * dialog.
+     */
+    val controller: DialogLaunchAnimator.Controller,
 
     /**
      * A callback that will be called with this [AnimatedDialog] after the dialog was dismissed and
@@ -383,17 +587,18 @@
     private var originalDialogBackgroundColor = Color.BLACK
 
     /**
-     * Whether we are currently launching/showing the dialog by animating it from [touchSurface].
+     * Whether we are currently launching/showing the dialog by animating it from its source
+     * controlled by [controller].
      */
     private var isLaunching = true
 
-    /** Whether we are currently dismissing/hiding the dialog by animating into [touchSurface]. */
+    /** Whether we are currently dismissing/hiding the dialog by animating into its source. */
     private var isDismissing = false
 
     private var dismissRequested = false
     var exitAnimationDisabled = false
 
-    private var isTouchSurfaceGhostDrawn = false
+    private var isSourceDrawnInDialog = false
     private var isOriginalDialogViewLaidOut = false
 
     /** A layout listener to animate the dialog height change. */
@@ -410,13 +615,19 @@
      */
     private var decorViewLayoutListener: View.OnLayoutChangeListener? = null
 
+    private var hasInstrumentedJank = false
+
     fun start() {
         if (cuj != null) {
-            val config = Configuration.Builder.withView(cuj.cujType, touchSurface)
-            if (cuj.tag != null) {
-                config.setTag(cuj.tag)
+            val config = controller.jankConfigurationBuilder(cuj.cujType)
+            if (config != null) {
+                if (cuj.tag != null) {
+                    config.setTag(cuj.tag)
+                }
+
+                interactionJankMonitor.begin(config)
+                hasInstrumentedJank = true
             }
-            interactionJankMonitor.begin(config)
         }
 
         // Create the dialog so that its onCreate() method is called, which usually sets the dialog
@@ -618,47 +829,45 @@
         // Show the dialog.
         dialog.show()
 
-        addTouchSurfaceGhost()
+        moveSourceDrawingToDialog()
     }
 
-    private fun addTouchSurfaceGhost() {
+    private fun moveSourceDrawingToDialog() {
         if (decorView.viewRootImpl == null) {
-            // Make sure that we have access to the dialog view root to synchronize the creation of
-            // the ghost.
-            decorView.post(::addTouchSurfaceGhost)
+            // Make sure that we have access to the dialog view root to move the drawing to the
+            // dialog overlay.
+            decorView.post(::moveSourceDrawingToDialog)
             return
         }
 
-        // Create a ghost of the touch surface (which will make the touch surface invisible) and add
-        // it to the host dialog. We trigger a one off synchronization to make sure that this is
-        // done in sync between the two different windows.
+        // Move the drawing of the source in the overlay of this dialog, then animate. We trigger a
+        // one-off synchronization to make sure that this is done in sync between the two different
+        // windows.
         synchronizeNextDraw(
             then = {
-                isTouchSurfaceGhostDrawn = true
+                isSourceDrawnInDialog = true
                 maybeStartLaunchAnimation()
             }
         )
-        GhostView.addGhost(touchSurface, decorView)
-
-        // The ghost of the touch surface was just created, so the touch surface is currently
-        // invisible. We need to make sure that it stays invisible as long as the dialog is shown or
-        // animating.
-        (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
+        controller.startDrawingInOverlayOf(decorView)
     }
 
     /**
-     * Synchronize the next draw of the touch surface and dialog view roots so that they are
-     * performed at the same time, in the same transaction. This is necessary to make sure that the
-     * ghost of the touch surface is drawn at the same time as the touch surface is made invisible
-     * (or inversely, removed from the UI when the touch surface is made visible).
+     * Synchronize the next draw of the source and dialog view roots so that they are performed at
+     * the same time, in the same transaction. This is necessary to make sure that the source is
+     * drawn in the overlay at the same time as it is removed from its original position (or
+     * inversely, removed from the overlay when the source is moved back to its original position).
      */
     private fun synchronizeNextDraw(then: () -> Unit) {
         if (forceDisableSynchronization) {
+            // Don't synchronize when inside an automated test.
             then()
             return
         }
 
-        ViewRootSync.synchronizeNextDraw(touchSurface, decorView, then)
+        ViewRootSync.synchronizeNextDraw(decorView, controller.viewRoot.view, then)
+        decorView.invalidate()
+        controller.viewRoot.view.invalidate()
     }
 
     private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
@@ -681,7 +890,7 @@
     }
 
     private fun maybeStartLaunchAnimation() {
-        if (!isTouchSurfaceGhostDrawn || !isOriginalDialogViewLaidOut) {
+        if (!isSourceDrawnInDialog || !isOriginalDialogViewLaidOut) {
             return
         }
 
@@ -690,19 +899,7 @@
 
         startAnimation(
             isLaunching = true,
-            onLaunchAnimationStart = {
-                // Remove the temporary ghost. Another ghost (that ghosts only the touch surface
-                // content, and not its background) will be added right after this and will be
-                // animated.
-                GhostView.removeGhost(touchSurface)
-            },
             onLaunchAnimationEnd = {
-                touchSurface.setTag(R.id.tag_launch_animation_running, null)
-
-                // We hide the touch surface when the dialog is showing. We will make this view
-                // visible again when dismissing the dialog.
-                touchSurface.visibility = View.INVISIBLE
-
                 isLaunching = false
 
                 // dismiss was called during the animation, dismiss again now to actually dismiss.
@@ -718,7 +915,10 @@
                         backgroundLayoutListener
                     )
                 }
-                cuj?.run { interactionJankMonitor.end(cujType) }
+
+                if (hasInstrumentedJank) {
+                    interactionJankMonitor.end(cuj!!.cujType)
+                }
             }
         )
     }
@@ -753,8 +953,8 @@
     }
 
     /**
-     * Hide the dialog into the touch surface and call [onAnimationFinished] when the animation is
-     * done (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually
+     * Hide the dialog into the source and call [onAnimationFinished] when the animation is done
+     * (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually
      * dismiss the dialog.
      */
     private fun hideDialogIntoView(onAnimationFinished: (Boolean) -> Unit) {
@@ -763,17 +963,9 @@
             decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
         }
 
-        if (!shouldAnimateDialogIntoView()) {
-            Log.i(TAG, "Skipping animation of dialog into the touch surface")
-
-            // Make sure we allow the touch surface to change its visibility again.
-            (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-
-            // If the view is invisible it's probably because of us, so we make it visible again.
-            if (touchSurface.visibility == View.INVISIBLE) {
-                touchSurface.visibility = View.VISIBLE
-            }
-
+        if (!shouldAnimateDialogIntoSource()) {
+            Log.i(TAG, "Skipping animation of dialog into the source")
+            controller.onExitAnimationCancelled()
             onAnimationFinished(false /* instantDismiss */)
             onDialogDismissed(this@AnimatedDialog)
             return
@@ -786,10 +978,6 @@
                 dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
             },
             onLaunchAnimationEnd = {
-                // Make sure we allow the touch surface to change its visibility again.
-                (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-
-                touchSurface.visibility = View.VISIBLE
                 val dialogContentWithBackground = this.dialogContentWithBackground!!
                 dialogContentWithBackground.visibility = View.INVISIBLE
 
@@ -799,14 +987,11 @@
                     )
                 }
 
-                // Make sure that the removal of the ghost and making the touch surface visible is
-                // done at the same time.
-                synchronizeNextDraw(
-                    then = {
-                        onAnimationFinished(true /* instantDismiss */)
-                        onDialogDismissed(this@AnimatedDialog)
-                    }
-                )
+                controller.stopDrawingInOverlay()
+                synchronizeNextDraw {
+                    onAnimationFinished(true /* instantDismiss */)
+                    onDialogDismissed(this@AnimatedDialog)
+                }
             }
         )
     }
@@ -816,27 +1001,34 @@
         onLaunchAnimationStart: () -> Unit = {},
         onLaunchAnimationEnd: () -> Unit = {}
     ) {
-        // Create 2 ghost controllers to animate both the dialog and the touch surface in the
-        // dialog.
-        val startView = if (isLaunching) touchSurface else dialogContentWithBackground!!
-        val endView = if (isLaunching) dialogContentWithBackground!! else touchSurface
-        val startViewController = GhostedViewLaunchAnimatorController(startView)
-        val endViewController = GhostedViewLaunchAnimatorController(endView)
-        startViewController.launchContainer = decorView
-        endViewController.launchContainer = decorView
+        // Create 2 controllers to animate both the dialog and the source.
+        val startController =
+            if (isLaunching) {
+                controller.createLaunchController()
+            } else {
+                GhostedViewLaunchAnimatorController(dialogContentWithBackground!!)
+            }
+        val endController =
+            if (isLaunching) {
+                GhostedViewLaunchAnimatorController(dialogContentWithBackground!!)
+            } else {
+                controller.createExitController()
+            }
+        startController.launchContainer = decorView
+        endController.launchContainer = decorView
 
-        val endState = endViewController.createAnimatorState()
+        val endState = endController.createAnimatorState()
         val controller =
             object : LaunchAnimator.Controller {
                 override var launchContainer: ViewGroup
-                    get() = startViewController.launchContainer
+                    get() = startController.launchContainer
                     set(value) {
-                        startViewController.launchContainer = value
-                        endViewController.launchContainer = value
+                        startController.launchContainer = value
+                        endController.launchContainer = value
                     }
 
                 override fun createAnimatorState(): LaunchAnimator.State {
-                    return startViewController.createAnimatorState()
+                    return startController.createAnimatorState()
                 }
 
                 override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -845,15 +1037,29 @@
                     // onLaunchAnimationStart on the controller (which will create its own ghost).
                     onLaunchAnimationStart()
 
-                    startViewController.onLaunchAnimationStart(isExpandingFullyAbove)
-                    endViewController.onLaunchAnimationStart(isExpandingFullyAbove)
+                    startController.onLaunchAnimationStart(isExpandingFullyAbove)
+                    endController.onLaunchAnimationStart(isExpandingFullyAbove)
                 }
 
                 override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
-                    startViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
-                    endViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
+                    // onLaunchAnimationEnd is called by an Animator at the end of the animation,
+                    // on a Choreographer animation tick. The following calls will move the animated
+                    // content from the dialog overlay back to its original position, and this
+                    // change must be reflected in the next frame given that we then sync the next
+                    // frame of both the content and dialog ViewRoots. However, in case that content
+                    // is rendered by Compose, whose compositions are also scheduled on a
+                    // Choreographer frame, any state change made *right now* won't be reflected in
+                    // the next frame given that a Choreographer frame can't schedule another and
+                    // have it happen in the same frame. So we post the forwarded calls to
+                    // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
+                    // that the move of the content back to its original window will be reflected in
+                    // the next frame right after [onLaunchAnimationEnd] is called.
+                    dialog.context.mainExecutor.execute {
+                        startController.onLaunchAnimationEnd(isExpandingFullyAbove)
+                        endController.onLaunchAnimationEnd(isExpandingFullyAbove)
 
-                    onLaunchAnimationEnd()
+                        onLaunchAnimationEnd()
+                    }
                 }
 
                 override fun onLaunchAnimationProgress(
@@ -861,11 +1067,11 @@
                     progress: Float,
                     linearProgress: Float
                 ) {
-                    startViewController.onLaunchAnimationProgress(state, progress, linearProgress)
+                    startController.onLaunchAnimationProgress(state, progress, linearProgress)
 
                     // The end view is visible only iff the starting view is not visible.
                     state.visible = !state.visible
-                    endViewController.onLaunchAnimationProgress(state, progress, linearProgress)
+                    endController.onLaunchAnimationProgress(state, progress, linearProgress)
 
                     // If the dialog content is complex, its dimension might change during the
                     // launch animation. The animation end position might also change during the
@@ -873,14 +1079,16 @@
                     // Therefore we update the end state to the new position/size. Usually the
                     // dialog dimension or position will change in the early frames, so changing the
                     // end state shouldn't really be noticeable.
-                    endViewController.fillGhostedViewState(endState)
+                    if (endController is GhostedViewLaunchAnimatorController) {
+                        endController.fillGhostedViewState(endState)
+                    }
                 }
             }
 
         launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
     }
 
-    private fun shouldAnimateDialogIntoView(): Boolean {
+    private fun shouldAnimateDialogIntoSource(): Boolean {
         // Don't animate if the dialog was previously hidden using hide() or if we disabled the exit
         // animation.
         if (exitAnimationDisabled || !dialog.isShowing) {
@@ -888,24 +1096,12 @@
         }
 
         // If we are dreaming, the dialog was probably closed because of that so we don't animate
-        // into the touchSurface.
+        // into the source.
         if (callback.isDreaming()) {
             return false
         }
 
-        // The touch surface should be invisible by now, if it's not then something else changed its
-        // visibility and we probably don't want to run the animation.
-        if (touchSurface.visibility != View.INVISIBLE) {
-            return false
-        }
-
-        // If the touch surface is not attached or one of its ancestors is not visible, then we
-        // don't run the animation either.
-        if (!touchSurface.isAttachedToWindow) {
-            return false
-        }
-
-        return (touchSurface.parent as? View)?.isShown ?: true
+        return controller.shouldAnimateExit()
     }
 
     /** A layout listener to animate the change of bounds of the dialog background. */
@@ -988,17 +1184,13 @@
         }
     }
 
-    fun prepareForStackDismiss(): View {
+    fun prepareForStackDismiss() {
         if (parentAnimatedDialog == null) {
-            return touchSurface
+            return
         }
         parentAnimatedDialog.exitAnimationDisabled = true
         parentAnimatedDialog.dialog.hide()
-        val view = parentAnimatedDialog.prepareForStackDismiss()
+        parentAnimatedDialog.prepareForStackDismiss()
         parentAnimatedDialog.dialog.dismiss()
-        // Make the touch surface invisible, so we end up animating to it when we actually
-        // dismiss the stack
-        view.visibility = View.INVISIBLE
-        return view
     }
 }
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt
new file mode 100644
index 0000000..1d808ba
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.SdkConstants.CLASS_CONTEXT
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiModifierListOwner
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.getParentOfType
+
+/**
+ * Warns if {@code Context.bindService}, {@code Context.bindServiceAsUser}, or {@code
+ * Context.unbindService} is not called on a {@code WorkerThread}
+ */
+@Suppress("UnstableApiUsage")
+class BindServiceOnMainThreadDetector : Detector(), SourceCodeScanner {
+
+    override fun getApplicableMethodNames(): List<String> {
+        return listOf("bindService", "bindServiceAsUser", "unbindService")
+    }
+
+    private fun hasWorkerThreadAnnotation(
+        context: JavaContext,
+        annotated: PsiModifierListOwner?
+    ): Boolean {
+        return context.evaluator.getAnnotations(annotated, inHierarchy = true).any { uAnnotation ->
+            uAnnotation.qualifiedName == "androidx.annotation.WorkerThread"
+        }
+    }
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
+            if (
+                !hasWorkerThreadAnnotation(context, node.getParentOfType(UMethod::class.java)) &&
+                    !hasWorkerThreadAnnotation(context, node.getParentOfType(UClass::class.java))
+            ) {
+                context.report(
+                    ISSUE,
+                    method,
+                    context.getLocation(node),
+                    "This method should be annotated with `@WorkerThread` because " +
+                        "it calls ${method.name}",
+                )
+            }
+        }
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE: Issue =
+            Issue.create(
+                id = "BindServiceOnMainThread",
+                briefDescription = "Service bound or unbound on main thread",
+                explanation =
+                    """
+                    Binding and unbinding services are synchronous calls to `ActivityManager`. \
+                    They usually take multiple milliseconds to complete. If called on the main \
+                    thread, it will likely cause missed frames. To fix it, use a `@Background \
+                    Executor` and annotate the calling method with `@WorkerThread`.
+                    """,
+                category = Category.PERFORMANCE,
+                priority = 8,
+                severity = Severity.WARNING,
+                implementation =
+                    Implementation(
+                        BindServiceOnMainThreadDetector::class.java,
+                        Scope.JAVA_FILE_SCOPE
+                    )
+            )
+    }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt
deleted file mode 100644
index 925fae0e..0000000
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt
+++ /dev/null
@@ -1,67 +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.internal.systemui.lint
-
-import com.android.tools.lint.detector.api.Category
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Implementation
-import com.android.tools.lint.detector.api.Issue
-import com.android.tools.lint.detector.api.JavaContext
-import com.android.tools.lint.detector.api.Scope
-import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.PsiMethod
-import org.jetbrains.uast.UCallExpression
-
-@Suppress("UnstableApiUsage")
-class BindServiceViaContextDetector : Detector(), SourceCodeScanner {
-
-    override fun getApplicableMethodNames(): List<String> {
-        return listOf("bindService", "bindServiceAsUser", "unbindService")
-    }
-
-    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
-        if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) {
-            context.report(
-                    ISSUE,
-                    method,
-                    context.getNameLocation(node),
-                    "Binding or unbinding services are synchronous calls, please make " +
-                            "sure you're on a @Background Executor."
-            )
-        }
-    }
-
-    companion object {
-        @JvmField
-        val ISSUE: Issue =
-            Issue.create(
-                id = "BindServiceViaContextDetector",
-                briefDescription = "Service bound/unbound via Context, please make sure " +
-                        "you're on a background thread.",
-                explanation =
-                "Binding or unbinding services are synchronous calls to ActivityManager, " +
-                        "they usually take multiple milliseconds to complete and will make" +
-                        "the caller drop frames. Make sure you're on a @Background Executor.",
-                category = Category.PERFORMANCE,
-                priority = 8,
-                severity = Severity.WARNING,
-                implementation =
-                Implementation(BindServiceViaContextDetector::class.java, Scope.JAVA_FILE_SCOPE)
-            )
-    }
-}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
index 8d48f09..1129929 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
@@ -16,6 +16,7 @@
 
 package com.android.internal.systemui.lint
 
+import com.android.SdkConstants.CLASS_CONTEXT
 import com.android.tools.lint.detector.api.Category
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Implementation
@@ -48,14 +49,14 @@
             return
         }
 
-        val evaulator = context.evaluator
-        if (evaulator.isMemberInSubClassOf(method, "android.content.Context")) {
+        val evaluator = context.evaluator
+        if (evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
             context.report(
                     ISSUE,
                     method,
                     context.getNameLocation(node),
-                    "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
-                            "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+                    "`Context.${method.name}()` should be replaced with " +
+                    "`BroadcastSender.${method.name}()`"
             )
         }
     }
@@ -65,14 +66,14 @@
         val ISSUE: Issue =
             Issue.create(
                 id = "BroadcastSentViaContext",
-                briefDescription = "Broadcast sent via Context instead of BroadcastSender.",
-                explanation =
-                "Broadcast was sent via " +
-                        "Context.sendBroadcast/Context.sendBroadcastAsUser. Please use " +
-                        "BroadcastSender.sendBroadcast/BroadcastSender.sendBroadcastAsUser " +
-                        "which will schedule dispatch of broadcasts on background thread. " +
-                        "Sending broadcasts on main thread causes jank due to synchronous " +
-                        "Binder calls.",
+                briefDescription = "Broadcast sent via `Context` instead of `BroadcastSender`",
+                // lint trims indents and converts \ to line continuations
+                explanation = """
+                        Broadcasts sent via `Context.sendBroadcast()` or \
+                        `Context.sendBroadcastAsUser()` will block the main thread and may cause \
+                        missed frames. Instead, use `BroadcastSender.sendBroadcast()` or \
+                        `BroadcastSender.sendBroadcastAsUser()` which will schedule and dispatch \
+                        broadcasts on a background worker thread.""",
                 category = Category.PERFORMANCE,
                 priority = 8,
                 severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt
deleted file mode 100644
index a629eee..0000000
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt
+++ /dev/null
@@ -1,66 +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.internal.systemui.lint
-
-import com.android.tools.lint.detector.api.Category
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Implementation
-import com.android.tools.lint.detector.api.Issue
-import com.android.tools.lint.detector.api.JavaContext
-import com.android.tools.lint.detector.api.Scope
-import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.PsiMethod
-import org.jetbrains.uast.UCallExpression
-
-@Suppress("UnstableApiUsage")
-class GetMainLooperViaContextDetector : Detector(), SourceCodeScanner {
-
-    override fun getApplicableMethodNames(): List<String> {
-        return listOf("getMainThreadHandler", "getMainLooper", "getMainExecutor")
-    }
-
-    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
-        if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) {
-            context.report(
-                    ISSUE,
-                    method,
-                    context.getNameLocation(node),
-                    "Please inject a @Main Executor instead."
-            )
-        }
-    }
-
-    companion object {
-        @JvmField
-        val ISSUE: Issue =
-                Issue.create(
-                        id = "GetMainLooperViaContextDetector",
-                        briefDescription = "Please use idiomatic SystemUI executors, injecting " +
-                                "them via Dagger.",
-                        explanation = "Injecting the @Main Executor is preferred in order to make" +
-                                "dependencies explicit and increase testability. It's much " +
-                                "easier to pass a FakeExecutor on your test ctor than to " +
-                                "deal with loopers in unit tests.",
-                        category = Category.LINT,
-                        priority = 8,
-                        severity = Severity.WARNING,
-                        implementation = Implementation(GetMainLooperViaContextDetector::class.java,
-                                Scope.JAVA_FILE_SCOPE)
-                )
-    }
-}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt
new file mode 100644
index 0000000..bab76ab
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.SdkConstants.CLASS_CONTEXT
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+@Suppress("UnstableApiUsage")
+class NonInjectedMainThreadDetector : Detector(), SourceCodeScanner {
+
+    override fun getApplicableMethodNames(): List<String> {
+        return listOf("getMainThreadHandler", "getMainLooper", "getMainExecutor")
+    }
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
+            context.report(
+                ISSUE,
+                method,
+                context.getNameLocation(node),
+                "Replace with injected `@Main Executor`."
+            )
+        }
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE: Issue =
+            Issue.create(
+                id = "NonInjectedMainThread",
+                briefDescription = "Main thread usage without dependency injection",
+                explanation =
+                    """
+                                Main thread should be injected using the `@Main Executor` instead \
+                                of using the accessors in `Context`. This is to make the \
+                                dependencies explicit and increase testability. It's much easier \
+                                to pass a `FakeExecutor` on test constructors than it is to deal \
+                                with loopers in unit tests.""",
+                category = Category.LINT,
+                priority = 8,
+                severity = Severity.WARNING,
+                implementation =
+                    Implementation(NonInjectedMainThreadDetector::class.java, Scope.JAVA_FILE_SCOPE)
+            )
+    }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
index 4eb7c7d..b622900 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
@@ -16,6 +16,7 @@
 
 package com.android.internal.systemui.lint
 
+import com.android.SdkConstants.CLASS_CONTEXT
 import com.android.tools.lint.detector.api.Category
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Implementation
@@ -32,7 +33,7 @@
 class NonInjectedServiceDetector : Detector(), SourceCodeScanner {
 
     override fun getApplicableMethodNames(): List<String> {
-        return listOf("getSystemService")
+        return listOf("getSystemService", "get")
     }
 
     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
@@ -40,14 +41,25 @@
         if (
             !evaluator.isStatic(method) &&
                 method.name == "getSystemService" &&
-                method.containingClass?.qualifiedName == "android.content.Context"
+                method.containingClass?.qualifiedName == CLASS_CONTEXT
         ) {
             context.report(
                 ISSUE,
                 method,
                 context.getNameLocation(node),
-                "Use @Inject to get the handle to a system-level services instead of using " +
-                    "Context.getSystemService()"
+                "Use `@Inject` to get system-level service handles instead of " +
+                    "`Context.getSystemService()`"
+            )
+        } else if (
+            evaluator.isStatic(method) &&
+                method.name == "get" &&
+                method.containingClass?.qualifiedName == "android.accounts.AccountManager"
+        ) {
+            context.report(
+                ISSUE,
+                method,
+                context.getNameLocation(node),
+                "Replace `AccountManager.get()` with an injected instance of `AccountManager`"
             )
         }
     }
@@ -57,14 +69,14 @@
         val ISSUE: Issue =
             Issue.create(
                 id = "NonInjectedService",
-                briefDescription =
-                    "System-level services should be retrieved using " +
-                        "@Inject instead of Context.getSystemService().",
+                briefDescription = "System service not injected",
                 explanation =
-                    "Context.getSystemService() should be avoided because it makes testing " +
-                        "difficult. Instead, use an injected service. For example, " +
-                        "instead of calling Context.getSystemService(UserManager.class), " +
-                        "use @Inject and add UserManager to the constructor",
+                    """
+                    `Context.getSystemService()` should be avoided because it makes testing \
+                    difficult. Instead, use an injected service. For example, instead of calling \
+                    `Context.getSystemService(UserManager.class)` in a class, annotate the class' \
+                    constructor with `@Inject` and add `UserManager` to the parameters.
+                    """,
                 category = Category.CORRECTNESS,
                 priority = 8,
                 severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
index eb71d32..4ba3afc 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
@@ -16,6 +16,7 @@
 
 package com.android.internal.systemui.lint
 
+import com.android.SdkConstants.CLASS_CONTEXT
 import com.android.tools.lint.detector.api.Category
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Implementation
@@ -35,12 +36,12 @@
     }
 
     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
-        if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) {
+        if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
             context.report(
                     ISSUE,
                     method,
                     context.getNameLocation(node),
-                    "BroadcastReceivers should be registered via BroadcastDispatcher."
+                    "Register `BroadcastReceiver` using `BroadcastDispatcher` instead of `Context`"
             )
         }
     }
@@ -49,14 +50,16 @@
         @JvmField
         val ISSUE: Issue =
             Issue.create(
-                    id = "RegisterReceiverViaContextDetector",
-                    briefDescription = "Broadcast registrations via Context are blocking " +
-                            "calls. Please use BroadcastDispatcher.",
-                    explanation =
-                    "Context#registerReceiver is a blocking call to the system server, " +
-                            "making it very likely that you'll drop a frame. Please use " +
-                            "BroadcastDispatcher instead (or move this call to a " +
-                            "@Background Executor.)",
+                    id = "RegisterReceiverViaContext",
+                    briefDescription = "Blocking broadcast registration",
+                    // lint trims indents and converts \ to line continuations
+                    explanation = """
+                            `Context.registerReceiver()` is a blocking call to the system server, \
+                            making it very likely that you'll drop a frame. Please use \
+                            `BroadcastDispatcher` instead, which registers the receiver on a \
+                             background thread. `BroadcastDispatcher` also improves our visibility \
+                             into ANRs.""",
+                            moreInfo = "go/identifying-broadcast-threads",
                     category = Category.PERFORMANCE,
                     priority = 8,
                     severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
index b006615..7be21a5 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
@@ -49,8 +49,7 @@
                 ISSUE_SLOW_USER_ID_QUERY,
                 method,
                 context.getNameLocation(node),
-                "ActivityManager.getCurrentUser() is slow. " +
-                    "Use UserTracker.getUserId() instead."
+                "Use `UserTracker.getUserId()` instead of `ActivityManager.getCurrentUser()`"
             )
         }
         if (
@@ -62,7 +61,7 @@
                 ISSUE_SLOW_USER_INFO_QUERY,
                 method,
                 context.getNameLocation(node),
-                "UserManager.getUserInfo() is slow. " + "Use UserTracker.getUserInfo() instead."
+                "Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`"
             )
         }
     }
@@ -72,11 +71,13 @@
         val ISSUE_SLOW_USER_ID_QUERY: Issue =
             Issue.create(
                 id = "SlowUserIdQuery",
-                briefDescription = "User ID queried using ActivityManager instead of UserTracker.",
+                briefDescription = "User ID queried using ActivityManager",
                 explanation =
-                    "ActivityManager.getCurrentUser() makes a binder call and is slow. " +
-                        "Instead, inject a UserTracker and call UserTracker.getUserId(). For " +
-                        "more info, see: http://go/multi-user-in-systemui-slides",
+                    """
+                    `ActivityManager.getCurrentUser()` uses a blocking binder call and is slow. \
+                    Instead, inject a `UserTracker` and call `UserTracker.getUserId()`.
+                    """,
+                moreInfo = "http://go/multi-user-in-systemui-slides",
                 category = Category.PERFORMANCE,
                 priority = 8,
                 severity = Severity.WARNING,
@@ -88,11 +89,13 @@
         val ISSUE_SLOW_USER_INFO_QUERY: Issue =
             Issue.create(
                 id = "SlowUserInfoQuery",
-                briefDescription = "User info queried using UserManager instead of UserTracker.",
+                briefDescription = "User info queried using UserManager",
                 explanation =
-                    "UserManager.getUserInfo() makes a binder call and is slow. " +
-                        "Instead, inject a UserTracker and call UserTracker.getUserInfo(). For " +
-                        "more info, see: http://go/multi-user-in-systemui-slides",
+                    """
+                    `UserManager.getUserInfo()` uses a blocking binder call and is slow. \
+                    Instead, inject a `UserTracker` and call `UserTracker.getUserInfo()`.
+                    """,
+                moreInfo = "http://go/multi-user-in-systemui-slides",
                 category = Category.PERFORMANCE,
                 priority = 8,
                 severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
index a584894..4eeeb85 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
@@ -47,7 +47,7 @@
                     ISSUE,
                     referenced,
                     context.getNameLocation(referenced),
-                    "Usage of Config.HARDWARE is highly encouraged."
+                    "Replace software bitmap with `Config.HARDWARE`"
             )
         }
     }
@@ -56,12 +56,12 @@
         @JvmField
         val ISSUE: Issue =
             Issue.create(
-                id = "SoftwareBitmapDetector",
-                briefDescription = "Software bitmap detected. Please use Config.HARDWARE instead.",
-                explanation =
-                "Software bitmaps occupy twice as much memory, when compared to Config.HARDWARE. " +
-                        "In case you need to manipulate the pixels, please consider to either use" +
-                        "a shader (encouraged), or a short lived software bitmap.",
+                id = "SoftwareBitmap",
+                briefDescription = "Software bitmap",
+                explanation = """
+                        Software bitmaps occupy twice as much memory as `Config.HARDWARE` bitmaps \
+                        do. However, hardware bitmaps are read-only. If you need to manipulate the \
+                        pixels, use a shader (preferably) or a short lived software bitmap.""",
                 category = Category.PERFORMANCE,
                 priority = 8,
                 severity = Severity.WARNING,
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index 312810b..cf7c1b5 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -28,11 +28,11 @@
 
     override val issues: List<Issue>
         get() = listOf(
-                BindServiceViaContextDetector.ISSUE,
+                BindServiceOnMainThreadDetector.ISSUE,
                 BroadcastSentViaContextDetector.ISSUE,
                 SlowUserQueryDetector.ISSUE_SLOW_USER_ID_QUERY,
                 SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY,
-                GetMainLooperViaContextDetector.ISSUE,
+                NonInjectedMainThreadDetector.ISSUE,
                 RegisterReceiverViaContextDetector.ISSUE,
                 SoftwareBitmapDetector.ISSUE,
                 NonInjectedServiceDetector.ISSUE,
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
index 26bd8d0..486af9d 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
@@ -16,16 +16,21 @@
 
 package com.android.internal.systemui.lint
 
+import com.android.annotations.NonNull
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+import org.intellij.lang.annotations.Language
+
+@Suppress("UnstableApiUsage")
+@NonNull
+private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(source).indented()
 
 /*
  * This file contains stubs of framework APIs and System UI classes for testing purposes only. The
  * stubs are not used in the lint detectors themselves.
  */
-@Suppress("UnstableApiUsage")
 internal val androidStubs =
     arrayOf(
-        java(
+        indentedJava(
             """
 package android.app;
 
@@ -34,7 +39,16 @@
 }
 """
         ),
-        java(
+        indentedJava(
+            """
+package android.accounts;
+
+public class AccountManager {
+    public static AccountManager get(Context context) { return null; }
+}
+"""
+        ),
+        indentedJava(
             """
 package android.os;
 import android.content.pm.UserInfo;
@@ -45,39 +59,39 @@
 }
 """
         ),
-        java("""
+        indentedJava("""
 package android.annotation;
 
 public @interface UserIdInt {}
 """),
-        java("""
+        indentedJava("""
 package android.content.pm;
 
 public class UserInfo {}
 """),
-        java("""
+        indentedJava("""
 package android.os;
 
 public class Looper {}
 """),
-        java("""
+        indentedJava("""
 package android.os;
 
 public class Handler {}
 """),
-        java("""
+        indentedJava("""
 package android.content;
 
 public class ServiceConnection {}
 """),
-        java("""
+        indentedJava("""
 package android.os;
 
 public enum UserHandle {
     ALL
 }
 """),
-        java(
+        indentedJava(
             """
 package android.content;
 import android.os.UserHandle;
@@ -108,7 +122,7 @@
 }
 """
         ),
-        java(
+        indentedJava(
             """
 package android.app;
 import android.content.Context;
@@ -116,7 +130,7 @@
 public class Activity extends Context {}
 """
         ),
-        java(
+        indentedJava(
             """
 package android.graphics;
 
@@ -132,17 +146,17 @@
 }
 """
         ),
-        java("""
+        indentedJava("""
 package android.content;
 
 public class BroadcastReceiver {}
 """),
-        java("""
+        indentedJava("""
 package android.content;
 
 public class IntentFilter {}
 """),
-        java(
+        indentedJava(
             """
 package com.android.systemui.settings;
 import android.content.pm.UserInfo;
@@ -153,4 +167,23 @@
 }
 """
         ),
+        indentedJava(
+            """
+package androidx.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+@Retention(SOURCE)
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
+public @interface WorkerThread {
+}
+"""
+        ),
     )
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
new file mode 100644
index 0000000..6ae8fd3
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class BindServiceOnMainThreadDetectorTest : LintDetectorTest() {
+
+    override fun getDetector(): Detector = BindServiceOnMainThreadDetector()
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+    override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE)
+
+    @Test
+    fun testBindService() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+                    import android.content.Context;
+
+                    public class TestClass {
+                        public void bind(Context context) {
+                          Intent intent = new Intent(Intent.ACTION_VIEW);
+                          context.bindService(intent, null, 0);
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BindServiceOnMainThreadDetector.ISSUE)
+            .run()
+            .expect(
+                """
+                src/test/pkg/TestClass.java:7: Warning: This method should be annotated with @WorkerThread because it calls bindService [BindServiceOnMainThread]
+                      context.bindService(intent, null, 0);
+                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+    }
+
+    @Test
+    fun testBindServiceAsUser() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+                    import android.content.Context;
+                    import android.os.UserHandle;
+
+                    public class TestClass {
+                        public void bind(Context context) {
+                          Intent intent = new Intent(Intent.ACTION_VIEW);
+                          context.bindServiceAsUser(intent, null, 0, UserHandle.ALL);
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BindServiceOnMainThreadDetector.ISSUE)
+            .run()
+            .expect(
+                """
+                src/test/pkg/TestClass.java:8: Warning: This method should be annotated with @WorkerThread because it calls bindServiceAsUser [BindServiceOnMainThread]
+                      context.bindServiceAsUser(intent, null, 0, UserHandle.ALL);
+                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+    }
+
+    @Test
+    fun testUnbindService() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+                    import android.content.Context;
+                    import android.content.ServiceConnection;
+
+                    public class TestClass {
+                        public void unbind(Context context, ServiceConnection connection) {
+                          context.unbindService(connection);
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BindServiceOnMainThreadDetector.ISSUE)
+            .run()
+            .expect(
+                """
+                src/test/pkg/TestClass.java:7: Warning: This method should be annotated with @WorkerThread because it calls unbindService [BindServiceOnMainThread]
+                      context.unbindService(connection);
+                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+    }
+
+    @Test
+    fun testWorkerMethod() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+                    import android.content.Context;
+                    import android.content.ServiceConnection;
+                    import androidx.annotation.WorkerThread;
+
+                    public class TestClass {
+                        @WorkerThread
+                        public void unbind(Context context, ServiceConnection connection) {
+                          context.unbindService(connection);
+                        }
+                    }
+
+                    public class ChildTestClass extends TestClass {
+                        @Override
+                        public void unbind(Context context, ServiceConnection connection) {
+                          context.unbindService(connection);
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BindServiceOnMainThreadDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun testWorkerClass() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+                    import android.content.Context;
+                    import android.content.ServiceConnection;
+                    import androidx.annotation.WorkerThread;
+
+                    @WorkerThread
+                    public class TestClass {
+                        public void unbind(Context context, ServiceConnection connection) {
+                          context.unbindService(connection);
+                        }
+                    }
+
+                    public class ChildTestClass extends TestClass {
+                        @Override
+                        public void unbind(Context context, ServiceConnection connection) {
+                          context.unbindService(connection);
+                        }
+
+                        public void bind(Context context, ServiceConnection connection) {
+                          context.bind(connection);
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BindServiceOnMainThreadDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    private val stubs = androidStubs
+}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
deleted file mode 100644
index 564afcb..0000000
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceViaContextDetectorTest.kt
+++ /dev/null
@@ -1,116 +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.internal.systemui.lint
-
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Issue
-import org.junit.Test
-
-@Suppress("UnstableApiUsage")
-class BindServiceViaContextDetectorTest : LintDetectorTest() {
-
-    override fun getDetector(): Detector = BindServiceViaContextDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
-
-    override fun getIssues(): List<Issue> = listOf(BindServiceViaContextDetector.ISSUE)
-
-    private val explanation = "Binding or unbinding services are synchronous calls"
-
-    @Test
-    fun testBindService() {
-        lint()
-            .files(
-                TestFiles.java(
-                        """
-                    package test.pkg;
-                    import android.content.Context;
-
-                    public class TestClass1 {
-                        public void bind(Context context) {
-                          Intent intent = new Intent(Intent.ACTION_VIEW);
-                          context.bindService(intent, null, 0);
-                        }
-                    }
-                """
-                    )
-                    .indented(),
-                *stubs
-            )
-            .issues(BindServiceViaContextDetector.ISSUE)
-            .run()
-            .expectWarningCount(1)
-            .expectContains(explanation)
-    }
-
-    @Test
-    fun testBindServiceAsUser() {
-        lint()
-            .files(
-                TestFiles.java(
-                        """
-                    package test.pkg;
-                    import android.content.Context;
-                    import android.os.UserHandle;
-
-                    public class TestClass1 {
-                        public void bind(Context context) {
-                          Intent intent = new Intent(Intent.ACTION_VIEW);
-                          context.bindServiceAsUser(intent, null, 0, UserHandle.ALL);
-                        }
-                    }
-                """
-                    )
-                    .indented(),
-                *stubs
-            )
-            .issues(BindServiceViaContextDetector.ISSUE)
-            .run()
-            .expectWarningCount(1)
-            .expectContains(explanation)
-    }
-
-    @Test
-    fun testUnbindService() {
-        lint()
-            .files(
-                TestFiles.java(
-                        """
-                    package test.pkg;
-                    import android.content.Context;
-                    import android.content.ServiceConnection;
-
-                    public class TestClass1 {
-                        public void unbind(Context context, ServiceConnection connection) {
-                          context.unbindService(connection);
-                        }
-                    }
-                """
-                    )
-                    .indented(),
-                *stubs
-            )
-            .issues(BindServiceViaContextDetector.ISSUE)
-            .run()
-            .expectWarningCount(1)
-            .expectContains(explanation)
-    }
-
-    private val stubs = androidStubs
-}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
index 06aee8e..7d42280 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
@@ -41,7 +41,7 @@
                     package test.pkg;
                     import android.content.Context;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void send(Context context) {
                           Intent intent = new Intent(Intent.ACTION_VIEW);
                           context.sendBroadcast(intent);
@@ -54,10 +54,13 @@
             )
             .issues(BroadcastSentViaContextDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains(
-                "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
-                    "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+            .expect(
+                """
+                src/test/pkg/TestClass.java:7: Warning: Context.sendBroadcast() should be replaced with BroadcastSender.sendBroadcast() [BroadcastSentViaContext]
+                      context.sendBroadcast(intent);
+                              ~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
             )
     }
 
@@ -71,7 +74,7 @@
                     import android.content.Context;
                     import android.os.UserHandle;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void send(Context context) {
                           Intent intent = new Intent(Intent.ACTION_VIEW);
                           context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
@@ -84,10 +87,13 @@
             )
             .issues(BroadcastSentViaContextDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains(
-                "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
-                    "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+            .expect(
+                """
+                src/test/pkg/TestClass.java:8: Warning: Context.sendBroadcastAsUser() should be replaced with BroadcastSender.sendBroadcastAsUser() [BroadcastSentViaContext]
+                      context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
+                              ~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
             )
     }
 
@@ -101,7 +107,7 @@
                     import android.app.Activity;
                     import android.os.UserHandle;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void send(Activity activity) {
                           Intent intent = new Intent(Intent.ACTION_VIEW);
                           activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
@@ -115,14 +121,44 @@
             )
             .issues(BroadcastSentViaContextDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains(
-                "Please don't call sendBroadcast/sendBroadcastAsUser directly on " +
-                    "Context, use com.android.systemui.broadcast.BroadcastSender instead."
+            .expect(
+                """
+                src/test/pkg/TestClass.java:8: Warning: Context.sendBroadcastAsUser() should be replaced with BroadcastSender.sendBroadcastAsUser() [BroadcastSentViaContext]
+                      activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
+                               ~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
             )
     }
 
     @Test
+    fun testSendBroadcastInBroadcastSender() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package com.android.systemui.broadcast;
+                    import android.app.Activity;
+                    import android.os.UserHandle;
+
+                    public class BroadcastSender {
+                        public void send(Activity activity) {
+                          Intent intent = new Intent(Intent.ACTION_VIEW);
+                          activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
+                        }
+
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BroadcastSentViaContextDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
     fun testNoopIfNoCall() {
         lint()
             .files(
@@ -131,7 +167,7 @@
                     package test.pkg;
                     import android.content.Context;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void sendBroadcast() {
                           Intent intent = new Intent(Intent.ACTION_VIEW);
                           context.startActivity(intent);
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
similarity index 64%
rename from packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
rename to packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
index c55f399..c468af8 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/GetMainLooperViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
@@ -24,14 +24,12 @@
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class GetMainLooperViaContextDetectorTest : LintDetectorTest() {
+class NonInjectedMainThreadDetectorTest : LintDetectorTest() {
 
-    override fun getDetector(): Detector = GetMainLooperViaContextDetector()
+    override fun getDetector(): Detector = NonInjectedMainThreadDetector()
     override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
-    override fun getIssues(): List<Issue> = listOf(GetMainLooperViaContextDetector.ISSUE)
-
-    private val explanation = "Please inject a @Main Executor instead."
+    override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE)
 
     @Test
     fun testGetMainThreadHandler() {
@@ -43,7 +41,7 @@
                     import android.content.Context;
                     import android.os.Handler;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void test(Context context) {
                           Handler mainThreadHandler = context.getMainThreadHandler();
                         }
@@ -53,10 +51,16 @@
                     .indented(),
                 *stubs
             )
-            .issues(GetMainLooperViaContextDetector.ISSUE)
+            .issues(NonInjectedMainThreadDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains(explanation)
+            .expect(
+                """
+                src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread]
+                      Handler mainThreadHandler = context.getMainThreadHandler();
+                                                          ~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
     }
 
     @Test
@@ -69,7 +73,7 @@
                     import android.content.Context;
                     import android.os.Looper;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void test(Context context) {
                           Looper mainLooper = context.getMainLooper();
                         }
@@ -79,10 +83,16 @@
                     .indented(),
                 *stubs
             )
-            .issues(GetMainLooperViaContextDetector.ISSUE)
+            .issues(NonInjectedMainThreadDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains(explanation)
+            .expect(
+                """
+                src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread]
+                      Looper mainLooper = context.getMainLooper();
+                                                  ~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
     }
 
     @Test
@@ -95,7 +105,7 @@
                     import android.content.Context;
                     import java.util.concurrent.Executor;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void test(Context context) {
                           Executor mainExecutor = context.getMainExecutor();
                         }
@@ -105,10 +115,16 @@
                     .indented(),
                 *stubs
             )
-            .issues(GetMainLooperViaContextDetector.ISSUE)
+            .issues(NonInjectedMainThreadDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains(explanation)
+            .expect(
+                """
+                src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread]
+                      Executor mainExecutor = context.getMainExecutor();
+                                                      ~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
     }
 
     private val stubs = androidStubs
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
index 6b9f88f..c83a35b 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
@@ -39,7 +39,7 @@
                         package test.pkg;
                         import android.content.Context;
 
-                        public class TestClass1 {
+                        public class TestClass {
                             public void getSystemServiceWithoutDagger(Context context) {
                                 context.getSystemService("user");
                             }
@@ -51,8 +51,14 @@
             )
             .issues(NonInjectedServiceDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains("Use @Inject to get the handle")
+            .expect(
+                """
+                src/test/pkg/TestClass.java:6: Warning: Use @Inject to get system-level service handles instead of Context.getSystemService() [NonInjectedService]
+                        context.getSystemService("user");
+                                ~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
     }
 
     @Test
@@ -65,7 +71,7 @@
                         import android.content.Context;
                         import android.os.UserManager;
 
-                        public class TestClass2 {
+                        public class TestClass {
                             public void getSystemServiceWithoutDagger(Context context) {
                                 context.getSystemService(UserManager.class);
                             }
@@ -77,8 +83,46 @@
             )
             .issues(NonInjectedServiceDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains("Use @Inject to get the handle")
+            .expect(
+                """
+                src/test/pkg/TestClass.java:7: Warning: Use @Inject to get system-level service handles instead of Context.getSystemService() [NonInjectedService]
+                        context.getSystemService(UserManager.class);
+                                ~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+    }
+
+    @Test
+    fun testGetAccountManager() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                        package test.pkg;
+                        import android.content.Context;
+                        import android.accounts.AccountManager;
+
+                        public class TestClass {
+                            public void getSystemServiceWithoutDagger(Context context) {
+                                AccountManager.get(context);
+                            }
+                        }
+                        """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(NonInjectedServiceDetector.ISSUE)
+            .run()
+            .expect(
+                """
+                src/test/pkg/TestClass.java:7: Warning: Replace AccountManager.get() with an injected instance of AccountManager [NonInjectedService]
+                        AccountManager.get(context);
+                                       ~~~
+                0 errors, 1 warnings
+                """
+            )
     }
 
     private val stubs = androidStubs
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
index 802ceba..ebcddeb 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
@@ -31,8 +31,6 @@
 
     override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE)
 
-    private val explanation = "BroadcastReceivers should be registered via BroadcastDispatcher."
-
     @Test
     fun testRegisterReceiver() {
         lint()
@@ -44,7 +42,7 @@
                     import android.content.Context;
                     import android.content.IntentFilter;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void bind(Context context, BroadcastReceiver receiver,
                             IntentFilter filter) {
                           context.registerReceiver(receiver, filter, 0);
@@ -57,8 +55,14 @@
             )
             .issues(RegisterReceiverViaContextDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains(explanation)
+            .expect(
+                """
+                src/test/pkg/TestClass.java:9: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext]
+                      context.registerReceiver(receiver, filter, 0);
+                              ~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
     }
 
     @Test
@@ -74,7 +78,7 @@
                     import android.os.Handler;
                     import android.os.UserHandle;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void bind(Context context, BroadcastReceiver receiver,
                             IntentFilter filter, Handler handler) {
                           context.registerReceiverAsUser(receiver, UserHandle.ALL, filter,
@@ -88,8 +92,14 @@
             )
             .issues(RegisterReceiverViaContextDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains(explanation)
+            .expect(
+                """
+                src/test/pkg/TestClass.java:11: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext]
+                      context.registerReceiverAsUser(receiver, UserHandle.ALL, filter,
+                              ~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
     }
 
     @Test
@@ -105,7 +115,7 @@
                     import android.os.Handler;
                     import android.os.UserHandle;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void bind(Context context, BroadcastReceiver receiver,
                             IntentFilter filter, Handler handler) {
                           context.registerReceiverForAllUsers(receiver, filter, "permission",
@@ -119,8 +129,14 @@
             )
             .issues(RegisterReceiverViaContextDetector.ISSUE)
             .run()
-            .expectWarningCount(1)
-            .expectContains(explanation)
+            .expect(
+                """
+                src/test/pkg/TestClass.java:11: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext]
+                      context.registerReceiverForAllUsers(receiver, filter, "permission",
+                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
     }
 
     private val stubs = androidStubs
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
index e265837..b03a11c 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
@@ -44,7 +44,7 @@
                         package test.pkg;
                         import android.app.ActivityManager;
 
-                        public class TestClass1 {
+                        public class TestClass {
                             public void slewlyGetCurrentUser() {
                                 ActivityManager.getCurrentUser();
                             }
@@ -59,10 +59,13 @@
                 SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY
             )
             .run()
-            .expectWarningCount(1)
-            .expectContains(
-                "ActivityManager.getCurrentUser() is slow. " +
-                    "Use UserTracker.getUserId() instead."
+            .expect(
+                """
+                src/test/pkg/TestClass.java:6: Warning: Use UserTracker.getUserId() instead of ActivityManager.getCurrentUser() [SlowUserIdQuery]
+                        ActivityManager.getCurrentUser();
+                                        ~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
             )
     }
 
@@ -75,7 +78,7 @@
                         package test.pkg;
                         import android.os.UserManager;
 
-                        public class TestClass2 {
+                        public class TestClass {
                             public void slewlyGetUserInfo(UserManager userManager) {
                                 userManager.getUserInfo();
                             }
@@ -90,9 +93,13 @@
                 SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY
             )
             .run()
-            .expectWarningCount(1)
-            .expectContains(
-                "UserManager.getUserInfo() is slow. " + "Use UserTracker.getUserInfo() instead."
+            .expect(
+                """
+                src/test/pkg/TestClass.java:6: Warning: Use UserTracker.getUserInfo() instead of UserManager.getUserInfo() [SlowUserInfoQuery]
+                        userManager.getUserInfo();
+                                    ~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
             )
     }
 
@@ -105,7 +112,7 @@
                         package test.pkg;
                         import com.android.systemui.settings.UserTracker;
 
-                        public class TestClass3 {
+                        public class TestClass {
                             public void quicklyGetUserId(UserTracker userTracker) {
                                 userTracker.getUserId();
                             }
@@ -132,7 +139,7 @@
                         package test.pkg;
                         import com.android.systemui.settings.UserTracker;
 
-                        public class TestClass4 {
+                        public class TestClass {
                             public void quicklyGetUserId(UserTracker userTracker) {
                                 userTracker.getUserInfo();
                             }
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
index fd6ab09..fb6537e 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
@@ -31,8 +31,6 @@
 
     override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE)
 
-    private val explanation = "Usage of Config.HARDWARE is highly encouraged."
-
     @Test
     fun testSoftwareBitmap() {
         lint()
@@ -41,7 +39,7 @@
                         """
                     import android.graphics.Bitmap;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void test() {
                           Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565);
                           Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
@@ -54,8 +52,17 @@
             )
             .issues(SoftwareBitmapDetector.ISSUE)
             .run()
-            .expectWarningCount(2)
-            .expectContains(explanation)
+            .expect(
+                """
+                src/android/graphics/Bitmap.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
+                        ARGB_8888,
+                        ~~~~~~~~~
+                src/android/graphics/Bitmap.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
+                        RGB_565,
+                        ~~~~~~~
+                0 errors, 2 warnings
+                """
+            )
     }
 
     @Test
@@ -66,7 +73,7 @@
                         """
                     import android.graphics.Bitmap;
 
-                    public class TestClass1 {
+                    public class TestClass {
                         public void test() {
                           Bitmap.createBitmap(300, 300, Bitmap.Config.HARDWARE);
                         }
@@ -78,7 +85,7 @@
             )
             .issues(SoftwareBitmapDetector.ISSUE)
             .run()
-            .expectWarningCount(0)
+            .expectClean()
     }
 
     private val stubs = androidStubs
diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp
index 4cfe392..fbdb526 100644
--- a/packages/SystemUI/compose/core/Android.bp
+++ b/packages/SystemUI/compose/core/Android.bp
@@ -30,8 +30,11 @@
     ],
 
     static_libs: [
+        "SystemUIAnimationLib",
+
         "androidx.compose.runtime_runtime",
         "androidx.compose.material3_material3",
+        "androidx.savedstate_savedstate",
     ],
 
     kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt
new file mode 100644
index 0000000..8f9a4da
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt
@@ -0,0 +1,363 @@
+/*
+ * 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.animation
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroupOverlay
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.Density
+import androidx.lifecycle.ViewTreeLifecycleOwner
+import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.savedstate.ViewTreeSavedStateRegistryOwner
+import com.android.systemui.animation.LaunchAnimator
+import kotlin.math.min
+
+/**
+ * Create an expandable shape that can launch into an Activity or a Dialog.
+ *
+ * Example:
+ * ```
+ *    Expandable(
+ *      color = MaterialTheme.colorScheme.primary,
+ *      shape = RoundedCornerShape(16.dp),
+ *    ) { controller ->
+ *      Row(
+ *        Modifier
+ *          // For activities:
+ *          .clickable { activityStarter.startActivity(intent, controller.forActivity()) }
+ *
+ *          // For dialogs:
+ *          .clickable { dialogLaunchAnimator.show(dialog, controller.forDialog()) }
+ *      ) { ... }
+ *    }
+ * ```
+ *
+ * @sample com.android.systemui.compose.gallery.ActivityLaunchScreen
+ * @sample com.android.systemui.compose.gallery.DialogLaunchScreen
+ */
+@Composable
+fun Expandable(
+    color: Color,
+    shape: Shape,
+    modifier: Modifier = Modifier,
+    contentColor: Color = contentColorFor(color),
+    content: @Composable (ExpandableController) -> Unit,
+) {
+    Expandable(
+        rememberExpandableController(color, shape, contentColor),
+        modifier,
+        content,
+    )
+}
+
+/**
+ * Create an expandable shape that can launch into an Activity or a Dialog.
+ *
+ * This overload can be used in cases where you need to create the [ExpandableController] before
+ * composing this [Expandable], for instance if something outside of this Expandable can trigger a
+ * launch animation
+ *
+ * Example:
+ * ```
+ *    // The controller that you can use to trigger the animations from anywhere.
+ *    val controller =
+ *        rememberExpandableController(
+ *          color = MaterialTheme.colorScheme.primary,
+ *          shape = RoundedCornerShape(16.dp),
+ *        )
+ *
+ *    Expandable(controller) {
+ *       ...
+ *    }
+ * ```
+ *
+ * @sample com.android.systemui.compose.gallery.ActivityLaunchScreen
+ * @sample com.android.systemui.compose.gallery.DialogLaunchScreen
+ */
+@Composable
+fun Expandable(
+    controller: ExpandableController,
+    modifier: Modifier = Modifier,
+    content: @Composable (ExpandableController) -> Unit,
+) {
+    val controller = controller as ExpandableControllerImpl
+    val color = controller.color
+    val contentColor = controller.contentColor
+    val shape = controller.shape
+
+    // TODO(b/230830644): Use movableContentOf to preserve the content state instead once the
+    // Compose libraries have been updated and include aosp/2163631.
+    val wrappedContent =
+        @Composable { controller: ExpandableController ->
+            CompositionLocalProvider(
+                LocalContentColor provides contentColor,
+            ) {
+                content(controller)
+            }
+        }
+
+    val thisExpandableSize by remember {
+        derivedStateOf { controller.boundsInComposeViewRoot.value.size }
+    }
+
+    // Make sure we don't read animatorState directly here to avoid recomposition every time the
+    // state changes (i.e. every frame of the animation).
+    val isAnimating by remember {
+        derivedStateOf {
+            controller.animatorState.value != null && controller.overlay.value != null
+        }
+    }
+
+    when {
+        isAnimating -> {
+            // Don't compose the movable content during the animation, as it should be composed only
+            // once at all times. We make this spacer exactly the same size as this Expandable when
+            // it is visible.
+            Spacer(
+                modifier
+                    .clip(shape)
+                    .requiredSize(with(controller.density) { thisExpandableSize.toDpSize() })
+            )
+
+            // The content and its animated background in the overlay. We draw it only when we are
+            // animating.
+            AnimatedContentInOverlay(
+                color,
+                thisExpandableSize,
+                controller.animatorState,
+                controller.overlay.value
+                    ?: error("AnimatedContentInOverlay shouldn't be composed with null overlay."),
+                controller,
+                wrappedContent,
+                controller.composeViewRoot,
+                { controller.currentComposeViewInOverlay.value = it },
+                controller.density,
+            )
+        }
+        controller.isDialogShowing.value -> {
+            Box(
+                modifier
+                    .drawWithContent { /* Don't draw anything when the dialog is shown. */}
+                    .onGloballyPositioned {
+                        controller.boundsInComposeViewRoot.value = it.boundsInRoot()
+                    }
+            ) { wrappedContent(controller) }
+        }
+        else -> {
+            Box(
+                modifier.clip(shape).background(color, shape).onGloballyPositioned {
+                    controller.boundsInComposeViewRoot.value = it.boundsInRoot()
+                }
+            ) { wrappedContent(controller) }
+        }
+    }
+}
+
+/** Draw [content] in [overlay] while respecting its screen position given by [animatorState]. */
+@Composable
+private fun AnimatedContentInOverlay(
+    color: Color,
+    sizeInOriginalLayout: Size,
+    animatorState: State<LaunchAnimator.State?>,
+    overlay: ViewGroupOverlay,
+    controller: ExpandableController,
+    content: @Composable (ExpandableController) -> Unit,
+    composeViewRoot: View,
+    onOverlayComposeViewChanged: (View?) -> Unit,
+    density: Density,
+) {
+    val compositionContext = rememberCompositionContext()
+    val context = LocalContext.current
+
+    // Create the ComposeView and force its content composition so that the movableContent is
+    // composed exactly once when we start animating.
+    val composeViewInOverlay =
+        remember(context, density) {
+            val startWidth = sizeInOriginalLayout.width
+            val startHeight = sizeInOriginalLayout.height
+            val contentModifier =
+                Modifier
+                    // Draw the content with the same size as it was at the start of the animation
+                    // so that its content is laid out exactly the same way.
+                    .requiredSize(with(density) { sizeInOriginalLayout.toDpSize() })
+                    .drawWithContent {
+                        val animatorState = animatorState.value ?: return@drawWithContent
+
+                        // Scale the content with the background while keeping its aspect ratio.
+                        val widthRatio =
+                            if (startWidth != 0f) {
+                                animatorState.width.toFloat() / startWidth
+                            } else {
+                                1f
+                            }
+                        val heightRatio =
+                            if (startHeight != 0f) {
+                                animatorState.height.toFloat() / startHeight
+                            } else {
+                                1f
+                            }
+                        val scale = min(widthRatio, heightRatio)
+                        scale(scale) { this@drawWithContent.drawContent() }
+                    }
+
+            val composeView =
+                ComposeView(context).apply {
+                    setContent {
+                        Box(
+                            Modifier.fillMaxSize().drawWithContent {
+                                val animatorState = animatorState.value ?: return@drawWithContent
+                                if (!animatorState.visible) {
+                                    return@drawWithContent
+                                }
+
+                                val topRadius = animatorState.topCornerRadius
+                                val bottomRadius = animatorState.bottomCornerRadius
+                                if (topRadius == bottomRadius) {
+                                    // Shortcut to avoid Outline calculation and allocation.
+                                    val cornerRadius = CornerRadius(topRadius)
+                                    drawRoundRect(color, cornerRadius = cornerRadius)
+                                } else {
+                                    val shape =
+                                        RoundedCornerShape(
+                                            topStart = topRadius,
+                                            topEnd = topRadius,
+                                            bottomStart = bottomRadius,
+                                            bottomEnd = bottomRadius,
+                                        )
+                                    val outline = shape.createOutline(size, layoutDirection, this)
+                                    drawOutline(outline, color = color)
+                                }
+
+                                drawContent()
+                            },
+                            // We center the content in the expanding container.
+                            contentAlignment = Alignment.Center,
+                        ) {
+                            Box(contentModifier) { content(controller) }
+                        }
+                    }
+                }
+
+            // Set the owners.
+            val overlayViewGroup =
+                getOverlayViewGroup(
+                    context,
+                    overlay,
+                )
+            ViewTreeLifecycleOwner.set(
+                overlayViewGroup,
+                ViewTreeLifecycleOwner.get(composeViewRoot),
+            )
+            ViewTreeViewModelStoreOwner.set(
+                overlayViewGroup,
+                ViewTreeViewModelStoreOwner.get(composeViewRoot),
+            )
+            ViewTreeSavedStateRegistryOwner.set(
+                overlayViewGroup,
+                ViewTreeSavedStateRegistryOwner.get(composeViewRoot),
+            )
+
+            composeView.setParentCompositionContext(compositionContext)
+
+            composeView
+        }
+
+    DisposableEffect(overlay, composeViewInOverlay) {
+        // Add the ComposeView to the overlay.
+        overlay.add(composeViewInOverlay)
+
+        val startState =
+            animatorState.value
+                ?: throw IllegalStateException(
+                    "AnimatedContentInOverlay shouldn't be composed with null animatorState."
+                )
+        measureAndLayoutComposeViewInOverlay(composeViewInOverlay, startState)
+        onOverlayComposeViewChanged(composeViewInOverlay)
+
+        onDispose {
+            composeViewInOverlay.disposeComposition()
+            overlay.remove(composeViewInOverlay)
+            onOverlayComposeViewChanged(null)
+        }
+    }
+}
+
+internal fun measureAndLayoutComposeViewInOverlay(
+    view: View,
+    state: LaunchAnimator.State,
+) {
+    val exactWidth = state.width
+    val exactHeight = state.height
+    view.measure(
+        View.MeasureSpec.makeSafeMeasureSpec(exactWidth, View.MeasureSpec.EXACTLY),
+        View.MeasureSpec.makeSafeMeasureSpec(exactHeight, View.MeasureSpec.EXACTLY),
+    )
+
+    val parent = view.parent as ViewGroup
+    val parentLocation = parent.locationOnScreen
+    val offsetX = parentLocation[0]
+    val offsetY = parentLocation[1]
+    view.layout(
+        state.left - offsetX,
+        state.top - offsetY,
+        state.right - offsetX,
+        state.bottom - offsetY,
+    )
+}
+
+// TODO(b/230830644): Add hidden API to ViewGroupOverlay to access this ViewGroup directly?
+private fun getOverlayViewGroup(context: Context, overlay: ViewGroupOverlay): ViewGroup {
+    val view = View(context)
+    overlay.add(view)
+    var current = view.parent
+    while (current.parent != null) {
+        current = current.parent
+    }
+    overlay.remove(view)
+    return current as ViewGroup
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
new file mode 100644
index 0000000..065c314
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
@@ -0,0 +1,306 @@
+/*
+ * 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.animation
+
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroupOverlay
+import android.view.ViewRootImpl
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.LaunchAnimator
+import kotlin.math.roundToInt
+
+/** A controller that can control animated launches. */
+interface ExpandableController {
+    /** Create an [ActivityLaunchAnimator.Controller] to animate into an Activity. */
+    fun forActivity(): ActivityLaunchAnimator.Controller
+
+    /** Create a [DialogLaunchAnimator.Controller] to animate into a Dialog. */
+    fun forDialog(): DialogLaunchAnimator.Controller
+}
+
+/**
+ * Create an [ExpandableController] to control an [Expandable]. This is useful if you need to create
+ * the controller before the [Expandable], for instance to handle clicks outside of the Expandable
+ * that would still trigger a dialog/activity launch animation.
+ */
+@Composable
+fun rememberExpandableController(
+    color: Color,
+    shape: Shape,
+    contentColor: Color = contentColorFor(color),
+): ExpandableController {
+    val composeViewRoot = LocalView.current
+    val density = LocalDensity.current
+    val layoutDirection = LocalLayoutDirection.current
+
+    // The current animation state, if we are currently animating a dialog or activity.
+    val animatorState = remember { mutableStateOf<LaunchAnimator.State?>(null) }
+
+    // Whether a dialog controlled by this ExpandableController is currently showing.
+    val isDialogShowing = remember { mutableStateOf(false) }
+
+    // The overlay in which we should animate the launch.
+    val overlay = remember { mutableStateOf<ViewGroupOverlay?>(null) }
+
+    // The current [ComposeView] being animated in the [overlay], if any.
+    val currentComposeViewInOverlay = remember { mutableStateOf<View?>(null) }
+
+    // The bounds in [composeViewRoot] of the expandable controlled by this controller.
+    val boundsInComposeViewRoot = remember { mutableStateOf(Rect.Zero) }
+
+    // Whether this composable is still composed. We only do the dialog exit animation if this is
+    // true.
+    val isComposed = remember { mutableStateOf(true) }
+    DisposableEffect(Unit) { onDispose { isComposed.value = false } }
+
+    return remember(color, contentColor, shape, composeViewRoot, density, layoutDirection) {
+        ExpandableControllerImpl(
+            color,
+            contentColor,
+            shape,
+            composeViewRoot,
+            density,
+            animatorState,
+            isDialogShowing,
+            overlay,
+            currentComposeViewInOverlay,
+            boundsInComposeViewRoot,
+            layoutDirection,
+            isComposed,
+        )
+    }
+}
+
+internal class ExpandableControllerImpl(
+    internal val color: Color,
+    internal val contentColor: Color,
+    internal val shape: Shape,
+    internal val composeViewRoot: View,
+    internal val density: Density,
+    internal val animatorState: MutableState<LaunchAnimator.State?>,
+    internal val isDialogShowing: MutableState<Boolean>,
+    internal val overlay: MutableState<ViewGroupOverlay?>,
+    internal val currentComposeViewInOverlay: MutableState<View?>,
+    internal val boundsInComposeViewRoot: MutableState<Rect>,
+    private val layoutDirection: LayoutDirection,
+    private val isComposed: State<Boolean>,
+) : ExpandableController {
+    override fun forActivity(): ActivityLaunchAnimator.Controller {
+        return activityController()
+    }
+
+    override fun forDialog(): DialogLaunchAnimator.Controller {
+        return dialogController()
+    }
+
+    /**
+     * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog
+     * animation. This controller will:
+     * 1. Compute the start/end animation state using [boundsInComposeViewRoot] and the location of
+     * composeViewRoot on the screen.
+     * 2. Update [animatorState] with the current animation state if we are animating, or null
+     * otherwise.
+     */
+    private fun launchController(): LaunchAnimator.Controller {
+        return object : LaunchAnimator.Controller {
+            private val rootLocationOnScreen = intArrayOf(0, 0)
+
+            override var launchContainer: ViewGroup = composeViewRoot.rootView as ViewGroup
+
+            override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+                animatorState.value = null
+            }
+
+            override fun onLaunchAnimationProgress(
+                state: LaunchAnimator.State,
+                progress: Float,
+                linearProgress: Float
+            ) {
+                // We copy state given that it's always the same object that is mutated by
+                // ActivityLaunchAnimator.
+                animatorState.value =
+                    LaunchAnimator.State(
+                            state.top,
+                            state.bottom,
+                            state.left,
+                            state.right,
+                            state.topCornerRadius,
+                            state.bottomCornerRadius,
+                        )
+                        .apply { visible = state.visible }
+
+                // Force measure and layout the ComposeView in the overlay whenever the animation
+                // state changes.
+                currentComposeViewInOverlay.value?.let {
+                    measureAndLayoutComposeViewInOverlay(it, state)
+                }
+            }
+
+            override fun createAnimatorState(): LaunchAnimator.State {
+                val boundsInRoot = boundsInComposeViewRoot.value
+                val outline =
+                    shape.createOutline(
+                        Size(boundsInRoot.width, boundsInRoot.height),
+                        layoutDirection,
+                        density,
+                    )
+
+                val (topCornerRadius, bottomCornerRadius) =
+                    when (outline) {
+                        is Outline.Rectangle -> 0f to 0f
+                        is Outline.Rounded -> {
+                            val roundRect = outline.roundRect
+
+                            // TODO(b/230830644): Add better support different corner radii.
+                            val topCornerRadius =
+                                maxOf(
+                                    roundRect.topLeftCornerRadius.x,
+                                    roundRect.topLeftCornerRadius.y,
+                                    roundRect.topRightCornerRadius.x,
+                                    roundRect.topRightCornerRadius.y,
+                                )
+                            val bottomCornerRadius =
+                                maxOf(
+                                    roundRect.bottomLeftCornerRadius.x,
+                                    roundRect.bottomLeftCornerRadius.y,
+                                    roundRect.bottomRightCornerRadius.x,
+                                    roundRect.bottomRightCornerRadius.y,
+                                )
+
+                            topCornerRadius to bottomCornerRadius
+                        }
+                        else ->
+                            error(
+                                "ExpandableState only supports (rounded) rectangles at the " +
+                                    "moment."
+                            )
+                    }
+
+                val rootLocation = rootLocationOnScreen()
+                return LaunchAnimator.State(
+                    top = rootLocation.y.roundToInt(),
+                    bottom = (rootLocation.y + boundsInRoot.height).roundToInt(),
+                    left = rootLocation.x.roundToInt(),
+                    right = (rootLocation.x + boundsInRoot.width).roundToInt(),
+                    topCornerRadius = topCornerRadius,
+                    bottomCornerRadius = bottomCornerRadius,
+                )
+            }
+
+            private fun rootLocationOnScreen(): Offset {
+                composeViewRoot.getLocationOnScreen(rootLocationOnScreen)
+                val boundsInRoot = boundsInComposeViewRoot.value
+                val x = rootLocationOnScreen[0] + boundsInRoot.left
+                val y = rootLocationOnScreen[1] + boundsInRoot.top
+                return Offset(x, y)
+            }
+        }
+    }
+
+    /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */
+    private fun activityController(): ActivityLaunchAnimator.Controller {
+        val delegate = launchController()
+        return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate {
+            override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+                delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+                overlay.value = composeViewRoot.rootView.overlay as ViewGroupOverlay
+            }
+
+            override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+                delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+                overlay.value = null
+            }
+        }
+    }
+
+    private fun dialogController(): DialogLaunchAnimator.Controller {
+        return object : DialogLaunchAnimator.Controller {
+            override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl
+            override val sourceIdentity: Any = this@ExpandableControllerImpl
+
+            override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
+                val newOverlay = viewGroup.overlay as ViewGroupOverlay
+                if (newOverlay != overlay.value) {
+                    overlay.value = newOverlay
+                }
+            }
+
+            override fun stopDrawingInOverlay() {
+                if (overlay.value != null) {
+                    overlay.value = null
+                }
+            }
+
+            override fun createLaunchController(): LaunchAnimator.Controller {
+                val delegate = launchController()
+                return object : LaunchAnimator.Controller by delegate {
+                    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+                        delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+
+                        // Make sure we don't draw this expandable when the dialog is showing.
+                        isDialogShowing.value = true
+                    }
+                }
+            }
+
+            override fun createExitController(): LaunchAnimator.Controller {
+                val delegate = launchController()
+                return object : LaunchAnimator.Controller by delegate {
+                    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+                        delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+                        isDialogShowing.value = false
+                    }
+                }
+            }
+
+            override fun shouldAnimateExit(): Boolean = isComposed.value
+
+            override fun onExitAnimationCancelled() {
+                isDialogShowing.value = false
+            }
+
+            override fun jankConfigurationBuilder(
+                cuj: Int
+            ): InteractionJankMonitor.Configuration.Builder? {
+                // TODO(b/252723237): Add support for jank monitoring when animating from a
+                // Composable.
+                return null
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/docs/device-entry/doze.md b/packages/SystemUI/docs/device-entry/doze.md
index 6b6dce5..10bd367 100644
--- a/packages/SystemUI/docs/device-entry/doze.md
+++ b/packages/SystemUI/docs/device-entry/doze.md
@@ -1,5 +1,7 @@
 # Doze
 
+`Dozing` is a low-powered state of the device. If Always-on Display (AOD), pulsing, or wake-gestures are enabled, then the device will enter the `dozing` state after a user intent to turn off the screen (ie: power button) or the screen times out.
+
 Always-on Display (AOD) provides an alternative 'screen-off' experience. Instead, of completely turning the display off, it provides a distraction-free, glanceable experience for the phone in a low-powered mode. In this low-powered mode, the display will have a lower refresh rate and the UI should frequently shift its displayed contents in order to prevent burn-in. The recommended max on-pixel-ratio (OPR) is 5% to reduce battery consumption.
 
 ![ss-aod](./imgs/aod.png)
@@ -58,7 +60,7 @@
 Refer to the documentation in [DozeSuppressors][15] for more information.
 
 ## AOD burn-in and image retention
-Because AOD will show an image on the screen for an elogated period of time, AOD designs must take into consideration burn-in (leaving a permanent mark on the screen). Temporary burn-in is called image-retention.
+Because AOD will show an image on the screen for an elongated period of time, AOD designs must take into consideration burn-in (leaving a permanent mark on the screen). Temporary burn-in is called image-retention.
 
 To prevent burn-in, it is recommended to often shift UI on the screen. [DozeUi][17] schedules a call to dozeTimeTick every minute to request a shift in UI for all elements on AOD. The amount of shift can be determined by undergoing simulated AOD testing since this may vary depending on the display.
 
diff --git a/packages/SystemUI/docs/device-entry/glossary.md b/packages/SystemUI/docs/device-entry/glossary.md
index f3d12c2..7f19b16 100644
--- a/packages/SystemUI/docs/device-entry/glossary.md
+++ b/packages/SystemUI/docs/device-entry/glossary.md
@@ -2,38 +2,38 @@
 
 ## Keyguard
 
-| Term                         | Description |
-| :-----------:                | ----------- |
-| Keyguard, [keyguard.md][1]   | Coordinates the first experience when turning on the display of a device, as long as the user has not specified a security method of NONE. Consists of the lock screen and bouncer.|
-| Lock screen<br><br>![ss_aod](imgs/lockscreen.png)| The first screen available when turning on the display of a device, as long as the user has not specified a security method of NONE. On the lock screen, users can access:<ul><li>Quick Settings - users can swipe down from the top of the screen to interact with quick settings tiles</li><li>[Keyguard Status Bar][9] - This special status bar shows SIM related information and system icons.</li><li>Clock - uses the font specified at [clock.xml][8]. If the clock font supports variable weights, users will experience delightful clock weight animations - in particular, on transitions between the lock screen and AOD.</li><li>Notifications - ability to view and interact with notifications depending on user lock screen notification settings: `Settings > Display > Lock screen > Privacy`</li><li>Message area - contains device information like biometric errors, charging information and device policy information. Also includes user configured information from `Settings > Display > Lock screen > Add text on lock screen`. </li><li>Bouncer - if the user has a primary authentication method, they can swipe up from the bottom of the screen to bring up the bouncer.</li></ul>The lock screen is one state of the notification shade. See [StatusBarState#KEYGUARD][10] and [StatusBarState#SHADE_LOCKED][10].|
-| Bouncer, [bouncer.md][2]<br><br>![ss_aod](imgs/bouncer_pin.png)| The component responsible for displaying the primary security method set by the user (password, PIN, pattern).  The bouncer can also show SIM-related security methods, allowing the user to unlock the device or SIM.|
-| Split shade                  | State of the shade (which keyguard is a part of) in which notifications are on the right side and Quick Settings on the left. For keyguard that means notifications being on the right side and clock with media being on the left.<br><br>Split shade is automatically activated - using resources - for big screens in landscape, see [sw600dp-land/config.xml][3] `config_use_split_notification_shade`.<br><br>In that state we can see the big clock more often - every time when media is not visible on the lock screen. When there is no media and no notifications - or we enter AOD - big clock is always positioned in the center of the screen.<br><br>The magic of positioning views happens by changing constraints of [NotificationsQuickSettingsContainer][4] and positioning elements vertically in [KeyguardClockPositionAlgorithm][5]|
-| Ambient display (AOD), [doze.md][6]<br><br>![ss_aod](imgs/aod.png)| UI shown when the device is in a low-powered display state. This is controlled by the doze component. The same lock screen views (ie: clock, notification shade) are used on AOD. The AOSP image on the left shows the usage of a clock that does not support variable weights which is why the clock is thicker in that image than what users see on Pixel devices.|
+| Term                                                               | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
+|--------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Keyguard, [keyguard.md][1]                                         | Coordinates the first experience when turning on the display of a device, as long as the user has not specified a security method of NONE. Consists of the lock screen and bouncer.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
+| Lock screen<br><br>![ss_aod](imgs/lockscreen.png)                  | The first screen available when turning on the display of a device, as long as the user has not specified a security method of NONE. On the lock screen, users can access:<ul><li>Quick Settings - users can swipe down from the top of the screen to interact with quick settings tiles</li><li>[Keyguard Status Bar][9] - This special status bar shows SIM related information and system icons.</li><li>Clock - uses the font specified at [clock.xml][8]. If the clock font supports variable weights, users will experience delightful clock weight animations - in particular, on transitions between the lock screen and AOD.</li><li>Notifications - ability to view and interact with notifications depending on user lock screen notification settings: `Settings > Display > Lock screen > Privacy`</li><li>Message area - contains device information like biometric errors, charging information and device policy information. Also includes user configured information from `Settings > Display > Lock screen > Add text on lock screen`. </li><li>Bouncer - if the user has a primary authentication method, they can swipe up from the bottom of the screen to bring up the bouncer.</li></ul>The lock screen is one state of the notification shade. See [StatusBarState#KEYGUARD][10] and [StatusBarState#SHADE_LOCKED][10]. |
+| Bouncer, [bouncer.md][2]<br><br>![ss_aod](imgs/bouncer_pin.png)    | The component responsible for displaying the primary security method set by the user (password, PIN, pattern).  The bouncer can also show SIM-related security methods, allowing the user to unlock the device or SIM.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
+| Split shade                                                        | State of the shade (which keyguard is a part of) in which notifications are on the right side and Quick Settings on the left. For keyguard that means notifications being on the right side and clock with media being on the left.<br><br>Split shade is automatically activated - using resources - for big screens in landscape, see [sw600dp-land/config.xml][3] `config_use_split_notification_shade`.<br><br>In that state we can see the big clock more often - every time when media is not visible on the lock screen. When there is no media and no notifications - or we enter AOD - big clock is always positioned in the center of the screen.<br><br>The magic of positioning views happens by changing constraints of [NotificationsQuickSettingsContainer][4] and positioning elements vertically in [KeyguardClockPositionAlgorithm][5]                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
+| Ambient display (AOD), [doze.md][6]<br><br>![ss_aod](imgs/aod.png) | UI shown when the device is in a low-powered display state. This is controlled by the doze component. The same lock screen views (ie: clock, notification shade) are used on AOD. The AOSP image on the left shows the usage of a clock that does not support variable weights which is why the clock is thicker in that image than what users see on Pixel devices.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
 
 ## General Authentication Terms
-| Term                     | Description |
-| -----------              | ----------- |
-| Primary Authentication   | The strongest form of authentication. Includes: Pin, pattern and password input.|
-| Biometric Authentication | Face or fingerprint input. Biometric authentication is categorized into different classes of security. See [Measuring Biometric Security][7].|
+| Term                     | Description                                                                                                                                   |
+|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
+| Primary Authentication   | The strongest form of authentication. Includes: Pin, pattern and password input.                                                              |
+| Biometric Authentication | Face or fingerprint input. Biometric authentication is categorized into different classes of security. See [Measuring Biometric Security][7]. |
 
 ## Face Authentication Terms
-| Term            | Description |
-| -----------     | ----------- |
-| Passive Authentication   | When a user hasn't explicitly requested an authentication method; however, it may still put the device in an unlocked state.<br><br>For example, face authentication is triggered immediately when waking the device; however, users may not have the intent of unlocking their device. Instead, they could have wanted to just check the lock screen. Because of this, SystemUI provides the option for a bypass OR non-bypass face authentication experience which have different user flows.<br><br>In contrast, fingerprint authentication is considered an active authentication method since users need to actively put their finger on the fingerprint sensor to authenticate. Therefore, it's an explicit request for authentication and SystemUI knows the user has the intent for device-entry.|
-| Bypass                   | Used to refer to the face authentication bypass device entry experience. We have this distinction because face auth is a passive authentication method (see above).|
-| Bypass User Journey <br><br>![ss_bypass](imgs/bypass.png)| Once the user successfully authenticates with face, the keyguard immediately dismisses and the user is brought to the home screen/last app.  This CUJ prioritizes speed of device entry. SystemUI hides interactive views (notifications) on the lock screen to avoid putting users in a state where the lock screen could immediately disappear while they're interacting with affordances on the lock screen.|
-| Non-bypass User Journey  | Once the user successfully authenticates with face, the device remains on keyguard until the user performs an action to indicate they'd like to enter the device (ie: swipe up on the lock screen or long press on the unlocked icon). This CUJ prioritizes notification visibility.|
+| Term                                                      | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
+|-----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Passive Authentication                                    | When a user hasn't explicitly requested an authentication method; however, it may still put the device in an unlocked state.<br><br>For example, face authentication is triggered immediately when waking the device; however, users may not have the intent of unlocking their device. Instead, they could have wanted to just check the lock screen. Because of this, SystemUI provides the option for a bypass OR non-bypass face authentication experience which have different user flows.<br><br>In contrast, fingerprint authentication is considered an active authentication method since users need to actively put their finger on the fingerprint sensor to authenticate. Therefore, it's an explicit request for authentication and SystemUI knows the user has the intent for device-entry. |
+| Bypass                                                    | Used to refer to the face authentication bypass device entry experience. We have this distinction because face auth is a passive authentication method (see above).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
+| Bypass User Journey <br><br>![ss_bypass](imgs/bypass.png) | Once the user successfully authenticates with face, the keyguard immediately dismisses and the user is brought to the home screen/last app.  This CUJ prioritizes speed of device entry. SystemUI hides interactive views (notifications) on the lock screen to avoid putting users in a state where the lock screen could immediately disappear while they're interacting with affordances on the lock screen.                                                                                                                                                                                                                                                                                                                                                                                           |
+| Non-bypass User Journey                                   | Once the user successfully authenticates with face, the device remains on keyguard until the user performs an action to indicate they'd like to enter the device (ie: swipe up on the lock screen or long press on the unlocked icon). This CUJ prioritizes notification visibility.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
 
 ## Fingerprint Authentication Terms
-| Term                                     | Description |
-| -----------                              | ----------- |
-| Under-display fingerprint sensor (UDFPS) | References the HW affordance for a fingerprint sensor that is under the display, which requires a software visual affordance. System UI supports showing the UDFPS affordance on the lock screen and on AOD. Users cannot authenticate from the screen-off state.<br><br>Supported SystemUI CUJs include:<ul><li> sliding finger on the screen to the UDFPS area to being authentication (as opposed to directly placing finger in the UDFPS area) </li><li> when a11y services are enabled, there is a haptic played when a touch is detected on UDFPS</li><li>after two hard-fingerprint-failures, the primary authentication bouncer is shown</li><li> when tapping on an affordance that requests to dismiss the lock screen, the user may see the UDFPS icon highlighted - see UDFPS bouncer</li></ul>|
-| UDFPS Bouncer                            | UI that highlights the UDFPS sensor. Users can get into this state after tapping on a notification from the lock screen or locked expanded shade.|
+| Term                                     | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
+|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Under-display fingerprint sensor (UDFPS) | References the HW affordance for a fingerprint sensor that is under the display, which requires a software visual affordance. System UI supports showing the UDFPS affordance on the lock screen and on AOD. Users cannot authenticate from the screen-off state.<br><br>Supported SystemUI CUJs include:<ul><li> sliding finger on the screen to the UDFPS area to being authentication (as opposed to directly placing finger in the UDFPS area) </li><li> when a11y services are enabled, there is a haptic played when a touch is detected on UDFPS</li><li>after multiple consecutive hard-fingerprint-failures, the primary authentication bouncer is shown. The exact number of attempts is defined in: [BiometricUnlockController#UDFPS_ATTEMPTS_BEFORE_SHOW_BOUNCER][4]</li><li> when tapping on an affordance that requests to dismiss the lock screen, the user may see the UDFPS icon highlighted - see UDFPS bouncer</li></ul> |
+| UDFPS Bouncer                            | UI that highlights the UDFPS sensor. Users can get into this state after tapping on a notification from the lock screen or locked expanded shade.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
 
 ## Other Authentication Terms
-| Term             | Description |
-| ----------      | ----------- |
-| Trust Agents    | Provides signals to the keyguard to allow it to lock less frequently.|
+| Term         | Description                                                           |
+|--------------|-----------------------------------------------------------------------|
+| Trust Agents | Provides signals to the keyguard to allow it to lock less frequently. |
 
 
 [1]: /frameworks/base/packages/SystemUI/docs/device-entry/keyguard.md
@@ -46,3 +46,4 @@
 [8]: /frameworks/base/packages/SystemUI/res-keyguard/font/clock.xml
 [9]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
 [10]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java
+[11]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index 9f275af..491ec20 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -493,7 +493,7 @@
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallFlags.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLogger.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
--packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
+-packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/ShadeStateListener.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
 -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
@@ -812,7 +812,7 @@
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
+-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/ShadeExpansionStateManagerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
diff --git a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
index 6939084..33c68bf1 100644
--- a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
+++ b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
@@ -16,6 +16,6 @@
   -->
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
-    <solid android:color="@android:color/white" />
+    <solid android:color="@android:color/transparent" />
     <corners android:radius="@dimen/qs_media_album_radius" />
 </shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 778dd45..2eebdc6 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1569,4 +1569,9 @@
     <dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen>
     <dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen>
     <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen>
+
+    <!-- Default device corner radius, used for assist UI -->
+    <dimen name="config_rounded_mask_size">0px</dimen>
+    <dimen name="config_rounded_mask_size_top">0px</dimen>
+    <dimen name="config_rounded_mask_size_bottom">0px</dimen>
 </resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
index d7a0b47..3efdc5a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt
@@ -17,6 +17,7 @@
 
 import android.graphics.Point
 import android.view.Surface
+import android.view.Surface.Rotation
 import android.view.View
 import android.view.WindowManager
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
@@ -58,14 +59,14 @@
      * Updates display properties in order to calculate the initial position for the views
      * Must be called before [registerViewForAnimation]
      */
-    fun updateDisplayProperties() {
+    @JvmOverloads
+    fun updateDisplayProperties(@Rotation rotation: Int = windowManager.defaultDisplay.rotation) {
         windowManager.defaultDisplay.getSize(screenSize)
 
         // Simple implementation to get current fold orientation,
         // this might not be correct on all devices
         // TODO: use JetPack WindowManager library to get the fold orientation
-        isVerticalFold = windowManager.defaultDisplay.rotation == Surface.ROTATION_0 ||
-            windowManager.defaultDisplay.rotation == Surface.ROTATION_180
+        isVerticalFold = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
     }
 
     /**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index 2111df5..647dd47 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -27,6 +27,8 @@
 import android.app.TaskInfo;
 import android.content.ComponentName;
 import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -233,17 +235,14 @@
     @ViewDebug.ExportedProperty(category="recents")
     public boolean isLocked;
 
+    public Point positionInParent;
+
+    public Rect appBounds;
+
     // Last snapshot data, only used for recent tasks
     public ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData =
             new ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData();
 
-    /**
-     * Indicates that this task for the desktop tile in recents.
-     *
-     * Used when desktop mode feature is enabled.
-     */
-    public boolean desktopTile;
-
     public Task() {
         // Do nothing
     }
@@ -274,7 +273,8 @@
         this(other.key, other.colorPrimary, other.colorBackground, other.isDockable,
                 other.isLocked, other.taskDescription, other.topActivity);
         lastSnapshotData.set(other.lastSnapshotData);
-        desktopTile = other.desktopTile;
+        positionInParent = other.positionInParent;
+        appBounds = other.appBounds;
     }
 
     /**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
index 22bffda..6087655 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
@@ -132,8 +132,11 @@
             mMainThreadHandler.postAtFrontOfQueue(() -> {
                 // If the screen rotation changes while locked, potentially update lock to flow with
                 // new screen rotation and hide any showing suggestions.
-                if (isRotationLocked()) {
-                    if (shouldOverrideUserLockPrefs(rotation)) {
+                boolean rotationLocked = isRotationLocked();
+                // The isVisible check makes the rotation button disappear when we are not locked
+                // (e.g. for tabletop auto-rotate).
+                if (rotationLocked || mRotationButton.isVisible()) {
+                    if (shouldOverrideUserLockPrefs(rotation) && rotationLocked) {
                         setRotationLockedAtAngle(rotation);
                     }
                     setRotateSuggestionButtonState(false /* visible */, true /* forced */);
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
index ec938b2..aca9907 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt
@@ -15,12 +15,11 @@
 package com.android.systemui.unfold.util
 
 import android.content.Context
-import android.os.RemoteException
-import android.view.IRotationWatcher
-import android.view.IWindowManager
 import android.view.Surface
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.RotationChangeProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
 
 /**
  * [UnfoldTransitionProgressProvider] that emits transition progress only when the display has
@@ -29,27 +28,21 @@
  */
 class NaturalRotationUnfoldProgressProvider(
     private val context: Context,
-    private val windowManagerInterface: IWindowManager,
+    private val rotationChangeProvider: RotationChangeProvider,
     unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider
 ) : UnfoldTransitionProgressProvider {
 
     private val scopedUnfoldTransitionProgressProvider =
         ScopedUnfoldTransitionProgressProvider(unfoldTransitionProgressProvider)
-    private val rotationWatcher = RotationWatcher()
 
     private var isNaturalRotation: Boolean = false
 
     fun init() {
-        try {
-            windowManagerInterface.watchRotation(rotationWatcher, context.display.displayId)
-        } catch (e: RemoteException) {
-            throw e.rethrowFromSystemServer()
-        }
-
-        onRotationChanged(context.display.rotation)
+        rotationChangeProvider.addCallback(rotationListener)
+        rotationListener.onRotationChanged(context.display.rotation)
     }
 
-    private fun onRotationChanged(rotation: Int) {
+    private val rotationListener = RotationListener { rotation ->
         val isNewRotationNatural =
             rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
 
@@ -60,12 +53,7 @@
     }
 
     override fun destroy() {
-        try {
-            windowManagerInterface.removeRotationWatcher(rotationWatcher)
-        } catch (e: RemoteException) {
-            e.rethrowFromSystemServer()
-        }
-
+        rotationChangeProvider.removeCallback(rotationListener)
         scopedUnfoldTransitionProgressProvider.destroy()
     }
 
@@ -76,10 +64,4 @@
     override fun removeCallback(listener: TransitionProgressListener) {
         scopedUnfoldTransitionProgressProvider.removeCallback(listener)
     }
-
-    private inner class RotationWatcher : IRotationWatcher.Stub() {
-        override fun onRotationChanged(rotation: Int) {
-            this@NaturalRotationUnfoldProgressProvider.onRotationChanged(rotation)
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index d58ba80..8792a21 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -3178,14 +3178,7 @@
      * Whether the keyguard is showing and not occluded.
      */
     public boolean isKeyguardVisible() {
-        return isKeyguardShowing() && !mKeyguardOccluded;
-    }
-
-    /**
-     * Whether the keyguard is showing. It may still be occluded and not visible.
-     */
-    public boolean isKeyguardShowing() {
-        return mKeyguardShowing;
+        return mKeyguardShowing && !mKeyguardOccluded;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index 8293c74..90f0446 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -24,10 +24,10 @@
 
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 
 /**
  *  Interface to control Keyguard View. It should be implemented by KeyguardViewManagers, which
@@ -94,11 +94,6 @@
     void setOccluded(boolean occluded, boolean animate);
 
     /**
-     * @return Whether the keyguard is showing
-     */
-    boolean isShowing();
-
-    /**
      * Dismisses the keyguard by going to the next screen or making it gone.
      */
     void dismissAndCollapse();
@@ -185,7 +180,7 @@
      */
     void registerCentralSurfaces(CentralSurfaces centralSurfaces,
             NotificationPanelViewController notificationPanelViewController,
-            @Nullable PanelExpansionStateManager panelExpansionStateManager,
+            @Nullable ShadeExpansionStateManager shadeExpansionStateManager,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer,
             KeyguardBypassController bypassController);
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
index 33e6ca4..9b441ad 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
@@ -21,6 +21,8 @@
 import android.view.Display;
 import android.view.Surface;
 
+import com.android.systemui.R;
+
 /**
  * Utility class for determining screen and corner dimensions.
  */
@@ -82,17 +84,13 @@
      * where the curve ends), in pixels.
      */
     public static int getCornerRadiusBottom(Context context) {
-        int radius = 0;
-
-        int resourceId = context.getResources().getIdentifier("config_rounded_mask_size_bottom",
-                "dimen", "com.android.systemui");
-        if (resourceId > 0) {
-            radius = context.getResources().getDimensionPixelSize(resourceId);
-        }
+        int radius = context.getResources().getDimensionPixelSize(
+                R.dimen.config_rounded_mask_size_bottom);
 
         if (radius == 0) {
             radius = getCornerRadiusDefault(context);
         }
+
         return radius;
     }
 
@@ -101,28 +99,17 @@
      * the curve ends), in pixels.
      */
     public static int getCornerRadiusTop(Context context) {
-        int radius = 0;
-
-        int resourceId = context.getResources().getIdentifier("config_rounded_mask_size_top",
-                "dimen", "com.android.systemui");
-        if (resourceId > 0) {
-            radius = context.getResources().getDimensionPixelSize(resourceId);
-        }
+        int radius = context.getResources().getDimensionPixelSize(
+                R.dimen.config_rounded_mask_size_top);
 
         if (radius == 0) {
             radius = getCornerRadiusDefault(context);
         }
+
         return radius;
     }
 
     private static int getCornerRadiusDefault(Context context) {
-        int radius = 0;
-
-        int resourceId = context.getResources().getIdentifier("config_rounded_mask_size",
-                "dimen", "com.android.systemui");
-        if (resourceId > 0) {
-            radius = context.getResources().getDimensionPixelSize(resourceId);
-        }
-        return radius;
+        return context.getResources().getDimensionPixelSize(R.dimen.config_rounded_mask_size);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
index 3ad2bef..4130cf5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
@@ -22,9 +22,9 @@
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
 import com.android.systemui.util.ViewController
 import java.io.PrintWriter
 
@@ -41,7 +41,7 @@
 abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>(
     view: T,
     protected val statusBarStateController: StatusBarStateController,
-    protected val panelExpansionStateManager: PanelExpansionStateManager,
+    protected val shadeExpansionStateManager: ShadeExpansionStateManager,
     protected val dialogManager: SystemUIDialogManager,
     private val dumpManager: DumpManager
 ) : ViewController<T>(view), Dumpable {
@@ -54,7 +54,7 @@
     private var dialogAlphaAnimator: ValueAnimator? = null
     private val dialogListener = SystemUIDialogManager.Listener { runDialogAlphaAnimator() }
 
-    private val panelExpansionListener = PanelExpansionListener { event ->
+    private val shadeExpansionListener = ShadeExpansionListener { event ->
         // Notification shade can be expanded but not visible (fraction: 0.0), for example
         // when a heads-up notification (HUN) is showing.
         notificationShadeVisible = event.expanded && event.fraction > 0f
@@ -108,13 +108,13 @@
     }
 
     override fun onViewAttached() {
-        panelExpansionStateManager.addExpansionListener(panelExpansionListener)
+        shadeExpansionStateManager.addExpansionListener(shadeExpansionListener)
         dialogManager.registerListener(dialogListener)
         dumpManager.registerDumpable(dumpTag, this)
     }
 
     override fun onViewDetached() {
-        panelExpansionStateManager.removeExpansionListener(panelExpansionListener)
+        shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener)
         dialogManager.unregisterListener(dialogListener)
         dumpManager.unregisterDumpable(dumpTag)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
index 4cd40d2..e6aeb43 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
@@ -17,8 +17,8 @@
 
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
 
 /**
  * Class that coordinates non-HBM animations for biometric prompt.
@@ -26,13 +26,13 @@
 class UdfpsBpViewController(
     view: UdfpsBpView,
     statusBarStateController: StatusBarStateController,
-    panelExpansionStateManager: PanelExpansionStateManager,
+    shadeExpansionStateManager: ShadeExpansionStateManager,
     systemUIDialogManager: SystemUIDialogManager,
     dumpManager: DumpManager
 ) : UdfpsAnimationViewController<UdfpsBpView>(
     view,
     statusBarStateController,
-    panelExpansionStateManager,
+    shadeExpansionStateManager,
     systemUIDialogManager,
     dumpManager
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 412dc05..a7648bf 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -63,12 +63,12 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -111,7 +111,7 @@
     private final WindowManager mWindowManager;
     private final DelayableExecutor mFgExecutor;
     @NonNull private final Executor mBiometricExecutor;
-    @NonNull private final PanelExpansionStateManager mPanelExpansionStateManager;
+    @NonNull private final ShadeExpansionStateManager mShadeExpansionStateManager;
     @NonNull private final StatusBarStateController mStatusBarStateController;
     @NonNull private final KeyguardStateController mKeyguardStateController;
     @NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager;
@@ -205,7 +205,7 @@
             mFgExecutor.execute(() -> UdfpsController.this.showUdfpsOverlay(
                     new UdfpsControllerOverlay(mContext, mFingerprintManager, mInflater,
                             mWindowManager, mAccessibilityManager, mStatusBarStateController,
-                            mPanelExpansionStateManager, mKeyguardViewManager,
+                            mShadeExpansionStateManager, mKeyguardViewManager,
                             mKeyguardUpdateMonitor, mDialogManager, mDumpManager,
                             mLockscreenShadeTransitionController, mConfigurationController,
                             mSystemClock, mKeyguardStateController,
@@ -245,7 +245,7 @@
                     mAcquiredReceived = true;
                     final UdfpsView view = mOverlay.getOverlayView();
                     if (view != null) {
-                        view.unconfigureDisplay();
+                        unconfigureDisplay(view);
                     }
                     if (acquiredGood) {
                         mOverlay.onAcquiredGood();
@@ -582,7 +582,7 @@
             @NonNull WindowManager windowManager,
             @NonNull StatusBarStateController statusBarStateController,
             @Main DelayableExecutor fgExecutor,
-            @NonNull PanelExpansionStateManager panelExpansionStateManager,
+            @NonNull ShadeExpansionStateManager shadeExpansionStateManager,
             @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             @NonNull DumpManager dumpManager,
             @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -615,7 +615,7 @@
         mFingerprintManager = checkNotNull(fingerprintManager);
         mWindowManager = windowManager;
         mFgExecutor = fgExecutor;
-        mPanelExpansionStateManager = panelExpansionStateManager;
+        mShadeExpansionStateManager = shadeExpansionStateManager;
         mStatusBarStateController = statusBarStateController;
         mKeyguardStateController = keyguardStateController;
         mKeyguardViewManager = statusBarKeyguardViewManager;
@@ -735,6 +735,19 @@
 
         mOverlay = null;
         mOrientationListener.disable();
+
+    }
+
+    private void unconfigureDisplay(@NonNull UdfpsView view) {
+        if (view.isDisplayConfigured()) {
+            view.unconfigureDisplay();
+
+            if (mCancelAodTimeoutAction != null) {
+                mCancelAodTimeoutAction.run();
+                mCancelAodTimeoutAction = null;
+            }
+            mIsAodInterruptActive = false;
+        }
     }
 
     /**
@@ -810,12 +823,12 @@
      * sensors, this can result in illumination persisting for longer than necessary.
      */
     void onCancelUdfps() {
-        if (mOverlay != null && mOverlay.getOverlayView() != null) {
-            onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView());
-        }
         if (!mIsAodInterruptActive) {
             return;
         }
+        if (mOverlay != null && mOverlay.getOverlayView() != null) {
+            onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView());
+        }
         if (mCancelAodTimeoutAction != null) {
             mCancelAodTimeoutAction.run();
             mCancelAodTimeoutAction = null;
@@ -909,15 +922,8 @@
             }
         }
         mOnFingerDown = false;
-        if (view.isDisplayConfigured()) {
-            view.unconfigureDisplay();
-        }
+        unconfigureDisplay(view);
 
-        if (mCancelAodTimeoutAction != null) {
-            mCancelAodTimeoutAction.run();
-            mCancelAodTimeoutAction = null;
-        }
-        mIsAodInterruptActive = false;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 1c62f8a..66a521c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -43,11 +43,11 @@
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.time.SystemClock
@@ -67,7 +67,7 @@
     private val windowManager: WindowManager,
     private val accessibilityManager: AccessibilityManager,
     private val statusBarStateController: StatusBarStateController,
-    private val panelExpansionStateManager: PanelExpansionStateManager,
+    private val shadeExpansionStateManager: ShadeExpansionStateManager,
     private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val dialogManager: SystemUIDialogManager,
@@ -192,7 +192,7 @@
                     },
                     enrollHelper ?: throw IllegalStateException("no enrollment helper"),
                     statusBarStateController,
-                    panelExpansionStateManager,
+                    shadeExpansionStateManager,
                     dialogManager,
                     dumpManager,
                     overlayParams.scaleFactor
@@ -202,7 +202,7 @@
                 UdfpsKeyguardViewController(
                     view.addUdfpsView(R.layout.udfps_keyguard_view),
                     statusBarStateController,
-                    panelExpansionStateManager,
+                    shadeExpansionStateManager,
                     statusBarKeyguardViewManager,
                     keyguardUpdateMonitor,
                     dumpManager,
@@ -221,7 +221,7 @@
                 UdfpsBpViewController(
                     view.addUdfpsView(R.layout.udfps_bp_view),
                     statusBarStateController,
-                    panelExpansionStateManager,
+                    shadeExpansionStateManager,
                     dialogManager,
                     dumpManager
                 )
@@ -231,7 +231,7 @@
                 UdfpsFpmOtherViewController(
                     view.addUdfpsView(R.layout.udfps_fpm_other_view),
                     statusBarStateController,
-                    panelExpansionStateManager,
+                    shadeExpansionStateManager,
                     dialogManager,
                     dumpManager
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
index 0b7bdde..e01273f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
@@ -22,8 +22,8 @@
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 
 /**
  * Class that coordinates non-HBM animations during enrollment.
@@ -54,11 +54,11 @@
             @NonNull UdfpsEnrollView view,
             @NonNull UdfpsEnrollHelper enrollHelper,
             @NonNull StatusBarStateController statusBarStateController,
-            @NonNull PanelExpansionStateManager panelExpansionStateManager,
+            @NonNull ShadeExpansionStateManager shadeExpansionStateManager,
             @NonNull SystemUIDialogManager systemUIDialogManager,
             @NonNull DumpManager dumpManager,
             float scaleFactor) {
-        super(view, statusBarStateController, panelExpansionStateManager, systemUIDialogManager,
+        super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager,
                 dumpManager);
         mEnrollProgressBarRadius = (int) (scaleFactor * getContext().getResources().getInteger(
                 R.integer.config_udfpsEnrollProgressBar));
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt
index 98205cf..7c23278 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt
@@ -17,8 +17,8 @@
 
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
 
 /**
  * Class that coordinates non-HBM animations for non keyguard, enrollment or biometric prompt
@@ -29,13 +29,13 @@
 class UdfpsFpmOtherViewController(
     view: UdfpsFpmOtherView,
     statusBarStateController: StatusBarStateController,
-    panelExpansionStateManager: PanelExpansionStateManager,
+    shadeExpansionStateManager: ShadeExpansionStateManager,
     systemUIDialogManager: SystemUIDialogManager,
     dumpManager: DumpManager
 ) : UdfpsAnimationViewController<UdfpsFpmOtherView>(
     view,
     statusBarStateController,
-    panelExpansionStateManager,
+    shadeExpansionStateManager,
     systemUIDialogManager,
     dumpManager
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
index 24b8933..4d7f89d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
@@ -31,6 +31,9 @@
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
@@ -38,9 +41,6 @@
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.time.SystemClock;
@@ -88,7 +88,7 @@
     protected UdfpsKeyguardViewController(
             @NonNull UdfpsKeyguardView view,
             @NonNull StatusBarStateController statusBarStateController,
-            @NonNull PanelExpansionStateManager panelExpansionStateManager,
+            @NonNull ShadeExpansionStateManager shadeExpansionStateManager,
             @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
             @NonNull DumpManager dumpManager,
@@ -100,7 +100,7 @@
             @NonNull SystemUIDialogManager systemUIDialogManager,
             @NonNull UdfpsController udfpsController,
             @NonNull ActivityLaunchAnimator activityLaunchAnimator) {
-        super(view, statusBarStateController, panelExpansionStateManager, systemUIDialogManager,
+        super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager,
                 dumpManager);
         mKeyguardViewManager = statusBarKeyguardViewManager;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -153,7 +153,7 @@
         mQsExpansion = mKeyguardViewManager.getQsExpansion();
         updateGenericBouncerVisibility();
         mConfigurationController.addCallback(mConfigurationListener);
-        getPanelExpansionStateManager().addExpansionListener(mPanelExpansionListener);
+        getShadeExpansionStateManager().addExpansionListener(mShadeExpansionListener);
         updateScaleFactor();
         mView.updatePadding();
         updateAlpha();
@@ -174,7 +174,7 @@
         mKeyguardViewManager.removeAlternateAuthInterceptor(mAlternateAuthInterceptor);
         mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false);
         mConfigurationController.removeCallback(mConfigurationListener);
-        getPanelExpansionStateManager().removeExpansionListener(mPanelExpansionListener);
+        getShadeExpansionStateManager().removeExpansionListener(mShadeExpansionListener);
         if (mLockScreenShadeTransitionController.getUdfpsKeyguardViewController() == this) {
             mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(null);
         }
@@ -219,7 +219,7 @@
                 mView.animateInUdfpsBouncer(null);
             }
 
-            if (mKeyguardViewManager.isOccluded()) {
+            if (mKeyguardStateController.isOccluded()) {
                 mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true);
             }
 
@@ -502,9 +502,9 @@
                 }
             };
 
-    private final PanelExpansionListener mPanelExpansionListener = new PanelExpansionListener() {
+    private final ShadeExpansionListener mShadeExpansionListener = new ShadeExpansionListener() {
         @Override
-        public void onPanelExpansionChanged(PanelExpansionChangeEvent event) {
+        public void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
             float fraction = event.getFraction();
             mPanelExpansionFraction =
                     mKeyguardViewManager.isBouncerInTransit() ? BouncerPanelExpansionCalculator
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index c2dffe8..d05bd51 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -28,6 +28,7 @@
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
 import com.android.systemui.people.PeopleProvider;
+import com.android.systemui.statusbar.QsFrameTranslateModule;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.unfold.FoldStateLogger;
 import com.android.systemui.unfold.FoldStateLoggingProvider;
@@ -63,6 +64,7 @@
 @Subcomponent(modules = {
         DefaultComponentBinder.class,
         DependencyProvider.class,
+        QsFrameTranslateModule.class,
         SystemUIBinder.class,
         SystemUIModule.class,
         SystemUICoreStartableModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index d70b971..dc3dadb 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -61,7 +61,6 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.QsFrameTranslateModule;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
@@ -133,7 +132,6 @@
             PeopleModule.class,
             PluginModule.class,
             PrivacyModule.class,
-            QsFrameTranslateModule.class,
             ScreenshotModule.class,
             SensorModule.class,
             MultiUserUtilsModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 696fc72..d1b7368 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -64,29 +64,26 @@
     private final Executor mExecutor;
     // A controller for the dream overlay container view (which contains both the status bar and the
     // content area).
-    private final DreamOverlayContainerViewController mDreamOverlayContainerViewController;
+    private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Nullable
     private final ComponentName mLowLightDreamComponent;
     private final UiEventLogger mUiEventLogger;
+    private final WindowManager mWindowManager;
 
     // A reference to the {@link Window} used to hold the dream overlay.
     private Window mWindow;
 
-    // True if the service has been destroyed.
-    private boolean mDestroyed;
+    // True if a dream has bound to the service and dream overlay service has started.
+    private boolean mStarted = false;
 
-    private final Complication.Host mHost = new Complication.Host() {
-        @Override
-        public void requestExitDream() {
-            mExecutor.execute(DreamOverlayService.this::requestExit);
-        }
-    };
+    // True if the service has been destroyed.
+    private boolean mDestroyed = false;
+
+    private final DreamOverlayComponent mDreamOverlayComponent;
 
     private final LifecycleRegistry mLifecycleRegistry;
 
-    private ViewModelStore mViewModelStore = new ViewModelStore();
-
     private DreamOverlayTouchMonitor mDreamOverlayTouchMonitor;
 
     private final KeyguardUpdateMonitorCallback mKeyguardCallback =
@@ -103,7 +100,7 @@
                 }
             };
 
-    private DreamOverlayStateController mStateController;
+    private final DreamOverlayStateController mStateController;
 
     @VisibleForTesting
     public enum DreamOverlayEvent implements UiEventLogger.UiEventEnum {
@@ -128,6 +125,7 @@
     public DreamOverlayService(
             Context context,
             @Main Executor executor,
+            WindowManager windowManager,
             DreamOverlayComponent.Factory dreamOverlayComponentFactory,
             DreamOverlayStateController stateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -136,19 +134,19 @@
                     ComponentName lowLightDreamComponent) {
         mContext = context;
         mExecutor = executor;
+        mWindowManager = windowManager;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mLowLightDreamComponent = lowLightDreamComponent;
         mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
         mStateController = stateController;
         mUiEventLogger = uiEventLogger;
 
-        final DreamOverlayComponent component =
-                dreamOverlayComponentFactory.create(mViewModelStore, mHost);
-        mDreamOverlayContainerViewController = component.getDreamOverlayContainerViewController();
+        final ViewModelStore viewModelStore = new ViewModelStore();
+        final Complication.Host host =
+                () -> mExecutor.execute(DreamOverlayService.this::requestExit);
+        mDreamOverlayComponent = dreamOverlayComponentFactory.create(viewModelStore, host);
+        mLifecycleRegistry = mDreamOverlayComponent.getLifecycleRegistry();
         setCurrentState(Lifecycle.State.CREATED);
-        mLifecycleRegistry = component.getLifecycleRegistry();
-        mDreamOverlayTouchMonitor = component.getDreamOverlayTouchMonitor();
-        mDreamOverlayTouchMonitor.init();
     }
 
     private void setCurrentState(Lifecycle.State state) {
@@ -159,34 +157,48 @@
     public void onDestroy() {
         mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback);
         setCurrentState(Lifecycle.State.DESTROYED);
-        final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
-        if (mWindow != null) {
-            windowManager.removeView(mWindow.getDecorView());
-        }
-        mStateController.setOverlayActive(false);
-        mStateController.setLowLightActive(false);
+
+        resetCurrentDreamOverlay();
+
         mDestroyed = true;
         super.onDestroy();
     }
 
     @Override
     public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
-        mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
         setCurrentState(Lifecycle.State.STARTED);
-        final ComponentName dreamComponent = getDreamComponent();
-        mStateController.setLowLightActive(
-                dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
+
         mExecutor.execute(() -> {
+            mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
+
             if (mDestroyed) {
                 // The task could still be executed after the service has been destroyed. Bail if
                 // that is the case.
                 return;
             }
+
+            if (mStarted) {
+                // Reset the current dream overlay before starting a new one. This can happen
+                // when two dreams overlap (briefly, for a smoother dream transition) and both
+                // dreams are bound to the dream overlay service.
+                resetCurrentDreamOverlay();
+            }
+
+            mDreamOverlayContainerViewController =
+                    mDreamOverlayComponent.getDreamOverlayContainerViewController();
+            mDreamOverlayTouchMonitor = mDreamOverlayComponent.getDreamOverlayTouchMonitor();
+            mDreamOverlayTouchMonitor.init();
+
             mStateController.setShouldShowComplications(shouldShowComplications());
             addOverlayWindowLocked(layoutParams);
             setCurrentState(Lifecycle.State.RESUMED);
             mStateController.setOverlayActive(true);
+            final ComponentName dreamComponent = getDreamComponent();
+            mStateController.setLowLightActive(
+                    dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
             mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START);
+
+            mStarted = true;
         });
     }
 
@@ -222,8 +234,7 @@
         removeContainerViewFromParent();
         mWindow.setContentView(mDreamOverlayContainerViewController.getContainerView());
 
-        final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
-        windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
+        mWindowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
     }
 
     private void removeContainerViewFromParent() {
@@ -238,4 +249,18 @@
         Log.w(TAG, "Removing dream overlay container view parent!");
         parentView.removeView(containerView);
     }
+
+    private void resetCurrentDreamOverlay() {
+        if (mStarted && mWindow != null) {
+            mWindowManager.removeView(mWindow.getDecorView());
+        }
+
+        mStateController.setOverlayActive(false);
+        mStateController.setLowLightActive(false);
+
+        mDreamOverlayContainerViewController = null;
+        mDreamOverlayTouchMonitor = null;
+
+        mStarted = false;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
index f769a23..0dba4ff 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
@@ -36,11 +36,11 @@
 
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.KeyguardBouncer;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
 import java.util.Optional;
@@ -154,8 +154,8 @@
 
     private void setPanelExpansion(float expansion, float dragDownAmount) {
         mCurrentExpansion = expansion;
-        PanelExpansionChangeEvent event =
-                new PanelExpansionChangeEvent(
+        ShadeExpansionChangeEvent event =
+                new ShadeExpansionChangeEvent(
                         /* fraction= */ mCurrentExpansion,
                         /* expanded= */ false,
                         /* tracking= */ true,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 5f96a3b..a908e94 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -666,7 +666,7 @@
             return
         }
 
-        if (keyguardViewController.isShowing && !playingCannedUnlockAnimation) {
+        if (keyguardStateController.isShowing && !playingCannedUnlockAnimation) {
             showOrHideSurfaceIfDismissAmountThresholdsReached()
 
             // If the surface is visible or it's about to be, start updating its appearance to
@@ -726,7 +726,7 @@
     private fun finishKeyguardExitRemoteAnimationIfReachThreshold() {
         // no-op if keyguard is not showing or animation is not enabled.
         if (!KeyguardService.sEnableRemoteKeyguardGoingAwayAnimation ||
-                !keyguardViewController.isShowing) {
+                !keyguardStateController.isShowing) {
             return
         }
 
@@ -849,7 +849,7 @@
      * animation.
      */
     fun hideKeyguardViewAfterRemoteAnimation() {
-        if (keyguardViewController.isShowing) {
+        if (keyguardStateController.isShowing) {
             // Hide the keyguard, with no fade out since we animated it away during the unlock.
 
             keyguardViewController.hide(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index da0b910..9a118e0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -124,6 +124,7 @@
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -134,7 +135,6 @@
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.util.DeviceConfigProxy;
@@ -1881,7 +1881,7 @@
         // if the keyguard is already showing, don't bother. check flags in both files
         // to account for the hiding animation which results in a delay and discrepancy
         // between flags
-        if (mShowing && mKeyguardViewControllerLazy.get().isShowing()) {
+        if (mShowing && mKeyguardStateController.isShowing()) {
             if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing");
             resetStateLocked();
             return;
@@ -2972,14 +2972,14 @@
      */
     public KeyguardViewController registerCentralSurfaces(CentralSurfaces centralSurfaces,
             NotificationPanelViewController panelView,
-            @Nullable PanelExpansionStateManager panelExpansionStateManager,
+            @Nullable ShadeExpansionStateManager shadeExpansionStateManager,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer, KeyguardBypassController bypassController) {
         mCentralSurfaces = centralSurfaces;
         mKeyguardViewControllerLazy.get().registerCentralSurfaces(
                 centralSurfaces,
                 panelView,
-                panelExpansionStateManager,
+                shadeExpansionStateManager,
                 biometricUnlockController,
                 notificationContainer,
                 bypassController);
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index 9dd18b2..80bff83 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -11,6 +11,7 @@
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.view.animation.PathInterpolator
 import android.widget.LinearLayout
 import androidx.annotation.VisibleForTesting
 import com.android.internal.logging.InstanceId
@@ -95,7 +96,8 @@
      * finished
      */
     @MediaLocation
-    private var currentEndLocation: Int = -1
+    @VisibleForTesting
+    var currentEndLocation: Int = -1
 
     /**
      * The ending location of the view where it ends when all animations and transitions have
@@ -126,7 +128,8 @@
     lateinit var settingsButton: View
         private set
     private val mediaContent: ViewGroup
-    private val pageIndicator: PageIndicator
+    @VisibleForTesting
+    val pageIndicator: PageIndicator
     private val visualStabilityCallback: OnReorderingAllowedListener
     private var needsReordering: Boolean = false
     private var keysNeedRemoval = mutableSetOf<String>()
@@ -149,6 +152,27 @@
                 }
             }
         }
+
+    companion object {
+        const val ANIMATION_BASE_DURATION = 2200f
+        const val DURATION = 167f
+        const val DETAILS_DELAY = 1067f
+        const val CONTROLS_DELAY = 1400f
+        const val PAGINATION_DELAY = 1900f
+        const val MEDIATITLES_DELAY = 1000f
+        const val MEDIACONTAINERS_DELAY = 967f
+        val TRANSFORM_BEZIER = PathInterpolator (0.68F, 0F, 0F, 1F)
+        val REVERSE_BEZIER = PathInterpolator (0F, 0.68F, 1F, 0F)
+
+        fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float {
+            val transformStartFraction = delay / ANIMATION_BASE_DURATION
+            val transformDurationFraction = duration / ANIMATION_BASE_DURATION
+            val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction)
+            return MathUtils.constrain((squishinessToTime - transformStartFraction) /
+                    transformDurationFraction, 0F, 1F)
+        }
+    }
+
     private val configListener = object : ConfigurationController.ConfigurationListener {
         override fun onDensityOrFontScaleChanged() {
             // System font changes should only happen when UMO is offscreen or a flicker may occur
@@ -633,12 +657,17 @@
         }
     }
 
-    private fun updatePageIndicatorAlpha() {
+    @VisibleForTesting
+    fun updatePageIndicatorAlpha() {
         val hostStates = mediaHostStatesManager.mediaHostStates
         val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
         val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
         val startAlpha = if (startIsVisible) 1.0f else 0.0f
-        val endAlpha = if (endIsVisible) 1.0f else 0.0f
+        // when squishing in split shade, only use endState, which keeps changing
+        // to provide squishFraction
+        val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
+        val endAlpha = (if (endIsVisible) 1.0f else 0.0f) *
+                calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION)
         var alpha = 1.0f
         if (!endIsVisible || !startIsVisible) {
             var progress = currentTransitionProgress
@@ -687,6 +716,7 @@
             mediaCarouselScrollHandler.setCarouselBounds(
                     currentCarouselWidth, currentCarouselHeight)
             updatePageIndicatorLocation()
+            updatePageIndicatorAlpha()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
index ef49fd3..a776897 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
@@ -47,7 +47,7 @@
  * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
  */
 private val translationConfig = PhysicsAnimator.SpringConfig(
-        SpringForce.STIFFNESS_MEDIUM,
+        SpringForce.STIFFNESS_LOW,
         SpringForce.DAMPING_RATIO_LOW_BOUNCY)
 
 /**
@@ -289,7 +289,10 @@
                 return false
             }
         }
-        if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
+        if (motionEvent.action == MotionEvent.ACTION_MOVE) {
+            // cancel on going animation if there is any.
+            PhysicsAnimator.getInstance(this).cancel()
+        } else if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
             // It's an up and the fling didn't take it above
             val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding
             val scrollXAmount: Int
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index bffb0fd..8645922 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -203,6 +203,14 @@
                 }
             }
 
+        override var squishFraction: Float = 1.0f
+            set(value) {
+                if (!value.equals(field)) {
+                    field = value
+                    changedListener?.invoke()
+                }
+            }
+
         override var showsOnlyActiveMedia: Boolean = false
             set(value) {
                 if (!value.equals(field)) {
@@ -253,6 +261,7 @@
         override fun copy(): MediaHostState {
             val mediaHostState = MediaHostStateHolder()
             mediaHostState.expansion = expansion
+            mediaHostState.squishFraction = squishFraction
             mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
             mediaHostState.measurementInput = measurementInput?.copy()
             mediaHostState.visible = visible
@@ -271,6 +280,9 @@
             if (expansion != other.expansion) {
                 return false
             }
+            if (squishFraction != other.squishFraction) {
+                return false
+            }
             if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
                 return false
             }
@@ -289,6 +301,7 @@
         override fun hashCode(): Int {
             var result = measurementInput?.hashCode() ?: 0
             result = 31 * result + expansion.hashCode()
+            result = 31 * result + squishFraction.hashCode()
             result = 31 * result + falsingProtectionNeeded.hashCode()
             result = 31 * result + showsOnlyActiveMedia.hashCode()
             result = 31 * result + if (visible) 1 else 2
@@ -329,6 +342,11 @@
     var expansion: Float
 
     /**
+     * Fraction of the height animation.
+     */
+    var squishFraction: Float
+
+    /**
      * Is this host only showing active media or is it showing all of them including resumption?
      */
     var showsOnlyActiveMedia: Boolean
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
index ac59175..faa7aae 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
@@ -18,8 +18,15 @@
 
 import android.content.Context
 import android.content.res.Configuration
+import androidx.annotation.VisibleForTesting
 import androidx.constraintlayout.widget.ConstraintSet
 import com.android.systemui.R
+import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.calculateAlpha
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.animation.MeasurementOutput
 import com.android.systemui.util.animation.TransitionLayout
@@ -50,6 +57,24 @@
     companion object {
         @JvmField
         val GUTS_ANIMATION_DURATION = 500L
+        val controlIds = setOf(
+                R.id.media_progress_bar,
+                R.id.actionNext,
+                R.id.actionPrev,
+                R.id.action0,
+                R.id.action1,
+                R.id.action2,
+                R.id.action3,
+                R.id.action4,
+                R.id.media_scrubbing_elapsed_time,
+                R.id.media_scrubbing_total_time
+        )
+
+        val detailIds = setOf(
+                R.id.header_title,
+                R.id.header_artist,
+                R.id.actionPlayPause,
+        )
     }
 
     /**
@@ -57,6 +82,7 @@
      */
     lateinit var sizeChangedListener: () -> Unit
     private var firstRefresh: Boolean = true
+    @VisibleForTesting
     private var transitionLayout: TransitionLayout? = null
     private val layoutController = TransitionLayoutController()
     private var animationDelay: Long = 0
@@ -279,10 +305,47 @@
     }
 
     /**
+     * Apply squishFraction to a copy of viewState such that the cached version is untouched.
+    */
+    internal fun squishViewState(
+        viewState: TransitionViewState,
+        squishFraction: Float
+    ): TransitionViewState {
+        val squishedViewState = viewState.copy()
+        squishedViewState.height = (squishedViewState.height * squishFraction).toInt()
+        controlIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
+            }
+        }
+
+        detailIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
+            }
+        }
+
+        RecommendationViewHolder.mediaContainersIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
+            }
+        }
+
+        RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
+            }
+        }
+
+        return squishedViewState
+    }
+
+    /**
      * Obtain a new viewState for a given media state. This usually returns a cached state, but if
      * it's not available, it will recreate one by measuring, which may be expensive.
      */
-    private fun obtainViewState(state: MediaHostState?): TransitionViewState? {
+     @VisibleForTesting
+     fun obtainViewState(state: MediaHostState?): TransitionViewState? {
         if (state == null || state.measurementInput == null) {
             return null
         }
@@ -291,41 +354,46 @@
         val viewState = viewStates[cacheKey]
         if (viewState != null) {
             // we already have cached this measurement, let's continue
+            if (state.squishFraction <= 1f) {
+                return squishViewState(viewState, state.squishFraction)
+            }
             return viewState
         }
         // Copy the key since this might call recursively into it and we're using tmpKey
         cacheKey = cacheKey.copy()
         val result: TransitionViewState?
 
-        if (transitionLayout != null) {
-            // Let's create a new measurement
-            if (state.expansion == 0.0f || state.expansion == 1.0f) {
-                result = transitionLayout!!.calculateViewState(
-                        state.measurementInput!!,
-                        constraintSetForExpansion(state.expansion),
-                        TransitionViewState())
+        if (transitionLayout == null) {
+            return null
+        }
+        // Let's create a new measurement
+        if (state.expansion == 0.0f || state.expansion == 1.0f) {
+            result = transitionLayout!!.calculateViewState(
+                    state.measurementInput!!,
+                    constraintSetForExpansion(state.expansion),
+                    TransitionViewState())
 
-                setGutsViewState(result)
-                // We don't want to cache interpolated or null states as this could quickly fill up
-                // our cache. We only cache the start and the end states since the interpolation
-                // is cheap
-                viewStates[cacheKey] = result
-            } else {
-                // This is an interpolated state
-                val startState = state.copy().also { it.expansion = 0.0f }
-
-                // Given that we have a measurement and a view, let's get (guaranteed) viewstates
-                // from the start and end state and interpolate them
-                val startViewState = obtainViewState(startState) as TransitionViewState
-                val endState = state.copy().also { it.expansion = 1.0f }
-                val endViewState = obtainViewState(endState) as TransitionViewState
-                result = layoutController.getInterpolatedState(
-                        startViewState,
-                        endViewState,
-                        state.expansion)
-            }
+            setGutsViewState(result)
+            // We don't want to cache interpolated or null states as this could quickly fill up
+            // our cache. We only cache the start and the end states since the interpolation
+            // is cheap
+            viewStates[cacheKey] = result
         } else {
-            result = null
+            // This is an interpolated state
+            val startState = state.copy().also { it.expansion = 0.0f }
+
+            // Given that we have a measurement and a view, let's get (guaranteed) viewstates
+            // from the start and end state and interpolate them
+            val startViewState = obtainViewState(startState) as TransitionViewState
+            val endState = state.copy().also { it.expansion = 1.0f }
+            val endViewState = obtainViewState(endState) as TransitionViewState
+            result = layoutController.getInterpolatedState(
+                    startViewState,
+                    endViewState,
+                    state.expansion)
+        }
+        if (state.squishFraction <= 1f) {
+            return squishViewState(result, state.squishFraction)
         }
         return result
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
index 52ac4e0..8ae75fc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
@@ -106,5 +106,20 @@
             R.id.media_subtitle2,
             R.id.media_subtitle3
         )
+
+        val mediaTitlesAndSubtitlesIds = setOf(
+            R.id.media_title1,
+            R.id.media_title2,
+            R.id.media_title3,
+            R.id.media_subtitle1,
+            R.id.media_subtitle2,
+            R.id.media_subtitle3
+        )
+
+        val mediaContainersIds = setOf(
+            R.id.media_cover1_container,
+            R.id.media_cover2_container,
+            R.id.media_cover3_container
+        )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index da9fefa..33021e3 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -48,7 +48,6 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.keyguard.KeyguardViewController;
 import com.android.systemui.Dumpable;
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
 import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
@@ -61,6 +60,7 @@
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -90,7 +90,7 @@
     private final AccessibilityManager mAccessibilityManager;
     private final Lazy<AssistManager> mAssistManagerLazy;
     private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
-    private final KeyguardViewController mKeyguardViewController;
+    private final KeyguardStateController mKeyguardStateController;
     private final UserTracker mUserTracker;
     private final SystemActions mSystemActions;
     private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
@@ -125,7 +125,7 @@
             OverviewProxyService overviewProxyService,
             Lazy<AssistManager> assistManagerLazy,
             Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
-            KeyguardViewController keyguardViewController,
+            KeyguardStateController keyguardStateController,
             NavigationModeController navigationModeController,
             UserTracker userTracker,
             DumpManager dumpManager) {
@@ -134,7 +134,7 @@
         mAccessibilityManager = accessibilityManager;
         mAssistManagerLazy = assistManagerLazy;
         mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
-        mKeyguardViewController = keyguardViewController;
+        mKeyguardStateController = keyguardStateController;
         mUserTracker = userTracker;
         mSystemActions = systemActions;
         accessibilityManager.addAccessibilityServicesStateChangeListener(this);
@@ -326,7 +326,7 @@
             shadeWindowView =
                     mCentralSurfacesOptionalLazy.get().get().getNotificationShadeWindowView();
         }
-        boolean isKeyguardShowing = mKeyguardViewController.isShowing();
+        boolean isKeyguardShowing = mKeyguardStateController.isShowing();
         boolean imeVisibleOnShade = shadeWindowView != null && shadeWindowView.isAttachedToWindow()
                 && shadeWindowView.getRootWindowInsets().isVisible(WindowInsets.Type.ime());
         return imeVisibleOnShade
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 1ef6426..0fe3d16 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -691,6 +691,15 @@
         if (mQSAnimator != null) {
             mQSAnimator.setPosition(expansion);
         }
+        if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD
+                || mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
+            // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen
+            // and media player expect no change by squishiness in lock screen shade
+            mQsMediaHost.setSquishFraction(1.0F);
+        } else {
+            mQsMediaHost.setSquishFraction(mSquishinessFraction);
+        }
+
     }
 
     private void setAlphaAnimationProgress(float progress) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 3e445dd..d393680 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -36,6 +36,7 @@
 import android.util.Log;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -182,6 +183,10 @@
         setBindService(true);
     }
 
+    /**
+     * Binds or unbinds to IQSService
+     */
+    @WorkerThread
     public void setBindService(boolean bind) {
         if (mBound && mUnbindImmediate) {
             // If we are already bound and expecting to unbind, this means we should stay bound
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index 309059f..95cc0dc 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -76,7 +76,7 @@
                 )
             } else {
                 // Create a new request of the same type which includes the top component
-                ScreenshotRequest(request.source, request.type, info.component)
+                ScreenshotRequest(request.type, request.source, info.component)
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
index 9654e03..793085a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -19,14 +19,14 @@
 import android.content.Intent
 import android.os.IBinder
 import android.util.Log
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
+import com.android.systemui.shade.ShadeExpansionStateManager
 import javax.inject.Inject
 
 /**
  * Provides state from the main SystemUI process on behalf of the Screenshot process.
  */
 internal class ScreenshotProxyService @Inject constructor(
-    private val mExpansionMgr: PanelExpansionStateManager
+    private val mExpansionMgr: ShadeExpansionStateManager
 ) : Service() {
 
     private val mBinder: IBinder = object : IScreenshotProxy.Stub() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 1110386..e331812 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -17,6 +17,8 @@
 package com.android.systemui.shade;
 
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
 
 import static androidx.constraintlayout.widget.ConstraintSet.END;
 import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
@@ -26,8 +28,15 @@
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 import static com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE;
 import static com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE;
+import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
+import static com.android.systemui.classifier.Classifier.GENERIC;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
+import static com.android.systemui.classifier.Classifier.UNLOCK;
+import static com.android.systemui.shade.NotificationPanelView.DEBUG;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
@@ -36,11 +45,10 @@
 import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING;
 import static com.android.systemui.util.DumpUtilsKt.asIndenting;
 
+import static java.lang.Float.isNaN;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
@@ -48,6 +56,8 @@
 import android.app.Fragment;
 import android.app.StatusBarManager;
 import android.content.ContentResolver;
+import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -73,11 +83,13 @@
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.MathUtils;
+import android.view.InputDevice;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
+import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.view.ViewPropertyAnimator;
 import android.view.ViewStub;
@@ -86,6 +98,7 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
 
 import androidx.annotation.Nullable;
@@ -178,6 +191,7 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.phone.BounceInterpolator;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -202,8 +216,6 @@
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelState;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -232,8 +244,13 @@
 import javax.inject.Provider;
 
 @CentralSurfacesComponent.CentralSurfacesScope
-public final class NotificationPanelViewController extends PanelViewController {
+public final class NotificationPanelViewController {
 
+    public static final String TAG = NotificationPanelView.class.getSimpleName();
+    public static final float FLING_MAX_LENGTH_SECONDS = 0.6f;
+    public static final float FLING_SPEED_UP_FACTOR = 0.6f;
+    public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f;
+    public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f;
     private static final boolean DEBUG_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
     private static final boolean SPEW_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
     private static final boolean DEBUG_DRAWABLE = false;
@@ -264,6 +281,22 @@
             ActivityLaunchAnimator.TIMINGS.getTotalDuration()
                     - CollapsedStatusBarFragment.FADE_IN_DURATION
                     - CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
+    private static final int NO_FIXED_DURATION = -1;
+    private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L;
+    private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L;
+    /**
+     * The factor of the usual high velocity that is needed in order to reach the maximum overshoot
+     * when flinging. A low value will make it that most flings will reach the maximum overshoot.
+     */
+    private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f;
+    private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
+    private final Resources mResources;
+    private final KeyguardStateController mKeyguardStateController;
+    private final SysuiStatusBarStateController mStatusBarStateController;
+    private final AmbientState mAmbientState;
+    private final LockscreenGestureLogger mLockscreenGestureLogger;
+    private final SystemClock mSystemClock;
+    private final ShadeLogger mShadeLog;
 
     private final DozeParameters mDozeParameters;
     private final OnHeightChangedListener mOnHeightChangedListener = new OnHeightChangedListener();
@@ -335,6 +368,28 @@
     private final LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
     private final RecordingController mRecordingController;
     private final PanelEventsEmitter mPanelEventsEmitter;
+    private final boolean mVibrateOnOpening;
+    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+    private final FlingAnimationUtils mFlingAnimationUtilsClosing;
+    private final FlingAnimationUtils mFlingAnimationUtilsDismissing;
+    private final LatencyTracker mLatencyTracker;
+    private final DozeLog mDozeLog;
+    /** Whether or not the NotificationPanelView can be expanded or collapsed with a drag. */
+    private final boolean mNotificationsDragEnabled;
+    private final Interpolator mBounceInterpolator;
+    private final NotificationShadeWindowController mNotificationShadeWindowController;
+    private final ShadeExpansionStateManager mShadeExpansionStateManager;
+    private long mDownTime;
+    private boolean mTouchSlopExceededBeforeDown;
+    private boolean mIsLaunchAnimationRunning;
+    private float mOverExpansion;
+    private CentralSurfaces mCentralSurfaces;
+    private HeadsUpManagerPhone mHeadsUpManager;
+    private float mExpandedHeight = 0;
+    private boolean mTracking;
+    private boolean mHintAnimationRunning;
+    private KeyguardBottomAreaView mKeyguardBottomArea;
+    private boolean mExpanding;
     private boolean mSplitShadeEnabled;
     /** The bottom padding reserved for elements of the keyguard measuring notifications. */
     private float mKeyguardNotificationBottomPadding;
@@ -708,6 +763,54 @@
     private final CameraGestureHelper mCameraGestureHelper;
     private final KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
     private final KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+    private float mMinExpandHeight;
+    private boolean mPanelUpdateWhenAnimatorEnds;
+    private boolean mHasVibratedOnOpen = false;
+    private int mFixedDuration = NO_FIXED_DURATION;
+    /** The overshoot amount when the panel flings open. */
+    private float mPanelFlingOvershootAmount;
+    /** The amount of pixels that we have overexpanded the last time with a gesture. */
+    private float mLastGesturedOverExpansion = -1;
+    /** Whether the current animator is the spring back animation. */
+    private boolean mIsSpringBackAnimation;
+    private boolean mInSplitShade;
+    private float mHintDistance;
+    private float mInitialOffsetOnTouch;
+    private boolean mCollapsedAndHeadsUpOnDown;
+    private float mExpandedFraction = 0;
+    private float mExpansionDragDownAmountPx = 0;
+    private boolean mPanelClosedOnDown;
+    private boolean mHasLayoutedSinceDown;
+    private float mUpdateFlingVelocity;
+    private boolean mUpdateFlingOnLayout;
+    private boolean mClosing;
+    private boolean mTouchSlopExceeded;
+    private int mTrackingPointer;
+    private int mTouchSlop;
+    private float mSlopMultiplier;
+    private boolean mTouchAboveFalsingThreshold;
+    private boolean mTouchStartedInEmptyArea;
+    private boolean mMotionAborted;
+    private boolean mUpwardsWhenThresholdReached;
+    private boolean mAnimatingOnDown;
+    private boolean mHandlingPointerUp;
+    private ValueAnimator mHeightAnimator;
+    /** Whether an instant expand request is currently pending and we are waiting for layout. */
+    private boolean mInstantExpanding;
+    private boolean mAnimateAfterExpanding;
+    private boolean mIsFlinging;
+    private String mViewName;
+    private float mInitialExpandY;
+    private float mInitialExpandX;
+    private boolean mTouchDisabled;
+    private boolean mInitialTouchFromKeyguard;
+    /** Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time. */
+    private float mNextCollapseSpeedUpFactor = 1.0f;
+    private boolean mGestureWaitForTouchSlop;
+    private boolean mIgnoreXTouchSlop;
+    private boolean mExpandLatencyTracking;
+    private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
+            mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
 
     @Inject
     public NotificationPanelViewController(NotificationPanelView view,
@@ -760,7 +863,7 @@
             LargeScreenShadeHeaderController largeScreenShadeHeaderController,
             ScreenOffAnimationController screenOffAnimationController,
             LockscreenGestureLogger lockscreenGestureLogger,
-            PanelExpansionStateManager panelExpansionStateManager,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             NotificationRemoteInputManager remoteInputManager,
             Optional<SysUIUnfoldComponent> unfoldComponent,
             InteractionJankMonitor interactionJankMonitor,
@@ -777,32 +880,73 @@
             CameraGestureHelper cameraGestureHelper,
             KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
             KeyguardBottomAreaInteractor keyguardBottomAreaInteractor) {
-        super(view,
-                falsingManager,
-                dozeLog,
-                keyguardStateController,
-                (SysuiStatusBarStateController) statusBarStateController,
-                notificationShadeWindowController,
-                vibratorHelper,
-                statusBarKeyguardViewManager,
-                latencyTracker,
-                flingAnimationUtilsBuilder.get(),
-                statusBarTouchableRegionManager,
-                lockscreenGestureLogger,
-                panelExpansionStateManager,
-                ambientState,
-                interactionJankMonitor,
-                shadeLogger,
-                systemClock);
+        keyguardStateController.addCallback(new KeyguardStateController.Callback() {
+            @Override
+            public void onKeyguardFadingAwayChanged() {
+                updateExpandedHeightToMaxHeight();
+            }
+        });
+        mAmbientState = ambientState;
         mView = view;
+        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mLockscreenGestureLogger = lockscreenGestureLogger;
+        mShadeExpansionStateManager = shadeExpansionStateManager;
+        mShadeLog = shadeLogger;
+        TouchHandler touchHandler = createTouchHandler();
+        mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+            @Override
+            public void onViewAttachedToWindow(View v) {
+                mViewName = mResources.getResourceName(mView.getId());
+            }
+
+            @Override
+            public void onViewDetachedFromWindow(View v) {
+            }
+        });
+
+        mView.addOnLayoutChangeListener(createLayoutChangeListener());
+        mView.setOnTouchListener(touchHandler);
+        mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener());
+
+        mResources = mView.getResources();
+        mKeyguardStateController = keyguardStateController;
+        mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
+        mNotificationShadeWindowController = notificationShadeWindowController;
+        FlingAnimationUtils.Builder fauBuilder = flingAnimationUtilsBuilder.get();
+        mFlingAnimationUtils = fauBuilder
+                .reset()
+                .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS)
+                .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
+                .build();
+        mFlingAnimationUtilsClosing = fauBuilder
+                .reset()
+                .setMaxLengthSeconds(FLING_CLOSING_MAX_LENGTH_SECONDS)
+                .setSpeedUpFactor(FLING_CLOSING_SPEED_UP_FACTOR)
+                .build();
+        mFlingAnimationUtilsDismissing = fauBuilder
+                .reset()
+                .setMaxLengthSeconds(0.5f)
+                .setSpeedUpFactor(0.6f)
+                .setX2(0.6f)
+                .setY2(0.84f)
+                .build();
+        mLatencyTracker = latencyTracker;
+        mBounceInterpolator = new BounceInterpolator();
+        mFalsingManager = falsingManager;
+        mDozeLog = dozeLog;
+        mNotificationsDragEnabled = mResources.getBoolean(
+                R.bool.config_enableNotificationShadeDrag);
         mVibratorHelper = vibratorHelper;
+        mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation);
+        mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
+        mInteractionJankMonitor = interactionJankMonitor;
+        mSystemClock = systemClock;
         mKeyguardMediaController = keyguardMediaController;
         mPrivacyDotViewController = privacyDotViewController;
         mMetricsLogger = metricsLogger;
         mConfigurationController = configurationController;
         mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder;
         mMediaHierarchyManager = mediaHierarchyManager;
-        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mNotificationsQSContainerController = notificationsQSContainerController;
         mNotificationListContainer = notificationListContainer;
         mNotificationStackSizeCalculator = notificationStackSizeCalculator;
@@ -824,7 +968,6 @@
         mLargeScreenShadeHeaderController = largeScreenShadeHeaderController;
         mLayoutInflater = layoutInflater;
         mFeatureFlags = featureFlags;
-        mFalsingManager = falsingManager;
         mFalsingCollector = falsingCollector;
         mPowerManager = powerManager;
         mWakeUpCoordinator = coordinator;
@@ -840,7 +983,6 @@
         mUserManager = userManager;
         mMediaDataManager = mediaDataManager;
         mTapAgainViewController = tapAgainViewController;
-        mInteractionJankMonitor = interactionJankMonitor;
         mSysUiState = sysUiState;
         mPanelEventsEmitter = panelEventsEmitter;
         pulseExpansionHandler.setPulseExpandAbortListener(() -> {
@@ -860,7 +1002,7 @@
                 new DynamicPrivacyControlListener();
         dynamicPrivacyController.addListener(dynamicPrivacyControlListener);
 
-        panelExpansionStateManager.addStateListener(this::onPanelStateChanged);
+        shadeExpansionStateManager.addStateListener(this::onPanelStateChanged);
 
         mBottomAreaShadeAlphaAnimator = ValueAnimator.ofFloat(1f, 0);
         mBottomAreaShadeAlphaAnimator.addUpdateListener(animation -> {
@@ -1042,9 +1184,14 @@
                 controller.setup(mNotificationContainerParent));
     }
 
-    @Override
-    protected void loadDimens() {
-        super.loadDimens();
+    @VisibleForTesting
+    void loadDimens() {
+        final ViewConfiguration configuration = ViewConfiguration.get(this.mView.getContext());
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
+        mHintDistance = mResources.getDimension(R.dimen.hint_move_distance);
+        mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount);
+        mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade);
         mFlingAnimationUtils = mFlingAnimationUtilsBuilder.get()
                 .setMaxLengthSeconds(0.4f).build();
         mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mView.getContext());
@@ -1717,11 +1864,10 @@
             // it's possible that nothing animated, so we replicate the termination
             // conditions of panelExpansionChanged here
             // TODO(b/200063118): This can likely go away in a future refactor CL.
-            getPanelExpansionStateManager().updateState(STATE_CLOSED);
+            getShadeExpansionStateManager().updateState(STATE_CLOSED);
         }
     }
 
-    @Override
     public void collapse(boolean delayed, float speedUpFactor) {
         if (!canPanelBeCollapsed()) {
             return;
@@ -1731,7 +1877,20 @@
             setQsExpandImmediate(true);
             setShowShelfOnly(true);
         }
-        super.collapse(delayed, speedUpFactor);
+        if (DEBUG) this.logf("collapse: " + this);
+        if (canPanelBeCollapsed()) {
+            cancelHeightAnimator();
+            notifyExpandingStarted();
+
+            // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
+            setIsClosing(true);
+            if (delayed) {
+                mNextCollapseSpeedUpFactor = speedUpFactor;
+                this.mView.postDelayed(mFlingCollapseRunnable, 120);
+            } else {
+                fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
+            }
+        }
     }
 
     private void setQsExpandImmediate(boolean expandImmediate) {
@@ -1751,10 +1910,15 @@
         setQsExpansion(mQsMinExpansionHeight);
     }
 
-    @Override
     @VisibleForTesting
-    protected void cancelHeightAnimator() {
-        super.cancelHeightAnimator();
+    void cancelHeightAnimator() {
+        if (mHeightAnimator != null) {
+            if (mHeightAnimator.isRunning()) {
+                mPanelUpdateWhenAnimatorEnds = false;
+            }
+            mHeightAnimator.cancel();
+        }
+        endClosing();
     }
 
     public void cancelAnimation() {
@@ -1822,28 +1986,123 @@
         }
     }
 
-    @Override
     public void fling(float vel, boolean expand) {
         GestureRecorder gr = mCentralSurfaces.getGestureRecorder();
         if (gr != null) {
             gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
         }
-        super.fling(vel, expand);
+        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
     }
 
-    @Override
-    protected void flingToHeight(float vel, boolean expand, float target,
+    @VisibleForTesting
+    void flingToHeight(float vel, boolean expand, float target,
             float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
         mHeadsUpTouchHelper.notifyFling(!expand);
         mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */);
         setClosingWithAlphaFadeout(!expand && !isOnKeyguard() && getFadeoutAlpha() == 1.0f);
         mNotificationStackScrollLayoutController.setPanelFlinging(true);
-        super.flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
+        if (target == mExpandedHeight && mOverExpansion == 0.0f) {
+            // We're at the target and didn't fling and there's no overshoot
+            onFlingEnd(false /* cancelled */);
+            return;
+        }
+        mIsFlinging = true;
+        // we want to perform an overshoot animation when flinging open
+        final boolean addOverscroll =
+                expand
+                        && !mInSplitShade // Split shade has its own overscroll logic
+                        && mStatusBarStateController.getState() != KEYGUARD
+                        && mOverExpansion == 0.0f
+                        && vel >= 0;
+        final boolean shouldSpringBack = addOverscroll || (mOverExpansion != 0.0f && expand);
+        float overshootAmount = 0.0f;
+        if (addOverscroll) {
+            // Let's overshoot depending on the amount of velocity
+            overshootAmount = MathUtils.lerp(
+                    0.2f,
+                    1.0f,
+                    MathUtils.saturate(vel
+                            / (this.mFlingAnimationUtils.getHighVelocityPxPerSecond()
+                            * FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT)));
+            overshootAmount += mOverExpansion / mPanelFlingOvershootAmount;
+        }
+        ValueAnimator animator = createHeightAnimator(target, overshootAmount);
+        if (expand) {
+            if (expandBecauseOfFalsing && vel < 0) {
+                vel = 0;
+            }
+            this.mFlingAnimationUtils.apply(animator, mExpandedHeight,
+                    target + overshootAmount * mPanelFlingOvershootAmount, vel,
+                    this.mView.getHeight());
+            if (vel == 0) {
+                animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION);
+            }
+        } else {
+            if (shouldUseDismissingAnimation()) {
+                if (vel == 0) {
+                    animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
+                    long duration = (long) (200 + mExpandedHeight / this.mView.getHeight() * 100);
+                    animator.setDuration(duration);
+                } else {
+                    mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel,
+                            this.mView.getHeight());
+                }
+            } else {
+                mFlingAnimationUtilsClosing.apply(
+                        animator, mExpandedHeight, target, vel, this.mView.getHeight());
+            }
+
+            // Make it shorter if we run a canned animation
+            if (vel == 0) {
+                animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor));
+            }
+            if (mFixedDuration != NO_FIXED_DURATION) {
+                animator.setDuration(mFixedDuration);
+            }
+        }
+        animator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                if (!mStatusBarStateController.isDozing()) {
+                    beginJankMonitoring();
+                }
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (shouldSpringBack && !mCancelled) {
+                    // After the shade is flinged open to an overscrolled state, spring back
+                    // the shade by reducing section padding to 0.
+                    springBack();
+                } else {
+                    onFlingEnd(mCancelled);
+                }
+            }
+        });
+        setAnimator(animator);
+        animator.start();
     }
 
-    @Override
-    protected void onFlingEnd(boolean cancelled) {
-        super.onFlingEnd(cancelled);
+    private void onFlingEnd(boolean cancelled) {
+        mIsFlinging = false;
+        // No overshoot when the animation ends
+        setOverExpansionInternal(0, false /* isFromGesture */);
+        setAnimator(null);
+        mKeyguardStateController.notifyPanelFlingEnd();
+        if (!cancelled) {
+            endJankMonitoring();
+            notifyExpandingFinished();
+        } else {
+            cancelJankMonitoring();
+        }
+        updatePanelExpansionAndVisibility();
         mNotificationStackScrollLayoutController.setPanelFlinging(false);
     }
 
@@ -1938,8 +2197,7 @@
         return mQsTracking;
     }
 
-    @Override
-    protected boolean isInContentBounds(float x, float y) {
+    private boolean isInContentBounds(float x, float y) {
         float stackScrollerX = mNotificationStackScrollLayoutController.getX();
         return !mNotificationStackScrollLayoutController
                 .isBelowLastNotification(x - stackScrollerX, y)
@@ -2072,9 +2330,8 @@
                         - mQsMinExpansionHeight));
     }
 
-    @Override
-    protected boolean shouldExpandWhenNotFlinging() {
-        if (super.shouldExpandWhenNotFlinging()) {
+    private boolean shouldExpandWhenNotFlinging() {
+        if (getExpandedFraction() > 0.5f) {
             return true;
         }
         if (mAllowExpandForSmallExpansion) {
@@ -2086,8 +2343,7 @@
         return false;
     }
 
-    @Override
-    protected float getOpeningHeight() {
+    private float getOpeningHeight() {
         return mNotificationStackScrollLayoutController.getOpeningHeight();
     }
 
@@ -2238,9 +2494,20 @@
         }
     }
 
-    @Override
-    protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
-        boolean expands = super.flingExpands(vel, vectorVel, x, y);
+    private boolean flingExpands(float vel, float vectorVel, float x, float y) {
+        boolean expands = true;
+        if (!this.mFalsingManager.isUnlockingDisabled()) {
+            @Classifier.InteractionType int interactionType = y - mInitialExpandY > 0
+                    ? QUICK_SETTINGS : (
+                    mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK);
+            if (!isFalseTouch(x, y, interactionType)) {
+                if (Math.abs(vectorVel) < this.mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
+                    expands = shouldExpandWhenNotFlinging();
+                } else {
+                    expands = vel > 0;
+                }
+            }
+        }
 
         // If we are already running a QS expansion, make sure that we keep the panel open.
         if (mQsExpansionAnimator != null) {
@@ -2249,8 +2516,7 @@
         return expands;
     }
 
-    @Override
-    protected boolean shouldGestureWaitForTouchSlop() {
+    private boolean shouldGestureWaitForTouchSlop() {
         if (mExpectingSynthesizedDown) {
             mExpectingSynthesizedDown = false;
             return false;
@@ -2328,7 +2594,7 @@
         }
     }
 
-    protected int getFalsingThreshold() {
+    private int getFalsingThreshold() {
         float factor = mCentralSurfaces.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
         return (int) (mQsFalsingThreshold * factor);
     }
@@ -3068,8 +3334,8 @@
         }
     }
 
-    @Override
-    protected boolean canCollapsePanelOnTouch() {
+    @VisibleForTesting
+    boolean canCollapsePanelOnTouch() {
         if (!isInSettings() && mBarState == KEYGUARD) {
             return true;
         }
@@ -3081,7 +3347,6 @@
         return !mSplitShadeEnabled && (isInSettings() || mIsPanelCollapseOnQQS);
     }
 
-    @Override
     public int getMaxPanelHeight() {
         int min = mStatusBarMinHeight;
         if (!(mBarState == KEYGUARD)
@@ -3115,8 +3380,7 @@
         return mIsExpanding;
     }
 
-    @Override
-    protected void onHeightUpdated(float expandedHeight) {
+    private void onHeightUpdated(float expandedHeight) {
         if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) {
             // Updating the clock position will set the top padding which might
             // trigger a new panel height and re-position the clock.
@@ -3295,9 +3559,7 @@
         mLockIconViewController.setAlpha(alpha);
     }
 
-    @Override
-    protected void onExpandingStarted() {
-        super.onExpandingStarted();
+    private void onExpandingStarted() {
         mNotificationStackScrollLayoutController.onExpansionStarted();
         mIsExpanding = true;
         mQsExpandedWhenExpandingStarted = mQsFullyExpanded;
@@ -3313,8 +3575,7 @@
         mQs.setHeaderListening(true);
     }
 
-    @Override
-    protected void onExpandingFinished() {
+    private void onExpandingFinished() {
         mScrimController.onExpandingFinished();
         mNotificationStackScrollLayoutController.onExpansionStopped();
         mHeadsUpManager.onExpandingFinished();
@@ -3366,18 +3627,58 @@
         mQs.setListening(listening);
     }
 
-    @Override
     public void expand(boolean animate) {
-        super.expand(animate);
+        if (isFullyCollapsed() || isCollapsing()) {
+            mInstantExpanding = true;
+            mAnimateAfterExpanding = animate;
+            mUpdateFlingOnLayout = false;
+            abortAnimations();
+            if (mTracking) {
+                // The panel is expanded after this call.
+                onTrackingStopped(true /* expands */);
+            }
+            if (mExpanding) {
+                notifyExpandingFinished();
+            }
+            updatePanelExpansionAndVisibility();
+            // Wait for window manager to pickup the change, so we know the maximum height of the
+            // panel then.
+            this.mView.getViewTreeObserver().addOnGlobalLayoutListener(
+                    new ViewTreeObserver.OnGlobalLayoutListener() {
+                        @Override
+                        public void onGlobalLayout() {
+                            if (!mInstantExpanding) {
+                                mView.getViewTreeObserver().removeOnGlobalLayoutListener(
+                                        this);
+                                return;
+                            }
+                            if (mCentralSurfaces.getNotificationShadeWindowView()
+                                    .isVisibleToUser()) {
+                                mView.getViewTreeObserver().removeOnGlobalLayoutListener(
+                                        this);
+                                if (mAnimateAfterExpanding) {
+                                    notifyExpandingStarted();
+                                    beginJankMonitoring();
+                                    fling(0, true /* expand */);
+                                } else {
+                                    setExpandedFraction(1f);
+                                }
+                                mInstantExpanding = false;
+                            }
+                        }
+                    });
+            // Make sure a layout really happens.
+            this.mView.requestLayout();
+        }
+
         setListening(true);
     }
 
-    @Override
     public void setOverExpansion(float overExpansion) {
         if (overExpansion == mOverExpansion) {
             return;
         }
-        super.setOverExpansion(overExpansion);
+        mOverExpansion = overExpansion;
         // Translating the quick settings by half the overexpansion to center it in the background
         // frame
         updateQsFrameTranslation();
@@ -3385,14 +3686,18 @@
     }
 
     private void updateQsFrameTranslation() {
-        mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs, mOverExpansion,
-                mQsTranslationForFullShadeTransition);
+        mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs,
+                mNavigationBarBottomHeight + mAmbientState.getStackTopMargin());
+
     }
 
-    @Override
-    protected void onTrackingStarted() {
+    private void onTrackingStarted() {
         mFalsingCollector.onTrackingStarted(!mKeyguardStateController.canDismissLockScreen());
-        super.onTrackingStarted();
+        endClosing();
+        mTracking = true;
+        mCentralSurfaces.onTrackingStarted();
+        notifyExpandingStarted();
+        updatePanelExpansionAndVisibility();
         mScrimController.onTrackingStarted();
         if (mQsFullyExpanded) {
             setQsExpandImmediate(true);
@@ -3402,10 +3707,11 @@
         cancelPendingPanelCollapse();
     }
 
-    @Override
-    protected void onTrackingStopped(boolean expand) {
+    private void onTrackingStopped(boolean expand) {
         mFalsingCollector.onTrackingStopped();
-        super.onTrackingStopped(expand);
+        mTracking = false;
+        mCentralSurfaces.onTrackingStopped(expand);
+        updatePanelExpansionAndVisibility();
         if (expand) {
             mNotificationStackScrollLayoutController.setOverScrollAmount(0.0f, true /* onTop */,
                     true /* animate */);
@@ -3422,37 +3728,48 @@
                 getHeight(), mNavigationBarBottomHeight);
     }
 
-    @Override
-    protected void startUnlockHintAnimation() {
+    @VisibleForTesting
+    void startUnlockHintAnimation() {
         if (mPowerManager.isPowerSaveMode() || mAmbientState.getDozeAmount() > 0f) {
             onUnlockHintStarted();
             onUnlockHintFinished();
             return;
         }
-        super.startUnlockHintAnimation();
+
+        // We don't need to hint the user if an animation is already running or the user is changing
+        // the expansion.
+        if (mHeightAnimator != null || mTracking) {
+            return;
+        }
+        notifyExpandingStarted();
+        startUnlockHintAnimationPhase1(() -> {
+            notifyExpandingFinished();
+            onUnlockHintFinished();
+            mHintAnimationRunning = false;
+        });
+        onUnlockHintStarted();
+        mHintAnimationRunning = true;
     }
 
-    @Override
-    protected void onUnlockHintFinished() {
-        super.onUnlockHintFinished();
+    @VisibleForTesting
+    void onUnlockHintFinished() {
+        mCentralSurfaces.onHintFinished();
         mScrimController.setExpansionAffectsAlpha(true);
         mNotificationStackScrollLayoutController.setUnlockHintRunning(false);
     }
 
-    @Override
-    protected void onUnlockHintStarted() {
-        super.onUnlockHintStarted();
+    @VisibleForTesting
+    void onUnlockHintStarted() {
+        mCentralSurfaces.onUnlockHintStarted();
         mScrimController.setExpansionAffectsAlpha(false);
         mNotificationStackScrollLayoutController.setUnlockHintRunning(true);
     }
 
-    @Override
-    protected boolean shouldUseDismissingAnimation() {
+    private boolean shouldUseDismissingAnimation() {
         return mBarState != StatusBarState.SHADE && (mKeyguardStateController.canDismissLockScreen()
                 || !isTracking());
     }
 
-    @Override
     public int getMaxPanelTransitionDistance() {
         // Traditionally the value is based on the number of notifications. On split-shade, we want
         // the required distance to be a specific and constant value, to make sure the expansion
@@ -3477,8 +3794,8 @@
         }
     }
 
-    @Override
-    protected boolean isTrackingBlocked() {
+    @VisibleForTesting
+    boolean isTrackingBlocked() {
         return mConflictingQsExpansionGesture && mQsExpanded || mBlockingExpansionForCurrentTouch;
     }
 
@@ -3500,19 +3817,17 @@
         return mIsLaunchTransitionFinished;
     }
 
-    @Override
     public void setIsLaunchAnimationRunning(boolean running) {
         boolean wasRunning = mIsLaunchAnimationRunning;
-        super.setIsLaunchAnimationRunning(running);
+        mIsLaunchAnimationRunning = running;
         if (wasRunning != mIsLaunchAnimationRunning) {
             mPanelEventsEmitter.notifyLaunchingActivityChanged(running);
         }
     }
 
-    @Override
-    protected void setIsClosing(boolean isClosing) {
+    private void setIsClosing(boolean isClosing) {
         boolean wasClosing = isClosing();
-        super.setIsClosing(isClosing);
+        mClosing = isClosing;
         if (wasClosing != isClosing) {
             mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing);
         }
@@ -3525,7 +3840,6 @@
         }
     }
 
-    @Override
     public boolean isDozing() {
         return mDozing;
     }
@@ -3542,8 +3856,7 @@
         mKeyguardStatusViewController.dozeTimeTick();
     }
 
-    @Override
-    protected boolean onMiddleClicked() {
+    private boolean onMiddleClicked() {
         switch (mBarState) {
             case KEYGUARD:
                 if (!mDozingOnDown) {
@@ -3602,15 +3915,13 @@
         updateVisibility();
     }
 
-    @Override
-    protected boolean shouldPanelBeVisible() {
+    private boolean shouldPanelBeVisible() {
         boolean headsUpVisible = mHeadsUpAnimatingAway || mHeadsUpPinnedMode;
         return headsUpVisible || isExpanded() || mBouncerShowing;
     }
 
-    @Override
     public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
-        super.setHeadsUpManager(headsUpManager);
+        mHeadsUpManager = headsUpManager;
         mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager,
                 mNotificationStackScrollLayoutController.getHeadsUpCallback(),
                 NotificationPanelViewController.this);
@@ -3624,8 +3935,7 @@
         // otherwise we update the state when the expansion is finished
     }
 
-    @Override
-    protected void onClosingFinished() {
+    private void onClosingFinished() {
         mCentralSurfaces.onClosingFinished();
         setClosingWithAlphaFadeout(false);
         mMediaHierarchyManager.closeGuts();
@@ -3714,8 +4024,7 @@
         mCentralSurfaces.clearNotificationEffects();
     }
 
-    @Override
-    protected boolean isPanelVisibleBecauseOfHeadsUp() {
+    private boolean isPanelVisibleBecauseOfHeadsUp() {
         return (mHeadsUpManager.hasPinnedHeadsUp() || mHeadsUpAnimatingAway)
                 && mBarState == StatusBarState.SHADE;
     }
@@ -3830,9 +4139,15 @@
         mNotificationBoundsAnimationDelay = delay;
     }
 
-    @Override
     public void setTouchAndAnimationDisabled(boolean disabled) {
-        super.setTouchAndAnimationDisabled(disabled);
+        mTouchDisabled = disabled;
+        if (mTouchDisabled) {
+            cancelHeightAnimator();
+            if (mTracking) {
+                onTrackingStopped(true /* expanded */);
+            }
+            notifyExpandingFinished();
+        }
         mNotificationStackScrollLayoutController.setAnimationsEnabled(!disabled);
     }
 
@@ -4032,9 +4347,14 @@
         mBlockingExpansionForCurrentTouch = mTracking;
     }
 
-    @Override
     public void dump(PrintWriter pw, String[] args) {
-        super.dump(pw, args);
+        pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
+                        + " tracking=%s timeAnim=%s%s "
+                        + "touchDisabled=%s" + "]",
+                this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(),
+                mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator,
+                ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""),
+                mTouchDisabled ? "T" : "f"));
         IndentingPrintWriter ipw = asIndenting(pw);
         ipw.increaseIndent();
         ipw.println("gestureExclusionRect:" + calculateGestureExclusionRect());
@@ -4177,126 +4497,13 @@
         mConfigurationListener.onThemeChanged();
     }
 
-    @Override
-    protected OnLayoutChangeListener createLayoutChangeListener() {
-        return new OnLayoutChangeListenerImpl();
+    private OnLayoutChangeListener createLayoutChangeListener() {
+        return new OnLayoutChangeListener();
     }
 
-    @Override
-    protected TouchHandler createTouchHandler() {
-        return new TouchHandler() {
-
-            private long mLastTouchDownTime = -1L;
-
-            @Override
-            public boolean onInterceptTouchEvent(MotionEvent event) {
-                if (SPEW_LOGCAT) {
-                    Log.v(TAG,
-                            "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX()
-                                    + "," + event.getY() + ")");
-                }
-                if (mQs.disallowPanelTouches()) {
-                    return false;
-                }
-                initDownStates(event);
-                // Do not let touches go to shade or QS if the bouncer is visible,
-                // but still let user swipe down to expand the panel, dismissing the bouncer.
-                if (mCentralSurfaces.isBouncerShowing()) {
-                    return true;
-                }
-                if (mCommandQueue.panelsEnabled()
-                        && !mNotificationStackScrollLayoutController.isLongPressInProgress()
-                        && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
-                    mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
-                    mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
-                    return true;
-                }
-                if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0)
-                        && mPulseExpansionHandler.onInterceptTouchEvent(event)) {
-                    return true;
-                }
-
-                if (!isFullyCollapsed() && onQsIntercept(event)) {
-                    if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true");
-                    return true;
-                }
-                return super.onInterceptTouchEvent(event);
-            }
-
-            @Override
-            public boolean onTouch(View v, MotionEvent event) {
-                if (event.getAction() == MotionEvent.ACTION_DOWN) {
-                    if (event.getDownTime() == mLastTouchDownTime) {
-                        // An issue can occur when swiping down after unlock, where multiple down
-                        // events are received in this handler with identical downTimes. Until the
-                        // source of the issue can be located, detect this case and ignore.
-                        // see b/193350347
-                        Log.w(TAG, "Duplicate down event detected... ignoring");
-                        return true;
-                    }
-                    mLastTouchDownTime = event.getDownTime();
-                }
-
-
-                if (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches()) {
-                    return false;
-                }
-
-                // Do not allow panel expansion if bouncer is scrimmed or showing over a dream,
-                // otherwise user would be able to pull down QS or expand the shade.
-                if (mCentralSurfaces.isBouncerShowingScrimmed()
-                        || mCentralSurfaces.isBouncerShowingOverDream()) {
-                    return false;
-                }
-
-                // Make sure the next touch won't the blocked after the current ends.
-                if (event.getAction() == MotionEvent.ACTION_UP
-                        || event.getAction() == MotionEvent.ACTION_CANCEL) {
-                    mBlockingExpansionForCurrentTouch = false;
-                }
-                // When touch focus transfer happens, ACTION_DOWN->ACTION_UP may happen immediately
-                // without any ACTION_MOVE event.
-                // In such case, simply expand the panel instead of being stuck at the bottom bar.
-                if (mLastEventSynthesizedDown && event.getAction() == MotionEvent.ACTION_UP) {
-                    expand(true /* animate */);
-                }
-                initDownStates(event);
-
-                // If pulse is expanding already, let's give it the touch. There are situations
-                // where the panel starts expanding even though we're also pulsing
-                boolean pulseShouldGetTouch = (!mIsExpanding
-                        && !shouldQuickSettingsIntercept(mDownX, mDownY, 0))
-                        || 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()
-                        && !mNotificationStackScrollLayoutController.isLongPressInProgress()
-                        && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
-                    mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
-                }
-                boolean handled = mHeadsUpTouchHelper.onTouchEvent(event);
-
-                if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
-                    mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
-                    return true;
-                }
-                if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
-                    mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
-                    handled = true;
-                }
-
-                if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded()
-                        && mStatusBarKeyguardViewManager.isShowing()) {
-                    mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX());
-                }
-
-                handled |= super.onTouch(v, event);
-                return !mDozing || mPulsing || handled;
-            }
-        };
+    @VisibleForTesting
+    TouchHandler createTouchHandler() {
+        return new TouchHandler();
     }
 
     private final PhoneStatusBarView.TouchEventHandler mStatusBarViewTouchEventHandler =
@@ -4348,8 +4555,7 @@
                 }
             };
 
-    @Override
-    protected OnConfigurationChangedListener createOnConfigurationChangedListener() {
+    private OnConfigurationChangedListener createOnConfigurationChangedListener() {
         return new OnConfigurationChangedListener();
     }
 
@@ -4407,10 +4613,597 @@
         }
         mSysUiState.setFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
                         isFullyExpanded() && !isInSettings())
-                .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, isInSettings())
+                .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, isFullyExpanded() && isInSettings())
                 .commitUpdate(mDisplayId);
     }
 
+    private void logf(String fmt, Object... args) {
+        Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
+    }
+
+    private void notifyExpandingStarted() {
+        if (!mExpanding) {
+            mExpanding = true;
+            onExpandingStarted();
+        }
+    }
+
+    private void notifyExpandingFinished() {
+        endClosing();
+        if (mExpanding) {
+            mExpanding = false;
+            onExpandingFinished();
+        }
+    }
+
+    private float getTouchSlop(MotionEvent event) {
+        // Adjust the touch slop if another gesture may be being performed.
+        return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
+                ? mTouchSlop * mSlopMultiplier
+                : mTouchSlop;
+    }
+
+    private void addMovement(MotionEvent event) {
+        // Add movement to velocity tracker using raw screen X and Y coordinates instead
+        // of window coordinates because the window frame may be moving at the same time.
+        float deltaX = event.getRawX() - event.getX();
+        float deltaY = event.getRawY() - event.getY();
+        event.offsetLocation(deltaX, deltaY);
+        mVelocityTracker.addMovement(event);
+        event.offsetLocation(-deltaX, -deltaY);
+    }
+
+    /** If the latency tracker is enabled, begins tracking expand latency. */
+    public void startExpandLatencyTracking() {
+        if (mLatencyTracker.isEnabled()) {
+            mLatencyTracker.onActionStart(LatencyTracker.ACTION_EXPAND_PANEL);
+            mExpandLatencyTracking = true;
+        }
+    }
+
+    private void startOpening(MotionEvent event) {
+        updatePanelExpansionAndVisibility();
+        // Reset at start so haptic can be triggered as soon as panel starts to open.
+        mHasVibratedOnOpen = false;
+        //TODO: keyguard opens QS a different way; log that too?
+
+        // Log the position of the swipe that opened the panel
+        float width = mCentralSurfaces.getDisplayWidth();
+        float height = mCentralSurfaces.getDisplayHeight();
+        int rot = mCentralSurfaces.getRotation();
+
+        mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND,
+                (int) (event.getX() / width * 100), (int) (event.getY() / height * 100), rot);
+        mLockscreenGestureLogger
+                .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND);
+    }
+
+    /**
+     * Maybe vibrate as panel is opened.
+     *
+     * @param openingWithTouch Whether the panel is being opened with touch. If the panel is instead
+     * being opened programmatically (such as by the open panel gesture), we always play haptic.
+     */
+    private void maybeVibrateOnOpening(boolean openingWithTouch) {
+        if (mVibrateOnOpening) {
+            if (!openingWithTouch || !mHasVibratedOnOpen) {
+                mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+                mHasVibratedOnOpen = true;
+            }
+        }
+    }
+
+    /**
+     * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
+     * horizontal direction
+     */
+    private boolean isDirectionUpwards(float x, float y) {
+        float xDiff = x - mInitialExpandX;
+        float yDiff = y - mInitialExpandY;
+        if (yDiff >= 0) {
+            return false;
+        }
+        return Math.abs(yDiff) >= Math.abs(xDiff);
+    }
+
+    /** Called when a MotionEvent is about to trigger Shade expansion. */
+    public void startExpandMotion(float newX, float newY, boolean startTracking,
+            float expandedHeight) {
+        if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) {
+            beginJankMonitoring();
+        }
+        mInitialOffsetOnTouch = expandedHeight;
+        mInitialExpandY = newY;
+        mInitialExpandX = newX;
+        mInitialTouchFromKeyguard = mKeyguardStateController.isShowing();
+        if (startTracking) {
+            mTouchSlopExceeded = true;
+            setExpandedHeight(mInitialOffsetOnTouch);
+            onTrackingStarted();
+        }
+    }
+
+    private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
+        mTrackingPointer = -1;
+        mAmbientState.setSwipingUp(false);
+        if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop
+                || Math.abs(y - mInitialExpandY) > mTouchSlop
+                || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
+            mVelocityTracker.computeCurrentVelocity(1000);
+            float vel = mVelocityTracker.getYVelocity();
+            float vectorVel = (float) Math.hypot(
+                    mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+
+            final boolean onKeyguard = mKeyguardStateController.isShowing();
+            final boolean expand;
+            if (mKeyguardStateController.isKeyguardFadingAway()
+                    || (mInitialTouchFromKeyguard && !onKeyguard)) {
+                // Don't expand for any touches that started from the keyguard and ended after the
+                // keyguard is gone.
+                expand = false;
+            } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
+                if (onKeyguard) {
+                    expand = true;
+                } else if (mCentralSurfaces.isBouncerShowingOverDream()) {
+                    expand = false;
+                } else {
+                    // If we get a cancel, put the shade back to the state it was in when the
+                    // gesture started
+                    expand = !mPanelClosedOnDown;
+                }
+            } else {
+                expand = flingExpands(vel, vectorVel, x, y);
+            }
+
+            mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
+                    mCentralSurfaces.isFalsingThresholdNeeded(),
+                    mCentralSurfaces.isWakeUpComingFromTouch());
+            // Log collapse gesture if on lock screen.
+            if (!expand && onKeyguard) {
+                float displayDensity = mCentralSurfaces.getDisplayDensity();
+                int heightDp = (int) Math.abs((y - mInitialExpandY) / displayDensity);
+                int velocityDp = (int) Math.abs(vel / displayDensity);
+                mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp);
+                mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK);
+            }
+            @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC
+                    : y - mInitialExpandY > 0 ? QUICK_SETTINGS
+                            : (mKeyguardStateController.canDismissLockScreen()
+                                    ? UNLOCK : BOUNCER_UNLOCK);
+
+            fling(vel, expand, isFalseTouch(x, y, interactionType));
+            onTrackingStopped(expand);
+            mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
+            if (mUpdateFlingOnLayout) {
+                mUpdateFlingVelocity = vel;
+            }
+        } else if (!mCentralSurfaces.isBouncerShowing()
+                && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()
+                && !mKeyguardStateController.isKeyguardGoingAway()) {
+            boolean expands = onEmptySpaceClick();
+            onTrackingStopped(expands);
+        }
+        mVelocityTracker.clear();
+    }
+
+    private float getCurrentExpandVelocity() {
+        mVelocityTracker.computeCurrentVelocity(1000);
+        return mVelocityTracker.getYVelocity();
+    }
+
+    private void endClosing() {
+        if (mClosing) {
+            setIsClosing(false);
+            onClosingFinished();
+        }
+    }
+
+    /**
+     * @param x the final x-coordinate when the finger was lifted
+     * @param y the final y-coordinate when the finger was lifted
+     * @return whether this motion should be regarded as a false touch
+     */
+    private boolean isFalseTouch(float x, float y,
+            @Classifier.InteractionType int interactionType) {
+        if (!mCentralSurfaces.isFalsingThresholdNeeded()) {
+            return false;
+        }
+        if (mFalsingManager.isClassifierEnabled()) {
+            return mFalsingManager.isFalseTouch(interactionType);
+        }
+        if (!mTouchAboveFalsingThreshold) {
+            return true;
+        }
+        if (mUpwardsWhenThresholdReached) {
+            return false;
+        }
+        return !isDirectionUpwards(x, y);
+    }
+
+    private void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
+        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
+    }
+
+    private void fling(float vel, boolean expand, float collapseSpeedUpFactor,
+            boolean expandBecauseOfFalsing) {
+        float target = expand ? getMaxPanelHeight() : 0;
+        if (!expand) {
+            setIsClosing(true);
+        }
+        flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
+    }
+
+    private void springBack() {
+        if (mOverExpansion == 0) {
+            onFlingEnd(false /* cancelled */);
+            return;
+        }
+        mIsSpringBackAnimation = true;
+        ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0);
+        animator.addUpdateListener(
+                animation -> setOverExpansionInternal((float) animation.getAnimatedValue(),
+                        false /* isFromGesture */));
+        animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION);
+        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        animator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mIsSpringBackAnimation = false;
+                onFlingEnd(mCancelled);
+            }
+        });
+        setAnimator(animator);
+        animator.start();
+    }
+
+    public String getName() {
+        return mViewName;
+    }
+
+    @VisibleForTesting
+    void setExpandedHeight(float height) {
+        if (DEBUG) logf("setExpandedHeight(%.1f)", height);
+        setExpandedHeightInternal(height);
+    }
+
+    private void updateExpandedHeightToMaxHeight() {
+        float currentMaxPanelHeight = getMaxPanelHeight();
+
+        if (isFullyCollapsed()) {
+            return;
+        }
+
+        if (currentMaxPanelHeight == mExpandedHeight) {
+            return;
+        }
+
+        if (mTracking && !isTrackingBlocked()) {
+            return;
+        }
+
+        if (mHeightAnimator != null && !mIsSpringBackAnimation) {
+            mPanelUpdateWhenAnimatorEnds = true;
+            return;
+        }
+
+        setExpandedHeight(currentMaxPanelHeight);
+    }
+
+    private void setExpandedHeightInternal(float h) {
+        if (isNaN(h)) {
+            Log.wtf(TAG, "ExpandedHeight set to NaN");
+        }
+        mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
+            if (mExpandLatencyTracking && h != 0f) {
+                DejankUtils.postAfterTraversal(
+                        () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL));
+                mExpandLatencyTracking = false;
+            }
+            float maxPanelHeight = getMaxPanelTransitionDistance();
+            if (mHeightAnimator == null) {
+                // Split shade has its own overscroll logic
+                if (mTracking && !mInSplitShade) {
+                    float overExpansionPixels = Math.max(0, h - maxPanelHeight);
+                    setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */);
+                }
+            }
+            mExpandedHeight = Math.min(h, maxPanelHeight);
+            // If we are closing the panel and we are almost there due to a slow decelerating
+            // interpolator, abort the animation.
+            if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
+                mExpandedHeight = 0f;
+                if (mHeightAnimator != null) {
+                    mHeightAnimator.end();
+                }
+            }
+            mExpansionDragDownAmountPx = h;
+            mExpandedFraction = Math.min(1f,
+                    maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
+            mAmbientState.setExpansionFraction(mExpandedFraction);
+            onHeightUpdated(mExpandedHeight);
+            updatePanelExpansionAndVisibility();
+        });
+    }
+
+    /**
+     * Set the current overexpansion
+     *
+     * @param overExpansion the amount of overexpansion to apply
+     * @param isFromGesture is this amount from a gesture and needs to be rubberBanded?
+     */
+    private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) {
+        if (!isFromGesture) {
+            mLastGesturedOverExpansion = -1;
+            setOverExpansion(overExpansion);
+        } else if (mLastGesturedOverExpansion != overExpansion) {
+            mLastGesturedOverExpansion = overExpansion;
+            final float heightForFullOvershoot = mView.getHeight() / 3.0f;
+            float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot);
+            newExpansion = Interpolators.getOvershootInterpolation(newExpansion);
+            setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f);
+        }
+    }
+
+    /** Sets the expanded height relative to a number from 0 to 1. */
+    public void setExpandedFraction(float frac) {
+        setExpandedHeight(getMaxPanelTransitionDistance() * frac);
+    }
+
+    @VisibleForTesting
+    float getExpandedHeight() {
+        return mExpandedHeight;
+    }
+
+    public float getExpandedFraction() {
+        return mExpandedFraction;
+    }
+
+    public boolean isFullyExpanded() {
+        return mExpandedHeight >= getMaxPanelHeight();
+    }
+
+    public boolean isFullyCollapsed() {
+        return mExpandedFraction <= 0.0f;
+    }
+
+    public boolean isCollapsing() {
+        return mClosing || mIsLaunchAnimationRunning;
+    }
+
+    public boolean isFlinging() {
+        return mIsFlinging;
+    }
+
+    public boolean isTracking() {
+        return mTracking;
+    }
+
+    /** Returns whether the shade can be collapsed. */
+    public boolean canPanelBeCollapsed() {
+        return !isFullyCollapsed() && !mTracking && !mClosing;
+    }
+
+    /** Collapses the shade instantly without animation. */
+    public void instantCollapse() {
+        abortAnimations();
+        setExpandedFraction(0f);
+        if (mExpanding) {
+            notifyExpandingFinished();
+        }
+        if (mInstantExpanding) {
+            mInstantExpanding = false;
+            updatePanelExpansionAndVisibility();
+        }
+    }
+
+    private void abortAnimations() {
+        cancelHeightAnimator();
+        mView.removeCallbacks(mFlingCollapseRunnable);
+    }
+
+    public boolean isUnlockHintRunning() {
+        return mHintAnimationRunning;
+    }
+
+    /**
+     * Phase 1: Move everything upwards.
+     */
+    private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
+        float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
+        ValueAnimator animator = createHeightAnimator(target);
+        animator.setDuration(250);
+        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        animator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mCancelled) {
+                    setAnimator(null);
+                    onAnimationFinished.run();
+                } else {
+                    startUnlockHintAnimationPhase2(onAnimationFinished);
+                }
+            }
+        });
+        animator.start();
+        setAnimator(animator);
+
+        final List<ViewPropertyAnimator> indicationAnimators =
+                mKeyguardBottomArea.getIndicationAreaAnimators();
+        for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) {
+            indicationAreaAnimator
+                    .translationY(-mHintDistance)
+                    .setDuration(250)
+                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                    .withEndAction(() -> indicationAreaAnimator
+                            .translationY(0)
+                            .setDuration(450)
+                            .setInterpolator(mBounceInterpolator)
+                            .start())
+                    .start();
+        }
+    }
+
+    private void setAnimator(ValueAnimator animator) {
+        mHeightAnimator = animator;
+        if (animator == null && mPanelUpdateWhenAnimatorEnds) {
+            mPanelUpdateWhenAnimatorEnds = false;
+            updateExpandedHeightToMaxHeight();
+        }
+    }
+
+    /**
+     * Phase 2: Bounce down.
+     */
+    private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
+        ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
+        animator.setDuration(450);
+        animator.setInterpolator(mBounceInterpolator);
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                setAnimator(null);
+                onAnimationFinished.run();
+                updatePanelExpansionAndVisibility();
+            }
+        });
+        animator.start();
+        setAnimator(animator);
+    }
+
+    private ValueAnimator createHeightAnimator(float targetHeight) {
+        return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */);
+    }
+
+    /**
+     * Create an animator that can also overshoot
+     *
+     * @param targetHeight the target height
+     * @param overshootAmount the amount of overshoot desired
+     */
+    private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) {
+        float startExpansion = mOverExpansion;
+        ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
+        animator.addUpdateListener(
+                animation -> {
+                    if (overshootAmount > 0.0f
+                            // Also remove the overExpansion when collapsing
+                            || (targetHeight == 0.0f && startExpansion != 0)) {
+                        final float expansion = MathUtils.lerp(
+                                startExpansion,
+                                mPanelFlingOvershootAmount * overshootAmount,
+                                Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
+                                        animator.getAnimatedFraction()));
+                        setOverExpansionInternal(expansion, false /* isFromGesture */);
+                    }
+                    setExpandedHeightInternal((float) animation.getAnimatedValue());
+                });
+        return animator;
+    }
+
+    /** Update the visibility of {@link NotificationPanelView} if necessary. */
+    private void updateVisibility() {
+        mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE);
+    }
+
+    /**
+     * Updates the panel expansion and {@link NotificationPanelView} visibility if necessary.
+     *
+     * TODO(b/200063118): Could public calls to this method be replaced with calls to
+     *   {@link #updateVisibility()}? That would allow us to make this method private.
+     */
+    public void updatePanelExpansionAndVisibility() {
+        mShadeExpansionStateManager.onPanelExpansionChanged(
+                mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
+        updateVisibility();
+    }
+
+    public boolean isExpanded() {
+        return mExpandedFraction > 0f
+                || mInstantExpanding
+                || isPanelVisibleBecauseOfHeadsUp()
+                || mTracking
+                || mHeightAnimator != null
+                && !mIsSpringBackAnimation;
+    }
+
+    /**
+     * Gets called when the user performs a click anywhere in the empty area of the panel.
+     *
+     * @return whether the panel will be expanded after the action performed by this method
+     */
+    private boolean onEmptySpaceClick() {
+        if (mHintAnimationRunning) {
+            return true;
+        }
+        return onMiddleClicked();
+    }
+
+    @VisibleForTesting
+    boolean isClosing() {
+        return mClosing;
+    }
+
+    /** Collapses the shade with an animation duration in milliseconds. */
+    public void collapseWithDuration(int animationDuration) {
+        mFixedDuration = animationDuration;
+        collapse(false /* delayed */, 1.0f /* speedUpFactor */);
+        mFixedDuration = NO_FIXED_DURATION;
+    }
+
+    /** Returns the NotificationPanelView. */
+    public ViewGroup getView() {
+        // TODO: remove this method, or at least reduce references to it.
+        return mView;
+    }
+
+    private void beginJankMonitoring() {
+        if (mInteractionJankMonitor == null) {
+            return;
+        }
+        InteractionJankMonitor.Configuration.Builder builder =
+                InteractionJankMonitor.Configuration.Builder.withView(
+                                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+                                mView)
+                        .setTag(isFullyCollapsed() ? "Expand" : "Collapse");
+        mInteractionJankMonitor.begin(builder);
+    }
+
+    private void endJankMonitoring() {
+        if (mInteractionJankMonitor == null) {
+            return;
+        }
+        InteractionJankMonitor.getInstance().end(
+                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
+    }
+
+    private void cancelJankMonitoring() {
+        if (mInteractionJankMonitor == null) {
+            return;
+        }
+        InteractionJankMonitor.getInstance().cancel(
+                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
+    }
+
+    private float getExpansionFraction() {
+        return mExpandedFraction;
+    }
+
+    private ShadeExpansionStateManager getShadeExpansionStateManager() {
+        return mShadeExpansionStateManager;
+    }
+
     private class OnHeightChangedListener implements ExpandableView.OnHeightChangedListener {
         @Override
         public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
@@ -4819,13 +5612,18 @@
         }
     }
 
-    private class OnLayoutChangeListenerImpl extends OnLayoutChangeListener {
-
+    private final class OnLayoutChangeListener implements View.OnLayoutChangeListener {
         @Override
         public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
                 int oldTop, int oldRight, int oldBottom) {
             DejankUtils.startDetectingBlockingIpcs("NVP#onLayout");
-            super.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom);
+            updateExpandedHeightToMaxHeight();
+            mHasLayoutedSinceDown = true;
+            if (mUpdateFlingOnLayout) {
+                abortAnimations();
+                fling(mUpdateFlingVelocity, true /* expands */);
+                mUpdateFlingOnLayout = false;
+            }
             updateMaxDisplayedNotifications(!shouldAvoidChangingNotificationsCount());
             setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth());
 
@@ -5083,4 +5881,361 @@
             }
         }
     }
+
+    /** Handles MotionEvents for the Shade. */
+    public final class TouchHandler implements View.OnTouchListener {
+        private long mLastTouchDownTime = -1L;
+
+        /** @see ViewGroup#onInterceptTouchEvent(MotionEvent)  */
+        public boolean onInterceptTouchEvent(MotionEvent event) {
+            if (SPEW_LOGCAT) {
+                Log.v(TAG,
+                        "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX()
+                                + "," + event.getY() + ")");
+            }
+            if (mQs.disallowPanelTouches()) {
+                return false;
+            }
+            initDownStates(event);
+            // Do not let touches go to shade or QS if the bouncer is visible,
+            // but still let user swipe down to expand the panel, dismissing the bouncer.
+            if (mCentralSurfaces.isBouncerShowing()) {
+                return true;
+            }
+            if (mCommandQueue.panelsEnabled()
+                    && !mNotificationStackScrollLayoutController.isLongPressInProgress()
+                    && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+                mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
+                mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
+                return true;
+            }
+            if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0)
+                    && mPulseExpansionHandler.onInterceptTouchEvent(event)) {
+                return true;
+            }
+
+            if (!isFullyCollapsed() && onQsIntercept(event)) {
+                if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true");
+                return true;
+            }
+            if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
+                    && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+                return false;
+            }
+
+            /* If the user drags anywhere inside the panel we intercept it if the movement is
+             upwards. This allows closing the shade from anywhere inside the panel.
+             We only do this if the current content is scrolled to the bottom, i.e.
+             canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling
+             gesture possible. */
+            int pointerIndex = event.findPointerIndex(mTrackingPointer);
+            if (pointerIndex < 0) {
+                pointerIndex = 0;
+                mTrackingPointer = event.getPointerId(pointerIndex);
+            }
+            final float x = event.getX(pointerIndex);
+            final float y = event.getY(pointerIndex);
+            boolean canCollapsePanel = canCollapsePanelOnTouch();
+
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                    mCentralSurfaces.userActivity();
+                    mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
+                    mMinExpandHeight = 0.0f;
+                    mDownTime = mSystemClock.uptimeMillis();
+                    if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) {
+                        cancelHeightAnimator();
+                        mTouchSlopExceeded = true;
+                        return true;
+                    }
+                    mInitialExpandY = y;
+                    mInitialExpandX = x;
+                    mTouchStartedInEmptyArea = !isInContentBounds(x, y);
+                    mTouchSlopExceeded = mTouchSlopExceededBeforeDown;
+                    mMotionAborted = false;
+                    mPanelClosedOnDown = isFullyCollapsed();
+                    mCollapsedAndHeadsUpOnDown = false;
+                    mHasLayoutedSinceDown = false;
+                    mUpdateFlingOnLayout = false;
+                    mTouchAboveFalsingThreshold = false;
+                    addMovement(event);
+                    break;
+                case MotionEvent.ACTION_POINTER_UP:
+                    final int upPointer = event.getPointerId(event.getActionIndex());
+                    if (mTrackingPointer == upPointer) {
+                        // gesture is ongoing, find a new pointer to track
+                        final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+                        mTrackingPointer = event.getPointerId(newIndex);
+                        mInitialExpandX = event.getX(newIndex);
+                        mInitialExpandY = event.getY(newIndex);
+                    }
+                    break;
+                case MotionEvent.ACTION_POINTER_DOWN:
+                    if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
+                        mMotionAborted = true;
+                        mVelocityTracker.clear();
+                    }
+                    break;
+                case MotionEvent.ACTION_MOVE:
+                    final float h = y - mInitialExpandY;
+                    addMovement(event);
+                    final boolean openShadeWithoutHun =
+                            mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown;
+                    if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown
+                            || openShadeWithoutHun) {
+                        float hAbs = Math.abs(h);
+                        float touchSlop = getTouchSlop(event);
+                        if ((h < -touchSlop
+                                || ((openShadeWithoutHun || mAnimatingOnDown) && hAbs > touchSlop))
+                                && hAbs > Math.abs(x - mInitialExpandX)) {
+                            cancelHeightAnimator();
+                            startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
+                            return true;
+                        }
+                    }
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                case MotionEvent.ACTION_UP:
+                    mVelocityTracker.clear();
+                    break;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                if (event.getDownTime() == mLastTouchDownTime) {
+                    // An issue can occur when swiping down after unlock, where multiple down
+                    // events are received in this handler with identical downTimes. Until the
+                    // source of the issue can be located, detect this case and ignore.
+                    // see b/193350347
+                    Log.w(TAG, "Duplicate down event detected... ignoring");
+                    return true;
+                }
+                mLastTouchDownTime = event.getDownTime();
+            }
+
+
+            if (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches()) {
+                return false;
+            }
+
+            // Do not allow panel expansion if bouncer is scrimmed or showing over a dream,
+            // otherwise user would be able to pull down QS or expand the shade.
+            if (mCentralSurfaces.isBouncerShowingScrimmed()
+                    || mCentralSurfaces.isBouncerShowingOverDream()) {
+                return false;
+            }
+
+            // Make sure the next touch won't the blocked after the current ends.
+            if (event.getAction() == MotionEvent.ACTION_UP
+                    || event.getAction() == MotionEvent.ACTION_CANCEL) {
+                mBlockingExpansionForCurrentTouch = false;
+            }
+            // When touch focus transfer happens, ACTION_DOWN->ACTION_UP may happen immediately
+            // without any ACTION_MOVE event.
+            // In such case, simply expand the panel instead of being stuck at the bottom bar.
+            if (mLastEventSynthesizedDown && event.getAction() == MotionEvent.ACTION_UP) {
+                expand(true /* animate */);
+            }
+            initDownStates(event);
+
+            // If pulse is expanding already, let's give it the touch. There are situations
+            // where the panel starts expanding even though we're also pulsing
+            boolean pulseShouldGetTouch = (!mIsExpanding
+                    && !shouldQuickSettingsIntercept(mDownX, mDownY, 0))
+                    || 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()
+                    && !mNotificationStackScrollLayoutController.isLongPressInProgress()
+                    && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
+                mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
+            }
+            boolean handled = mHeadsUpTouchHelper.onTouchEvent(event);
+
+            if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
+                mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
+                return true;
+            }
+            if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
+                mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
+                handled = true;
+            }
+
+            if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded()
+                    && mKeyguardStateController.isShowing()) {
+                mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX());
+            }
+
+            handled |= handleTouch(event);
+            return !mDozing || mPulsing || handled;
+        }
+
+        private boolean handleTouch(MotionEvent event) {
+            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;
+            }
+
+            // If dragging should not expand the notifications shade, then return false.
+            if (!mNotificationsDragEnabled) {
+                if (mTracking) {
+                    // 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;
+            }
+
+            // On expanding, single mouse click expands the panel instead of dragging.
+            if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+                if (event.getAction() == MotionEvent.ACTION_UP) {
+                    expand(true);
+                }
+                return true;
+            }
+
+            /*
+             * We capture touch events here and update the expand height here in case according to
+             * the users fingers. This also handles multi-touch.
+             *
+             * Flinging is also enabled in order to open or close the shade.
+             */
+
+            int pointerIndex = event.findPointerIndex(mTrackingPointer);
+            if (pointerIndex < 0) {
+                pointerIndex = 0;
+                mTrackingPointer = event.getPointerId(pointerIndex);
+            }
+            final float x = event.getX(pointerIndex);
+            final float y = event.getY(pointerIndex);
+
+            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
+                mIgnoreXTouchSlop = true;
+            }
+
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                    startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
+                    mMinExpandHeight = 0.0f;
+                    mPanelClosedOnDown = isFullyCollapsed();
+                    mHasLayoutedSinceDown = false;
+                    mUpdateFlingOnLayout = false;
+                    mMotionAborted = false;
+                    mDownTime = mSystemClock.uptimeMillis();
+                    mTouchAboveFalsingThreshold = false;
+                    mCollapsedAndHeadsUpOnDown =
+                            isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
+                    addMovement(event);
+                    boolean regularHeightAnimationRunning = mHeightAnimator != null
+                            && !mHintAnimationRunning && !mIsSpringBackAnimation;
+                    if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) {
+                        mTouchSlopExceeded = regularHeightAnimationRunning
+                                || mTouchSlopExceededBeforeDown;
+                        cancelHeightAnimator();
+                        onTrackingStarted();
+                    }
+                    if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()
+                            && !mCentralSurfaces.isBouncerShowing()) {
+                        startOpening(event);
+                    }
+                    break;
+
+                case MotionEvent.ACTION_POINTER_UP:
+                    final int upPointer = event.getPointerId(event.getActionIndex());
+                    if (mTrackingPointer == upPointer) {
+                        // gesture is ongoing, find a new pointer to track
+                        final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+                        final float newY = event.getY(newIndex);
+                        final float newX = event.getX(newIndex);
+                        mTrackingPointer = event.getPointerId(newIndex);
+                        mHandlingPointerUp = true;
+                        startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
+                        mHandlingPointerUp = false;
+                    }
+                    break;
+                case MotionEvent.ACTION_POINTER_DOWN:
+                    if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
+                        mMotionAborted = true;
+                        endMotionEvent(event, x, y, true /* forceCancel */);
+                        return false;
+                    }
+                    break;
+                case MotionEvent.ACTION_MOVE:
+                    addMovement(event);
+                    if (!isFullyCollapsed()) {
+                        maybeVibrateOnOpening(true /* openingWithTouch */);
+                    }
+                    float h = y - mInitialExpandY;
+
+                    // If the panel was collapsed when touching, we only need to check for the
+                    // y-component of the gesture, as we have no conflicting horizontal gesture.
+                    if (Math.abs(h) > getTouchSlop(event)
+                            && (Math.abs(h) > Math.abs(x - mInitialExpandX)
+                            || mIgnoreXTouchSlop)) {
+                        mTouchSlopExceeded = true;
+                        if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
+                            if (mInitialOffsetOnTouch != 0f) {
+                                startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
+                                h = 0;
+                            }
+                            cancelHeightAnimator();
+                            onTrackingStarted();
+                        }
+                    }
+                    float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
+                    newHeight = Math.max(newHeight, mMinExpandHeight);
+                    if (-h >= getFalsingThreshold()) {
+                        mTouchAboveFalsingThreshold = true;
+                        mUpwardsWhenThresholdReached = isDirectionUpwards(x, y);
+                    }
+                    if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {
+                        // Count h==0 as part of swipe-up,
+                        // otherwise {@link NotificationStackScrollLayout}
+                        // wrongly enables stack height updates at the start of lockscreen swipe-up
+                        mAmbientState.setSwipingUp(h <= 0);
+                        setExpandedHeightInternal(newHeight);
+                    }
+                    break;
+
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    addMovement(event);
+                    endMotionEvent(event, x, y, false /* forceCancel */);
+                    // mHeightAnimator is null, there is no remaining frame, ends instrumenting.
+                    if (mHeightAnimator == null) {
+                        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                            endJankMonitoring();
+                        } else {
+                            cancelJankMonitoring();
+                        }
+                    }
+                    break;
+            }
+            return !mGestureWaitForTouchSlop || mTracking;
+        }
+    }
+
+    /** Listens for config changes. */
+    public class OnConfigurationChangedListener implements
+            NotificationPanelView.OnConfigurationChangedListener {
+        @Override
+        public void onConfigurationChanged(Configuration newConfig) {
+            loadDimens();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 6be9bbb..65bd58d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -52,7 +52,6 @@
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 
 import java.io.PrintWriter;
@@ -91,7 +90,7 @@
     private boolean mExpandingBelowNotch;
     private final DockManager mDockManager;
     private final NotificationPanelViewController mNotificationPanelViewController;
-    private final PanelExpansionStateManager mPanelExpansionStateManager;
+    private final ShadeExpansionStateManager mShadeExpansionStateManager;
 
     private boolean mIsTrackingBarGesture = false;
 
@@ -104,7 +103,7 @@
             NotificationShadeDepthController depthController,
             NotificationShadeWindowView notificationShadeWindowView,
             NotificationPanelViewController notificationPanelViewController,
-            PanelExpansionStateManager panelExpansionStateManager,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             StatusBarWindowStateController statusBarWindowStateController,
@@ -124,7 +123,7 @@
         mView = notificationShadeWindowView;
         mDockManager = dockManager;
         mNotificationPanelViewController = notificationPanelViewController;
-        mPanelExpansionStateManager = panelExpansionStateManager;
+        mShadeExpansionStateManager = shadeExpansionStateManager;
         mDepthController = depthController;
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
@@ -404,7 +403,7 @@
         setDragDownHelper(mLockscreenShadeTransitionController.getTouchHelper());
 
         mDepthController.setRoot(mView);
-        mPanelExpansionStateManager.addExpansionListener(mDepthController);
+        mShadeExpansionStateManager.addExpansionListener(mDepthController);
     }
 
     public NotificationShadeWindowView getView() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
deleted file mode 100644
index b4ce95c..0000000
--- a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
+++ /dev/null
@@ -1,1494 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shade;
-
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-
-import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
-import static com.android.systemui.classifier.Classifier.GENERIC;
-import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
-import static com.android.systemui.classifier.Classifier.UNLOCK;
-import static com.android.systemui.shade.NotificationPanelView.DEBUG;
-
-import static java.lang.Float.isNaN;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.VibrationEffect;
-import android.util.Log;
-import android.util.MathUtils;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewPropertyAnimator;
-import android.view.ViewTreeObserver;
-import android.view.animation.Interpolator;
-
-import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.util.LatencyTracker;
-import com.android.systemui.DejankUtils;
-import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
-import com.android.systemui.classifier.Classifier;
-import com.android.systemui.doze.DozeLog;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.notification.stack.AmbientState;
-import com.android.systemui.statusbar.phone.BounceInterpolator;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
-import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
-import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
-import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.time.SystemClock;
-import com.android.wm.shell.animation.FlingAnimationUtils;
-
-import java.io.PrintWriter;
-import java.util.List;
-
-public abstract class PanelViewController {
-    public static final String TAG = NotificationPanelView.class.getSimpleName();
-    public static final float FLING_MAX_LENGTH_SECONDS = 0.6f;
-    public static final float FLING_SPEED_UP_FACTOR = 0.6f;
-    public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f;
-    public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f;
-    private static final int NO_FIXED_DURATION = -1;
-    private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L;
-    private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L;
-
-    /**
-     * The factor of the usual high velocity that is needed in order to reach the maximum overshoot
-     * when flinging. A low value will make it that most flings will reach the maximum overshoot.
-     */
-    private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f;
-
-    protected long mDownTime;
-    protected boolean mTouchSlopExceededBeforeDown;
-    private float mMinExpandHeight;
-    private boolean mPanelUpdateWhenAnimatorEnds;
-    private final boolean mVibrateOnOpening;
-    private boolean mHasVibratedOnOpen = false;
-    protected boolean mIsLaunchAnimationRunning;
-    private int mFixedDuration = NO_FIXED_DURATION;
-    protected float mOverExpansion;
-
-    /**
-     * The overshoot amount when the panel flings open
-     */
-    private float mPanelFlingOvershootAmount;
-
-    /**
-     * The amount of pixels that we have overexpanded the last time with a gesture
-     */
-    private float mLastGesturedOverExpansion = -1;
-
-    /**
-     * Is the current animator the spring back animation?
-     */
-    private boolean mIsSpringBackAnimation;
-
-    private boolean mInSplitShade;
-
-    private void logf(String fmt, Object... args) {
-        Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
-    }
-
-    protected CentralSurfaces mCentralSurfaces;
-    protected HeadsUpManagerPhone mHeadsUpManager;
-    protected final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
-
-    private float mHintDistance;
-    private float mInitialOffsetOnTouch;
-    private boolean mCollapsedAndHeadsUpOnDown;
-    private float mExpandedFraction = 0;
-    private float mExpansionDragDownAmountPx = 0;
-    protected float mExpandedHeight = 0;
-    private boolean mPanelClosedOnDown;
-    private boolean mHasLayoutedSinceDown;
-    private float mUpdateFlingVelocity;
-    private boolean mUpdateFlingOnLayout;
-    private boolean mClosing;
-    protected boolean mTracking;
-    private boolean mTouchSlopExceeded;
-    private int mTrackingPointer;
-    private int mTouchSlop;
-    private float mSlopMultiplier;
-    protected boolean mHintAnimationRunning;
-    private boolean mTouchAboveFalsingThreshold;
-    private boolean mTouchStartedInEmptyArea;
-    private boolean mMotionAborted;
-    private boolean mUpwardsWhenThresholdReached;
-    private boolean mAnimatingOnDown;
-    private boolean mHandlingPointerUp;
-
-    private ValueAnimator mHeightAnimator;
-    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
-    private final FlingAnimationUtils mFlingAnimationUtils;
-    private final FlingAnimationUtils mFlingAnimationUtilsClosing;
-    private final FlingAnimationUtils mFlingAnimationUtilsDismissing;
-    private final LatencyTracker mLatencyTracker;
-    private final FalsingManager mFalsingManager;
-    private final DozeLog mDozeLog;
-    private final VibratorHelper mVibratorHelper;
-
-    /**
-     * Whether an instant expand request is currently pending and we are just waiting for layout.
-     */
-    private boolean mInstantExpanding;
-    private boolean mAnimateAfterExpanding;
-    private boolean mIsFlinging;
-
-    private String mViewName;
-    private float mInitialExpandY;
-    private float mInitialExpandX;
-    private boolean mTouchDisabled;
-    private boolean mInitialTouchFromKeyguard;
-
-    /**
-     * Whether or not the NotificationPanelView can be expanded or collapsed with a drag.
-     */
-    private final boolean mNotificationsDragEnabled;
-
-    private final Interpolator mBounceInterpolator;
-    protected KeyguardBottomAreaView mKeyguardBottomArea;
-
-    /**
-     * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time.
-     */
-    private float mNextCollapseSpeedUpFactor = 1.0f;
-
-    protected boolean mExpanding;
-    private boolean mGestureWaitForTouchSlop;
-    private boolean mIgnoreXTouchSlop;
-    private boolean mExpandLatencyTracking;
-    private final NotificationPanelView mView;
-    private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
-    private final NotificationShadeWindowController mNotificationShadeWindowController;
-    protected final Resources mResources;
-    protected final KeyguardStateController mKeyguardStateController;
-    protected final SysuiStatusBarStateController mStatusBarStateController;
-    protected final AmbientState mAmbientState;
-    protected final LockscreenGestureLogger mLockscreenGestureLogger;
-    private final PanelExpansionStateManager mPanelExpansionStateManager;
-    private final InteractionJankMonitor mInteractionJankMonitor;
-    protected final SystemClock mSystemClock;
-
-    protected final ShadeLogger mShadeLog;
-
-    protected abstract void onExpandingFinished();
-
-    protected void onExpandingStarted() {
-    }
-
-    protected void notifyExpandingStarted() {
-        if (!mExpanding) {
-            mExpanding = true;
-            onExpandingStarted();
-        }
-    }
-
-    protected final void notifyExpandingFinished() {
-        endClosing();
-        if (mExpanding) {
-            mExpanding = false;
-            onExpandingFinished();
-        }
-    }
-
-    protected AmbientState getAmbientState() {
-        return mAmbientState;
-    }
-
-    public PanelViewController(
-            NotificationPanelView view,
-            FalsingManager falsingManager,
-            DozeLog dozeLog,
-            KeyguardStateController keyguardStateController,
-            SysuiStatusBarStateController statusBarStateController,
-            NotificationShadeWindowController notificationShadeWindowController,
-            VibratorHelper vibratorHelper,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
-            LatencyTracker latencyTracker,
-            FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
-            StatusBarTouchableRegionManager statusBarTouchableRegionManager,
-            LockscreenGestureLogger lockscreenGestureLogger,
-            PanelExpansionStateManager panelExpansionStateManager,
-            AmbientState ambientState,
-            InteractionJankMonitor interactionJankMonitor,
-            ShadeLogger shadeLogger,
-            SystemClock systemClock) {
-        keyguardStateController.addCallback(new KeyguardStateController.Callback() {
-            @Override
-            public void onKeyguardFadingAwayChanged() {
-                updateExpandedHeightToMaxHeight();
-            }
-        });
-        mAmbientState = ambientState;
-        mView = view;
-        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
-        mLockscreenGestureLogger = lockscreenGestureLogger;
-        mPanelExpansionStateManager = panelExpansionStateManager;
-        mShadeLog = shadeLogger;
-        TouchHandler touchHandler = createTouchHandler();
-        mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
-            @Override
-            public void onViewAttachedToWindow(View v) {
-                mViewName = mResources.getResourceName(mView.getId());
-            }
-
-            @Override
-            public void onViewDetachedFromWindow(View v) {
-            }
-        });
-
-        mView.addOnLayoutChangeListener(createLayoutChangeListener());
-        mView.setOnTouchListener(touchHandler);
-        mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener());
-
-        mResources = mView.getResources();
-        mKeyguardStateController = keyguardStateController;
-        mStatusBarStateController = statusBarStateController;
-        mNotificationShadeWindowController = notificationShadeWindowController;
-        mFlingAnimationUtils = flingAnimationUtilsBuilder
-                .reset()
-                .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS)
-                .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
-                .build();
-        mFlingAnimationUtilsClosing = flingAnimationUtilsBuilder
-                .reset()
-                .setMaxLengthSeconds(FLING_CLOSING_MAX_LENGTH_SECONDS)
-                .setSpeedUpFactor(FLING_CLOSING_SPEED_UP_FACTOR)
-                .build();
-        mFlingAnimationUtilsDismissing = flingAnimationUtilsBuilder
-                .reset()
-                .setMaxLengthSeconds(0.5f)
-                .setSpeedUpFactor(0.6f)
-                .setX2(0.6f)
-                .setY2(0.84f)
-                .build();
-        mLatencyTracker = latencyTracker;
-        mBounceInterpolator = new BounceInterpolator();
-        mFalsingManager = falsingManager;
-        mDozeLog = dozeLog;
-        mNotificationsDragEnabled = mResources.getBoolean(
-                R.bool.config_enableNotificationShadeDrag);
-        mVibratorHelper = vibratorHelper;
-        mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation);
-        mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
-        mInteractionJankMonitor = interactionJankMonitor;
-        mSystemClock = systemClock;
-    }
-
-    protected void loadDimens() {
-        final ViewConfiguration configuration = ViewConfiguration.get(mView.getContext());
-        mTouchSlop = configuration.getScaledTouchSlop();
-        mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
-        mHintDistance = mResources.getDimension(R.dimen.hint_move_distance);
-        mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount);
-        mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade);
-    }
-
-    protected float getTouchSlop(MotionEvent event) {
-        // Adjust the touch slop if another gesture may be being performed.
-        return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
-                ? mTouchSlop * mSlopMultiplier
-                : mTouchSlop;
-    }
-
-    private void addMovement(MotionEvent event) {
-        // Add movement to velocity tracker using raw screen X and Y coordinates instead
-        // of window coordinates because the window frame may be moving at the same time.
-        float deltaX = event.getRawX() - event.getX();
-        float deltaY = event.getRawY() - event.getY();
-        event.offsetLocation(deltaX, deltaY);
-        mVelocityTracker.addMovement(event);
-        event.offsetLocation(-deltaX, -deltaY);
-    }
-
-    public void setTouchAndAnimationDisabled(boolean disabled) {
-        mTouchDisabled = disabled;
-        if (mTouchDisabled) {
-            cancelHeightAnimator();
-            if (mTracking) {
-                onTrackingStopped(true /* expanded */);
-            }
-            notifyExpandingFinished();
-        }
-    }
-
-    public void startExpandLatencyTracking() {
-        if (mLatencyTracker.isEnabled()) {
-            mLatencyTracker.onActionStart(LatencyTracker.ACTION_EXPAND_PANEL);
-            mExpandLatencyTracking = true;
-        }
-    }
-
-    private void startOpening(MotionEvent event) {
-        updatePanelExpansionAndVisibility();
-        // Reset at start so haptic can be triggered as soon as panel starts to open.
-        mHasVibratedOnOpen = false;
-        //TODO: keyguard opens QS a different way; log that too?
-
-        // Log the position of the swipe that opened the panel
-        float width = mCentralSurfaces.getDisplayWidth();
-        float height = mCentralSurfaces.getDisplayHeight();
-        int rot = mCentralSurfaces.getRotation();
-
-        mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND,
-                (int) (event.getX() / width * 100), (int) (event.getY() / height * 100), rot);
-        mLockscreenGestureLogger
-                .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND);
-    }
-
-    /**
-     * Maybe vibrate as panel is opened.
-     *
-     * @param openingWithTouch Whether the panel is being opened with touch. If the panel is instead
-     * being opened programmatically (such as by the open panel gesture), we always play haptic.
-     */
-    protected void maybeVibrateOnOpening(boolean openingWithTouch) {
-        if (mVibrateOnOpening) {
-            if (!openingWithTouch || !mHasVibratedOnOpen) {
-                mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
-                mHasVibratedOnOpen = true;
-            }
-        }
-    }
-
-    protected abstract float getOpeningHeight();
-
-    /**
-     * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
-     * horizontal direction
-     */
-    private boolean isDirectionUpwards(float x, float y) {
-        float xDiff = x - mInitialExpandX;
-        float yDiff = y - mInitialExpandY;
-        if (yDiff >= 0) {
-            return false;
-        }
-        return Math.abs(yDiff) >= Math.abs(xDiff);
-    }
-
-    public void startExpandMotion(float newX, float newY, boolean startTracking,
-            float expandedHeight) {
-        if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) {
-            beginJankMonitoring();
-        }
-        mInitialOffsetOnTouch = expandedHeight;
-        mInitialExpandY = newY;
-        mInitialExpandX = newX;
-        mInitialTouchFromKeyguard = mKeyguardStateController.isShowing();
-        if (startTracking) {
-            mTouchSlopExceeded = true;
-            setExpandedHeight(mInitialOffsetOnTouch);
-            onTrackingStarted();
-        }
-    }
-
-    private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
-        mTrackingPointer = -1;
-        mAmbientState.setSwipingUp(false);
-        if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop
-                || Math.abs(y - mInitialExpandY) > mTouchSlop
-                || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
-            mVelocityTracker.computeCurrentVelocity(1000);
-            float vel = mVelocityTracker.getYVelocity();
-            float vectorVel = (float) Math.hypot(
-                    mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
-
-            final boolean onKeyguard = mKeyguardStateController.isShowing();
-            final boolean expand;
-            if (mKeyguardStateController.isKeyguardFadingAway()
-                    || (mInitialTouchFromKeyguard && !onKeyguard)) {
-                // Don't expand for any touches that started from the keyguard and ended after the
-                // keyguard is gone.
-                expand = false;
-            } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
-                if (onKeyguard) {
-                    expand = true;
-                } else if (mCentralSurfaces.isBouncerShowingOverDream()) {
-                    expand = false;
-                } else {
-                    // If we get a cancel, put the shade back to the state it was in when the
-                    // gesture started
-                    expand = !mPanelClosedOnDown;
-                }
-            } else {
-                expand = flingExpands(vel, vectorVel, x, y);
-            }
-
-            mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
-                    mCentralSurfaces.isFalsingThresholdNeeded(),
-                    mCentralSurfaces.isWakeUpComingFromTouch());
-            // Log collapse gesture if on lock screen.
-            if (!expand && onKeyguard) {
-                float displayDensity = mCentralSurfaces.getDisplayDensity();
-                int heightDp = (int) Math.abs((y - mInitialExpandY) / displayDensity);
-                int velocityDp = (int) Math.abs(vel / displayDensity);
-                mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp);
-                mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK);
-            }
-            @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC
-                    : y - mInitialExpandY > 0 ? QUICK_SETTINGS
-                            : (mKeyguardStateController.canDismissLockScreen()
-                                    ? UNLOCK : BOUNCER_UNLOCK);
-
-            fling(vel, expand, isFalseTouch(x, y, interactionType));
-            onTrackingStopped(expand);
-            mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
-            if (mUpdateFlingOnLayout) {
-                mUpdateFlingVelocity = vel;
-            }
-        } else if (!mCentralSurfaces.isBouncerShowing()
-                && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()
-                && !mKeyguardStateController.isKeyguardGoingAway()) {
-            boolean expands = onEmptySpaceClick();
-            onTrackingStopped(expands);
-        }
-        mVelocityTracker.clear();
-    }
-
-    protected float getCurrentExpandVelocity() {
-        mVelocityTracker.computeCurrentVelocity(1000);
-        return mVelocityTracker.getYVelocity();
-    }
-
-    protected abstract int getFalsingThreshold();
-
-    protected abstract boolean shouldGestureWaitForTouchSlop();
-
-    protected void onTrackingStopped(boolean expand) {
-        mTracking = false;
-        mCentralSurfaces.onTrackingStopped(expand);
-        updatePanelExpansionAndVisibility();
-    }
-
-    protected void onTrackingStarted() {
-        endClosing();
-        mTracking = true;
-        mCentralSurfaces.onTrackingStarted();
-        notifyExpandingStarted();
-        updatePanelExpansionAndVisibility();
-    }
-
-    /**
-     * @return Whether a pair of coordinates are inside the visible view content bounds.
-     */
-    protected abstract boolean isInContentBounds(float x, float y);
-
-    protected void cancelHeightAnimator() {
-        if (mHeightAnimator != null) {
-            if (mHeightAnimator.isRunning()) {
-                mPanelUpdateWhenAnimatorEnds = false;
-            }
-            mHeightAnimator.cancel();
-        }
-        endClosing();
-    }
-
-    private void endClosing() {
-        if (mClosing) {
-            setIsClosing(false);
-            onClosingFinished();
-        }
-    }
-
-    protected abstract boolean canCollapsePanelOnTouch();
-
-    protected float getContentHeight() {
-        return mExpandedHeight;
-    }
-
-    /**
-     * @param vel       the current vertical velocity of the motion
-     * @param vectorVel the length of the vectorial velocity
-     * @return whether a fling should expands the panel; contracts otherwise
-     */
-    protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
-        if (mFalsingManager.isUnlockingDisabled()) {
-            return true;
-        }
-
-        @Classifier.InteractionType int interactionType = y - mInitialExpandY > 0
-                ? QUICK_SETTINGS : (
-                        mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK);
-
-        if (isFalseTouch(x, y, interactionType)) {
-            return true;
-        }
-        if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
-            return shouldExpandWhenNotFlinging();
-        } else {
-            return vel > 0;
-        }
-    }
-
-    protected boolean shouldExpandWhenNotFlinging() {
-        return getExpandedFraction() > 0.5f;
-    }
-
-    /**
-     * @param x the final x-coordinate when the finger was lifted
-     * @param y the final y-coordinate when the finger was lifted
-     * @return whether this motion should be regarded as a false touch
-     */
-    private boolean isFalseTouch(float x, float y,
-            @Classifier.InteractionType int interactionType) {
-        if (!mCentralSurfaces.isFalsingThresholdNeeded()) {
-            return false;
-        }
-        if (mFalsingManager.isClassifierEnabled()) {
-            return mFalsingManager.isFalseTouch(interactionType);
-        }
-        if (!mTouchAboveFalsingThreshold) {
-            return true;
-        }
-        if (mUpwardsWhenThresholdReached) {
-            return false;
-        }
-        return !isDirectionUpwards(x, y);
-    }
-
-    protected void fling(float vel, boolean expand) {
-        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
-    }
-
-    protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
-        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
-    }
-
-    protected void fling(float vel, boolean expand, float collapseSpeedUpFactor,
-            boolean expandBecauseOfFalsing) {
-        float target = expand ? getMaxPanelHeight() : 0;
-        if (!expand) {
-            setIsClosing(true);
-        }
-        flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
-    }
-
-    protected void flingToHeight(float vel, boolean expand, float target,
-            float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
-        if (target == mExpandedHeight && mOverExpansion == 0.0f) {
-            // We're at the target and didn't fling and there's no overshoot
-            onFlingEnd(false /* cancelled */);
-            return;
-        }
-        mIsFlinging = true;
-        // we want to perform an overshoot animation when flinging open
-        final boolean addOverscroll =
-                expand
-                        && !mInSplitShade // Split shade has its own overscroll logic
-                        && mStatusBarStateController.getState() != StatusBarState.KEYGUARD
-                        && mOverExpansion == 0.0f
-                        && vel >= 0;
-        final boolean shouldSpringBack = addOverscroll || (mOverExpansion != 0.0f && expand);
-        float overshootAmount = 0.0f;
-        if (addOverscroll) {
-            // Let's overshoot depending on the amount of velocity
-            overshootAmount = MathUtils.lerp(
-                    0.2f,
-                    1.0f,
-                    MathUtils.saturate(vel
-                            / (mFlingAnimationUtils.getHighVelocityPxPerSecond()
-                                    * FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT)));
-            overshootAmount += mOverExpansion / mPanelFlingOvershootAmount;
-        }
-        ValueAnimator animator = createHeightAnimator(target, overshootAmount);
-        if (expand) {
-            if (expandBecauseOfFalsing && vel < 0) {
-                vel = 0;
-            }
-            mFlingAnimationUtils.apply(animator, mExpandedHeight,
-                    target + overshootAmount * mPanelFlingOvershootAmount, vel, mView.getHeight());
-            if (vel == 0) {
-                animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION);
-            }
-        } else {
-            if (shouldUseDismissingAnimation()) {
-                if (vel == 0) {
-                    animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
-                    long duration = (long) (200 + mExpandedHeight / mView.getHeight() * 100);
-                    animator.setDuration(duration);
-                } else {
-                    mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel,
-                            mView.getHeight());
-                }
-            } else {
-                mFlingAnimationUtilsClosing.apply(
-                        animator, mExpandedHeight, target, vel, mView.getHeight());
-            }
-
-            // Make it shorter if we run a canned animation
-            if (vel == 0) {
-                animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor));
-            }
-            if (mFixedDuration != NO_FIXED_DURATION) {
-                animator.setDuration(mFixedDuration);
-            }
-        }
-        animator.addListener(new AnimatorListenerAdapter() {
-            private boolean mCancelled;
-
-            @Override
-            public void onAnimationStart(Animator animation) {
-                if (!mStatusBarStateController.isDozing()) {
-                    beginJankMonitoring();
-                }
-            }
-
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                mCancelled = true;
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (shouldSpringBack && !mCancelled) {
-                    // After the shade is flinged open to an overscrolled state, spring back
-                    // the shade by reducing section padding to 0.
-                    springBack();
-                } else {
-                    onFlingEnd(mCancelled);
-                }
-            }
-        });
-        setAnimator(animator);
-        animator.start();
-    }
-
-    private void springBack() {
-        if (mOverExpansion == 0) {
-            onFlingEnd(false /* cancelled */);
-            return;
-        }
-        mIsSpringBackAnimation = true;
-        ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0);
-        animator.addUpdateListener(
-                animation -> setOverExpansionInternal((float) animation.getAnimatedValue(),
-                        false /* isFromGesture */));
-        animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION);
-        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        animator.addListener(new AnimatorListenerAdapter() {
-            private boolean mCancelled;
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                mCancelled = true;
-            }
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mIsSpringBackAnimation = false;
-                onFlingEnd(mCancelled);
-            }
-        });
-        setAnimator(animator);
-        animator.start();
-    }
-
-    protected void onFlingEnd(boolean cancelled) {
-        mIsFlinging = false;
-        // No overshoot when the animation ends
-        setOverExpansionInternal(0, false /* isFromGesture */);
-        setAnimator(null);
-        mKeyguardStateController.notifyPanelFlingEnd();
-        if (!cancelled) {
-            endJankMonitoring();
-            notifyExpandingFinished();
-        } else {
-            cancelJankMonitoring();
-        }
-        updatePanelExpansionAndVisibility();
-    }
-
-    protected abstract boolean shouldUseDismissingAnimation();
-
-    public String getName() {
-        return mViewName;
-    }
-
-    public void setExpandedHeight(float height) {
-        if (DEBUG) logf("setExpandedHeight(%.1f)", height);
-        setExpandedHeightInternal(height);
-    }
-
-    void updateExpandedHeightToMaxHeight() {
-        float currentMaxPanelHeight = getMaxPanelHeight();
-
-        if (isFullyCollapsed()) {
-            return;
-        }
-
-        if (currentMaxPanelHeight == mExpandedHeight) {
-            return;
-        }
-
-        if (mTracking && !isTrackingBlocked()) {
-            return;
-        }
-
-        if (mHeightAnimator != null && !mIsSpringBackAnimation) {
-            mPanelUpdateWhenAnimatorEnds = true;
-            return;
-        }
-
-        setExpandedHeight(currentMaxPanelHeight);
-    }
-
-    /**
-     * Returns drag down distance after which panel should be fully expanded. Usually it's the
-     * same as max panel height but for large screen devices (especially split shade) we might
-     * want to return different value to shorten drag distance
-     */
-    public abstract int getMaxPanelTransitionDistance();
-
-    public void setExpandedHeightInternal(float h) {
-        if (isNaN(h)) {
-            Log.wtf(TAG, "ExpandedHeight set to NaN");
-        }
-        mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
-            if (mExpandLatencyTracking && h != 0f) {
-                DejankUtils.postAfterTraversal(
-                        () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL));
-                mExpandLatencyTracking = false;
-            }
-            float maxPanelHeight = getMaxPanelTransitionDistance();
-            if (mHeightAnimator == null) {
-                // Split shade has its own overscroll logic
-                if (mTracking && !mInSplitShade) {
-                    float overExpansionPixels = Math.max(0, h - maxPanelHeight);
-                    setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */);
-                }
-            }
-            mExpandedHeight = Math.min(h, maxPanelHeight);
-            // If we are closing the panel and we are almost there due to a slow decelerating
-            // interpolator, abort the animation.
-            if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
-                mExpandedHeight = 0f;
-                if (mHeightAnimator != null) {
-                    mHeightAnimator.end();
-                }
-            }
-            mExpansionDragDownAmountPx = h;
-            mExpandedFraction = Math.min(1f,
-                    maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
-            mAmbientState.setExpansionFraction(mExpandedFraction);
-            onHeightUpdated(mExpandedHeight);
-            updatePanelExpansionAndVisibility();
-        });
-    }
-
-    /**
-     * @return true if the panel tracking should be temporarily blocked; this is used when a
-     * conflicting gesture (opening QS) is happening
-     */
-    protected abstract boolean isTrackingBlocked();
-
-    protected void setOverExpansion(float overExpansion) {
-        mOverExpansion = overExpansion;
-    }
-
-    /**
-     * Set the current overexpansion
-     *
-     * @param overExpansion the amount of overexpansion to apply
-     * @param isFromGesture is this amount from a gesture and needs to be rubberBanded?
-     */
-    private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) {
-        if (!isFromGesture) {
-            mLastGesturedOverExpansion = -1;
-            setOverExpansion(overExpansion);
-        } else if (mLastGesturedOverExpansion != overExpansion) {
-            mLastGesturedOverExpansion = overExpansion;
-            final float heightForFullOvershoot = mView.getHeight() / 3.0f;
-            float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot);
-            newExpansion = Interpolators.getOvershootInterpolation(newExpansion);
-            setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f);
-        }
-    }
-
-    protected abstract void onHeightUpdated(float expandedHeight);
-
-    /**
-     * This returns the maximum height of the panel. Children should override this if their
-     * desired height is not the full height.
-     *
-     * @return the default implementation simply returns the maximum height.
-     */
-    protected abstract int getMaxPanelHeight();
-
-    public void setExpandedFraction(float frac) {
-        setExpandedHeight(getMaxPanelTransitionDistance() * frac);
-    }
-
-    public float getExpandedHeight() {
-        return mExpandedHeight;
-    }
-
-    public float getExpandedFraction() {
-        return mExpandedFraction;
-    }
-
-    public boolean isFullyExpanded() {
-        return mExpandedHeight >= getMaxPanelHeight();
-    }
-
-    public boolean isFullyCollapsed() {
-        return mExpandedFraction <= 0.0f;
-    }
-
-    public boolean isCollapsing() {
-        return mClosing || mIsLaunchAnimationRunning;
-    }
-
-    public boolean isFlinging() {
-        return mIsFlinging;
-    }
-
-    public boolean isTracking() {
-        return mTracking;
-    }
-
-    public void collapse(boolean delayed, float speedUpFactor) {
-        if (DEBUG) logf("collapse: " + this);
-        if (canPanelBeCollapsed()) {
-            cancelHeightAnimator();
-            notifyExpandingStarted();
-
-            // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
-            setIsClosing(true);
-            if (delayed) {
-                mNextCollapseSpeedUpFactor = speedUpFactor;
-                mView.postDelayed(mFlingCollapseRunnable, 120);
-            } else {
-                fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
-            }
-        }
-    }
-
-    public boolean canPanelBeCollapsed() {
-        return !isFullyCollapsed() && !mTracking && !mClosing;
-    }
-
-    private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
-            mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
-
-    public void expand(final boolean animate) {
-        if (!isFullyCollapsed() && !isCollapsing()) {
-            return;
-        }
-
-        mInstantExpanding = true;
-        mAnimateAfterExpanding = animate;
-        mUpdateFlingOnLayout = false;
-        abortAnimations();
-        if (mTracking) {
-            onTrackingStopped(true /* expands */); // The panel is expanded after this call.
-        }
-        if (mExpanding) {
-            notifyExpandingFinished();
-        }
-        updatePanelExpansionAndVisibility();
-
-        // Wait for window manager to pickup the change, so we know the maximum height of the panel
-        // then.
-        mView.getViewTreeObserver().addOnGlobalLayoutListener(
-                new ViewTreeObserver.OnGlobalLayoutListener() {
-                    @Override
-                    public void onGlobalLayout() {
-                        if (!mInstantExpanding) {
-                            mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-                            return;
-                        }
-                        if (mCentralSurfaces.getNotificationShadeWindowView().isVisibleToUser()) {
-                            mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-                            if (mAnimateAfterExpanding) {
-                                notifyExpandingStarted();
-                                beginJankMonitoring();
-                                fling(0, true /* expand */);
-                            } else {
-                                setExpandedFraction(1f);
-                            }
-                            mInstantExpanding = false;
-                        }
-                    }
-                });
-
-        // Make sure a layout really happens.
-        mView.requestLayout();
-    }
-
-    public void instantCollapse() {
-        abortAnimations();
-        setExpandedFraction(0f);
-        if (mExpanding) {
-            notifyExpandingFinished();
-        }
-        if (mInstantExpanding) {
-            mInstantExpanding = false;
-            updatePanelExpansionAndVisibility();
-        }
-    }
-
-    private void abortAnimations() {
-        cancelHeightAnimator();
-        mView.removeCallbacks(mFlingCollapseRunnable);
-    }
-
-    protected abstract void onClosingFinished();
-
-    protected void startUnlockHintAnimation() {
-
-        // We don't need to hint the user if an animation is already running or the user is changing
-        // the expansion.
-        if (mHeightAnimator != null || mTracking) {
-            return;
-        }
-        notifyExpandingStarted();
-        startUnlockHintAnimationPhase1(() -> {
-            notifyExpandingFinished();
-            onUnlockHintFinished();
-            mHintAnimationRunning = false;
-        });
-        onUnlockHintStarted();
-        mHintAnimationRunning = true;
-    }
-
-    protected void onUnlockHintFinished() {
-        mCentralSurfaces.onHintFinished();
-    }
-
-    protected void onUnlockHintStarted() {
-        mCentralSurfaces.onUnlockHintStarted();
-    }
-
-    public boolean isUnlockHintRunning() {
-        return mHintAnimationRunning;
-    }
-
-    /**
-     * Phase 1: Move everything upwards.
-     */
-    private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
-        float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
-        ValueAnimator animator = createHeightAnimator(target);
-        animator.setDuration(250);
-        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        animator.addListener(new AnimatorListenerAdapter() {
-            private boolean mCancelled;
-
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                mCancelled = true;
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (mCancelled) {
-                    setAnimator(null);
-                    onAnimationFinished.run();
-                } else {
-                    startUnlockHintAnimationPhase2(onAnimationFinished);
-                }
-            }
-        });
-        animator.start();
-        setAnimator(animator);
-
-        final List<ViewPropertyAnimator> indicationAnimators =
-                mKeyguardBottomArea.getIndicationAreaAnimators();
-        for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) {
-            indicationAreaAnimator
-                    .translationY(-mHintDistance)
-                    .setDuration(250)
-                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                    .withEndAction(() -> indicationAreaAnimator
-                            .translationY(0)
-                            .setDuration(450)
-                            .setInterpolator(mBounceInterpolator)
-                            .start())
-                    .start();
-        }
-    }
-
-    private void setAnimator(ValueAnimator animator) {
-        mHeightAnimator = animator;
-        if (animator == null && mPanelUpdateWhenAnimatorEnds) {
-            mPanelUpdateWhenAnimatorEnds = false;
-            updateExpandedHeightToMaxHeight();
-        }
-    }
-
-    /**
-     * Phase 2: Bounce down.
-     */
-    private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
-        ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
-        animator.setDuration(450);
-        animator.setInterpolator(mBounceInterpolator);
-        animator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                setAnimator(null);
-                onAnimationFinished.run();
-                updatePanelExpansionAndVisibility();
-            }
-        });
-        animator.start();
-        setAnimator(animator);
-    }
-
-    private ValueAnimator createHeightAnimator(float targetHeight) {
-        return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */);
-    }
-
-    /**
-     * Create an animator that can also overshoot
-     *
-     * @param targetHeight the target height
-     * @param overshootAmount the amount of overshoot desired
-     */
-    private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) {
-        float startExpansion = mOverExpansion;
-        ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
-        animator.addUpdateListener(
-                animation -> {
-                    if (overshootAmount > 0.0f
-                            // Also remove the overExpansion when collapsing
-                            || (targetHeight == 0.0f && startExpansion != 0)) {
-                        final float expansion = MathUtils.lerp(
-                                startExpansion,
-                                mPanelFlingOvershootAmount * overshootAmount,
-                                Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
-                                        animator.getAnimatedFraction()));
-                        setOverExpansionInternal(expansion, false /* isFromGesture */);
-                    }
-                    setExpandedHeightInternal((float) animation.getAnimatedValue());
-                });
-        return animator;
-    }
-
-    /** Update the visibility of {@link NotificationPanelView} if necessary. */
-    public void updateVisibility() {
-        mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE);
-    }
-
-    /** Returns true if {@link NotificationPanelView} should be visible. */
-    abstract protected boolean shouldPanelBeVisible();
-
-    /**
-     * Updates the panel expansion and {@link NotificationPanelView} visibility if necessary.
-     *
-     * TODO(b/200063118): Could public calls to this method be replaced with calls to
-     *   {@link #updateVisibility()}? That would allow us to make this method private.
-     */
-    public void updatePanelExpansionAndVisibility() {
-        mPanelExpansionStateManager.onPanelExpansionChanged(
-                mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
-        updateVisibility();
-    }
-
-    public boolean isExpanded() {
-        return mExpandedFraction > 0f
-                || mInstantExpanding
-                || isPanelVisibleBecauseOfHeadsUp()
-                || mTracking
-                || mHeightAnimator != null
-                && !mIsSpringBackAnimation;
-    }
-
-    protected abstract boolean isPanelVisibleBecauseOfHeadsUp();
-
-    /**
-     * Gets called when the user performs a click anywhere in the empty area of the panel.
-     *
-     * @return whether the panel will be expanded after the action performed by this method
-     */
-    protected boolean onEmptySpaceClick() {
-        if (mHintAnimationRunning) {
-            return true;
-        }
-        return onMiddleClicked();
-    }
-
-    protected abstract boolean onMiddleClicked();
-
-    protected abstract boolean isDozing();
-
-    public void dump(PrintWriter pw, String[] args) {
-        pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
-                        + " tracking=%s timeAnim=%s%s "
-                        + "touchDisabled=%s" + "]",
-                this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(),
-                mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator,
-                ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""),
-                mTouchDisabled ? "T" : "f"));
-    }
-
-    public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
-        mHeadsUpManager = headsUpManager;
-    }
-
-    public void setIsLaunchAnimationRunning(boolean running) {
-        mIsLaunchAnimationRunning = running;
-    }
-
-    protected void setIsClosing(boolean isClosing) {
-        mClosing = isClosing;
-    }
-
-    protected boolean isClosing() {
-        return mClosing;
-    }
-
-    public void collapseWithDuration(int animationDuration) {
-        mFixedDuration = animationDuration;
-        collapse(false /* delayed */, 1.0f /* speedUpFactor */);
-        mFixedDuration = NO_FIXED_DURATION;
-    }
-
-    public ViewGroup getView() {
-        // TODO: remove this method, or at least reduce references to it.
-        return mView;
-    }
-
-    protected abstract OnLayoutChangeListener createLayoutChangeListener();
-
-    protected abstract TouchHandler createTouchHandler();
-
-    protected OnConfigurationChangedListener createOnConfigurationChangedListener() {
-        return new OnConfigurationChangedListener();
-    }
-
-    public class TouchHandler implements View.OnTouchListener {
-
-        public boolean onInterceptTouchEvent(MotionEvent event) {
-            if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
-                    && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
-                return false;
-            }
-
-            /*
-             * If the user drags anywhere inside the panel we intercept it if the movement is
-             * upwards. This allows closing the shade from anywhere inside the panel.
-             *
-             * We only do this if the current content is scrolled to the bottom,
-             * i.e canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling
-             * gesture
-             * possible.
-             */
-            int pointerIndex = event.findPointerIndex(mTrackingPointer);
-            if (pointerIndex < 0) {
-                pointerIndex = 0;
-                mTrackingPointer = event.getPointerId(pointerIndex);
-            }
-            final float x = event.getX(pointerIndex);
-            final float y = event.getY(pointerIndex);
-            boolean canCollapsePanel = canCollapsePanelOnTouch();
-
-            switch (event.getActionMasked()) {
-                case MotionEvent.ACTION_DOWN:
-                    mCentralSurfaces.userActivity();
-                    mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
-                    mMinExpandHeight = 0.0f;
-                    mDownTime = mSystemClock.uptimeMillis();
-                    if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) {
-                        cancelHeightAnimator();
-                        mTouchSlopExceeded = true;
-                        return true;
-                    }
-                    mInitialExpandY = y;
-                    mInitialExpandX = x;
-                    mTouchStartedInEmptyArea = !isInContentBounds(x, y);
-                    mTouchSlopExceeded = mTouchSlopExceededBeforeDown;
-                    mMotionAborted = false;
-                    mPanelClosedOnDown = isFullyCollapsed();
-                    mCollapsedAndHeadsUpOnDown = false;
-                    mHasLayoutedSinceDown = false;
-                    mUpdateFlingOnLayout = false;
-                    mTouchAboveFalsingThreshold = false;
-                    addMovement(event);
-                    break;
-                case MotionEvent.ACTION_POINTER_UP:
-                    final int upPointer = event.getPointerId(event.getActionIndex());
-                    if (mTrackingPointer == upPointer) {
-                        // gesture is ongoing, find a new pointer to track
-                        final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
-                        mTrackingPointer = event.getPointerId(newIndex);
-                        mInitialExpandX = event.getX(newIndex);
-                        mInitialExpandY = event.getY(newIndex);
-                    }
-                    break;
-                case MotionEvent.ACTION_POINTER_DOWN:
-                    if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
-                        mMotionAborted = true;
-                        mVelocityTracker.clear();
-                    }
-                    break;
-                case MotionEvent.ACTION_MOVE:
-                    final float h = y - mInitialExpandY;
-                    addMovement(event);
-                    final boolean openShadeWithoutHun =
-                            mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown;
-                    if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown
-                            || openShadeWithoutHun) {
-                        float hAbs = Math.abs(h);
-                        float touchSlop = getTouchSlop(event);
-                        if ((h < -touchSlop
-                                || ((openShadeWithoutHun || mAnimatingOnDown) && hAbs > touchSlop))
-                                && hAbs > Math.abs(x - mInitialExpandX)) {
-                            cancelHeightAnimator();
-                            startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
-                            return true;
-                        }
-                    }
-                    break;
-                case MotionEvent.ACTION_CANCEL:
-                case MotionEvent.ACTION_UP:
-                    mVelocityTracker.clear();
-                    break;
-            }
-            return false;
-        }
-
-        @Override
-        public boolean onTouch(View v, MotionEvent event) {
-            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;
-            }
-
-            // If dragging should not expand the notifications shade, then return false.
-            if (!mNotificationsDragEnabled) {
-                if (mTracking) {
-                    // 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;
-            }
-
-            // On expanding, single mouse click expands the panel instead of dragging.
-            if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
-                if (event.getAction() == MotionEvent.ACTION_UP) {
-                    expand(true);
-                }
-                return true;
-            }
-
-            /*
-             * We capture touch events here and update the expand height here in case according to
-             * the users fingers. This also handles multi-touch.
-             *
-             * Flinging is also enabled in order to open or close the shade.
-             */
-
-            int pointerIndex = event.findPointerIndex(mTrackingPointer);
-            if (pointerIndex < 0) {
-                pointerIndex = 0;
-                mTrackingPointer = event.getPointerId(pointerIndex);
-            }
-            final float x = event.getX(pointerIndex);
-            final float y = event.getY(pointerIndex);
-
-            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
-                mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
-                mIgnoreXTouchSlop = true;
-            }
-
-            switch (event.getActionMasked()) {
-                case MotionEvent.ACTION_DOWN:
-                    startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
-                    mMinExpandHeight = 0.0f;
-                    mPanelClosedOnDown = isFullyCollapsed();
-                    mHasLayoutedSinceDown = false;
-                    mUpdateFlingOnLayout = false;
-                    mMotionAborted = false;
-                    mDownTime = mSystemClock.uptimeMillis();
-                    mTouchAboveFalsingThreshold = false;
-                    mCollapsedAndHeadsUpOnDown =
-                            isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
-                    addMovement(event);
-                    boolean regularHeightAnimationRunning = mHeightAnimator != null
-                            && !mHintAnimationRunning && !mIsSpringBackAnimation;
-                    if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) {
-                        mTouchSlopExceeded = regularHeightAnimationRunning
-                                        || mTouchSlopExceededBeforeDown;
-                        cancelHeightAnimator();
-                        onTrackingStarted();
-                    }
-                    if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()
-                            && !mCentralSurfaces.isBouncerShowing()) {
-                        startOpening(event);
-                    }
-                    break;
-
-                case MotionEvent.ACTION_POINTER_UP:
-                    final int upPointer = event.getPointerId(event.getActionIndex());
-                    if (mTrackingPointer == upPointer) {
-                        // gesture is ongoing, find a new pointer to track
-                        final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
-                        final float newY = event.getY(newIndex);
-                        final float newX = event.getX(newIndex);
-                        mTrackingPointer = event.getPointerId(newIndex);
-                        mHandlingPointerUp = true;
-                        startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
-                        mHandlingPointerUp = false;
-                    }
-                    break;
-                case MotionEvent.ACTION_POINTER_DOWN:
-                    if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
-                        mMotionAborted = true;
-                        endMotionEvent(event, x, y, true /* forceCancel */);
-                        return false;
-                    }
-                    break;
-                case MotionEvent.ACTION_MOVE:
-                    addMovement(event);
-                    if (!isFullyCollapsed()) {
-                        maybeVibrateOnOpening(true /* openingWithTouch */);
-                    }
-                    float h = y - mInitialExpandY;
-
-                    // If the panel was collapsed when touching, we only need to check for the
-                    // y-component of the gesture, as we have no conflicting horizontal gesture.
-                    if (Math.abs(h) > getTouchSlop(event)
-                            && (Math.abs(h) > Math.abs(x - mInitialExpandX)
-                            || mIgnoreXTouchSlop)) {
-                        mTouchSlopExceeded = true;
-                        if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
-                            if (mInitialOffsetOnTouch != 0f) {
-                                startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
-                                h = 0;
-                            }
-                            cancelHeightAnimator();
-                            onTrackingStarted();
-                        }
-                    }
-                    float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
-                    newHeight = Math.max(newHeight, mMinExpandHeight);
-                    if (-h >= getFalsingThreshold()) {
-                        mTouchAboveFalsingThreshold = true;
-                        mUpwardsWhenThresholdReached = isDirectionUpwards(x, y);
-                    }
-                    if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {
-                        // Count h==0 as part of swipe-up,
-                        // otherwise {@link NotificationStackScrollLayout}
-                        // wrongly enables stack height updates at the start of lockscreen swipe-up
-                        mAmbientState.setSwipingUp(h <= 0);
-                        setExpandedHeightInternal(newHeight);
-                    }
-                    break;
-
-                case MotionEvent.ACTION_UP:
-                case MotionEvent.ACTION_CANCEL:
-                    addMovement(event);
-                    endMotionEvent(event, x, y, false /* forceCancel */);
-                    // mHeightAnimator is null, there is no remaining frame, ends instrumenting.
-                    if (mHeightAnimator == null) {
-                        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
-                            endJankMonitoring();
-                        } else {
-                            cancelJankMonitoring();
-                        }
-                    }
-                    break;
-            }
-            return !mGestureWaitForTouchSlop || mTracking;
-        }
-    }
-
-    protected abstract class OnLayoutChangeListener implements View.OnLayoutChangeListener {
-        @Override
-        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
-                int oldTop, int oldRight, int oldBottom) {
-            updateExpandedHeightToMaxHeight();
-            mHasLayoutedSinceDown = true;
-            if (mUpdateFlingOnLayout) {
-                abortAnimations();
-                fling(mUpdateFlingVelocity, true /* expands */);
-                mUpdateFlingOnLayout = false;
-            }
-        }
-    }
-
-    public class OnConfigurationChangedListener implements
-            NotificationPanelView.OnConfigurationChangedListener {
-        @Override
-        public void onConfigurationChanged(Configuration newConfig) {
-            loadDimens();
-        }
-    }
-
-    private void beginJankMonitoring() {
-        if (mInteractionJankMonitor == null) {
-            return;
-        }
-        InteractionJankMonitor.Configuration.Builder builder =
-                InteractionJankMonitor.Configuration.Builder.withView(
-                                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
-                                mView)
-                        .setTag(isFullyCollapsed() ? "Expand" : "Collapse");
-        mInteractionJankMonitor.begin(builder);
-    }
-
-    private void endJankMonitoring() {
-        if (mInteractionJankMonitor == null) {
-            return;
-        }
-        InteractionJankMonitor.getInstance().end(
-                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
-    }
-
-    private void cancelJankMonitoring() {
-        if (mInteractionJankMonitor == null) {
-            return;
-        }
-        InteractionJankMonitor.getInstance().cancel(
-                InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
-    }
-
-    protected float getExpansionFraction() {
-        return mExpandedFraction;
-    }
-
-    protected PanelExpansionStateManager getPanelExpansionStateManager() {
-        return mPanelExpansionStateManager;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionChangeEvent.kt
similarity index 91%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionChangeEvent.kt
index 7c61b29..71dfafa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionChangeEvent.kt
@@ -13,11 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.shade
 
 import android.annotation.FloatRange
 
-data class PanelExpansionChangeEvent(
+data class ShadeExpansionChangeEvent(
     /** 0 when collapsed, 1 when fully expanded. */
     @FloatRange(from = 0.0, to = 1.0) val fraction: Float,
     /** Whether the panel should be considered expanded */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionListener.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.kt
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionListener.kt
index d003824..a5a9ffd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionListener.kt
@@ -13,14 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.shade
 
 /** A listener interface to be notified of expansion events for the notification panel. */
-fun interface PanelExpansionListener {
+fun interface ShadeExpansionListener {
     /**
      * Invoked whenever the notification panel expansion changes, at every animation frame. This is
      * the main expansion that happens when the user is swiping up to dismiss the lock screen and
      * swiping to pull down the notification shade.
      */
-    fun onPanelExpansionChanged(event: PanelExpansionChangeEvent)
+    fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
similarity index 79%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index 6b7c42e..f617d47 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.shade
 
 import android.annotation.IntDef
 import android.util.Log
@@ -29,10 +29,10 @@
  * TODO(b/200063118): Make this class the one source of truth for the state of panel expansion.
  */
 @SysUISingleton
-class PanelExpansionStateManager @Inject constructor() {
+class ShadeExpansionStateManager @Inject constructor() {
 
-    private val expansionListeners = mutableListOf<PanelExpansionListener>()
-    private val stateListeners = mutableListOf<PanelStateListener>()
+    private val expansionListeners = mutableListOf<ShadeExpansionListener>()
+    private val stateListeners = mutableListOf<ShadeStateListener>()
 
     @PanelState private var state: Int = STATE_CLOSED
     @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f
@@ -45,24 +45,25 @@
      *
      * Listener will also be immediately notified with the current values.
      */
-    fun addExpansionListener(listener: PanelExpansionListener) {
+    fun addExpansionListener(listener: ShadeExpansionListener) {
         expansionListeners.add(listener)
         listener.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount))
+            ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)
+        )
     }
 
     /** Removes an expansion listener. */
-    fun removeExpansionListener(listener: PanelExpansionListener) {
+    fun removeExpansionListener(listener: ShadeExpansionListener) {
         expansionListeners.remove(listener)
     }
 
     /** Adds a listener that will be notified when the panel state has changed. */
-    fun addStateListener(listener: PanelStateListener) {
+    fun addStateListener(listener: ShadeStateListener) {
         stateListeners.add(listener)
     }
 
     /** Removes a state listener. */
-    fun removeStateListener(listener: PanelStateListener) {
+    fun removeStateListener(listener: ShadeStateListener) {
         stateListeners.remove(listener)
     }
 
@@ -110,25 +111,26 @@
 
         debugLog(
             "panelExpansionChanged:" +
-                    "start state=${oldState.panelStateToString()} " +
-                    "end state=${state.panelStateToString()} " +
-                    "f=$fraction " +
-                    "expanded=$expanded " +
-                    "tracking=$tracking " +
-                    "dragDownPxAmount=$dragDownPxAmount " +
-                    "${if (fullyOpened) " fullyOpened" else ""} " +
-                    if (fullyClosed) " fullyClosed" else ""
+                "start state=${oldState.panelStateToString()} " +
+                "end state=${state.panelStateToString()} " +
+                "f=$fraction " +
+                "expanded=$expanded " +
+                "tracking=$tracking " +
+                "dragDownPxAmount=$dragDownPxAmount " +
+                "${if (fullyOpened) " fullyOpened" else ""} " +
+                if (fullyClosed) " fullyClosed" else ""
         )
 
         val expansionChangeEvent =
-            PanelExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)
+            ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)
         expansionListeners.forEach { it.onPanelExpansionChanged(expansionChangeEvent) }
     }
 
     /** Updates the panel state if necessary. */
     fun updateState(@PanelState state: Int) {
         debugLog(
-            "update state: ${this.state.panelStateToString()} -> ${state.panelStateToString()}")
+            "update state: ${this.state.panelStateToString()} -> ${state.panelStateToString()}"
+        )
         if (this.state != state) {
             updateStateInternal(state)
         }
@@ -165,5 +167,5 @@
     }
 }
 
-private val TAG = PanelExpansionStateManager::class.simpleName
+private val TAG = ShadeExpansionStateManager::class.simpleName
 private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateListener.kt
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeStateListener.kt
index ca667dd..74468a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateListener.kt
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone.panelstate
+package com.android.systemui.shade
 
 /** A listener interface to be notified of state change events for the notification panel. */
-fun interface PanelStateListener {
-    /** Called when the panel's expansion state has changed.   */
+fun interface ShadeStateListener {
+    /** Called when the panel's expansion state has changed. */
     fun onPanelStateChanged(@PanelState state: Int)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
index 618c892..a77c21a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
@@ -7,12 +7,12 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.shade.PanelState
+import com.android.systemui.shade.STATE_OPENING
+import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.PanelState
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.util.LargeScreenUtils
@@ -35,7 +35,7 @@
     private var inSplitShade = false
     private var splitShadeScrimTransitionDistance = 0
     private var lastExpansionFraction: Float? = null
-    private var lastExpansionEvent: PanelExpansionChangeEvent? = null
+    private var lastExpansionEvent: ShadeExpansionChangeEvent? = null
     private var currentPanelState: Int? = null
 
     init {
@@ -61,8 +61,8 @@
         onStateChanged()
     }
 
-    fun onPanelExpansionChanged(panelExpansionChangeEvent: PanelExpansionChangeEvent) {
-        lastExpansionEvent = panelExpansionChangeEvent
+    fun onPanelExpansionChanged(shadeExpansionChangeEvent: ShadeExpansionChangeEvent) {
+        lastExpansionEvent = shadeExpansionChangeEvent
         onStateChanged()
     }
 
@@ -75,7 +75,7 @@
     }
 
     private fun calculateScrimExpansionFraction(
-        expansionEvent: PanelExpansionChangeEvent,
+        expansionEvent: ShadeExpansionChangeEvent,
         @PanelState panelState: Int?
     ): Float {
         return if (canUseCustomFraction(panelState)) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt
index 6c3a028..22e847d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt
@@ -1,6 +1,6 @@
 package com.android.systemui.shade.transition
 
-import com.android.systemui.statusbar.phone.panelstate.PanelState
+import com.android.systemui.shade.PanelState
 
 /** Represents an over scroller for the non-lockscreen shade. */
 interface ShadeOverScroller {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
index 58acfb4..1e8208f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
@@ -7,13 +7,13 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.qs.QS
 import com.android.systemui.shade.NotificationPanelViewController
+import com.android.systemui.shade.PanelState
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.panelStateToString
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
-import com.android.systemui.statusbar.phone.panelstate.PanelState
-import com.android.systemui.statusbar.phone.panelstate.panelStateToString
 import com.android.systemui.statusbar.policy.ConfigurationController
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -24,7 +24,7 @@
 @Inject
 constructor(
     configurationController: ConfigurationController,
-    panelExpansionStateManager: PanelExpansionStateManager,
+    shadeExpansionStateManager: ShadeExpansionStateManager,
     dumpManager: DumpManager,
     private val context: Context,
     private val splitShadeOverScrollerFactory: SplitShadeOverScroller.Factory,
@@ -39,7 +39,7 @@
 
     private var inSplitShade = false
     private var currentPanelState: Int? = null
-    private var lastPanelExpansionChangeEvent: PanelExpansionChangeEvent? = null
+    private var lastShadeExpansionChangeEvent: ShadeExpansionChangeEvent? = null
 
     private val splitShadeOverScroller by lazy {
         splitShadeOverScrollerFactory.create({ qs }, { notificationStackScrollLayoutController })
@@ -60,8 +60,8 @@
                     updateResources()
                 }
             })
-        panelExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged)
-        panelExpansionStateManager.addStateListener(this::onPanelStateChanged)
+        shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged)
+        shadeExpansionStateManager.addStateListener(this::onPanelStateChanged)
         dumpManager.registerDumpable("ShadeTransitionController") { printWriter, _ ->
             dump(printWriter)
         }
@@ -77,8 +77,8 @@
         scrimShadeTransitionController.onPanelStateChanged(state)
     }
 
-    private fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) {
-        lastPanelExpansionChangeEvent = event
+    private fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
+        lastShadeExpansionChangeEvent = event
         shadeOverScroller.onDragDownAmountChanged(event.dragDownPxAmount)
         scrimShadeTransitionController.onPanelExpansionChanged(event)
     }
@@ -95,7 +95,7 @@
                 inSplitShade: $inSplitShade
                 isScreenUnlocked: ${isScreenUnlocked()}
                 currentPanelState: ${currentPanelState?.panelStateToString()}
-                lastPanelExpansionChangeEvent: $lastPanelExpansionChangeEvent
+                lastPanelExpansionChangeEvent: $lastShadeExpansionChangeEvent
                 qs.isInitialized: ${this::qs.isInitialized}
                 npvc.isInitialized: ${this::notificationPanelViewController.isInitialized}
                 nssl.isInitialized: ${this::notificationStackScrollLayoutController.isInitialized}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
index 204dd3c..8c57194 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
@@ -10,11 +10,11 @@
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.qs.QS
+import com.android.systemui.shade.PanelState
+import com.android.systemui.shade.STATE_CLOSED
+import com.android.systemui.shade.STATE_OPENING
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.PanelState
-import com.android.systemui.statusbar.phone.panelstate.STATE_CLOSED
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING
 import com.android.systemui.statusbar.policy.ConfigurationController
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -37,6 +37,7 @@
     private var previousOverscrollAmount = 0
     private var dragDownAmount: Float = 0f
     @PanelState private var panelState: Int = STATE_CLOSED
+
     private var releaseOverScrollAnimator: Animator? = null
 
     private val qS: QS
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index cb13fcf..b5879ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -38,12 +38,12 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionListener
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.LargeScreenUtils
@@ -69,7 +69,7 @@
     private val context: Context,
     dumpManager: DumpManager,
     configurationController: ConfigurationController
-) : PanelExpansionListener, Dumpable {
+) : ShadeExpansionListener, Dumpable {
     companion object {
         private const val WAKE_UP_ANIMATION_ENABLED = true
         private const val VELOCITY_SCALE = 100f
@@ -338,7 +338,7 @@
     /**
      * Update blurs when pulling down the shade
      */
-    override fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) {
+    override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
         val rawFraction = event.fraction
         val tracking = event.tracking
         val timestamp = SystemClock.elapsedRealtimeNanos()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
index 7807738..59afb18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java
@@ -36,8 +36,7 @@
     /**
      * Calculate and translate the QS Frame on the Y-axis.
      */
-    public abstract void translateQsFrame(View qsFrame, QS qs, float overExpansion,
-            float qsTranslationForFullShadeTransition);
+    public abstract void translateQsFrame(View qsFrame, QS qs, int bottomInset);
 
     /**
      * Calculate the top padding for notifications panel. This could be the supplied
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
index 33e2245..85b522c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java
@@ -27,6 +27,8 @@
 
 /**
  * Default implementation of QS Translation. This by default does not do much.
+ * This class can be subclassed to allow System UI variants the flexibility to change position of
+ * the Quick Settings frame.
  */
 @SysUISingleton
 public class QsFrameTranslateImpl extends QsFrameTranslateController {
@@ -37,8 +39,8 @@
     }
 
     @Override
-    public void translateQsFrame(View qsFrame, QS qs, float overExpansion,
-            float qsTranslationForFullShadeTransition) {
+    public void translateQsFrame(View qsFrame, QS qs, int bottomInset) {
+        // Empty implementation by default, meant to be overridden by subclasses.
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
index 1be4c04..b5c7ef5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.notification;
 
-import android.annotation.Nullable;
 import android.util.ArraySet;
 
 import androidx.annotation.VisibleForTesting;
@@ -25,7 +24,6 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import javax.inject.Inject;
@@ -43,7 +41,6 @@
 
     private boolean mLastDynamicUnlocked;
     private boolean mCacheInvalid;
-    @Nullable private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
 
     @Inject
     DynamicPrivacyController(NotificationLockscreenUserManager notificationLockscreenUserManager,
@@ -100,7 +97,7 @@
      * contents aren't revealed yet?
      */
     public boolean isInLockedDownShade() {
-        if (!isStatusBarKeyguardShowing() || !mKeyguardStateController.isMethodSecure()) {
+        if (!mKeyguardStateController.isShowing() || !mKeyguardStateController.isMethodSecure()) {
             return false;
         }
         int state = mStateController.getState();
@@ -113,16 +110,7 @@
         return true;
     }
 
-    private boolean isStatusBarKeyguardShowing() {
-        return mStatusBarKeyguardViewManager != null && mStatusBarKeyguardViewManager.isShowing();
-    }
-
-    public void setStatusBarKeyguardViewManager(
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
-        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
-    }
-
     public interface Listener {
         void onDynamicPrivacyChanged();
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 126a986..7242506 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -21,6 +21,8 @@
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionListener
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -28,8 +30,6 @@
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
 import javax.inject.Inject
@@ -42,7 +42,7 @@
     private val bypassController: KeyguardBypassController,
     private val dozeParameters: DozeParameters,
     private val screenOffAnimationController: ScreenOffAnimationController
-) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, PanelExpansionListener {
+) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener {
 
     private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>(
         "notificationVisibility") {
@@ -293,7 +293,7 @@
         this.state = newState
     }
 
-    override fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) {
+    override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
         val collapsedEnough = event.fraction <= 0.9f
         if (collapsedEnough != this.collapsedEnoughToHide) {
             val couldShowPulsingHuns = canShowPulsingHuns
@@ -426,4 +426,4 @@
          */
         @JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {}
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index ccf6fec..8f3eb4f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -440,6 +440,42 @@
         override fun onEntryCleanUp(entry: NotificationEntry) {
             mHeadsUpViewBinder.abortBindCallback(entry)
         }
+
+        /**
+         * Identify notifications whose heads-up state changes when the notification rankings are
+         * updated, and have those changed notifications alert if necessary.
+         *
+         * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any
+         * handling of ranking changes needs to take into account that we may have just made a
+         * PostedEntry for some of these notifications.
+         */
+        override fun onRankingApplied() {
+            // Because a ranking update may cause some notifications that are no longer (or were
+            // never) in mPostedEntries to need to alert, we need to check every notification
+            // known to the pipeline.
+            for (entry in mNotifPipeline.allNotifs) {
+                // The only entries we can consider alerting for here are entries that have never
+                // interrupted and that now say they should heads up; if they've alerted in the
+                // past, we don't want to incorrectly alert a second time if there wasn't an
+                // explicit notification update.
+                if (entry.hasInterrupted()) continue
+
+                // The cases where we should consider this notification to be updated:
+                // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
+                //   state
+                // - if it is present in PostedEntries and the previous state of shouldHeadsUp
+                //   differs from the updated one
+                val shouldHeadsUpEver = mNotificationInterruptStateProvider.checkHeadsUp(entry,
+                                /* log= */ false)
+                val postedShouldHeadsUpEver = mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
+                val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
+
+                if (shouldUpdateEntry) {
+                    mLogger.logEntryUpdatedByRanking(entry.key, shouldHeadsUpEver)
+                    onEntryUpdated(entry)
+                }
+            }
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 204a494..8625cdb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -59,4 +59,13 @@
                     " numPostedEntries=$int1 logicalGroupSize=$int2"
         })
     }
+
+    fun logEntryUpdatedByRanking(key: String, shouldHun: Boolean) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = key
+            bool1 = shouldHun
+        }, {
+            "updating entry via ranking applied: $str1 updated shouldHeadsUp=$bool1"
+        })
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 087dc71..1b00648 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -1487,7 +1487,7 @@
             l.setAlpha(alpha);
         }
         if (mChildrenContainer != null) {
-            mChildrenContainer.setAlpha(alpha);
+            mChildrenContainer.setContentAlpha(alpha);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index ce465bc..2719dd8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -44,7 +44,8 @@
 import javax.inject.Inject;
 
 /**
- * A global state to track all input states for the algorithm.
+ * Global state to track all input states for
+ * {@link com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm}.
  */
 @SysUISingleton
 public class AmbientState implements Dumpable {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 7b23a56..0dda263 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -461,6 +461,20 @@
         return mAttachedChildren;
     }
 
+    /**
+     * Sets the alpha on the content, while leaving the background of the container itself as is.
+     *
+     * @param alpha alpha value to apply to the content
+     */
+    public void setContentAlpha(float alpha) {
+        for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
+            mNotificationHeader.getChildAt(i).setAlpha(alpha);
+        }
+        for (ExpandableNotificationRow child : getAttachedChildren()) {
+            child.setContentAlpha(alpha);
+        }
+    }
+
     /** To be called any time the rows have been updated */
     public void updateExpansionStates() {
         if (mChildrenExpanded || mUserLocked) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 836cacc..55c577f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1115,6 +1115,10 @@
         updateAlgorithmLayoutMinHeight();
         updateOwnTranslationZ();
 
+        // Give The Algorithm information regarding the QS height so it can layout notifications
+        // properly. Needed for some devices that grows notifications down-to-top
+        mStackScrollAlgorithm.updateQSFrameTop(mQsHeader == null ? 0 : mQsHeader.getHeight());
+
         // Once the layout has finished, we don't need to animate any scrolling clampings anymore.
         mAnimateStackYForContentHeightChange = false;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 8d28f75..0502159 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -417,12 +417,19 @@
     }
 
     /**
+     * Update the position of QS Frame.
+     */
+    public void updateQSFrameTop(int qsHeight) {
+        // Intentionally empty for sub-classes in other device form factors to override
+    }
+
+    /**
      * Determine the positions for the views. This is the main part of the algorithm.
      *
      * @param algorithmState The state in which the current pass of the algorithm is currently in
      * @param ambientState   The current ambient state
      */
-    private void updatePositionsForState(StackScrollAlgorithmState algorithmState,
+    protected void updatePositionsForState(StackScrollAlgorithmState algorithmState,
             AmbientState ambientState) {
         if (!ambientState.isOnKeyguard()
                 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
@@ -448,7 +455,7 @@
      * @return Fraction to apply to view height and gap between views.
      *         Does not include shelf height even if shelf is showing.
      */
-    private float getExpansionFractionWithoutShelf(
+    protected float getExpansionFractionWithoutShelf(
             StackScrollAlgorithmState algorithmState,
             AmbientState ambientState) {
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index fe43137..a2798f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -163,7 +163,6 @@
     private PowerManager.WakeLock mWakeLock;
     private final com.android.systemui.shade.ShadeController mShadeController;
     private final KeyguardUpdateMonitor mUpdateMonitor;
-    private final DozeParameters mDozeParameters;
     private final KeyguardStateController mKeyguardStateController;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final SessionTracker mSessionTracker;
@@ -278,7 +277,7 @@
             KeyguardStateController keyguardStateController, Handler handler,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             @Main Resources resources,
-            KeyguardBypassController keyguardBypassController, DozeParameters dozeParameters,
+            KeyguardBypassController keyguardBypassController,
             MetricsLogger metricsLogger, DumpManager dumpManager,
             PowerManager powerManager,
             NotificationMediaManager notificationMediaManager,
@@ -294,7 +293,6 @@
         mPowerManager = powerManager;
         mShadeController = shadeController;
         mUpdateMonitor = keyguardUpdateMonitor;
-        mDozeParameters = dozeParameters;
         mUpdateMonitor.registerCallback(this);
         mMediaManager = notificationMediaManager;
         mLatencyTracker = latencyTracker;
@@ -552,7 +550,7 @@
         boolean deviceDreaming = mUpdateMonitor.isDreaming();
 
         if (!mUpdateMonitor.isDeviceInteractive()) {
-            if (!mKeyguardViewController.isShowing()
+            if (!mKeyguardStateController.isShowing()
                     && !mScreenOffAnimationController.isKeyguardShowDelayed()) {
                 if (mKeyguardStateController.isUnlocked()) {
                     return MODE_WAKE_AND_UNLOCK;
@@ -569,7 +567,7 @@
         if (unlockingAllowed && deviceDreaming) {
             return MODE_WAKE_AND_UNLOCK_FROM_DREAM;
         }
-        if (mKeyguardViewController.isShowing()) {
+        if (mKeyguardStateController.isShowing()) {
             if (mKeyguardViewController.bouncerIsOrWillBeShowing() && unlockingAllowed) {
                 return MODE_DISMISS_BOUNCER;
             } else if (unlockingAllowed) {
@@ -588,7 +586,7 @@
         boolean bypass = mKeyguardBypassController.getBypassEnabled()
                 || mAuthController.isUdfpsFingerDown();
         if (!mUpdateMonitor.isDeviceInteractive()) {
-            if (!mKeyguardViewController.isShowing()) {
+            if (!mKeyguardStateController.isShowing()) {
                 return bypass ? MODE_WAKE_AND_UNLOCK : MODE_ONLY_WAKE;
             } else if (!unlockingAllowed) {
                 return bypass ? MODE_SHOW_BOUNCER : MODE_NONE;
@@ -612,7 +610,7 @@
         if (unlockingAllowed && mKeyguardStateController.isOccluded()) {
             return MODE_UNLOCK_COLLAPSING;
         }
-        if (mKeyguardViewController.isShowing()) {
+        if (mKeyguardStateController.isShowing()) {
             if ((mKeyguardViewController.bouncerIsOrWillBeShowing()
                     || mKeyguardBypassController.getAltBouncerShowing()) && unlockingAllowed) {
                 return MODE_DISMISS_BOUNCER;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 52a45d6..1e95dad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -363,7 +363,7 @@
             mKeyguardUpdateMonitor.onCameraLaunched();
         }
 
-        if (!mStatusBarKeyguardViewManager.isShowing()) {
+        if (!mKeyguardStateController.isShowing()) {
             final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext);
             mCentralSurfaces.startActivityDismissingKeyguard(cameraIntent,
                     false /* onlyProvisioned */, true /* dismissShade */,
@@ -420,7 +420,7 @@
         // TODO(b/169087248) Possibly add haptics here for emergency action. Currently disabled for
         // app-side haptic experimentation.
 
-        if (!mStatusBarKeyguardViewManager.isShowing()) {
+        if (!mKeyguardStateController.isShowing()) {
             mCentralSurfaces.startActivityDismissingKeyguard(emergencyIntent,
                     false /* onlyProvisioned */, true /* dismissShade */,
                     true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 01a92b8..9958ec7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -183,6 +183,8 @@
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.AutoHideUiElement;
 import com.android.systemui.statusbar.BackDropView;
@@ -221,8 +223,6 @@
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneModule;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -476,7 +476,6 @@
     private final KeyguardStateController mKeyguardStateController;
     private final HeadsUpManagerPhone mHeadsUpManager;
     private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
-    private final DynamicPrivacyController mDynamicPrivacyController;
     private final FalsingCollector mFalsingCollector;
     private final FalsingManager mFalsingManager;
     private final BroadcastDispatcher mBroadcastDispatcher;
@@ -514,7 +513,7 @@
 
     private final NotificationGutsManager mGutsManager;
     private final NotificationLogger mNotificationLogger;
-    private final PanelExpansionStateManager mPanelExpansionStateManager;
+    private final ShadeExpansionStateManager mShadeExpansionStateManager;
     private final KeyguardViewMediator mKeyguardViewMediator;
     protected final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
     private final BrightnessSliderController.Factory mBrightnessSliderFactory;
@@ -700,7 +699,7 @@
             NotificationGutsManager notificationGutsManager,
             NotificationLogger notificationLogger,
             NotificationInterruptStateProvider notificationInterruptStateProvider,
-            PanelExpansionStateManager panelExpansionStateManager,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             KeyguardViewMediator keyguardViewMediator,
             DisplayMetrics displayMetrics,
             MetricsLogger metricsLogger,
@@ -779,14 +778,13 @@
         mHeadsUpManager = headsUpManagerPhone;
         mKeyguardIndicationController = keyguardIndicationController;
         mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
-        mDynamicPrivacyController = dynamicPrivacyController;
         mFalsingCollector = falsingCollector;
         mFalsingManager = falsingManager;
         mBroadcastDispatcher = broadcastDispatcher;
         mGutsManager = notificationGutsManager;
         mNotificationLogger = notificationLogger;
         mNotificationInterruptStateProvider = notificationInterruptStateProvider;
-        mPanelExpansionStateManager = panelExpansionStateManager;
+        mShadeExpansionStateManager = shadeExpansionStateManager;
         mKeyguardViewMediator = keyguardViewMediator;
         mDisplayMetrics = displayMetrics;
         mMetricsLogger = metricsLogger;
@@ -851,7 +849,7 @@
 
         mScreenOffAnimationController = screenOffAnimationController;
 
-        mPanelExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged);
+        mShadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged);
 
         mBubbleExpandListener = (isExpanding, key) ->
                 mContext.getMainExecutor().execute(this::updateScrimController);
@@ -1141,7 +1139,7 @@
         // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot.
         mNotificationLogger.setUpWithContainer(mNotifListContainer);
         mNotificationIconAreaController.setupShelf(mNotificationShelfController);
-        mPanelExpansionStateManager.addExpansionListener(mWakeUpCoordinator);
+        mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator);
         mUserSwitcherController.init(mNotificationShadeWindowView);
 
         // Allow plugins to reference DarkIconDispatcher and StatusBarStateController
@@ -1377,7 +1375,7 @@
         }
     }
 
-    private void onPanelExpansionChanged(PanelExpansionChangeEvent event) {
+    private void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
         float fraction = event.getFraction();
         boolean tracking = event.getTracking();
         dispatchPanelExpansionForKeyguardDismiss(fraction, tracking);
@@ -1561,7 +1559,7 @@
         mKeyguardViewMediator.registerCentralSurfaces(
                 /* statusBar= */ this,
                 mNotificationPanelViewController,
-                mPanelExpansionStateManager,
+                mShadeExpansionStateManager,
                 mBiometricUnlockController,
                 mStackScroller,
                 mKeyguardBypassController);
@@ -1570,7 +1568,6 @@
                 .setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
         mRemoteInputManager.addControllerCallback(mStatusBarKeyguardViewManager);
-        mDynamicPrivacyController.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
 
         mLightBarController.setBiometricUnlockController(mBiometricUnlockController);
         mMediaManager.setBiometricUnlockController(mBiometricUnlockController);
@@ -2082,7 +2079,7 @@
 
         // Trimming will happen later if Keyguard is showing - doing it here might cause a jank in
         // the bouncer appear animation.
-        if (!mStatusBarKeyguardViewManager.isShowing()) {
+        if (!mKeyguardStateController.isShowing()) {
             WindowManagerGlobal.getInstance().trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
         }
     }
@@ -2519,8 +2516,8 @@
         };
         // Do not deferKeyguard when occluded because, when keyguard is occluded,
         // we do not launch the activity until keyguard is done.
-        boolean occluded = mStatusBarKeyguardViewManager.isShowing()
-                && mStatusBarKeyguardViewManager.isOccluded();
+        boolean occluded = mKeyguardStateController.isShowing()
+                && mKeyguardStateController.isOccluded();
         boolean deferred = !occluded;
         executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShadeDirectly,
                 willLaunchResolverActivity, deferred /* deferred */, animate);
@@ -2590,8 +2587,8 @@
             @Override
             public boolean onDismiss() {
                 if (runnable != null) {
-                    if (mStatusBarKeyguardViewManager.isShowing()
-                            && mStatusBarKeyguardViewManager.isOccluded()) {
+                    if (mKeyguardStateController.isShowing()
+                            && mKeyguardStateController.isOccluded()) {
                         mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
                     } else {
                         mMainExecutor.execute(runnable);
@@ -2685,7 +2682,7 @@
 
     private void executeWhenUnlocked(OnDismissAction action, boolean requiresShadeOpen,
             boolean afterKeyguardGone) {
-        if (mStatusBarKeyguardViewManager.isShowing() && requiresShadeOpen) {
+        if (mKeyguardStateController.isShowing() && requiresShadeOpen) {
             mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
         }
         dismissKeyguardThenExecute(action, null /* cancelAction */,
@@ -2709,7 +2706,7 @@
             mBiometricUnlockController.startWakeAndUnlock(
                     BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING);
         }
-        if (mStatusBarKeyguardViewManager.isShowing()) {
+        if (mKeyguardStateController.isShowing()) {
             mStatusBarKeyguardViewManager.dismissWithAction(action, cancelAction,
                     afterKeyguardGone);
         } else {
@@ -2845,8 +2842,8 @@
     }
 
     private void logStateToEventlog() {
-        boolean isShowing = mStatusBarKeyguardViewManager.isShowing();
-        boolean isOccluded = mStatusBarKeyguardViewManager.isOccluded();
+        boolean isShowing = mKeyguardStateController.isShowing();
+        boolean isOccluded = mKeyguardStateController.isOccluded();
         boolean isBouncerShowing = mStatusBarKeyguardViewManager.isBouncerShowing();
         boolean isSecure = mKeyguardStateController.isMethodSecure();
         boolean unlocked = mKeyguardStateController.canDismissLockScreen();
@@ -3242,18 +3239,17 @@
         Trace.traceCounter(Trace.TRACE_TAG_APP, "dozing", mDozing ? 1 : 0);
         Trace.beginSection("CentralSurfaces#updateDozingState");
 
-        boolean visibleNotOccluded = mStatusBarKeyguardViewManager.isShowing()
-                && !mStatusBarKeyguardViewManager.isOccluded();
+        boolean keyguardVisible = mKeyguardStateController.isVisible();
         // If we're dozing and we'll be animating the screen off, the keyguard isn't currently
         // visible but will be shortly for the animation, so we should proceed as if it's visible.
-        boolean visibleNotOccludedOrWillBe =
-                visibleNotOccluded || (mDozing && mDozeParameters.shouldDelayKeyguardShow());
+        boolean keyguardVisibleOrWillBe =
+                keyguardVisible || (mDozing && mDozeParameters.shouldDelayKeyguardShow());
 
         boolean wakeAndUnlock = mBiometricUnlockController.getMode()
                 == BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
         boolean animate = (!mDozing && mDozeServiceHost.shouldAnimateWakeup() && !wakeAndUnlock)
                 || (mDozing && mDozeParameters.shouldControlScreenOff()
-                && visibleNotOccludedOrWillBe);
+                && keyguardVisibleOrWillBe);
 
         mNotificationPanelViewController.setDozing(mDozing, animate);
         updateQsExpansionEnabled();
@@ -3934,11 +3930,7 @@
 
     @Override
     public boolean isKeyguardShowing() {
-        if (mStatusBarKeyguardViewManager == null) {
-            Slog.i(TAG, "isKeyguardShowing() called before startKeyguard(), returning true");
-            return true;
-        }
-        return mStatusBarKeyguardViewManager.isShowing();
+        return mKeyguardStateController.isShowing();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 6e1cf13..b9312c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -68,6 +68,9 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.NotificationMediaManager;
@@ -77,9 +80,6 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
 import com.android.systemui.statusbar.phone.KeyguardBouncer.BouncerExpansionCallback;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.unfold.FoldAodAnimationController;
@@ -103,7 +103,7 @@
 @SysUISingleton
 public class StatusBarKeyguardViewManager implements RemoteInputController.Callback,
         StatusBarStateController.StateListener, ConfigurationController.ConfigurationListener,
-        PanelExpansionListener, NavigationModeController.ModeChangedListener,
+        ShadeExpansionListener, NavigationModeController.ModeChangedListener,
         KeyguardViewController, FoldAodAnimationController.FoldAodAnimationStatus {
 
     // When hiding the Keyguard with timing supplied from WindowManager, better be early than late.
@@ -229,8 +229,6 @@
     private View mNotificationContainer;
 
     @Nullable protected KeyguardBouncer mBouncer;
-    protected boolean mShowing;
-    protected boolean mOccluded;
     protected boolean mRemoteInputActive;
     private boolean mGlobalActionsVisible = false;
     private boolean mLastGlobalActionsVisible = false;
@@ -277,10 +275,9 @@
             new KeyguardUpdateMonitorCallback() {
         @Override
         public void onEmergencyCallAction() {
-
             // Since we won't get a setOccluded call we have to reset the view manually such that
             // the bouncer goes away.
-            if (mOccluded) {
+            if (mKeyguardStateController.isOccluded()) {
                 reset(true /* hideBouncerWhenShowing */);
             }
         }
@@ -338,7 +335,7 @@
     @Override
     public void registerCentralSurfaces(CentralSurfaces centralSurfaces,
             NotificationPanelViewController notificationPanelViewController,
-            PanelExpansionStateManager panelExpansionStateManager,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer,
             KeyguardBypassController bypassController) {
@@ -352,8 +349,8 @@
             mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
         }
         mNotificationPanelViewController = notificationPanelViewController;
-        if (panelExpansionStateManager != null) {
-            panelExpansionStateManager.addExpansionListener(this);
+        if (shadeExpansionStateManager != null) {
+            shadeExpansionStateManager.addExpansionListener(this);
         }
         mBypassController = bypassController;
         mNotificationContainer = notificationContainer;
@@ -445,7 +442,7 @@
     }
 
     @Override
-    public void onPanelExpansionChanged(PanelExpansionChangeEvent event) {
+    public void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
         float fraction = event.getFraction();
         boolean tracking = event.getTracking();
         // Avoid having the shade and the bouncer open at the same time over a dream.
@@ -479,7 +476,7 @@
             } else {
                 mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
             }
-        } else if (mShowing && !hideBouncerOverDream) {
+        } else if (mKeyguardStateController.isShowing()  && !hideBouncerOverDream) {
             if (!isWakeAndUnlocking()
                     && !(mBiometricUnlockController.getMode() == MODE_DISMISS_BOUNCER)
                     && !mCentralSurfaces.isInLaunchTransition()
@@ -500,7 +497,7 @@
                     mBouncerInteractor.show(/* isScrimmed= */false);
                 }
             }
-        } else if (!mShowing && isBouncerInTransit()) {
+        } else if (!mKeyguardStateController.isShowing()  && isBouncerInTransit()) {
             // Keyguard is not visible anymore, but expansion animation was still running.
             // We need to hide the bouncer, otherwise it will be stuck in transit.
             if (mBouncer != null) {
@@ -533,9 +530,8 @@
     @Override
     public void show(Bundle options) {
         Trace.beginSection("StatusBarKeyguardViewManager#show");
-        mShowing = true;
         mNotificationShadeWindowController.setKeyguardShowing(true);
-        mKeyguardStateController.notifyKeyguardState(mShowing,
+        mKeyguardStateController.notifyKeyguardState(true,
                 mKeyguardStateController.isOccluded());
         reset(true /* hideBouncerWhenShowing */);
         SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
@@ -599,7 +595,7 @@
         } else {
             mBouncerInteractor.hide();
         }
-        if (mShowing) {
+        if (mKeyguardStateController.isShowing()) {
             // If we were showing the bouncer and then aborting, we need to also clear out any
             // potential actions unless we actually unlocked.
             cancelPostAuthActions();
@@ -616,7 +612,7 @@
     public void showBouncer(boolean scrimmed) {
         resetAlternateAuth(false);
 
-        if (mShowing && !isBouncerShowing()) {
+        if (mKeyguardStateController.isShowing()  && !isBouncerShowing()) {
             if (mBouncer != null) {
                 mBouncer.show(false /* resetSecuritySelection */, scrimmed);
             } else {
@@ -633,7 +629,7 @@
 
     public void dismissWithAction(OnDismissAction r, Runnable cancelAction,
             boolean afterKeyguardGone, String message) {
-        if (mShowing) {
+        if (mKeyguardStateController.isShowing()) {
             try {
                 Trace.beginSection("StatusBarKeyguardViewManager#dismissWithAction");
                 cancelPendingWakeupAction();
@@ -719,11 +715,12 @@
 
     @Override
     public void reset(boolean hideBouncerWhenShowing) {
-        if (mShowing) {
+        if (mKeyguardStateController.isShowing()) {
+            final boolean isOccluded = mKeyguardStateController.isOccluded();
             // Hide quick settings.
-            mNotificationPanelViewController.resetViews(/* animate= */ !mOccluded);
+            mNotificationPanelViewController.resetViews(/* animate= */ !isOccluded);
             // Hide bouncer and quick-quick settings.
-            if (mOccluded && !mDozing) {
+            if (isOccluded && !mDozing) {
                 mCentralSurfaces.hideKeyguard();
                 if (hideBouncerWhenShowing || needsFullscreenBouncer()) {
                     hideBouncer(false /* destroyView */);
@@ -805,7 +802,8 @@
     private void setDozing(boolean dozing) {
         if (mDozing != dozing) {
             mDozing = dozing;
-            if (dozing || mBouncer.needsFullscreenBouncer() || mOccluded) {
+            if (dozing || mBouncer.needsFullscreenBouncer()
+                    || mKeyguardStateController.isOccluded()) {
                 reset(dozing /* hideBouncerWhenShowing */);
             }
             updateStates();
@@ -838,18 +836,23 @@
 
     @Override
     public void setOccluded(boolean occluded, boolean animate) {
-        final boolean isOccluding = !mOccluded && occluded;
-        final boolean isUnOccluding = mOccluded && !occluded;
-        setOccludedAndUpdateStates(occluded);
+        final boolean wasOccluded = mKeyguardStateController.isOccluded();
+        final boolean isOccluding = !wasOccluded && occluded;
+        final boolean isUnOccluding = wasOccluded  && !occluded;
+        mKeyguardStateController.notifyKeyguardState(
+                mKeyguardStateController.isShowing(), occluded);
+        updateStates();
+        final boolean isShowing = mKeyguardStateController.isShowing();
+        final boolean isOccluded = mKeyguardStateController.isOccluded();
 
-        if (mShowing && isOccluding) {
+        if (isShowing && isOccluding) {
             SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
                     SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__OCCLUDED);
             if (mCentralSurfaces.isInLaunchTransition()) {
                 final Runnable endRunnable = new Runnable() {
                     @Override
                     public void run() {
-                        mNotificationShadeWindowController.setKeyguardOccluded(mOccluded);
+                        mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
                         reset(true /* hideBouncerWhenShowing */);
                     }
                 };
@@ -864,19 +867,19 @@
                 // When isLaunchingActivityOverLockscreen() is true, we know for sure that the post
                 // collapse runnables will be run.
                 mShadeController.get().addPostCollapseAction(() -> {
-                    mNotificationShadeWindowController.setKeyguardOccluded(mOccluded);
+                    mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
                     reset(true /* hideBouncerWhenShowing */);
                 });
                 return;
             }
-        } else if (mShowing && isUnOccluding) {
+        } else if (isShowing && isUnOccluding) {
             SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
                     SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__SHOWN);
         }
-        if (mShowing) {
-            mMediaManager.updateMediaMetaData(false, animate && !mOccluded);
+        if (isShowing) {
+            mMediaManager.updateMediaMetaData(false, animate && !isOccluded);
         }
-        mNotificationShadeWindowController.setKeyguardOccluded(mOccluded);
+        mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
 
         // setDozing(false) will call reset once we stop dozing. Also, if we're going away, there's
         // no need to reset the keyguard views as we'll be gone shortly. Resetting now could cause
@@ -886,20 +889,11 @@
             // by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
             reset(isOccluding /* hideBouncerWhenShowing*/);
         }
-        if (animate && !mOccluded && mShowing && !bouncerIsShowing()) {
+        if (animate && !isOccluded && isShowing && !bouncerIsShowing()) {
             mCentralSurfaces.animateKeyguardUnoccluding();
         }
     }
 
-    private void setOccludedAndUpdateStates(boolean occluded) {
-        mOccluded = occluded;
-        updateStates();
-    }
-
-    public boolean isOccluded() {
-        return mOccluded;
-    }
-
     @Override
     public void startPreHideAnimation(Runnable finishRunnable) {
         if (bouncerIsShowing()) {
@@ -930,8 +924,7 @@
     @Override
     public void hide(long startTime, long fadeoutDuration) {
         Trace.beginSection("StatusBarKeyguardViewManager#hide");
-        mShowing = false;
-        mKeyguardStateController.notifyKeyguardState(mShowing,
+        mKeyguardStateController.notifyKeyguardState(false,
                 mKeyguardStateController.isOccluded());
         launchPendingWakeupAction();
 
@@ -1080,11 +1073,6 @@
                 KeyguardUpdateMonitor.getCurrentUser()) != KeyguardSecurityModel.SecurityMode.None;
     }
 
-    @Override
-    public boolean isShowing() {
-        return mShowing;
-    }
-
     /**
      * Returns whether a back invocation can be handled, which depends on whether the keyguard
      * is currently showing (which itself is derived from multiple states).
@@ -1187,8 +1175,8 @@
     };
 
     protected void updateStates() {
-        boolean showing = mShowing;
-        boolean occluded = mOccluded;
+        boolean showing = mKeyguardStateController.isShowing();
+        boolean occluded = mKeyguardStateController.isOccluded();
         boolean bouncerShowing = bouncerIsShowing();
         boolean bouncerIsOrWillBeShowing = bouncerIsOrWillBeShowing();
         boolean bouncerDismissible = !isFullscreenBouncer();
@@ -1222,13 +1210,6 @@
             mNotificationShadeWindowController.setBouncerShowing(bouncerShowing);
             mCentralSurfaces.setBouncerShowing(bouncerShowing);
         }
-
-        if (occluded != mLastOccluded || mFirstUpdate) {
-            mKeyguardStateController.notifyKeyguardState(showing, occluded);
-        }
-        if (occluded != mLastOccluded || mShowing != showing || mFirstUpdate) {
-            mKeyguardUpdateManager.setKeyguardShowing(showing, occluded);
-        }
         if (bouncerIsOrWillBeShowing != mLastBouncerIsOrWillBeShowing || mFirstUpdate
                 || bouncerShowing != mLastBouncerShowing) {
             mKeyguardUpdateManager.sendKeyguardBouncerChanged(bouncerIsOrWillBeShowing,
@@ -1279,12 +1260,12 @@
     public boolean isNavBarVisible() {
         boolean isWakeAndUnlockPulsing = mBiometricUnlockController != null
                 && mBiometricUnlockController.getMode() == MODE_WAKE_AND_UNLOCK_PULSING;
-        boolean keyguardShowing = mShowing && !mOccluded;
+        boolean keyguardVisible = mKeyguardStateController.isVisible();
         boolean hideWhileDozing = mDozing && !isWakeAndUnlockPulsing;
-        boolean keyguardWithGestureNav = (keyguardShowing && !mDozing && !mScreenOffAnimationPlaying
+        boolean keyguardWithGestureNav = (keyguardVisible && !mDozing && !mScreenOffAnimationPlaying
                 || mPulsing && !mIsDocked)
                 && mGesturalNav;
-        return (!keyguardShowing && !hideWhileDozing && !mScreenOffAnimationPlaying
+        return (!keyguardVisible && !hideWhileDozing && !mScreenOffAnimationPlaying
                 || bouncerIsShowing()
                 || mRemoteInputActive
                 || keyguardWithGestureNav
@@ -1416,7 +1397,7 @@
         DismissWithActionRequest request = mPendingWakeupAction;
         mPendingWakeupAction = null;
         if (request != null) {
-            if (mShowing) {
+            if (mKeyguardStateController.isShowing()) {
                 dismissWithAction(request.dismissAction, request.cancelAction,
                         request.afterKeyguardGone, request.message);
             } else if (request.dismissAction != null) {
@@ -1435,10 +1416,10 @@
 
     public boolean bouncerNeedsScrimming() {
         // When a dream overlay is active, scrimming will cause any expansion to immediately expand.
-        return (mOccluded && !mDreamOverlayStateController.isOverlayActive())
+        return (mKeyguardStateController.isOccluded()
+                && !mDreamOverlayStateController.isOverlayActive())
                 || bouncerWillDismissWithAction()
-                || (bouncerIsShowing()
-                && bouncerIsScrimmed())
+                || (bouncerIsShowing() && bouncerIsScrimmed())
                 || isFullscreenBouncer();
     }
 
@@ -1457,8 +1438,6 @@
 
     public void dump(PrintWriter pw) {
         pw.println("StatusBarKeyguardViewManager:");
-        pw.println("  mShowing: " + mShowing);
-        pw.println("  mOccluded: " + mOccluded);
         pw.println("  mRemoteInputActive: " + mRemoteInputActive);
         pw.println("  mDozing: " + mDozing);
         pw.println("  mAfterKeyguardGoneAction: " + mAfterKeyguardGoneAction);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 627cfb7..dc90266 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -41,6 +41,7 @@
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.NotificationsQuickSettingsContainer;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.NotificationShelfController;
@@ -64,7 +65,6 @@
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragmentLogger;
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -284,7 +284,7 @@
             SystemStatusAnimationScheduler animationScheduler,
             StatusBarLocationPublisher locationPublisher,
             NotificationIconAreaController notificationIconAreaController,
-            PanelExpansionStateManager panelExpansionStateManager,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             FeatureFlags featureFlags,
             StatusBarIconController statusBarIconController,
             StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory,
@@ -306,7 +306,7 @@
                 animationScheduler,
                 locationPublisher,
                 notificationIconAreaController,
-                panelExpansionStateManager,
+                shadeExpansionStateManager,
                 featureFlags,
                 statusBarIconController,
                 darkIconManagerFactory,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index e1215ee..9f3fd72 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -53,6 +53,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DisableFlagsLogger.DisableState;
 import com.android.systemui.statusbar.OperatorNameView;
@@ -74,7 +75,6 @@
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent.Startable;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.EncryptionHelper;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.CarrierConfigTracker;
@@ -127,7 +127,7 @@
     private final StatusBarLocationPublisher mLocationPublisher;
     private final FeatureFlags mFeatureFlags;
     private final NotificationIconAreaController mNotificationIconAreaController;
-    private final PanelExpansionStateManager mPanelExpansionStateManager;
+    private final ShadeExpansionStateManager mShadeExpansionStateManager;
     private final StatusBarIconController mStatusBarIconController;
     private final CarrierConfigTracker mCarrierConfigTracker;
     private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
@@ -184,7 +184,7 @@
             SystemStatusAnimationScheduler animationScheduler,
             StatusBarLocationPublisher locationPublisher,
             NotificationIconAreaController notificationIconAreaController,
-            PanelExpansionStateManager panelExpansionStateManager,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             FeatureFlags featureFlags,
             StatusBarIconController statusBarIconController,
             StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory,
@@ -206,7 +206,7 @@
         mAnimationScheduler = animationScheduler;
         mLocationPublisher = locationPublisher;
         mNotificationIconAreaController = notificationIconAreaController;
-        mPanelExpansionStateManager = panelExpansionStateManager;
+        mShadeExpansionStateManager = shadeExpansionStateManager;
         mFeatureFlags = featureFlags;
         mStatusBarIconController = statusBarIconController;
         mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
@@ -490,7 +490,7 @@
     }
 
     private boolean shouldHideNotificationIcons() {
-        if (!mPanelExpansionStateManager.isClosed()
+        if (!mShadeExpansionStateManager.isClosed()
                 && mNotificationPanelViewController.hideStatusBarIconsWhenExpanded()) {
             return true;
         }
@@ -536,7 +536,7 @@
      * don't set the clock GONE otherwise it'll mess up the animation.
      */
     private int clockHiddenMode() {
-        if (!mPanelExpansionStateManager.isClosed() && !mKeyguardStateController.isShowing()
+        if (!mShadeExpansionStateManager.isClosed() && !mKeyguardStateController.isShowing()
                 && !mStatusBarStateController.isDozing()) {
             return View.INVISIBLE;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
index c96faab..062c3d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -21,7 +21,9 @@
 /** Provides information about the current wifi network. */
 sealed class WifiNetworkModel {
     /** A model representing that we have no active wifi network. */
-    object Inactive : WifiNetworkModel()
+    object Inactive : WifiNetworkModel() {
+        override fun toString() = "WifiNetwork.Inactive"
+    }
 
     /**
      * A model representing that our wifi network is actually a carrier merged network, meaning it's
@@ -29,7 +31,9 @@
      *
      * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
      */
-    object CarrierMerged : WifiNetworkModel()
+    object CarrierMerged : WifiNetworkModel() {
+        override fun toString() = "WifiNetwork.CarrierMerged"
+    }
 
     /** Provides information about an active wifi network. */
     data class Active(
@@ -72,6 +76,24 @@
             }
         }
 
+        override fun toString(): String {
+            // Only include the passpoint-related values in the string if we have them. (Most
+            // networks won't have them so they'll be mostly clutter.)
+            val passpointString =
+                if (isPasspointAccessPoint ||
+                    isOnlineSignUpForPasspointAccessPoint ||
+                    passpointProviderFriendlyName != null) {
+                    ", isPasspointAp=$isPasspointAccessPoint, " +
+                        "isOnlineSignUpForPasspointAp=$isOnlineSignUpForPasspointAccessPoint, " +
+                        "passpointName=$passpointProviderFriendlyName"
+            } else {
+                ""
+            }
+
+            return "WifiNetworkModel.Active(networkId=$networkId, isValidated=$isValidated, " +
+                "level=$level, ssid=$ssid$passpointString)"
+        }
+
         companion object {
             @VisibleForTesting
             internal const val MIN_VALID_LEVEL = 0
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
index 250d9d4..1ae1eae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
@@ -35,9 +35,16 @@
     }
 
     /**
-     * If the lock screen is visible.
-     * The keyguard is also visible when the device is asleep or in always on mode, except when
-     * the screen timed out and the user can unlock by quickly pressing power.
+     * If the keyguard is visible. This is unrelated to being locked or not.
+     */
+    default boolean isVisible() {
+        return isShowing() && !isOccluded();
+    }
+
+    /**
+     * If the keyguard is showing. This includes when it's occluded by an activity, and when
+     * the device is asleep or in always on mode, except when the screen timed out and the user
+     * can unlock by quickly pressing power.
      *
      * This is unrelated to being locked or not.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index 437d4d4..cc6fdcc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -181,6 +181,7 @@
         if (mShowing == showing && mOccluded == occluded) return;
         mShowing = showing;
         mOccluded = occluded;
+        mKeyguardUpdateMonitor.setKeyguardShowing(showing, occluded);
         Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events",
                 "Keyguard showing: " + showing + " occluded: " + occluded);
         notifyKeyguardChanged();
@@ -387,6 +388,8 @@
     @Override
     public void dump(PrintWriter pw, String[] args) {
         pw.println("KeyguardStateController:");
+        pw.println("  mShowing: " + mShowing);
+        pw.println("  mOccluded: " + mOccluded);
         pw.println("  mSecure: " + mSecure);
         pw.println("  mCanDismissLockScreen: " + mCanDismissLockScreen);
         pw.println("  mTrustManaged: " + mTrustManaged);
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index fc20ac2..6ed3a09 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -26,8 +26,6 @@
 import android.view.Choreographer
 import android.view.Display
 import android.view.DisplayInfo
-import android.view.IRotationWatcher
-import android.view.IWindowManager
 import android.view.Surface
 import android.view.SurfaceControl
 import android.view.SurfaceControlViewHost
@@ -40,6 +38,7 @@
 import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.statusbar.LinearLightRevealEffect
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.RotationChangeProvider
 import com.android.systemui.util.traceSection
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper
 import java.util.Optional
@@ -58,7 +57,7 @@
     private val displayAreaHelper: Optional<DisplayAreaHelper>,
     @Main private val executor: Executor,
     @UiBackground private val backgroundExecutor: Executor,
-    private val windowManagerInterface: IWindowManager
+    private val rotationChangeProvider: RotationChangeProvider,
 ) {
 
     private val transitionListener = TransitionListener()
@@ -78,7 +77,7 @@
     fun init() {
         deviceStateManager.registerCallback(executor, FoldListener())
         unfoldTransitionProgressProvider.addCallback(transitionListener)
-        windowManagerInterface.watchRotation(rotationWatcher, context.display.displayId)
+        rotationChangeProvider.addCallback(rotationWatcher)
 
         val containerBuilder =
             SurfaceControl.Builder(SurfaceSession())
@@ -86,7 +85,9 @@
                 .setName("unfold-overlay-container")
 
         displayAreaHelper.get().attachToRootDisplayArea(
-                Display.DEFAULT_DISPLAY, containerBuilder) { builder ->
+            Display.DEFAULT_DISPLAY,
+            containerBuilder
+        ) { builder ->
             executor.execute {
                 overlayContainer = builder.build()
 
@@ -244,8 +245,8 @@
         }
     }
 
-    private inner class RotationWatcher : IRotationWatcher.Stub() {
-        override fun onRotationChanged(newRotation: Int) =
+    private inner class RotationWatcher : RotationChangeProvider.RotationListener {
+        override fun onRotationChanged(newRotation: Int) {
             traceSection("UnfoldLightRevealOverlayAnimation#onRotationChanged") {
                 if (currentRotation != newRotation) {
                     currentRotation = newRotation
@@ -253,6 +254,7 @@
                     root?.relayout(getLayoutParams())
                 }
             }
+        }
     }
 
     private inner class FoldListener :
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
index eea6ac0..59ad24a 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
@@ -17,11 +17,11 @@
 package com.android.systemui.unfold
 
 import android.content.Context
-import android.view.IWindowManager
 import com.android.systemui.keyguard.LifecycleScreenStatusProvider
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
 import com.android.systemui.unfold.system.SystemUnfoldSharedModule
 import com.android.systemui.unfold.updates.FoldStateProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
 import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
@@ -65,11 +65,11 @@
     @Singleton
     fun provideNaturalRotationProgressProvider(
         context: Context,
-        windowManager: IWindowManager,
+        rotationChangeProvider: RotationChangeProvider,
         unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider>
     ): Optional<NaturalRotationUnfoldProgressProvider> =
         unfoldTransitionProgressProvider.map { provider ->
-            NaturalRotationUnfoldProgressProvider(context, windowManager, provider)
+            NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, provider)
         }
 
     @Provides
diff --git a/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java
new file mode 100644
index 0000000..dd9683f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.assist.ui;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DisplayUtilsTest extends SysuiTestCase {
+
+    @Mock
+    Resources mResources;
+    @Mock
+    Context mMockContext;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testGetCornerRadii_noOverlay() {
+        assertEquals(0, DisplayUtils.getCornerRadiusBottom(mContext));
+        assertEquals(0, DisplayUtils.getCornerRadiusTop(mContext));
+    }
+
+    @Test
+    public void testGetCornerRadii_onlyDefaultOverridden() {
+        when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size)).thenReturn(100);
+        when(mMockContext.getResources()).thenReturn(mResources);
+
+        assertEquals(100, DisplayUtils.getCornerRadiusBottom(mMockContext));
+        assertEquals(100, DisplayUtils.getCornerRadiusTop(mMockContext));
+    }
+
+    @Test
+    public void testGetCornerRadii_allOverridden() {
+        when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size)).thenReturn(100);
+        when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size_top)).thenReturn(
+                150);
+        when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size_bottom)).thenReturn(
+                200);
+        when(mMockContext.getResources()).thenReturn(mResources);
+
+        assertEquals(200, DisplayUtils.getCornerRadiusBottom(mMockContext));
+        assertEquals(150, DisplayUtils.getCornerRadiusTop(mMockContext));
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 5c564e6..baeabc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -17,13 +17,23 @@
 package com.android.systemui.biometrics
 
 import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants.*
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
+import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
 import android.hardware.fingerprint.FingerprintManager
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
-import android.view.*
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.Surface
 import android.view.Surface.Rotation
+import android.view.View
+import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -32,11 +42,11 @@
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.time.SystemClock
@@ -52,8 +62,8 @@
 import org.mockito.Mock
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
 
 private const val REQUEST_ID = 2L
 
@@ -75,7 +85,7 @@
     @Mock private lateinit var windowManager: WindowManager
     @Mock private lateinit var accessibilityManager: AccessibilityManager
     @Mock private lateinit var statusBarStateController: StatusBarStateController
-    @Mock private lateinit var panelExpansionStateManager: PanelExpansionStateManager
+    @Mock private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
     @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     @Mock private lateinit var dialogManager: SystemUIDialogManager
@@ -117,7 +127,7 @@
     private fun withReason(@ShowReason reason: Int, block: () -> Unit) {
         controllerOverlay = UdfpsControllerOverlay(
             context, fingerprintManager, inflater, windowManager, accessibilityManager,
-            statusBarStateController, panelExpansionStateManager, statusBarKeyguardViewManager,
+            statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager,
             keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
             configurationController, systemClock, keyguardStateController,
             unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 53e8c6e..8923ba8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -38,6 +38,7 @@
 import static org.mockito.Mockito.when;
 
 import android.graphics.Rect;
+import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.SensorProperties;
@@ -71,12 +72,12 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.Execution;
@@ -245,7 +246,7 @@
                 mWindowManager,
                 mStatusBarStateController,
                 mFgExecutor,
-                new PanelExpansionStateManager(),
+                new ShadeExpansionStateManager(),
                 mStatusBarKeyguardViewManager,
                 mDumpManager,
                 mKeyguardUpdateMonitor,
@@ -682,7 +683,7 @@
     }
 
     @Test
-    public void aodInterruptCancelTimeoutActionWhenFingerUp() throws RemoteException {
+    public void aodInterruptCancelTimeoutActionOnFingerUp() throws RemoteException {
         when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true);
         when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
 
@@ -734,6 +735,56 @@
     }
 
     @Test
+    public void aodInterruptCancelTimeoutActionOnAcquired() throws RemoteException {
+        when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true);
+        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
+
+        // GIVEN AOD interrupt
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+        mScreenObserver.onScreenTurnedOn();
+        mFgExecutor.runAllReady();
+        mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
+        mFgExecutor.runAllReady();
+
+        // Configure UdfpsView to accept the acquired event
+        when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+
+        // WHEN acquired is received
+        mOverlayController.onAcquired(TEST_UDFPS_SENSOR_ID,
+                BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD);
+
+        // Configure UdfpsView to accept the ACTION_DOWN event
+        when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
+
+        // WHEN ACTION_DOWN is received
+        verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
+        MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
+        mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
+        mBiometricsExecutor.runAllReady();
+        downEvent.recycle();
+
+        // WHEN ACTION_MOVE is received
+        MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0);
+        mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
+        mBiometricsExecutor.runAllReady();
+        moveEvent.recycle();
+        mFgExecutor.runAllReady();
+
+        // Configure UdfpsView to accept the finger up event
+        when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+
+        // WHEN it times out
+        mFgExecutor.advanceClockToNext();
+        mFgExecutor.runAllReady();
+
+        // THEN the display should be unconfigured once. If the timeout action is not
+        // cancelled, the display would be unconfigured twice which would cause two
+        // FP attempts.
+        verify(mUdfpsView, times(1)).unconfigureDisplay();
+    }
+
+    @Test
     public void aodInterruptScreenOff() throws RemoteException {
         // GIVEN screen off
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
index b61bda8..c0f9c82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
@@ -41,14 +41,14 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -76,7 +76,7 @@
     @Mock
     private StatusBarStateController mStatusBarStateController;
     @Mock
-    private PanelExpansionStateManager mPanelExpansionStateManager;
+    private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Mock
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     @Mock
@@ -109,8 +109,8 @@
     @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor;
     private StatusBarStateController.StateListener mStatusBarStateListener;
 
-    @Captor private ArgumentCaptor<PanelExpansionListener> mExpansionListenerCaptor;
-    private List<PanelExpansionListener> mExpansionListeners;
+    @Captor private ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor;
+    private List<ShadeExpansionListener> mExpansionListeners;
 
     @Captor private ArgumentCaptor<StatusBarKeyguardViewManager.AlternateAuthInterceptor>
             mAltAuthInterceptorCaptor;
@@ -130,7 +130,7 @@
         mController = new UdfpsKeyguardViewController(
                 mView,
                 mStatusBarStateController,
-                mPanelExpansionStateManager,
+                mShadeExpansionStateManager,
                 mStatusBarKeyguardViewManager,
                 mKeyguardUpdateMonitor,
                 mDumpManager,
@@ -182,8 +182,8 @@
         mController.onViewDetached();
 
         verify(mStatusBarStateController).removeCallback(mStatusBarStateListener);
-        for (PanelExpansionListener listener : mExpansionListeners) {
-            verify(mPanelExpansionStateManager).removeExpansionListener(listener);
+        for (ShadeExpansionListener listener : mExpansionListeners) {
+            verify(mShadeExpansionStateManager).removeExpansionListener(listener);
         }
         verify(mKeyguardStateController).removeCallback(mKeyguardStateControllerCallback);
     }
@@ -513,7 +513,7 @@
     }
 
     private void captureStatusBarExpansionListeners() {
-        verify(mPanelExpansionStateManager, times(2))
+        verify(mShadeExpansionStateManager, times(2))
                 .addExpansionListener(mExpansionListenerCaptor.capture());
         // first (index=0) is from super class, UdfpsAnimationViewController.
         // second (index=1) is from UdfpsKeyguardViewController
@@ -521,10 +521,10 @@
     }
 
     private void updateStatusBarExpansion(float fraction, boolean expanded) {
-        PanelExpansionChangeEvent event =
-                new PanelExpansionChangeEvent(
+        ShadeExpansionChangeEvent event =
+                new ShadeExpansionChangeEvent(
                         fraction, expanded, /* tracking= */ false, /* dragDownPxAmount= */ 0f);
-        for (PanelExpansionListener listener : mExpansionListeners) {
+        for (ShadeExpansionListener listener : mExpansionListeners) {
             listener.onPanelExpansionChanged(event);
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index eec33ca..f370be1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -27,10 +28,10 @@
 import android.content.Intent;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.service.dreams.DreamService;
 import android.service.dreams.IDreamOverlay;
 import android.service.dreams.IDreamOverlayCallback;
 import android.testing.AndroidTestingRunner;
+import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.view.WindowManagerImpl;
@@ -53,6 +54,8 @@
 import org.junit.Rule;
 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;
 
@@ -61,6 +64,7 @@
 public class DreamOverlayServiceTest extends SysuiTestCase {
     private static final ComponentName LOW_LIGHT_COMPONENT = new ComponentName("package",
             "lowlight");
+    private static final String DREAM_COMPONENT = "package/dream";
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
 
@@ -108,12 +112,14 @@
     @Mock
     UiEventLogger mUiEventLogger;
 
+    @Captor
+    ArgumentCaptor<View> mViewCaptor;
+
     DreamOverlayService mService;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mContext.addMockSystemService(WindowManager.class, mWindowManager);
 
         when(mDreamOverlayComponent.getDreamOverlayContainerViewController())
                 .thenReturn(mDreamOverlayContainerViewController);
@@ -129,7 +135,7 @@
         when(mDreamOverlayContainerViewController.getContainerView())
                 .thenReturn(mDreamOverlayContainerView);
 
-        mService = new DreamOverlayService(mContext, mMainExecutor,
+        mService = new DreamOverlayService(mContext, mMainExecutor, mWindowManager,
                 mDreamOverlayComponentFactory,
                 mStateController,
                 mKeyguardUpdateMonitor,
@@ -143,7 +149,8 @@
         final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback);
+        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
         verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
@@ -157,7 +164,8 @@
         final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback);
+        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
         verify(mWindowManager).addView(any(), any());
@@ -169,7 +177,8 @@
         final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback);
+        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
         verify(mDreamOverlayContainerViewController).init();
@@ -186,49 +195,76 @@
         final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback);
+        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
         verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView);
     }
 
     @Test
-    public void testShouldShowComplicationsFalseByDefault() {
-        mService.onBind(new Intent());
+    public void testShouldShowComplicationsSetByStartDream() throws RemoteException {
+        final IBinder proxy = mService.onBind(new Intent());
+        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
 
-        assertThat(mService.shouldShowComplications()).isFalse();
-    }
-
-    @Test
-    public void testShouldShowComplicationsSetByIntentExtra() {
-        final Intent intent = new Intent();
-        intent.putExtra(DreamService.EXTRA_SHOW_COMPLICATIONS, true);
-        mService.onBind(intent);
+        // Inform the overlay service of dream starting.
+        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                true /*shouldShowComplication*/);
 
         assertThat(mService.shouldShowComplications()).isTrue();
     }
 
     @Test
-    public void testLowLightSetByIntentExtra() throws RemoteException {
-        final Intent intent = new Intent();
-        intent.putExtra(DreamService.EXTRA_DREAM_COMPONENT, LOW_LIGHT_COMPONENT);
-
-        final IBinder proxy = mService.onBind(intent);
+    public void testLowLightSetByStartDream() throws RemoteException {
+        final IBinder proxy = mService.onBind(new Intent());
         final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
-        assertThat(mService.getDreamComponent()).isEqualTo(LOW_LIGHT_COMPONENT);
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback);
+        overlay.startDream(mWindowParams, mDreamOverlayCallback,
+                LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
         mMainExecutor.runAllReady();
 
+        assertThat(mService.getDreamComponent()).isEqualTo(LOW_LIGHT_COMPONENT);
         verify(mStateController).setLowLightActive(true);
     }
 
     @Test
-    public void testDestroy() {
+    public void testDestroy() throws RemoteException {
+        final IBinder proxy = mService.onBind(new Intent());
+        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+
+        // Inform the overlay service of dream starting.
+        overlay.startDream(mWindowParams, mDreamOverlayCallback,
+                LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
+        mMainExecutor.runAllReady();
+
+        // Verify view added.
+        verify(mWindowManager).addView(mViewCaptor.capture(), any());
+
+        // Service destroyed.
         mService.onDestroy();
         mMainExecutor.runAllReady();
 
+        // Verify view removed.
+        verify(mWindowManager).removeView(mViewCaptor.getValue());
+
+        // Verify state correctly set.
+        verify(mKeyguardUpdateMonitor).removeCallback(any());
+        verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED);
+        verify(mStateController).setOverlayActive(false);
+        verify(mStateController).setLowLightActive(false);
+    }
+
+    @Test
+    public void testDoNotRemoveViewOnDestroyIfOverlayNotStarted() {
+        // Service destroyed without ever starting dream.
+        mService.onDestroy();
+        mMainExecutor.runAllReady();
+
+        // Verify no view is removed.
+        verify(mWindowManager, never()).removeView(any());
+
+        // Verify state still correctly set.
         verify(mKeyguardUpdateMonitor).removeCallback(any());
         verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED);
         verify(mStateController).setOverlayActive(false);
@@ -245,7 +281,8 @@
         final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
 
         // Inform the overlay service of dream starting.
-        overlay.startDream(mWindowParams, mDreamOverlayCallback);
+        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                false /*shouldShowComplication*/);
 
         // Destroy the service.
         mService.onDestroy();
@@ -255,4 +292,44 @@
 
         verify(mWindowManager, never()).addView(any(), any());
     }
+
+    @Test
+    public void testResetCurrentOverlayWhenConnectedToNewDream() throws RemoteException {
+        final IBinder proxy = mService.onBind(new Intent());
+        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+
+        // Inform the overlay service of dream starting. Do not show dream complications.
+        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                false /*shouldShowComplication*/);
+        mMainExecutor.runAllReady();
+
+        // Verify that a new window is added.
+        verify(mWindowManager).addView(mViewCaptor.capture(), any());
+        final View windowDecorView = mViewCaptor.getValue();
+
+        // Assert that the overlay is not showing complications.
+        assertThat(mService.shouldShowComplications()).isFalse();
+
+        clearInvocations(mDreamOverlayComponent);
+        clearInvocations(mWindowManager);
+
+        // New dream starting with dream complications showing. Note that when a new dream is
+        // binding to the dream overlay service, it receives the same instance of IBinder as the
+        // first one.
+        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                true /*shouldShowComplication*/);
+        mMainExecutor.runAllReady();
+
+        // Assert that the overlay is showing complications.
+        assertThat(mService.shouldShowComplications()).isTrue();
+
+        // Verify that the old overlay window has been removed, and a new one created.
+        verify(mWindowManager).removeView(windowDecorView);
+        verify(mWindowManager).addView(any(), any());
+
+        // Verify that new instances of overlay container view controller and overlay touch monitor
+        // are created.
+        verify(mDreamOverlayComponent).getDreamOverlayContainerViewController();
+        verify(mDreamOverlayComponent).getDreamOverlayTouchMonitor();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
index c3fca29..4bd53c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
@@ -41,12 +41,12 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.KeyguardBouncer;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
 import org.junit.Before;
@@ -285,8 +285,8 @@
         final float dragDownAmount = event2.getY() - event1.getY();
 
         // Ensure correct expansion passed in.
-        PanelExpansionChangeEvent event =
-                new PanelExpansionChangeEvent(
+        ShadeExpansionChangeEvent event =
+                new ShadeExpansionChangeEvent(
                         expansion, /* expanded= */ false, /* tracking= */ true, dragDownAmount);
         verify(mStatusBarKeyguardViewManager).onPanelExpansionChanged(event);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 21c018a..39f3c96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -176,7 +176,7 @@
 
         // and the keyguard goes away
         mViewMediator.setShowingLocked(false);
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
         mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false);
 
         TestableLooper.get(this).processAllMessages();
@@ -201,7 +201,7 @@
 
         // and the keyguard goes away
         mViewMediator.setShowingLocked(false);
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
         mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false);
 
         TestableLooper.get(this).processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
index 5ad3542..f34c2ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
@@ -25,6 +25,11 @@
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.PAGINATION_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.media.MediaHierarchyManager.Companion.LOCATION_QS
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
@@ -398,4 +403,24 @@
         // added to the end because it was active less recently.
         assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
     }
+
+    @Test
+    fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
+        val delta = 0.0001F
+        val paginationSquishMiddle = TRANSFORM_BEZIER.getInterpolation(
+                (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION)
+        val paginationSquishEnd = TRANSFORM_BEZIER.getInterpolation(
+                (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+        whenever(mediaHostStatesManager.mediaHostStates)
+            .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
+        whenever(mediaHostState.visible).thenReturn(true)
+        mediaCarouselController.currentEndLocation = LOCATION_QS
+        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle)
+        mediaCarouselController.updatePageIndicatorAlpha()
+        assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta)
+
+        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd)
+        mediaCarouselController.updatePageIndicatorAlpha()
+        assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
new file mode 100644
index 0000000..622a512
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.media
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.TransitionLayout
+import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.animation.WidgetState
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.floatThat
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class MediaViewControllerTest : SysuiTestCase() {
+    private val mediaHostStateHolder = MediaHost.MediaHostStateHolder()
+    private val mediaHostStatesManager = MediaHostStatesManager()
+    private val configurationController =
+        com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context)
+    private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+    private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+    @Mock lateinit var logger: MediaViewLogger
+    @Mock private lateinit var mockViewState: TransitionViewState
+    @Mock private lateinit var mockCopiedState: TransitionViewState
+    @Mock private lateinit var detailWidgetState: WidgetState
+    @Mock private lateinit var controlWidgetState: WidgetState
+    @Mock private lateinit var mediaTitleWidgetState: WidgetState
+    @Mock private lateinit var mediaContainerWidgetState: WidgetState
+
+    val delta = 0.0001F
+
+    private lateinit var mediaViewController: MediaViewController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mediaViewController =
+            MediaViewController(context, configurationController, mediaHostStatesManager, logger)
+    }
+
+    @Test
+    fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() {
+        mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
+        player.measureState = TransitionViewState().apply { this.height = 100 }
+        mediaHostStateHolder.expansion = 1f
+        val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+        val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+        mediaHostStateHolder.measurementInput =
+            MeasurementInput(widthMeasureSpec, heightMeasureSpec)
+
+        // Test no squish
+        mediaHostStateHolder.squishFraction = 1f
+        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
+
+        // Test half squish
+        mediaHostStateHolder.squishFraction = 0.5f
+        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
+    }
+
+    @Test
+    fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() {
+        mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION)
+        recommendation.measureState = TransitionViewState().apply { this.height = 100 }
+        mediaHostStateHolder.expansion = 1f
+        val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+        val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+        mediaHostStateHolder.measurementInput =
+            MeasurementInput(widthMeasureSpec, heightMeasureSpec)
+
+        // Test no squish
+        mediaHostStateHolder.squishFraction = 1f
+        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
+
+        // Test half squish
+        mediaHostStateHolder.squishFraction = 0.5f
+        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
+    }
+
+    @Test
+    fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() {
+        whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+        whenever(mockCopiedState.widgetStates)
+            .thenReturn(
+                mutableMapOf(
+                    R.id.media_progress_bar to controlWidgetState,
+                    R.id.header_artist to detailWidgetState
+                )
+            )
+
+        val detailSquishMiddle =
+            TRANSFORM_BEZIER.getInterpolation(
+                (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, detailSquishMiddle)
+        verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+        val detailSquishEnd =
+            TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+        mediaViewController.squishViewState(mockViewState, detailSquishEnd)
+        verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+
+        val controlSquishMiddle =
+            TRANSFORM_BEZIER.getInterpolation(
+                (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, controlSquishMiddle)
+        verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+        val controlSquishEnd =
+            TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+        mediaViewController.squishViewState(mockViewState, controlSquishEnd)
+        verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+    }
+
+    @Test
+    fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() {
+        whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+        whenever(mockCopiedState.widgetStates)
+            .thenReturn(
+                mutableMapOf(
+                    R.id.media_title1 to mediaTitleWidgetState,
+                    R.id.media_cover1_container to mediaContainerWidgetState
+                )
+            )
+
+        val containerSquishMiddle =
+            TRANSFORM_BEZIER.getInterpolation(
+                (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, containerSquishMiddle)
+        verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+        val containerSquishEnd =
+            TRANSFORM_BEZIER.getInterpolation(
+                (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, containerSquishEnd)
+        verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+
+        val titleSquishMiddle =
+            TRANSFORM_BEZIER.getInterpolation(
+                (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, titleSquishMiddle)
+        verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+        val titleSquishEnd =
+            TRANSFORM_BEZIER.getInterpolation(
+                (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, titleSquishEnd)
+        verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
index 8073103..6c03730 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
@@ -39,7 +39,6 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.keyguard.KeyguardViewController;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
 import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
@@ -49,6 +48,7 @@
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -113,7 +113,7 @@
         mNavBarHelper = new NavBarHelper(mContext, mAccessibilityManager,
                 mAccessibilityButtonModeObserver, mAccessibilityButtonTargetObserver,
                 mSystemActions, mOverviewProxyService, mAssistManagerLazy,
-                () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardViewController.class),
+                () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class),
                 mNavigationModeController, mUserTracker, mDumpManager);
 
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 51f0953..0e9d279 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -72,7 +72,6 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
-import com.android.keyguard.KeyguardViewController;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.SysuiTestableContext;
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
@@ -194,7 +193,7 @@
     @Mock
     private CentralSurfaces mCentralSurfaces;
     @Mock
-    private KeyguardViewController mKeyguardViewController;
+    private KeyguardStateController mKeyguardStateController;
     @Mock
     private UserContextProvider mUserContextProvider;
     @Mock
@@ -240,7 +239,7 @@
                     mock(AccessibilityButtonTargetsObserver.class),
                     mSystemActions, mOverviewProxyService,
                     () -> mock(AssistManager.class), () -> Optional.of(mCentralSurfaces),
-                    mKeyguardViewController, mock(NavigationModeController.class),
+                    mKeyguardStateController, mock(NavigationModeController.class),
                     mock(UserTracker.class), mock(DumpManager.class)));
             mNavigationBar = createNavBar(mContext);
             mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal);
@@ -380,7 +379,7 @@
 
         // Verify navbar didn't alter and showing back icon when the keyguard is showing without
         // requesting IME insets visible.
-        doReturn(true).when(mKeyguardViewController).isShowing();
+        doReturn(true).when(mKeyguardStateController).isShowing();
         mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
                 BACK_DISPOSITION_DEFAULT, true);
         assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 5cb27a4..46a502a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -99,13 +99,14 @@
             policy.getDefaultDisplayId(),
             DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
 
-        val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+        val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER)
         val processor = RequestProcessor(imageCapture, policy, flags, scope)
 
         val processedRequest = processor.process(request)
 
         // Request has topComponent added, but otherwise unchanged.
         assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
+        assertThat(processedRequest.source).isEqualTo(SCREENSHOT_OTHER)
         assertThat(processedRequest.topComponent).isEqualTo(component)
     }
 
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 0c60d3c..37be343 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -20,12 +20,12 @@
 
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
+import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
 import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN;
-import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -156,7 +156,6 @@
 import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
 import com.android.systemui.statusbar.phone.TapAgainViewController;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -297,8 +296,8 @@
     private final FalsingManagerFake mFalsingManager = new FalsingManagerFake();
     private final Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty();
     private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
-    private final PanelExpansionStateManager mPanelExpansionStateManager =
-            new PanelExpansionStateManager();
+    private final ShadeExpansionStateManager mShadeExpansionStateManager =
+            new ShadeExpansionStateManager();
     private FragmentHostManager.FragmentListener mFragmentListener;
 
     @Before
@@ -475,7 +474,7 @@
                 mLargeScreenShadeHeaderController,
                 mScreenOffAnimationController,
                 mLockscreenGestureLogger,
-                mPanelExpansionStateManager,
+                mShadeExpansionStateManager,
                 mNotificationRemoteInputManager,
                 mSysUIUnfoldComponent,
                 mInteractionJankMonitor,
@@ -1252,10 +1251,10 @@
     @Test
     public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() {
         enableSplitShade(/* enabled= */ true);
-        mPanelExpansionStateManager.updateState(STATE_CLOSED);
+        mShadeExpansionStateManager.updateState(STATE_CLOSED);
         assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
 
-        mPanelExpansionStateManager.updateState(STATE_OPENING);
+        mShadeExpansionStateManager.updateState(STATE_OPENING);
 
         assertThat(mNotificationPanelViewController.mQsExpandImmediate).isTrue();
     }
@@ -1263,11 +1262,11 @@
     @Test
     public void testQsNotToBeImmediatelyExpandedWhenGoingFromUnlockedToLocked() {
         enableSplitShade(/* enabled= */ true);
-        mPanelExpansionStateManager.updateState(STATE_CLOSED);
+        mShadeExpansionStateManager.updateState(STATE_CLOSED);
 
         mStatusBarStateController.setState(KEYGUARD);
         // going to lockscreen would trigger STATE_OPENING
-        mPanelExpansionStateManager.updateState(STATE_OPENING);
+        mShadeExpansionStateManager.updateState(STATE_OPENING);
 
         assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
     }
@@ -1275,11 +1274,11 @@
     @Test
     public void testQsImmediateResetsWhenPanelOpensOrCloses() {
         mNotificationPanelViewController.mQsExpandImmediate = true;
-        mPanelExpansionStateManager.updateState(STATE_OPEN);
+        mShadeExpansionStateManager.updateState(STATE_OPEN);
         assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
 
         mNotificationPanelViewController.mQsExpandImmediate = true;
-        mPanelExpansionStateManager.updateState(STATE_CLOSED);
+        mShadeExpansionStateManager.updateState(STATE_CLOSED);
         assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
     }
 
@@ -1300,7 +1299,7 @@
 
     @Test
     public void testPanelClosedWhenClosingQsInSplitShade() {
-        mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
+        mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
                 /* expanded= */ true, /* tracking= */ false, /* dragDownPxAmount= */ 0);
         enableSplitShade(/* enabled= */ true);
         mNotificationPanelViewController.setExpandedFraction(1f);
@@ -1312,7 +1311,7 @@
 
     @Test
     public void testPanelStaysOpenWhenClosingQs() {
-        mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
+        mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
                 /* expanded= */ true, /* tracking= */ false, /* dragDownPxAmount= */ 0);
         mNotificationPanelViewController.setExpandedFraction(1f);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 481e4e9..db7e017 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -41,7 +41,6 @@
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -117,7 +116,7 @@
             notificationShadeDepthController,
             view,
             notificationPanelViewController,
-            PanelExpansionStateManager(),
+            ShadeExpansionStateManager(),
             stackScrollLayoutController,
             statusBarKeyguardViewManager,
             statusBarWindowStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
index 4a7dec9..26a0770 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
@@ -51,7 +51,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.tuner.TunerService;
 
@@ -118,7 +117,7 @@
                 mNotificationShadeDepthController,
                 mView,
                 mNotificationPanelViewController,
-                new PanelExpansionStateManager(),
+                new ShadeExpansionStateManager(),
                 mNotificationStackScrollLayoutController,
                 mStatusBarKeyguardViewManager,
                 mStatusBarWindowStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt
new file mode 100644
index 0000000..a601b67
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class ShadeExpansionStateManagerTest : SysuiTestCase() {
+
+    private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
+
+    @Before
+    fun setUp() {
+        shadeExpansionStateManager = ShadeExpansionStateManager()
+    }
+
+    @Test
+    fun onPanelExpansionChanged_listenerNotified() {
+        val listener = TestShadeExpansionListener()
+        shadeExpansionStateManager.addExpansionListener(listener)
+        val fraction = 0.6f
+        val expanded = true
+        val tracking = true
+        val dragDownAmount = 1234f
+
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            fraction,
+            expanded,
+            tracking,
+            dragDownAmount
+        )
+
+        assertThat(listener.fraction).isEqualTo(fraction)
+        assertThat(listener.expanded).isEqualTo(expanded)
+        assertThat(listener.tracking).isEqualTo(tracking)
+        assertThat(listener.dragDownAmountPx).isEqualTo(dragDownAmount)
+    }
+
+    @Test
+    fun addExpansionListener_listenerNotifiedOfCurrentValues() {
+        val fraction = 0.6f
+        val expanded = true
+        val tracking = true
+        val dragDownAmount = 1234f
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            fraction,
+            expanded,
+            tracking,
+            dragDownAmount
+        )
+        val listener = TestShadeExpansionListener()
+
+        shadeExpansionStateManager.addExpansionListener(listener)
+
+        assertThat(listener.fraction).isEqualTo(fraction)
+        assertThat(listener.expanded).isEqualTo(expanded)
+        assertThat(listener.tracking).isEqualTo(tracking)
+        assertThat(listener.dragDownAmountPx).isEqualTo(dragDownAmount)
+    }
+
+    @Test
+    fun updateState_listenerNotified() {
+        val listener = TestShadeStateListener()
+        shadeExpansionStateManager.addStateListener(listener)
+
+        shadeExpansionStateManager.updateState(STATE_OPEN)
+
+        assertThat(listener.state).isEqualTo(STATE_OPEN)
+    }
+
+    /* ***** [PanelExpansionStateManager.onPanelExpansionChanged] test cases *******/
+
+    /* Fraction < 1 test cases */
+
+    @Test
+    fun onPEC_fractionLessThanOne_expandedTrue_trackingFalse_becomesStateOpening() {
+        val listener = TestShadeStateListener()
+        shadeExpansionStateManager.addStateListener(listener)
+
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            fraction = 0.5f,
+            expanded = true,
+            tracking = false,
+            dragDownPxAmount = 0f
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_OPENING)
+    }
+
+    @Test
+    fun onPEC_fractionLessThanOne_expandedTrue_trackingTrue_becomesStateOpening() {
+        val listener = TestShadeStateListener()
+        shadeExpansionStateManager.addStateListener(listener)
+
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            fraction = 0.5f,
+            expanded = true,
+            tracking = true,
+            dragDownPxAmount = 0f
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_OPENING)
+    }
+
+    @Test
+    fun onPEC_fractionLessThanOne_expandedFalse_trackingFalse_becomesStateClosed() {
+        val listener = TestShadeStateListener()
+        shadeExpansionStateManager.addStateListener(listener)
+        // Start out on a different state
+        shadeExpansionStateManager.updateState(STATE_OPEN)
+
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            fraction = 0.5f,
+            expanded = false,
+            tracking = false,
+            dragDownPxAmount = 0f
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_CLOSED)
+    }
+
+    @Test
+    fun onPEC_fractionLessThanOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() {
+        val listener = TestShadeStateListener()
+        shadeExpansionStateManager.addStateListener(listener)
+        // Start out on a different state
+        shadeExpansionStateManager.updateState(STATE_OPEN)
+
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            fraction = 0.5f,
+            expanded = false,
+            tracking = true,
+            dragDownPxAmount = 0f
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_OPEN)
+    }
+
+    /* Fraction = 1 test cases */
+
+    @Test
+    fun onPEC_fractionOne_expandedTrue_trackingFalse_becomesStateOpeningThenStateOpen() {
+        val listener = TestShadeStateListener()
+        shadeExpansionStateManager.addStateListener(listener)
+
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            fraction = 1f,
+            expanded = true,
+            tracking = false,
+            dragDownPxAmount = 0f
+        )
+
+        assertThat(listener.previousState).isEqualTo(STATE_OPENING)
+        assertThat(listener.state).isEqualTo(STATE_OPEN)
+    }
+
+    @Test
+    fun onPEC_fractionOne_expandedTrue_trackingTrue_becomesStateOpening() {
+        val listener = TestShadeStateListener()
+        shadeExpansionStateManager.addStateListener(listener)
+
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            fraction = 1f,
+            expanded = true,
+            tracking = true,
+            dragDownPxAmount = 0f
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_OPENING)
+    }
+
+    @Test
+    fun onPEC_fractionOne_expandedFalse_trackingFalse_becomesStateClosed() {
+        val listener = TestShadeStateListener()
+        shadeExpansionStateManager.addStateListener(listener)
+        // Start out on a different state
+        shadeExpansionStateManager.updateState(STATE_OPEN)
+
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            fraction = 1f,
+            expanded = false,
+            tracking = false,
+            dragDownPxAmount = 0f
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_CLOSED)
+    }
+
+    @Test
+    fun onPEC_fractionOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() {
+        val listener = TestShadeStateListener()
+        shadeExpansionStateManager.addStateListener(listener)
+        // Start out on a different state
+        shadeExpansionStateManager.updateState(STATE_OPEN)
+
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            fraction = 1f,
+            expanded = false,
+            tracking = true,
+            dragDownPxAmount = 0f
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_OPEN)
+    }
+
+    /* ***** end [PanelExpansionStateManager.onPanelExpansionChanged] test cases ******/
+
+    class TestShadeExpansionListener : ShadeExpansionListener {
+        var fraction: Float = 0f
+        var expanded: Boolean = false
+        var tracking: Boolean = false
+        var dragDownAmountPx: Float = 0f
+
+        override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
+            this.fraction = event.fraction
+            this.expanded = event.expanded
+            this.tracking = event.tracking
+            this.dragDownAmountPx = event.dragDownPxAmount
+        }
+    }
+
+    class TestShadeStateListener : ShadeStateListener {
+        @PanelState var previousState: Int = STATE_CLOSED
+        @PanelState var state: Int = STATE_CLOSED
+
+        override fun onPanelStateChanged(state: Int) {
+            this.previousState = this.state
+            this.state = state
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
index 6be76a6..84f8656 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
@@ -5,13 +5,13 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.shade.STATE_CLOSED
+import com.android.systemui.shade.STATE_OPEN
+import com.android.systemui.shade.STATE_OPENING
+import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.STATE_CLOSED
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPEN
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import org.junit.Before
@@ -148,7 +148,7 @@
 
     companion object {
         val EXPANSION_EVENT =
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 0.5f, expanded = true, tracking = true, dragDownPxAmount = 10f)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
index b6f8326..7cac854 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
@@ -7,12 +7,12 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.qs.QS
 import com.android.systemui.shade.NotificationPanelViewController
+import com.android.systemui.shade.STATE_OPENING
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import org.junit.Before
 import org.junit.Test
@@ -40,7 +40,7 @@
     private lateinit var controller: ShadeTransitionController
 
     private val configurationController = FakeConfigurationController()
-    private val panelExpansionStateManager = PanelExpansionStateManager()
+    private val shadeExpansionStateManager = ShadeExpansionStateManager()
 
     @Before
     fun setUp() {
@@ -49,7 +49,7 @@
         controller =
             ShadeTransitionController(
                 configurationController,
-                panelExpansionStateManager,
+                shadeExpansionStateManager,
                 dumpManager,
                 context,
                 splitShadeOverScrollerFactory = { _, _ -> splitShadeOverScroller },
@@ -166,7 +166,7 @@
     }
 
     private fun startPanelExpansion() {
-        panelExpansionStateManager.onPanelExpansionChanged(
+        shadeExpansionStateManager.onPanelExpansionChanged(
             DEFAULT_EXPANSION_EVENT.fraction,
             DEFAULT_EXPANSION_EVENT.expanded,
             DEFAULT_EXPANSION_EVENT.tracking,
@@ -194,7 +194,7 @@
     companion object {
         private const val DEFAULT_DRAG_DOWN_AMOUNT = 123f
         private val DEFAULT_EXPANSION_EVENT =
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 0.5f,
                 expanded = true,
                 tracking = true,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt
index aafd871..0e48b48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt
@@ -7,11 +7,11 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.qs.QS
+import com.android.systemui.shade.STATE_CLOSED
+import com.android.systemui.shade.STATE_OPEN
+import com.android.systemui.shade.STATE_OPENING
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.STATE_CLOSED
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPEN
-import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import org.junit.Before
 import org.junit.Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
index f9e279e..5b34a95 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
@@ -3,72 +3,72 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
+import java.lang.Thread.UncaughtExceptionHandler
 import org.junit.Assert.assertThrows
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import org.mockito.Mock
-import org.mockito.Mockito.only
 import org.mockito.Mockito.any
+import org.mockito.Mockito.only
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
-import java.lang.Thread.UncaughtExceptionHandler
 
 @SmallTest
 class UncaughtExceptionPreHandlerTest : SysuiTestCase() {
-    private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
+  private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
 
-    @Mock
-    private lateinit var mockHandler: UncaughtExceptionHandler
+  @Mock private lateinit var mockHandler: UncaughtExceptionHandler
 
-    @Mock
-    private lateinit var mockHandler2: UncaughtExceptionHandler
+  @Mock private lateinit var mockHandler2: UncaughtExceptionHandler
 
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        Thread.setUncaughtExceptionPreHandler(null)
-        preHandlerManager = UncaughtExceptionPreHandlerManager()
+  @Before
+  fun setUp() {
+    MockitoAnnotations.initMocks(this)
+    Thread.setUncaughtExceptionPreHandler(null)
+    preHandlerManager = UncaughtExceptionPreHandlerManager()
+  }
+
+  @Test
+  fun registerHandler_registersOnceOnly() {
+    preHandlerManager.registerHandler(mockHandler)
+    preHandlerManager.registerHandler(mockHandler)
+    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+    verify(mockHandler, only()).uncaughtException(any(), any())
+  }
+
+  @Test
+  fun registerHandler_setsUncaughtExceptionPreHandler() {
+    Thread.setUncaughtExceptionPreHandler(null)
+    preHandlerManager.registerHandler(mockHandler)
+    assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
+  }
+
+  @Test
+  fun registerHandler_preservesOriginalHandler() {
+    Thread.setUncaughtExceptionPreHandler(mockHandler)
+    preHandlerManager.registerHandler(mockHandler2)
+    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+    verify(mockHandler, only()).uncaughtException(any(), any())
+  }
+
+  @Test
+  @Ignore
+  fun registerHandler_toleratesHandlersThatThrow() {
+    `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
+    preHandlerManager.registerHandler(mockHandler2)
+    preHandlerManager.registerHandler(mockHandler)
+    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+    verify(mockHandler2, only()).uncaughtException(any(), any())
+    verify(mockHandler, only()).uncaughtException(any(), any())
+  }
+
+  @Test
+  fun registerHandler_doesNotSetUpTwice() {
+    UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
+    assertThrows(IllegalStateException::class.java) {
+      preHandlerManager.registerHandler(mockHandler)
     }
-
-    @Test
-    fun registerHandler_registersOnceOnly() {
-        preHandlerManager.registerHandler(mockHandler)
-        preHandlerManager.registerHandler(mockHandler)
-        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-        verify(mockHandler, only()).uncaughtException(any(), any())
-    }
-
-    @Test
-    fun registerHandler_setsUncaughtExceptionPreHandler() {
-        Thread.setUncaughtExceptionPreHandler(null)
-        preHandlerManager.registerHandler(mockHandler)
-        assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
-    }
-
-    @Test
-    fun registerHandler_preservesOriginalHandler() {
-        Thread.setUncaughtExceptionPreHandler(mockHandler)
-        preHandlerManager.registerHandler(mockHandler2)
-        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-        verify(mockHandler, only()).uncaughtException(any(), any())
-    }
-
-    @Test
-    fun registerHandler_toleratesHandlersThatThrow() {
-        `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
-        preHandlerManager.registerHandler(mockHandler2)
-        preHandlerManager.registerHandler(mockHandler)
-        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-        verify(mockHandler2, only()).uncaughtException(any(), any())
-        verify(mockHandler, only()).uncaughtException(any(), any())
-    }
-
-    @Test
-    fun registerHandler_doesNotSetUpTwice() {
-        UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
-        assertThrows(IllegalStateException::class.java) {
-            preHandlerManager.registerHandler(mockHandler)
-        }
-    }
-}
\ No newline at end of file
+  }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index 6446fb5..77b1e37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -28,10 +28,10 @@
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScrimController
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.WallpaperController
@@ -137,7 +137,7 @@
     @Test
     fun onPanelExpansionChanged_apliesBlur_ifShade() {
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
         verify(shadeAnimation).animateTo(eq(maxBlur))
     }
@@ -145,7 +145,7 @@
     @Test
     fun onPanelExpansionChanged_animatesBlurIn_ifShade() {
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 0.01f, expanded = false, tracking = false, dragDownPxAmount = 0f))
         verify(shadeAnimation).animateTo(eq(maxBlur))
     }
@@ -155,7 +155,7 @@
         onPanelExpansionChanged_animatesBlurIn_ifShade()
         clearInvocations(shadeAnimation)
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 0f, expanded = false, tracking = false, dragDownPxAmount = 0f))
         verify(shadeAnimation).animateTo(eq(0))
     }
@@ -163,7 +163,7 @@
     @Test
     fun onPanelExpansionChanged_animatesBlurOut_ifFlick() {
         val event =
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)
         onPanelExpansionChanged_apliesBlur_ifShade()
         clearInvocations(shadeAnimation)
@@ -184,7 +184,7 @@
         onPanelExpansionChanged_animatesBlurOut_ifFlick()
         clearInvocations(shadeAnimation)
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 0.6f, expanded = true, tracking = true, dragDownPxAmount = 0f))
         verify(shadeAnimation).animateTo(eq(maxBlur))
     }
@@ -192,7 +192,7 @@
     @Test
     fun onPanelExpansionChanged_respectsMinPanelPullDownFraction() {
         val event =
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 0.5f, expanded = true, tracking = true, dragDownPxAmount = 0f)
         notificationShadeDepthController.panelPullDownMinFraction = 0.5f
         notificationShadeDepthController.onPanelExpansionChanged(event)
@@ -220,7 +220,7 @@
         statusBarState = StatusBarState.KEYGUARD
         notificationShadeDepthController.qsPanelExpansion = 1f
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
         notificationShadeDepthController.updateBlurCallback.doFrame(0)
         verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false))
@@ -231,7 +231,7 @@
         statusBarState = StatusBarState.KEYGUARD
         notificationShadeDepthController.qsPanelExpansion = 0.25f
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
         notificationShadeDepthController.updateBlurCallback.doFrame(0)
         verify(wallpaperController)
@@ -243,7 +243,7 @@
         enableSplitShade()
 
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
         notificationShadeDepthController.updateBlurCallback.doFrame(0)
 
@@ -255,7 +255,7 @@
         disableSplitShade()
 
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
         notificationShadeDepthController.updateBlurCallback.doFrame(0)
 
@@ -269,7 +269,7 @@
         val expanded = true
         val tracking = false
         val dragDownPxAmount = 0f
-        val event = PanelExpansionChangeEvent(rawFraction, expanded, tracking, dragDownPxAmount)
+        val event = ShadeExpansionChangeEvent(rawFraction, expanded, tracking, dragDownPxAmount)
         val inOrder = Mockito.inOrder(wallpaperController)
 
         notificationShadeDepthController.onPanelExpansionChanged(event)
@@ -333,7 +333,7 @@
     @Test
     fun updateBlurCallback_setsBlur_whenExpanded() {
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
         `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
         notificationShadeDepthController.updateBlurCallback.doFrame(0)
@@ -343,7 +343,7 @@
     @Test
     fun updateBlurCallback_ignoreShadeBlurUntilHidden_overridesZoom() {
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
         `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
         notificationShadeDepthController.blursDisabledForAppLaunch = true
@@ -361,7 +361,7 @@
     @Test
     fun ignoreBlurForUnlock_ignores() {
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
         `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
 
@@ -378,7 +378,7 @@
     @Test
     fun ignoreBlurForUnlock_doesNotIgnore() {
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
         `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
 
@@ -410,7 +410,7 @@
         `when`(brightnessSpring.ratio).thenReturn(1f)
         // And shade is blurred
         notificationShadeDepthController.onPanelExpansionChanged(
-            PanelExpansionChangeEvent(
+            ShadeExpansionChangeEvent(
                 fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f))
         `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat())
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
index b719c7f..a6381d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
@@ -32,7 +32,6 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.Assert;
@@ -58,8 +57,6 @@
         mDynamicPrivacyController = new DynamicPrivacyController(
                 mLockScreenUserManager, mKeyguardStateController,
                 mock(StatusBarStateController.class));
-        mDynamicPrivacyController.setStatusBarKeyguardViewManager(
-                mock(StatusBarKeyguardViewManager.class));
         mDynamicPrivacyController.addListener(mListener);
         // Disable dynamic privacy by default
         allowNotificationsInPublic(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 2970807..340bc96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -47,6 +47,8 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.withArgCaptor
 import com.android.systemui.util.time.FakeSystemClock
+import java.util.ArrayList
+import java.util.function.Consumer
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -57,10 +59,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.ArrayList
-import java.util.function.Consumer
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -671,8 +671,64 @@
         verify(mHeadsUpManager, never()).showNotification(mGroupChild2)
     }
 
+    @Test
+    fun testOnRankingApplied_newEntryShouldAlert() {
+        // GIVEN that mEntry has never interrupted in the past, and now should
+        assertFalse(mEntry.hasInterrupted())
+        setShouldHeadsUp(mEntry)
+        whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+
+        // WHEN a ranking applied update occurs
+        mCollectionListener.onRankingApplied()
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+        mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+        // THEN the notification is shown
+        finishBind(mEntry)
+        verify(mHeadsUpManager).showNotification(mEntry)
+    }
+
+    @Test
+    fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() {
+        // GIVEN that mEntry has alerted in the past
+        mEntry.setInterruption()
+        setShouldHeadsUp(mEntry)
+        whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+
+        // WHEN a ranking applied update occurs
+        mCollectionListener.onRankingApplied()
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+        mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+        // THEN the notification is never bound or shown
+        verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+        verify(mHeadsUpManager, never()).showNotification(any())
+    }
+
+    @Test
+    fun testOnRankingApplied_entryUpdatedToHun() {
+        // GIVEN that mEntry is added in a state where it should not HUN
+        setShouldHeadsUp(mEntry, false)
+        mCollectionListener.onEntryAdded(mEntry)
+
+        // and it is then updated such that it should now HUN
+        setShouldHeadsUp(mEntry)
+        whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+
+        // WHEN a ranking applied update occurs
+        mCollectionListener.onRankingApplied()
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+        mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+        // THEN the notification is shown
+        finishBind(mEntry)
+        verify(mHeadsUpManager).showNotification(mEntry)
+    }
+
     private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
         whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should)
+        whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any()))
+                .thenReturn(should)
     }
 
     private fun finishBind(entry: NotificationEntry) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index cd0cc33..6fa2174 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -100,8 +100,6 @@
     @Mock
     private AuthController mAuthController;
     @Mock
-    private DozeParameters mDozeParameters;
-    @Mock
     private MetricsLogger mMetricsLogger;
     @Mock
     private NotificationMediaManager mNotificationMediaManager;
@@ -127,7 +125,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         TestableResources res = getContext().getOrCreateTestableResources();
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
         when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
         when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
         when(mKeyguardStateController.isUnlocked()).thenReturn(false);
@@ -139,7 +137,7 @@
         mBiometricUnlockController = new BiometricUnlockController(mDozeScrimController,
                 mKeyguardViewMediator, mScrimController, mShadeController,
                 mNotificationShadeWindowController, mKeyguardStateController, mHandler,
-                mUpdateMonitor, res.getResources(), mKeyguardBypassController, mDozeParameters,
+                mUpdateMonitor, res.getResources(), mKeyguardBypassController,
                 mMetricsLogger, mDumpManager, mPowerManager,
                 mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
                 mAuthController, mStatusBarStateController, mKeyguardUnlockAnimationController,
@@ -177,7 +175,7 @@
     public void onBiometricAuthenticated_whenFingerprintAndNotInteractive_wakeAndUnlock() {
         reset(mUpdateMonitor);
         reset(mStatusBarKeyguardViewManager);
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
         when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
         when(mDozeScrimController.isPulsing()).thenReturn(true);
         // the value of isStrongBiometric doesn't matter here since we only care about the returned
@@ -194,7 +192,7 @@
     public void onBiometricAuthenticated_whenDeviceIsAlreadyUnlocked_wakeAndUnlock() {
         reset(mUpdateMonitor);
         reset(mStatusBarKeyguardViewManager);
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
         when(mKeyguardStateController.isUnlocked()).thenReturn(true);
         when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
         when(mDozeScrimController.isPulsing()).thenReturn(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index f510e48..ad497a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -116,6 +116,7 @@
 import com.android.systemui.shade.NotificationShadeWindowViewController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeControllerImpl;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -150,7 +151,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -413,7 +413,7 @@
                 mNotificationGutsManager,
                 notificationLogger,
                 mNotificationInterruptStateProvider,
-                new PanelExpansionStateManager(),
+                new ShadeExpansionStateManager(),
                 mKeyguardViewMediator,
                 new DisplayMetrics(),
                 mMetricsLogger,
@@ -486,7 +486,7 @@
         when(mKeyguardViewMediator.registerCentralSurfaces(
                 any(CentralSurfacesImpl.class),
                 any(NotificationPanelViewController.class),
-                any(PanelExpansionStateManager.class),
+                any(ShadeExpansionStateManager.class),
                 any(BiometricUnlockController.class),
                 any(ViewGroup.class),
                 any(KeyguardBypassController.class)))
@@ -516,32 +516,32 @@
 
     @Test
     public void executeRunnableDismissingKeyguard_nullRunnable_showingAndOccluded() {
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(true);
 
         mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false);
     }
 
     @Test
     public void executeRunnableDismissingKeyguard_nullRunnable_showing() {
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
 
         mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false);
     }
 
     @Test
     public void executeRunnableDismissingKeyguard_nullRunnable_notShowing() {
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
 
         mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false);
     }
 
     @Test
     public void executeRunnableDismissingKeyguard_dreaming_notShowing() throws RemoteException {
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
         when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(true);
 
         mCentralSurfaces.executeRunnableDismissingKeyguard(() ->  {},
@@ -555,8 +555,8 @@
 
     @Test
     public void executeRunnableDismissingKeyguard_notDreaming_notShowing() throws RemoteException {
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
         when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(false);
 
         mCentralSurfaces.executeRunnableDismissingKeyguard(() ->  {},
@@ -571,10 +571,10 @@
     @Test
     public void lockscreenStateMetrics_notShowing() {
         // uninteresting state, except that fingerprint must be non-zero
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
         when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
         // interesting state
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
         when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
         when(mKeyguardStateController.isMethodSecure()).thenReturn(false);
         mCentralSurfaces.onKeyguardViewManagerStatesUpdated();
@@ -589,10 +589,10 @@
     @Test
     public void lockscreenStateMetrics_notShowing_secure() {
         // uninteresting state, except that fingerprint must be non-zero
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
         when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
         // interesting state
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false);
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
         when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
         when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
 
@@ -608,10 +608,10 @@
     @Test
     public void lockscreenStateMetrics_isShowing() {
         // uninteresting state, except that fingerprint must be non-zero
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
         when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
         // interesting state
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
         when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
         when(mKeyguardStateController.isMethodSecure()).thenReturn(false);
 
@@ -627,10 +627,10 @@
     @Test
     public void lockscreenStateMetrics_isShowing_secure() {
         // uninteresting state, except that fingerprint must be non-zero
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
         when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
         // interesting state
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
         when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
         when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
 
@@ -646,10 +646,10 @@
     @Test
     public void lockscreenStateMetrics_isShowingBouncer() {
         // uninteresting state, except that fingerprint must be non-zero
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
         when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
         // interesting state
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
         when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
         when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
 
@@ -1053,9 +1053,9 @@
     }
 
     @Test
-    public void startActivityDismissingKeyguard_isShowingandIsOccluded() {
-        when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true);
-        when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(true);
+    public void startActivityDismissingKeyguard_isShowingAndIsOccluded() {
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(true);
         mCentralSurfaces.startActivityDismissingKeyguard(
                 new Intent(),
                 /* onlyProvisioned = */false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
new file mode 100644
index 0000000..a986777
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java
@@ -0,0 +1,145 @@
+/*
+ * 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.phone;
+
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+/**
+ * Mock implementation of KeyguardStateController which tracks showing and occluded states
+ * based on {@link #notifyKeyguardState(boolean showing, boolean occluded)}}.
+ */
+public class FakeKeyguardStateController implements KeyguardStateController {
+    private boolean mShowing;
+    private boolean mOccluded;
+    private boolean mCanDismissLockScreen;
+
+    @Override
+    public void notifyKeyguardState(boolean showing, boolean occluded) {
+        mShowing = showing;
+        mOccluded = occluded;
+    }
+
+    @Override
+    public boolean isShowing() {
+        return mShowing;
+    }
+
+    @Override
+    public boolean isOccluded() {
+        return mOccluded;
+    }
+
+    public void setCanDismissLockScreen(boolean canDismissLockScreen) {
+        mCanDismissLockScreen = canDismissLockScreen;
+    }
+
+    @Override
+    public boolean canDismissLockScreen() {
+        return mCanDismissLockScreen;
+    }
+
+    @Override
+    public boolean isBouncerShowing() {
+        return false;
+    }
+
+    @Override
+    public boolean isKeyguardScreenRotationAllowed() {
+        return false;
+    }
+
+    @Override
+    public boolean isMethodSecure() {
+        return true;
+    }
+
+    @Override
+    public boolean isTrusted() {
+        return false;
+    }
+
+    @Override
+    public boolean isKeyguardGoingAway() {
+        return false;
+    }
+
+    @Override
+    public boolean isKeyguardFadingAway() {
+        return false;
+    }
+
+    @Override
+    public boolean isLaunchTransitionFadingAway() {
+        return false;
+    }
+
+    @Override
+    public long getKeyguardFadingAwayDuration() {
+        return 0;
+    }
+
+    @Override
+    public long getKeyguardFadingAwayDelay() {
+        return 0;
+    }
+
+    @Override
+    public long calculateGoingToFullShadeDelay() {
+        return 0;
+    }
+
+    @Override
+    public float getDismissAmount() {
+        return 0f;
+    }
+
+    @Override
+    public boolean isDismissingFromSwipe() {
+        return false;
+    }
+
+    @Override
+    public boolean isFlingingToDismissKeyguard() {
+        return false;
+    }
+
+    @Override
+    public boolean isFlingingToDismissKeyguardDuringSwipeGesture() {
+        return false;
+    }
+
+    @Override
+    public boolean isSnappingKeyguardBackAfterSwipe() {
+        return false;
+    }
+
+    @Override
+    public void notifyPanelFlingStart(boolean dismiss) {
+    }
+
+    @Override
+    public void notifyPanelFlingEnd() {
+    }
+
+    @Override
+    public void addCallback(Callback listener) {
+    }
+
+    @Override
+    public void removeCallback(Callback listener) {
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 04ad1f8..8da8d04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -61,14 +62,13 @@
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 
 import com.google.common.truth.Truth;
@@ -89,12 +89,11 @@
 @TestableLooper.RunWithLooper
 public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
 
-    private static final PanelExpansionChangeEvent EXPANSION_EVENT =
+    private static final ShadeExpansionChangeEvent EXPANSION_EVENT =
             expansionEvent(/* fraction= */ 0.5f, /* expanded= */ false, /* tracking= */ true);
 
     @Mock private ViewMediatorCallback mViewMediatorCallback;
     @Mock private LockPatternUtils mLockPatternUtils;
-    @Mock private KeyguardStateController mKeyguardStateController;
     @Mock private CentralSurfaces mCentralSurfaces;
     @Mock private ViewGroup mContainer;
     @Mock private NotificationPanelViewController mNotificationPanelView;
@@ -123,6 +122,8 @@
 
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private KeyguardBouncer.BouncerExpansionCallback mBouncerExpansionCallback;
+    private FakeKeyguardStateController mKeyguardStateController =
+            spy(new FakeKeyguardStateController());
 
     @Mock private ViewRootImpl mViewRootImpl;
     @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
@@ -176,11 +177,10 @@
         mStatusBarKeyguardViewManager.registerCentralSurfaces(
                 mCentralSurfaces,
                 mNotificationPanelView,
-                new PanelExpansionStateManager(),
+                new ShadeExpansionStateManager(),
                 mBiometricUnlockController,
                 mNotificationContainer,
                 mBypassController);
-        when(mKeyguardStateController.isOccluded()).thenReturn(false);
         mStatusBarKeyguardViewManager.show(null);
         ArgumentCaptor<KeyguardBouncer.BouncerExpansionCallback> callbackArgumentCaptor =
                 ArgumentCaptor.forClass(KeyguardBouncer.BouncerExpansionCallback.class);
@@ -253,7 +253,7 @@
 
     @Test
     public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
-        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+        mKeyguardStateController.setCanDismissLockScreen(false);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
         verify(mBouncer).show(eq(false), eq(false));
 
@@ -340,13 +340,12 @@
     }
 
     @Test
-    public void setOccluded_onKeyguardOccludedChangedCalledCorrectly() {
+    public void setOccluded_onKeyguardOccludedChangedCalled() {
         clearInvocations(mKeyguardStateController);
         clearInvocations(mKeyguardUpdateMonitor);
 
-        // Should be false to start, so no invocations
         mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
-        verify(mKeyguardStateController, never()).notifyKeyguardState(anyBoolean(), anyBoolean());
+        verify(mKeyguardStateController).notifyKeyguardState(true, false);
 
         clearInvocations(mKeyguardUpdateMonitor);
         clearInvocations(mKeyguardStateController);
@@ -357,8 +356,8 @@
         clearInvocations(mKeyguardUpdateMonitor);
         clearInvocations(mKeyguardStateController);
 
-        mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
-        verify(mKeyguardStateController, never()).notifyKeyguardState(anyBoolean(), anyBoolean());
+        mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
+        verify(mKeyguardStateController).notifyKeyguardState(true, false);
     }
 
     @Test
@@ -426,7 +425,7 @@
         when(mAlternateAuthInterceptor.isShowingAlternateAuthBouncer()).thenReturn(true);
         assertTrue(
                 "Is showing not accurate when alternative auth showing",
-                mStatusBarKeyguardViewManager.isShowing());
+                mStatusBarKeyguardViewManager.isBouncerShowing());
     }
 
     @Test
@@ -520,9 +519,9 @@
         Truth.assertThat(mStatusBarKeyguardViewManager.isBouncerInTransit()).isFalse();
     }
 
-    private static PanelExpansionChangeEvent expansionEvent(
+    private static ShadeExpansionChangeEvent expansionEvent(
             float fraction, boolean expanded, boolean tracking) {
-        return new PanelExpansionChangeEvent(
+        return new ShadeExpansionChangeEvent(
                 fraction, expanded, tracking, /* dragDownPxAmount= */ 0f);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index a3c6e95..63467e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -54,6 +54,7 @@
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DisableFlagsLogger;
 import com.android.systemui.statusbar.OperatorNameViewController;
@@ -66,7 +67,6 @@
 import com.android.systemui.statusbar.phone.StatusBarLocationPublisher;
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.CarrierConfigTracker;
 import com.android.systemui.util.concurrency.FakeExecutor;
@@ -441,7 +441,7 @@
                 mAnimationScheduler,
                 mLocationPublisher,
                 mMockNotificationAreaController,
-                new PanelExpansionStateManager(),
+                new ShadeExpansionStateManager(),
                 mock(FeatureFlags.class),
                 mStatusBarIconController,
                 mIconManagerFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
deleted file mode 100644
index c4f8049..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone.panelstate
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-
-@SmallTest
-class PanelExpansionStateManagerTest : SysuiTestCase() {
-
-    private lateinit var panelExpansionStateManager: PanelExpansionStateManager
-
-    @Before
-    fun setUp() {
-        panelExpansionStateManager = PanelExpansionStateManager()
-    }
-
-    @Test
-    fun onPanelExpansionChanged_listenerNotified() {
-        val listener = TestPanelExpansionListener()
-        panelExpansionStateManager.addExpansionListener(listener)
-        val fraction = 0.6f
-        val expanded = true
-        val tracking = true
-        val dragDownAmount = 1234f
-
-        panelExpansionStateManager.onPanelExpansionChanged(
-            fraction, expanded, tracking, dragDownAmount)
-
-        assertThat(listener.fraction).isEqualTo(fraction)
-        assertThat(listener.expanded).isEqualTo(expanded)
-        assertThat(listener.tracking).isEqualTo(tracking)
-        assertThat(listener.dragDownAmountPx).isEqualTo(dragDownAmount)
-    }
-
-    @Test
-    fun addExpansionListener_listenerNotifiedOfCurrentValues() {
-        val fraction = 0.6f
-        val expanded = true
-        val tracking = true
-        val dragDownAmount = 1234f
-        panelExpansionStateManager.onPanelExpansionChanged(
-            fraction, expanded, tracking, dragDownAmount)
-        val listener = TestPanelExpansionListener()
-
-        panelExpansionStateManager.addExpansionListener(listener)
-
-        assertThat(listener.fraction).isEqualTo(fraction)
-        assertThat(listener.expanded).isEqualTo(expanded)
-        assertThat(listener.tracking).isEqualTo(tracking)
-        assertThat(listener.dragDownAmountPx).isEqualTo(dragDownAmount)
-    }
-
-    @Test
-    fun updateState_listenerNotified() {
-        val listener = TestPanelStateListener()
-        panelExpansionStateManager.addStateListener(listener)
-
-        panelExpansionStateManager.updateState(STATE_OPEN)
-
-        assertThat(listener.state).isEqualTo(STATE_OPEN)
-    }
-
-    /* ***** [PanelExpansionStateManager.onPanelExpansionChanged] test cases *******/
-
-    /* Fraction < 1 test cases */
-
-    @Test
-    fun onPEC_fractionLessThanOne_expandedTrue_trackingFalse_becomesStateOpening() {
-        val listener = TestPanelStateListener()
-        panelExpansionStateManager.addStateListener(listener)
-
-        panelExpansionStateManager.onPanelExpansionChanged(
-            fraction = 0.5f, expanded = true, tracking = false, dragDownPxAmount = 0f)
-
-        assertThat(listener.state).isEqualTo(STATE_OPENING)
-    }
-
-    @Test
-    fun onPEC_fractionLessThanOne_expandedTrue_trackingTrue_becomesStateOpening() {
-        val listener = TestPanelStateListener()
-        panelExpansionStateManager.addStateListener(listener)
-
-        panelExpansionStateManager.onPanelExpansionChanged(
-            fraction = 0.5f, expanded = true, tracking = true, dragDownPxAmount = 0f)
-
-        assertThat(listener.state).isEqualTo(STATE_OPENING)
-    }
-
-    @Test
-    fun onPEC_fractionLessThanOne_expandedFalse_trackingFalse_becomesStateClosed() {
-        val listener = TestPanelStateListener()
-        panelExpansionStateManager.addStateListener(listener)
-        // Start out on a different state
-        panelExpansionStateManager.updateState(STATE_OPEN)
-
-        panelExpansionStateManager.onPanelExpansionChanged(
-            fraction = 0.5f, expanded = false, tracking = false, dragDownPxAmount = 0f)
-
-        assertThat(listener.state).isEqualTo(STATE_CLOSED)
-    }
-
-    @Test
-    fun onPEC_fractionLessThanOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() {
-        val listener = TestPanelStateListener()
-        panelExpansionStateManager.addStateListener(listener)
-        // Start out on a different state
-        panelExpansionStateManager.updateState(STATE_OPEN)
-
-        panelExpansionStateManager.onPanelExpansionChanged(
-            fraction = 0.5f, expanded = false, tracking = true, dragDownPxAmount = 0f)
-
-        assertThat(listener.state).isEqualTo(STATE_OPEN)
-    }
-
-    /* Fraction = 1 test cases */
-
-    @Test
-    fun onPEC_fractionOne_expandedTrue_trackingFalse_becomesStateOpeningThenStateOpen() {
-        val listener = TestPanelStateListener()
-        panelExpansionStateManager.addStateListener(listener)
-
-        panelExpansionStateManager.onPanelExpansionChanged(
-            fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)
-
-        assertThat(listener.previousState).isEqualTo(STATE_OPENING)
-        assertThat(listener.state).isEqualTo(STATE_OPEN)
-    }
-
-    @Test
-    fun onPEC_fractionOne_expandedTrue_trackingTrue_becomesStateOpening() {
-        val listener = TestPanelStateListener()
-        panelExpansionStateManager.addStateListener(listener)
-
-        panelExpansionStateManager.onPanelExpansionChanged(
-            fraction = 1f, expanded = true, tracking = true, dragDownPxAmount = 0f)
-
-        assertThat(listener.state).isEqualTo(STATE_OPENING)
-    }
-
-    @Test
-    fun onPEC_fractionOne_expandedFalse_trackingFalse_becomesStateClosed() {
-        val listener = TestPanelStateListener()
-        panelExpansionStateManager.addStateListener(listener)
-        // Start out on a different state
-        panelExpansionStateManager.updateState(STATE_OPEN)
-
-        panelExpansionStateManager.onPanelExpansionChanged(
-            fraction = 1f, expanded = false, tracking = false, dragDownPxAmount = 0f)
-
-        assertThat(listener.state).isEqualTo(STATE_CLOSED)
-    }
-
-    @Test
-    fun onPEC_fractionOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() {
-        val listener = TestPanelStateListener()
-        panelExpansionStateManager.addStateListener(listener)
-        // Start out on a different state
-        panelExpansionStateManager.updateState(STATE_OPEN)
-
-        panelExpansionStateManager.onPanelExpansionChanged(
-            fraction = 1f, expanded = false, tracking = true, dragDownPxAmount = 0f)
-
-        assertThat(listener.state).isEqualTo(STATE_OPEN)
-    }
-
-    /* ***** end [PanelExpansionStateManager.onPanelExpansionChanged] test cases ******/
-
-    class TestPanelExpansionListener : PanelExpansionListener {
-        var fraction: Float = 0f
-        var expanded: Boolean = false
-        var tracking: Boolean = false
-        var dragDownAmountPx: Float = 0f
-
-        override fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) {
-            this.fraction = event.fraction
-            this.expanded = event.expanded
-            this.tracking = event.tracking
-            this.dragDownAmountPx = event.dragDownPxAmount
-        }
-    }
-
-    class TestPanelStateListener : PanelStateListener {
-        @PanelState var previousState: Int = STATE_CLOSED
-        @PanelState var state: Int = STATE_CLOSED
-
-        override fun onPanelStateChanged(state: Int) {
-            this.previousState = this.state
-            this.state = state
-        }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index 929e529..a3ad028 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -23,9 +23,9 @@
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.statusbar.connectivity.WifiIcons
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
+import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
@@ -144,7 +144,12 @@
 
         /** A function that, given a context, calculates the correct content description string. */
         val contentDescription: (Context) -> String,
-    )
+
+        /** A human-readable description used for the test names. */
+        val description: String,
+    ) {
+        override fun toString() = description
+    }
 
     // Note: We use default values for the boolean parameters to reflect a "typical configuration"
     //   for wifi. This allows each TestCase to only define the parameter values that are critical
@@ -158,12 +163,21 @@
 
         /** The expected output. Null if we expect the output to be null. */
         val expected: Expected?
-    )
+    ) {
+        override fun toString(): String {
+            return "when INPUT(enabled=$enabled, " +
+                "forceHidden=$forceHidden, " +
+                "showWhenEnabled=$alwaysShowIconWhenEnabled, " +
+                "hasDataCaps=$hasDataCapabilities, " +
+                "network=$network) then " +
+                "EXPECTED($expected)"
+        }
+    }
 
     companion object {
-        @Parameters(name = "{0}")
-        @JvmStatic
-        fun data(): Collection<TestCase> =
+        @Parameters(name = "{0}") @JvmStatic fun data(): Collection<TestCase> = testData
+
+        private val testData: List<TestCase> =
             listOf(
                 // Enabled = false => no networks shown
                 TestCase(
@@ -215,11 +229,12 @@
                     network = WifiNetworkModel.Inactive,
                     expected =
                         Expected(
-                            iconResource = WifiIcons.WIFI_NO_NETWORK,
+                            iconResource = WIFI_NO_NETWORK,
                             contentDescription = { context ->
                                 "${context.getString(WIFI_NO_CONNECTION)}," +
                                     context.getString(NO_INTERNET)
-                            }
+                            },
+                            description = "No network icon",
                         ),
                 ),
                 TestCase(
@@ -231,7 +246,8 @@
                             contentDescription = { context ->
                                 "${context.getString(WIFI_CONNECTION_STRENGTH[4])}," +
                                     context.getString(NO_INTERNET)
-                            }
+                            },
+                            description = "No internet level 4 icon",
                         ),
                 ),
                 TestCase(
@@ -242,7 +258,8 @@
                             iconResource = WIFI_FULL_ICONS[2],
                             contentDescription = { context ->
                                 context.getString(WIFI_CONNECTION_STRENGTH[2])
-                            }
+                            },
+                            description = "Full internet level 2 icon",
                         ),
                 ),
 
@@ -252,11 +269,12 @@
                     network = WifiNetworkModel.Inactive,
                     expected =
                         Expected(
-                            iconResource = WifiIcons.WIFI_NO_NETWORK,
+                            iconResource = WIFI_NO_NETWORK,
                             contentDescription = { context ->
                                 "${context.getString(WIFI_NO_CONNECTION)}," +
                                     context.getString(NO_INTERNET)
-                            }
+                            },
+                            description = "No network icon",
                         ),
                 ),
                 TestCase(
@@ -268,7 +286,8 @@
                             contentDescription = { context ->
                                 "${context.getString(WIFI_CONNECTION_STRENGTH[2])}," +
                                     context.getString(NO_INTERNET)
-                            }
+                            },
+                            description = "No internet level 2 icon",
                         ),
                 ),
                 TestCase(
@@ -279,7 +298,8 @@
                             iconResource = WIFI_FULL_ICONS[0],
                             contentDescription = { context ->
                                 context.getString(WIFI_CONNECTION_STRENGTH[0])
-                            }
+                            },
+                            description = "Full internet level 0 icon",
                         ),
                 ),
 
@@ -309,7 +329,8 @@
                             iconResource = WIFI_FULL_ICONS[4],
                             contentDescription = { context ->
                                 context.getString(WIFI_CONNECTION_STRENGTH[4])
-                            }
+                            },
+                            description = "Full internet level 4 icon",
                         ),
                 ),
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
index 7e07040..e18dd3a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
@@ -25,16 +25,21 @@
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
 import com.android.systemui.unfold.system.ActivityManagerActivityTypeProvider
 import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
 import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executor
 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.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
@@ -48,6 +53,12 @@
     @Mock
     private lateinit var handler: Handler
 
+    @Mock
+    private lateinit var rotationChangeProvider: RotationChangeProvider
+
+    @Captor
+    private lateinit var rotationListener: ArgumentCaptor<RotationListener>
+
     private val foldProvider = TestFoldProvider()
     private val screenOnStatusProvider = TestScreenOnStatusProvider()
     private val testHingeAngleProvider = TestHingeAngleProvider()
@@ -76,6 +87,7 @@
                 screenOnStatusProvider,
                 foldProvider,
                 activityTypeProvider,
+                rotationChangeProvider,
                 context.mainExecutor,
                 handler
             )
@@ -92,6 +104,8 @@
             })
         foldStateProvider.start()
 
+        verify(rotationChangeProvider).addCallback(capture(rotationListener))
+
         whenever(handler.postDelayed(any<Runnable>(), any())).then { invocationOnMock ->
             scheduledRunnable = invocationOnMock.getArgument<Runnable>(0)
             scheduledRunnableDelay = invocationOnMock.getArgument<Long>(1)
@@ -372,6 +386,27 @@
         assertThat(testHingeAngleProvider.isStarted).isFalse()
     }
 
+    @Test
+    fun onRotationChanged_whileInProgress_cancelled() {
+        setFoldState(folded = false)
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING)
+
+        rotationListener.value.onRotationChanged(1)
+
+        assertThat(foldUpdates).containsExactly(
+            FOLD_UPDATE_START_OPENING, FOLD_UPDATE_FINISH_HALF_OPEN)
+    }
+
+    @Test
+    fun onRotationChanged_whileNotInProgress_noUpdates() {
+        setFoldState(folded = true)
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED)
+
+        rotationListener.value.onRotationChanged(1)
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED)
+    }
+
     private fun setupForegroundActivityType(isHomeActivity: Boolean?) {
         whenever(activityTypeProvider.isHomeActivity).thenReturn(isHomeActivity)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt
new file mode 100644
index 0000000..85cfef7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.unfold.updates
+
+import android.testing.AndroidTestingRunner
+import android.view.IRotationWatcher
+import android.view.IWindowManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class RotationChangeProviderTest : SysuiTestCase() {
+
+    private lateinit var rotationChangeProvider: RotationChangeProvider
+
+    @Mock lateinit var windowManagerInterface: IWindowManager
+    @Mock lateinit var listener: RotationListener
+    @Captor lateinit var rotationWatcher: ArgumentCaptor<IRotationWatcher>
+    private val fakeExecutor = FakeExecutor(FakeSystemClock())
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        rotationChangeProvider =
+            RotationChangeProvider(windowManagerInterface, context, fakeExecutor)
+        rotationChangeProvider.addCallback(listener)
+        fakeExecutor.runAllReady()
+        verify(windowManagerInterface).watchRotation(rotationWatcher.capture(), anyInt())
+    }
+
+    @Test
+    fun onRotationChanged_rotationUpdated_listenerReceivesIt() {
+        sendRotationUpdate(42)
+
+        verify(listener).onRotationChanged(42)
+    }
+
+    @Test
+    fun onRotationChanged_subscribersRemoved_noRotationChangeReceived() {
+        sendRotationUpdate(42)
+        verify(listener).onRotationChanged(42)
+
+        rotationChangeProvider.removeCallback(listener)
+        fakeExecutor.runAllReady()
+        sendRotationUpdate(43)
+
+        verify(windowManagerInterface).removeRotationWatcher(any())
+        verifyNoMoreInteractions(listener)
+    }
+
+    private fun sendRotationUpdate(newRotation: Int) {
+        rotationWatcher.value.onRotationChanged(newRotation)
+        fakeExecutor.runAllReady()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
index b2cedbf..a25469b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt
@@ -16,18 +16,19 @@
 package com.android.systemui.unfold.util
 
 import android.testing.AndroidTestingRunner
-import android.view.IRotationWatcher
-import android.view.IWindowManager
 import android.view.Surface
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.unfold.TestUnfoldTransitionProvider
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
-import com.android.systemui.util.mockito.any
+import com.android.systemui.unfold.updates.RotationChangeProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
+import com.android.systemui.util.mockito.capture
 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.Mockito.clearInvocations
 import org.mockito.Mockito.never
@@ -38,32 +39,26 @@
 @SmallTest
 class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() {
 
-    @Mock
-    lateinit var windowManager: IWindowManager
+    @Mock lateinit var rotationChangeProvider: RotationChangeProvider
 
     private val sourceProvider = TestUnfoldTransitionProvider()
 
-    @Mock
-    lateinit var transitionListener: TransitionProgressListener
+    @Mock lateinit var transitionListener: TransitionProgressListener
+
+    @Captor private lateinit var rotationListenerCaptor: ArgumentCaptor<RotationListener>
 
     lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
 
-    private val rotationWatcherCaptor =
-        ArgumentCaptor.forClass(IRotationWatcher.Stub::class.java)
-
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        progressProvider = NaturalRotationUnfoldProgressProvider(
-            context,
-            windowManager,
-            sourceProvider
-        )
+        progressProvider =
+            NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, sourceProvider)
 
         progressProvider.init()
 
-        verify(windowManager).watchRotation(rotationWatcherCaptor.capture(), any())
+        verify(rotationChangeProvider).addCallback(capture(rotationListenerCaptor))
 
         progressProvider.addCallback(transitionListener)
     }
@@ -127,6 +122,6 @@
     }
 
     private fun onRotationChanged(rotation: Int) {
-        rotationWatcherCaptor.value.onRotationChanged(rotation)
+        rotationListenerCaptor.value.onRotationChanged(rotation)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
index 15ba672..4ca1fd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt
@@ -26,6 +26,7 @@
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import org.junit.Assert.assertTrue
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -35,6 +36,7 @@
 
     private val serializer = IpcSerializer()
 
+    @Ignore("b/253046405")
     @Test
     fun serializeManyIncomingIpcs(): Unit = runBlocking(Dispatchers.Main.immediate) {
         val processor = launch(start = CoroutineStart.LAZY) { serializer.process() }
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
index a5ec0a4..5a868a4 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
@@ -20,10 +20,12 @@
 import android.content.Context
 import android.hardware.SensorManager
 import android.os.Handler
+import android.view.IWindowManager
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
 import com.android.systemui.unfold.dagger.UnfoldBackground
 import com.android.systemui.unfold.dagger.UnfoldMain
 import com.android.systemui.unfold.updates.FoldProvider
+import com.android.systemui.unfold.updates.RotationChangeProvider
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
 import com.android.systemui.unfold.util.CurrentActivityTypeProvider
 import com.android.systemui.unfold.util.UnfoldTransitionATracePrefix
@@ -39,11 +41,11 @@
  *
  * This component is meant to be used for places that don't use dagger. By providing those
  * parameters to the factory, all dagger objects are correctly instantiated. See
- * [createUnfoldTransitionProgressProvider] for an example.
+ * [createUnfoldSharedComponent] for an example.
  */
 @Singleton
 @Component(modules = [UnfoldSharedModule::class])
-internal interface UnfoldSharedComponent {
+interface UnfoldSharedComponent {
 
     @Component.Factory
     interface Factory {
@@ -58,9 +60,11 @@
             @BindsInstance @UnfoldMain executor: Executor,
             @BindsInstance @UnfoldBackground backgroundExecutor: Executor,
             @BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
+            @BindsInstance windowManager: IWindowManager,
             @BindsInstance contentResolver: ContentResolver = context.contentResolver
         ): UnfoldSharedComponent
     }
 
     val unfoldTransitionProvider: Optional<UnfoldTransitionProgressProvider>
+    val rotationChangeProvider: RotationChangeProvider
 }
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
index 402dd84..a1ed178 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.hardware.SensorManager
 import android.os.Handler
+import android.view.IWindowManager
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
 import com.android.systemui.unfold.updates.FoldProvider
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
@@ -27,14 +28,15 @@
 import java.util.concurrent.Executor
 
 /**
- * Factory for [UnfoldTransitionProgressProvider].
+ * Factory for [UnfoldSharedComponent].
  *
- * This is needed as Launcher has to create the object manually. If dagger is available, this object
- * is provided in [UnfoldSharedModule].
+ * This wraps the autogenerated factory (for discoverability), and is needed as Launcher has to
+ * create the object manually. If dagger is available, this object is provided in
+ * [UnfoldSharedModule].
  *
  * This should **never** be called from sysui, as the object is already provided in that process.
  */
-fun createUnfoldTransitionProgressProvider(
+fun createUnfoldSharedComponent(
     context: Context,
     config: UnfoldTransitionConfig,
     screenStatusProvider: ScreenStatusProvider,
@@ -44,8 +46,9 @@
     mainHandler: Handler,
     mainExecutor: Executor,
     backgroundExecutor: Executor,
-    tracingTagPrefix: String
-): UnfoldTransitionProgressProvider =
+    tracingTagPrefix: String,
+    windowManager: IWindowManager,
+): UnfoldSharedComponent =
     DaggerUnfoldSharedComponent.factory()
         .create(
             context,
@@ -57,9 +60,6 @@
             mainHandler,
             mainExecutor,
             backgroundExecutor,
-            tracingTagPrefix)
-        .unfoldTransitionProvider
-        .orElse(null)
-        ?: throw IllegalStateException(
-            "Trying to create " +
-                "UnfoldTransitionProgressProvider when the transition is disabled")
+            tracingTagPrefix,
+            windowManager,
+        )
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
index d54481c..7117aaf 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt
@@ -26,7 +26,8 @@
  *
  * onTransitionProgress callback could be called on each frame.
  *
- * Use [createUnfoldTransitionProgressProvider] to create instances of this interface
+ * Use [createUnfoldSharedComponent] to create instances of this interface when dagger is not
+ * available.
  */
 interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgressListener> {
 
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
index 19cfc80..07473b3 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.unfold.dagger.UnfoldMain
 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
+import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
 import com.android.systemui.unfold.updates.hinge.FULLY_CLOSED_DEGREES
 import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES
 import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
@@ -40,22 +41,24 @@
     private val screenStatusProvider: ScreenStatusProvider,
     private val foldProvider: FoldProvider,
     private val activityTypeProvider: CurrentActivityTypeProvider,
+    private val rotationChangeProvider: RotationChangeProvider,
     @UnfoldMain private val mainExecutor: Executor,
     @UnfoldMain private val handler: Handler
 ) : FoldStateProvider {
 
     private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf()
 
-    @FoldUpdate
-    private var lastFoldUpdate: Int? = null
+    @FoldUpdate private var lastFoldUpdate: Int? = null
 
-    @FloatRange(from = 0.0, to = 180.0)
-    private var lastHingeAngle: Float = 0f
+    @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f
 
     private val hingeAngleListener = HingeAngleListener()
     private val screenListener = ScreenStatusListener()
     private val foldStateListener = FoldStateListener()
-    private val timeoutRunnable = TimeoutRunnable()
+    private val timeoutRunnable = Runnable { cancelAnimation() }
+    private val rotationListener = RotationListener {
+        if (isTransitionInProgress) cancelAnimation()
+    }
 
     /**
      * Time after which [FOLD_UPDATE_FINISH_HALF_OPEN] is emitted following a
@@ -72,6 +75,7 @@
         foldProvider.registerCallback(foldStateListener, mainExecutor)
         screenStatusProvider.addCallback(screenListener)
         hingeAngleProvider.addCallback(hingeAngleListener)
+        rotationChangeProvider.addCallback(rotationListener)
     }
 
     override fun stop() {
@@ -79,6 +83,7 @@
         foldProvider.unregisterCallback(foldStateListener)
         hingeAngleProvider.removeCallback(hingeAngleListener)
         hingeAngleProvider.stop()
+        rotationChangeProvider.removeCallback(rotationListener)
     }
 
     override fun addCallback(listener: FoldUpdatesListener) {
@@ -90,14 +95,15 @@
     }
 
     override val isFinishedOpening: Boolean
-        get() = !isFolded &&
+        get() =
+            !isFolded &&
                 (lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN ||
-                        lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
+                    lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
 
     private val isTransitionInProgress: Boolean
         get() =
             lastFoldUpdate == FOLD_UPDATE_START_OPENING ||
-                    lastFoldUpdate == FOLD_UPDATE_START_CLOSING
+                lastFoldUpdate == FOLD_UPDATE_START_CLOSING
 
     private fun onHingeAngle(angle: Float) {
         if (DEBUG) {
@@ -168,7 +174,7 @@
 
     private fun notifyFoldUpdate(@FoldUpdate update: Int) {
         if (DEBUG) {
-            Log.d(TAG, stateToString(update))
+            Log.d(TAG, update.name())
         }
         outputListeners.forEach { it.onFoldUpdate(update) }
         lastFoldUpdate = update
@@ -185,6 +191,8 @@
         handler.removeCallbacks(timeoutRunnable)
     }
 
+    private fun cancelAnimation(): Unit = notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
+
     private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener {
 
         override fun onScreenTurnedOn() {
@@ -225,16 +233,10 @@
             onHingeAngle(angle)
         }
     }
-
-    private inner class TimeoutRunnable : Runnable {
-        override fun run() {
-            notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
-        }
-    }
 }
 
-private fun stateToString(@FoldUpdate update: Int): String {
-    return when (update) {
+fun @receiver:FoldUpdate Int.name() =
+    when (this) {
         FOLD_UPDATE_START_OPENING -> "START_OPENING"
         FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
         FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> "UNFOLDED_SCREEN_AVAILABLE"
@@ -243,15 +245,12 @@
         FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED"
         else -> "UNKNOWN"
     }
-}
 
 private const val TAG = "DeviceFoldProvider"
 private const val DEBUG = false
 
 /** Threshold after which we consider the device fully unfolded. */
-@VisibleForTesting
-const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
+@VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
 
 /** Fold animation on top of apps only when the angle exceeds this threshold. */
-@VisibleForTesting
-const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
+@VisibleForTesting const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
new file mode 100644
index 0000000..0cf8224
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.unfold.updates
+
+import android.content.Context
+import android.os.RemoteException
+import android.view.IRotationWatcher
+import android.view.IWindowManager
+import android.view.Surface.Rotation
+import com.android.systemui.unfold.dagger.UnfoldMain
+import com.android.systemui.unfold.util.CallbackController
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * Allows to subscribe to rotation changes.
+ *
+ * This is needed as rotation updates from [IWindowManager] are received in a binder thread, while
+ * most of the times we want them in the main one. Updates are provided for the display associated
+ * to [context].
+ */
+class RotationChangeProvider
+@Inject
+constructor(
+    private val windowManagerInterface: IWindowManager,
+    private val context: Context,
+    @UnfoldMain private val mainExecutor: Executor,
+) : CallbackController<RotationChangeProvider.RotationListener> {
+
+    private val listeners = mutableListOf<RotationListener>()
+
+    private val rotationWatcher = RotationWatcher()
+
+    override fun addCallback(listener: RotationListener) {
+        mainExecutor.execute {
+            if (listeners.isEmpty()) {
+                subscribeToRotation()
+            }
+            listeners += listener
+        }
+    }
+
+    override fun removeCallback(listener: RotationListener) {
+        mainExecutor.execute {
+            listeners -= listener
+            if (listeners.isEmpty()) {
+                unsubscribeToRotation()
+            }
+        }
+    }
+
+    private fun subscribeToRotation() {
+        try {
+            windowManagerInterface.watchRotation(rotationWatcher, context.displayId)
+        } catch (e: RemoteException) {
+            throw e.rethrowFromSystemServer()
+        }
+    }
+
+    private fun unsubscribeToRotation() {
+        try {
+            windowManagerInterface.removeRotationWatcher(rotationWatcher)
+        } catch (e: RemoteException) {
+            throw e.rethrowFromSystemServer()
+        }
+    }
+
+    /** Gets notified of rotation changes. */
+    fun interface RotationListener {
+        /** Called once rotation changes. */
+        fun onRotationChanged(@Rotation newRotation: Int)
+    }
+
+    private inner class RotationWatcher : IRotationWatcher.Stub() {
+        override fun onRotationChanged(rotation: Int) {
+            mainExecutor.execute { listeners.forEach { it.onRotationChanged(rotation) } }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
index 62f94ed..1a5f31c 100644
--- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java
+++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
@@ -30,7 +30,10 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 
 /** Probe for ambient light. */
 final class ALSProbe implements Probe {
@@ -47,12 +50,18 @@
 
     private boolean mEnabled = false;
     private boolean mDestroyed = false;
+    private boolean mDestroyRequested = false;
+    private boolean mDisableRequested = false;
+    private volatile NextConsumer mNextConsumer = null;
     private volatile float mLastAmbientLux = -1;
 
     private final SensorEventListener mLightSensorListener = new SensorEventListener() {
         @Override
         public void onSensorChanged(SensorEvent event) {
             mLastAmbientLux = event.values[0];
+            if (mNextConsumer != null) {
+                completeNextConsumer(mLastAmbientLux);
+            }
         }
 
         @Override
@@ -102,29 +111,84 @@
 
     @Override
     public synchronized void enable() {
-        if (!mDestroyed) {
+        if (!mDestroyed && !mDestroyRequested) {
+            mDisableRequested = false;
             enableLightSensorLoggingLocked();
         }
     }
 
     @Override
     public synchronized void disable() {
-        if (!mDestroyed) {
+        mDisableRequested = true;
+
+        // if a final consumer is set it will call destroy/disable on the next value if requested
+        if (!mDestroyed && mNextConsumer == null) {
             disableLightSensorLoggingLocked();
         }
     }
 
     @Override
     public synchronized void destroy() {
-        disable();
-        mDestroyed = true;
+        mDestroyRequested = true;
+
+        // if a final consumer is set it will call destroy/disable on the next value if requested
+        if (!mDestroyed && mNextConsumer == null) {
+            disable();
+            mDestroyed = true;
+        }
     }
 
     /** The most recent lux reading. */
-    public float getCurrentLux() {
+    public float getMostRecentLux() {
         return mLastAmbientLux;
     }
 
+    /**
+     * Register a listener for the next available ALS reading, which will be reported to the given
+     * consumer even if this probe is {@link #disable()}'ed or {@link #destroy()}'ed before a value
+     * is available.
+     *
+     * This method is intended to be used for event logs that occur when the screen may be
+     * off and sampling may have been {@link #disable()}'ed. In these cases, this method will turn
+     * on the sensor (if needed), fetch & report the first value, and then destroy or disable this
+     * probe (if needed).
+     *
+     * @param consumer consumer to notify when the data is available
+     * @param handler handler for notifying the consumer, or null
+     */
+    public synchronized void awaitNextLux(@NonNull Consumer<Float> consumer,
+            @Nullable Handler handler) {
+        final NextConsumer nextConsumer = new NextConsumer(consumer, handler);
+        final float current = mLastAmbientLux;
+        if (current > 0) {
+            nextConsumer.consume(current);
+        } else if (mDestroyed) {
+            nextConsumer.consume(-1f);
+        } else if (mNextConsumer != null) {
+            mNextConsumer.add(nextConsumer);
+        } else {
+            mNextConsumer = nextConsumer;
+            enableLightSensorLoggingLocked();
+        }
+    }
+
+    private synchronized void completeNextConsumer(float value) {
+        Slog.v(TAG, "Finishing next consumer");
+
+        final NextConsumer consumer = mNextConsumer;
+        mNextConsumer = null;
+
+        if (mDestroyRequested) {
+            destroy();
+        } else if (mDisableRequested) {
+            disable();
+        }
+
+        if (consumer != null) {
+            consumer.consume(value);
+        }
+    }
+
     private void enableLightSensorLoggingLocked() {
         if (!mEnabled) {
             mEnabled = true;
@@ -160,4 +224,30 @@
                 + mLightSensorListener.hashCode());
         disable();
     }
+
+    private static class NextConsumer {
+        @NonNull private final Consumer<Float> mConsumer;
+        @Nullable private final Handler mHandler;
+        @NonNull private final List<NextConsumer> mOthers = new ArrayList<>();
+
+        private NextConsumer(@NonNull Consumer<Float> consumer, @Nullable Handler handler) {
+            mConsumer = consumer;
+            mHandler = handler;
+        }
+
+        public void consume(float value) {
+            if (mHandler != null) {
+                mHandler.post(() -> mConsumer.accept(value));
+            } else {
+                mConsumer.accept(value);
+            }
+            for (NextConsumer c : mOthers) {
+                c.consume(value);
+            }
+        }
+
+        public void add(NextConsumer consumer) {
+            mOthers.add(consumer);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
index d6ca8a6..27a70c5 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
@@ -62,8 +62,7 @@
     /** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */
     public void authenticate(OperationContext operationContext,
             int statsModality, int statsAction, int statsClient, boolean isDebug, long latency,
-            int authState, boolean requireConfirmation,
-            int targetUserId, float ambientLightLux) {
+            int authState, boolean requireConfirmation, int targetUserId, float ambientLightLux) {
         FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_AUTHENTICATED,
                 statsModality,
                 targetUserId,
@@ -80,6 +79,16 @@
                 operationContext.isAod);
     }
 
+    /** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */
+    public void authenticate(OperationContext operationContext,
+            int statsModality, int statsAction, int statsClient, boolean isDebug, long latency,
+            int authState, boolean requireConfirmation, int targetUserId, ALSProbe alsProbe) {
+        alsProbe.awaitNextLux((ambientLightLux) -> {
+            authenticate(operationContext, statsModality, statsAction, statsClient, isDebug,
+                    latency, authState, requireConfirmation, targetUserId, ambientLightLux);
+        }, null /* handler */);
+    }
+
     /** {@see FrameworkStatsLog.BIOMETRIC_ENROLLED}. */
     public void enroll(int statsModality, int statsAction, int statsClient,
             int targetUserId, long latency, boolean enrollSuccessful, float ambientLightLux) {
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
index 02b350e..55fe854 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
@@ -220,7 +220,7 @@
                     + ", RequireConfirmation: " + requireConfirmation
                     + ", State: " + authState
                     + ", Latency: " + latency
-                    + ", Lux: " + mALSProbe.getCurrentLux());
+                    + ", Lux: " + mALSProbe.getMostRecentLux());
         } else {
             Slog.v(TAG, "Authentication latency: " + latency);
         }
@@ -231,7 +231,7 @@
 
         mSink.authenticate(operationContext, mStatsModality, mStatsAction, mStatsClient,
                 Utils.isDebugEnabled(context, targetUserId),
-                latency, authState, requireConfirmation, targetUserId, mALSProbe.getCurrentLux());
+                latency, authState, requireConfirmation, targetUserId, mALSProbe);
     }
 
     /** Log enrollment outcome. */
@@ -245,7 +245,7 @@
                     + ", User: " + targetUserId
                     + ", Client: " + mStatsClient
                     + ", Latency: " + latency
-                    + ", Lux: " + mALSProbe.getCurrentLux()
+                    + ", Lux: " + mALSProbe.getMostRecentLux()
                     + ", Success: " + enrollSuccessful);
         } else {
             Slog.v(TAG, "Enroll latency: " + latency);
@@ -256,7 +256,7 @@
         }
 
         mSink.enroll(mStatsModality, mStatsAction, mStatsClient,
-                targetUserId, latency, enrollSuccessful, mALSProbe.getCurrentLux());
+                targetUserId, latency, enrollSuccessful, mALSProbe.getMostRecentLux());
     }
 
     /** Report unexpected enrollment reported by the HAL. */
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index b3f42be..fa75100 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -333,6 +333,9 @@
                 mALSProbeCallback.getProbe().disable();
             }
         });
+        if (getBiometricContext().isAwake()) {
+            mALSProbeCallback.getProbe().enable();
+        }
 
         if (session.hasContextMethods()) {
             return session.getSession().authenticateWithContext(mOperationId, opContext);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index f11801f..fd1fdce 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -4868,6 +4868,13 @@
         }
 
         @Override
+        public int getHintsFromListenerNoToken() {
+            synchronized (mNotificationLock) {
+                return mListenerHints;
+            }
+        }
+
+        @Override
         public void requestInterruptionFilterFromListener(INotificationListener token,
                 int interruptionFilter) throws RemoteException {
             final long identity = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
index fd6ec06..f744d00 100644
--- a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
+++ b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
@@ -46,6 +46,8 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
 
 class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener,
         SensorEventListener {
@@ -275,7 +277,7 @@
     public void onSensorChanged(SensorEvent event) {
         // Using log space to represent human sensation (Fechner's Law) instead of lux
         // because lux values causes bright flashes to skew the average very high.
-        addElement(event.timestamp, Math.max(0,
+        addElement(TimeUnit.NANOSECONDS.toMillis(event.timestamp), Math.max(0,
                 (int) (Math.log(event.values[0]) * LIGHT_VALUE_MULTIPLIER)));
         updateLightSession();
         mHandler.removeCallbacksAndMessages(mDelayedUpdateToken);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 9c080e8..2eb2cf6 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4076,7 +4076,11 @@
             // to the restarted activity.
             nowVisible = mVisibleRequested;
         }
-        mTransitionController.requestCloseTransitionIfNeeded(this);
+        // upgrade transition trigger to task if this is the last activity since it means we are
+        // closing the task.
+        final WindowContainer trigger = remove && task != null && task.getChildCount() == 1
+                ? task : this;
+        mTransitionController.requestCloseTransitionIfNeeded(trigger);
         cleanUp(true /* cleanServices */, true /* setState */);
         if (remove) {
             if (mStartingData != null && mVisible && task != null) {
@@ -7634,6 +7638,31 @@
         ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */);
     }
 
+    /**
+     * Returns the requested {@link Configuration.Orientation} for the current activity.
+     *
+     * <p>When The current orientation is set to {@link SCREEN_ORIENTATION_BEHIND} it returns the
+     * requested orientation for the activity below which is the first activity with an explicit
+     * (different from {@link SCREEN_ORIENTATION_UNSET}) orientation which is not {@link
+     * SCREEN_ORIENTATION_BEHIND}.
+     */
+    @Configuration.Orientation
+    @Override
+    int getRequestedConfigurationOrientation(boolean forDisplay) {
+        if (mOrientation == SCREEN_ORIENTATION_BEHIND && task != null) {
+            // We use Task here because we want to be consistent with what happens in
+            // multi-window mode where other tasks orientations are ignored.
+            final ActivityRecord belowCandidate = task.getActivity(
+                    a -> a.mOrientation != SCREEN_ORIENTATION_UNSET && !a.finishing
+                            && a.mOrientation != ActivityInfo.SCREEN_ORIENTATION_BEHIND, this,
+                    false /* includeBoundary */, true /* traverseTopToBottom */);
+            if (belowCandidate != null) {
+                return belowCandidate.getRequestedConfigurationOrientation(forDisplay);
+            }
+        }
+        return super.getRequestedConfigurationOrientation(forDisplay);
+    }
+
     @Override
     void onCancelFixedRotationTransform(int originalDisplayRotation) {
         if (this != mDisplayContent.getLastOrientationSource()) {
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
new file mode 100644
index 0000000..5e44d6c
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -0,0 +1,125 @@
+/*
+ * 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.server.wm;
+
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
+
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
+import android.os.SystemProperties;
+import android.util.Slog;
+
+import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
+
+/**
+ * The class that defines default launch params for tasks in desktop mode
+ */
+public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
+
+    private static final String TAG =
+            TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM;
+    private static final boolean DEBUG = false;
+
+    // Desktop mode feature flag.
+    static final boolean DESKTOP_MODE_SUPPORTED = SystemProperties.getBoolean(
+            "persist.wm.debug.desktop_mode", false);
+    // Override default freeform task width when desktop mode is enabled. In dips.
+    private static final int DESKTOP_MODE_DEFAULT_WIDTH_DP = SystemProperties.getInt(
+            "persist.wm.debug.desktop_mode.default_width", 840);
+    // Override default freeform task height when desktop mode is enabled. In dips.
+    private static final int DESKTOP_MODE_DEFAULT_HEIGHT_DP = SystemProperties.getInt(
+            "persist.wm.debug.desktop_mode.default_height", 630);
+
+    private StringBuilder mLogBuilder;
+
+    @Override
+    public int onCalculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
+            @Nullable ActivityRecord activity, @Nullable ActivityRecord source,
+            @Nullable ActivityOptions options, @Nullable ActivityStarter.Request request, int phase,
+            LaunchParamsController.LaunchParams currentParams,
+            LaunchParamsController.LaunchParams outParams) {
+
+        initLogBuilder(task, activity);
+        int result = calculate(task, layout, activity, source, options, request, phase,
+                currentParams, outParams);
+        outputLog();
+        return result;
+    }
+
+    private int calculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
+            @Nullable ActivityRecord activity, @Nullable ActivityRecord source,
+            @Nullable ActivityOptions options, @Nullable ActivityStarter.Request request, int phase,
+            LaunchParamsController.LaunchParams currentParams,
+            LaunchParamsController.LaunchParams outParams) {
+
+        if (task == null) {
+            appendLog("task null, skipping");
+            return RESULT_SKIP;
+        }
+        if (phase != PHASE_BOUNDS) {
+            appendLog("not in bounds phase, skipping");
+            return RESULT_SKIP;
+        }
+        if (!task.inFreeformWindowingMode()) {
+            appendLog("not a freeform task, skipping");
+            return RESULT_SKIP;
+        }
+        if (!currentParams.mBounds.isEmpty()) {
+            appendLog("currentParams has bounds set, not overriding");
+            return RESULT_SKIP;
+        }
+
+        // Copy over any values
+        outParams.set(currentParams);
+
+        // Update width and height with default desktop mode values
+        float density = (float) task.getConfiguration().densityDpi / DENSITY_DEFAULT;
+        final int width = (int) (DESKTOP_MODE_DEFAULT_WIDTH_DP * density + 0.5f);
+        final int height = (int) (DESKTOP_MODE_DEFAULT_HEIGHT_DP * density + 0.5f);
+        outParams.mBounds.right = width;
+        outParams.mBounds.bottom = height;
+
+        // Center the task in window bounds
+        Rect windowBounds = task.getWindowConfiguration().getBounds();
+        outParams.mBounds.offset(windowBounds.centerX() - outParams.mBounds.centerX(),
+                windowBounds.centerY() - outParams.mBounds.centerY());
+
+        appendLog("setting desktop mode task bounds to %s", outParams.mBounds);
+
+        return RESULT_DONE;
+    }
+
+    private void initLogBuilder(Task task, ActivityRecord activity) {
+        if (DEBUG) {
+            mLogBuilder = new StringBuilder(
+                    "DesktopModeLaunchParamsModifier: task=" + task + " activity=" + activity);
+        }
+    }
+
+    private void appendLog(String format, Object... args) {
+        if (DEBUG) mLogBuilder.append(" ").append(String.format(format, args));
+    }
+
+    private void outputLog() {
+        if (DEBUG) Slog.d(TAG, mLogBuilder.toString());
+    }
+}
diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java
new file mode 100644
index 0000000..a6f8557
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DeviceStateController.java
@@ -0,0 +1,96 @@
+/*
+ * 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.server.wm;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.function.Consumer;
+
+/**
+ * Class that registers callbacks with the {@link DeviceStateManager} and
+ * responds to fold state changes by forwarding such events to a delegate.
+ */
+final class DeviceStateController {
+    private final DeviceStateManager mDeviceStateManager;
+    private final Context mContext;
+
+    private FoldStateListener mDeviceStateListener;
+
+    public enum FoldState {
+        UNKNOWN, OPEN, FOLDED, HALF_FOLDED
+    }
+
+    DeviceStateController(Context context, Handler handler, Consumer<FoldState> delegate) {
+        mContext = context;
+        mDeviceStateManager = mContext.getSystemService(DeviceStateManager.class);
+        if (mDeviceStateManager != null) {
+            mDeviceStateListener = new FoldStateListener(mContext, delegate);
+            mDeviceStateManager
+                    .registerCallback(new HandlerExecutor(handler),
+                            mDeviceStateListener);
+        }
+    }
+
+    void unregisterFromDeviceStateManager() {
+        if (mDeviceStateListener != null) {
+            mDeviceStateManager.unregisterCallback(mDeviceStateListener);
+        }
+    }
+
+    /**
+     * A listener for half-fold device state events that dispatches state changes to a delegate.
+     */
+    static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
+
+        private final int[] mHalfFoldedDeviceStates;
+        private final int[] mFoldedDeviceStates;
+
+        @Nullable
+        private FoldState mLastResult;
+        private final Consumer<FoldState> mDelegate;
+
+        FoldStateListener(Context context, Consumer<FoldState> delegate) {
+            mFoldedDeviceStates = context.getResources().getIntArray(
+                    com.android.internal.R.array.config_foldedDeviceStates);
+            mHalfFoldedDeviceStates = context.getResources().getIntArray(
+                    com.android.internal.R.array.config_halfFoldedDeviceStates);
+            mDelegate = delegate;
+        }
+
+        @Override
+        public void onStateChanged(int state) {
+            final boolean halfFolded = ArrayUtils.contains(mHalfFoldedDeviceStates, state);
+            FoldState result;
+            if (halfFolded) {
+                result = FoldState.HALF_FOLDED;
+            } else {
+                final boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
+                result = folded ? FoldState.FOLDED : FoldState.OPEN;
+            }
+            if (mLastResult == null || !mLastResult.equals(result)) {
+                mLastResult = result;
+                mDelegate.accept(result);
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 71c80fb..38f6a53 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -564,6 +564,7 @@
     final FixedRotationTransitionListener mFixedRotationTransitionListener =
             new FixedRotationTransitionListener();
 
+    private final DeviceStateController mDeviceStateController;
     private final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
     final RemoteDisplayChangeController mRemoteDisplayChangeController;
 
@@ -1119,6 +1120,13 @@
 
         mDisplayPolicy = new DisplayPolicy(mWmService, this);
         mDisplayRotation = new DisplayRotation(mWmService, this);
+
+        mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH,
+                newFoldState -> {
+                    mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState);
+                    mDisplayRotation.foldStateChanged(newFoldState);
+                });
+
         mCloseToSquareMaxAspectRatio = mWmService.mContext.getResources().getFloat(
                 R.dimen.config_closeToSquareDisplayMaxAspectRatio);
         if (isDefaultDisplay) {
@@ -3218,7 +3226,7 @@
             mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener);
             handleAnimatingStoppedAndTransition();
             mWmService.stopFreezingDisplayLocked();
-            mDisplaySwitchTransitionLauncher.destroy();
+            mDeviceStateController.unregisterFromDeviceStateManager();
             super.removeImmediately();
             if (DEBUG_DISPLAY) Slog.v(TAG_WM, "Removing display=" + this);
             mPointerEventDispatcher.dispose();
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 97609a7..a8d13c5 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -40,6 +40,7 @@
 
 import android.annotation.AnimRes;
 import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -108,6 +109,8 @@
     private OrientationListener mOrientationListener;
     private StatusBarManagerInternal mStatusBarManagerInternal;
     private SettingsObserver mSettingsObserver;
+    @Nullable
+    private FoldController mFoldController;
 
     @ScreenOrientation
     private int mCurrentAppOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
@@ -238,6 +241,10 @@
             mOrientationListener.setCurrentRotation(mRotation);
             mSettingsObserver = new SettingsObserver(uiHandler);
             mSettingsObserver.observe();
+            if (mSupportAutoRotation && mContext.getResources().getBoolean(
+                    R.bool.config_windowManagerHalfFoldAutoRotateOverride)) {
+                mFoldController = new FoldController();
+            }
         }
     }
 
@@ -436,7 +443,17 @@
 
         final int oldRotation = mRotation;
         final int lastOrientation = mLastOrientation;
-        final int rotation = rotationForOrientation(lastOrientation, oldRotation);
+        int rotation = rotationForOrientation(lastOrientation, oldRotation);
+        // Use the saved rotation for tabletop mode, if set.
+        if (mFoldController != null && mFoldController.shouldRevertOverriddenRotation()) {
+            int prevRotation = rotation;
+            rotation = mFoldController.revertOverriddenRotation();
+            ProtoLog.v(WM_DEBUG_ORIENTATION,
+                    "Reverting orientation. Rotating to %s from %s rather than %s.",
+                    Surface.rotationToString(rotation),
+                    Surface.rotationToString(oldRotation),
+                    Surface.rotationToString(prevRotation));
+        }
         ProtoLog.v(WM_DEBUG_ORIENTATION,
                 "Computed rotation=%s (%d) for display id=%d based on lastOrientation=%s (%d) and "
                         + "oldRotation=%s (%d)",
@@ -1138,7 +1155,8 @@
             // If we don't support auto-rotation then bail out here and ignore
             // the sensor and any rotation lock settings.
             preferredRotation = -1;
-        } else if ((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE
+        } else if (((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE
+                            || isTabletopAutoRotateOverrideEnabled())
                         && (orientation == ActivityInfo.SCREEN_ORIENTATION_USER
                                 || orientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
                                 || orientation == ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
@@ -1292,10 +1310,17 @@
         return false;
     }
 
+    private boolean isTabletopAutoRotateOverrideEnabled() {
+        return mFoldController != null && mFoldController.overrideFrozenRotation();
+    }
+
     private boolean isRotationChoicePossible(int orientation) {
         // Rotation choice is only shown when the user is in locked mode.
         if (mUserRotationMode != WindowManagerPolicy.USER_ROTATION_LOCKED) return false;
 
+        // Don't show rotation choice if we are in tabletop or book modes.
+        if (isTabletopAutoRotateOverrideEnabled()) return false;
+
         // We should only enable rotation choice if the rotation isn't forced by the lid, dock,
         // demo, hdmi, vr, etc mode.
 
@@ -1496,6 +1521,74 @@
         proto.end(token);
     }
 
+    /**
+     * Called by the DeviceStateManager callback when the device state changes.
+     */
+    void foldStateChanged(DeviceStateController.FoldState foldState) {
+        if (mFoldController != null) {
+            synchronized (mLock) {
+                mFoldController.foldStateChanged(foldState);
+            }
+        }
+    }
+
+    private class FoldController {
+        @Surface.Rotation
+        private int mHalfFoldSavedRotation = -1; // No saved rotation
+        private DeviceStateController.FoldState mFoldState =
+                DeviceStateController.FoldState.UNKNOWN;
+
+        boolean overrideFrozenRotation() {
+            return mFoldState == DeviceStateController.FoldState.HALF_FOLDED;
+        }
+
+        boolean shouldRevertOverriddenRotation() {
+            return mFoldState == DeviceStateController.FoldState.OPEN // When transitioning to open.
+                    && mHalfFoldSavedRotation != -1 // Ignore if we've already reverted.
+                    && mUserRotationMode
+                    == WindowManagerPolicy.USER_ROTATION_LOCKED; // Ignore if we're unlocked.
+        }
+
+        int revertOverriddenRotation() {
+            int savedRotation = mHalfFoldSavedRotation;
+            mHalfFoldSavedRotation = -1;
+            return savedRotation;
+        }
+
+        void foldStateChanged(DeviceStateController.FoldState newState) {
+            ProtoLog.v(WM_DEBUG_ORIENTATION,
+                    "foldStateChanged: displayId %d, halfFoldStateChanged %s, "
+                    + "saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, "
+                    + "mLastOrientation: %d, mRotation: %d",
+                    mDisplayContent.getDisplayId(), newState.name(), mHalfFoldSavedRotation,
+                    mUserRotation, mLastSensorRotation, mLastOrientation, mRotation);
+            if (mFoldState == DeviceStateController.FoldState.UNKNOWN) {
+                mFoldState = newState;
+                return;
+            }
+            if (newState == DeviceStateController.FoldState.HALF_FOLDED
+                    && mFoldState != DeviceStateController.FoldState.HALF_FOLDED) {
+                // The device has transitioned to HALF_FOLDED state: save the current rotation and
+                // update the device rotation.
+                mHalfFoldSavedRotation = mRotation;
+                mFoldState = newState;
+                // Now mFoldState is set to HALF_FOLDED, the overrideFrozenRotation function will
+                // return true, so rotation is unlocked.
+                mService.updateRotation(false /* alwaysSendConfiguration */,
+                        false /* forceRelayout */);
+            } else {
+                // Revert the rotation to our saved value if we transition from HALF_FOLDED.
+                mRotation = mHalfFoldSavedRotation;
+                // Tell the device to update its orientation (mFoldState is still HALF_FOLDED here
+                // so we will override USER_ROTATION_LOCKED and allow a rotation).
+                mService.updateRotation(false /* alwaysSendConfiguration */,
+                        false /* forceRelayout */);
+                // Once we are rotated, set mFoldstate, effectively removing the lock override.
+                mFoldState = newState;
+            }
+        }
+    }
+
     private class OrientationListener extends WindowOrientationListener implements Runnable {
         transient boolean mEnabled;
 
diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java
index 7bd2a4a..e74e5787 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsController.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsController.java
@@ -64,6 +64,10 @@
     void registerDefaultModifiers(ActivityTaskSupervisor supervisor) {
         // {@link TaskLaunchParamsModifier} handles window layout preferences.
         registerModifier(new TaskLaunchParamsModifier(supervisor));
+        if (DesktopModeLaunchParamsModifier.DESKTOP_MODE_SUPPORTED) {
+            // {@link DesktopModeLaunchParamsModifier} handles default task size changes
+            registerModifier(new DesktopModeLaunchParamsModifier());
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
index a89894d..30bdc34 100644
--- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
+++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
@@ -24,10 +24,7 @@
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.Context;
 import android.graphics.Rect;
-import android.hardware.devicestate.DeviceStateManager;
-import android.os.HandlerExecutor;
 import android.window.DisplayAreaInfo;
 import android.window.TransitionRequestInfo;
 import android.window.WindowContainerTransaction;
@@ -36,11 +33,8 @@
 
     private final DisplayContent mDisplayContent;
     private final WindowManagerService mService;
-    private final DeviceStateManager mDeviceStateManager;
     private final TransitionController mTransitionController;
 
-    private DeviceStateListener mDeviceStateListener;
-
     /**
      * If on a foldable device represents whether the device is folded or not
      */
@@ -52,21 +46,15 @@
         mDisplayContent = displayContent;
         mService = displayContent.mWmService;
         mTransitionController = transitionController;
-
-        mDeviceStateManager = mService.mContext.getSystemService(DeviceStateManager.class);
-
-        if (mDeviceStateManager != null) {
-            mDeviceStateListener = new DeviceStateListener(mService.mContext);
-            mDeviceStateManager
-                    .registerCallback(new HandlerExecutor(mDisplayContent.mWmService.mH),
-                            mDeviceStateListener);
-        }
     }
 
-    public void destroy() {
-        if (mDeviceStateManager != null) {
-            mDeviceStateManager.unregisterCallback(mDeviceStateListener);
-        }
+    /**
+     *   Called by the DeviceStateManager callback when the state changes.
+     */
+    void foldStateChanged(DeviceStateController.FoldState newFoldState) {
+        // Ignore transitions to/from half-folded.
+        if (newFoldState == DeviceStateController.FoldState.HALF_FOLDED) return;
+        mIsFolded = newFoldState == DeviceStateController.FoldState.FOLDED;
     }
 
     /**
@@ -143,10 +131,4 @@
         mTransition = null;
     }
 
-    class DeviceStateListener extends DeviceStateManager.FoldStateListener {
-
-        DeviceStateListener(Context context) {
-            super(context, newIsFolded -> mIsFolded = newIsFolded);
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 7f22242..2866f42 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2021,7 +2021,12 @@
                 // non-fullscreen bounds. Then when this new PIP task exits PIP, it can restore
                 // to its previous freeform bounds.
                 rootTask.setLastNonFullscreenBounds(task.mLastNonFullscreenBounds);
-                rootTask.setBounds(task.getBounds());
+                // When creating a new Task for PiP, set its initial bounds as the TaskFragment in
+                // case the activity is embedded, so that it can be animated to PiP window from the
+                // current bounds.
+                // Use Task#setBoundsUnchecked to skip checking windowing mode as the windowing mode
+                // will be updated later after this is collected in transition.
+                rootTask.setBoundsUnchecked(r.getTaskFragment().getBounds());
 
                 // Move the last recents animation transaction from original task to the new one.
                 if (task.mLastRecentsAnimationTransaction != null) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 6ce96ec..731754e 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2618,6 +2618,13 @@
         return boundsChange;
     }
 
+    /** Sets the requested bounds regardless of the windowing mode. */
+    int setBoundsUnchecked(@NonNull Rect bounds) {
+        final int boundsChange = super.setBounds(bounds);
+        updateSurfaceBounds();
+        return boundsChange;
+    }
+
     @Override
     public boolean isCompatible(int windowingMode, int activityType) {
         // TODO: Should we just move this to ConfigurationContainer?
@@ -5920,10 +5927,7 @@
             return BOUNDS_CHANGE_NONE;
         }
 
-        final int result = super.setBounds(!inMultiWindowMode() ? null : bounds);
-
-        updateSurfaceBounds();
-        return result;
+        return setBoundsUnchecked(!inMultiWindowMode() ? null : bounds);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 526a366..46253c1 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1595,6 +1595,10 @@
             if (info.mEndParent != null) {
                 change.setParent(info.mEndParent.mRemoteToken.toWindowContainerToken());
             }
+            if (info.mStartParent != null && info.mStartParent.mRemoteToken != null
+                    && target.getParent() != info.mStartParent) {
+                change.setLastParent(info.mStartParent.mRemoteToken.toWindowContainerToken());
+            }
             change.setMode(info.getTransitMode(target));
             change.setStartAbsBounds(info.mAbsoluteBounds);
             change.setFlags(info.getChangeFlags(target));
@@ -1874,15 +1878,15 @@
                 flags |= FLAG_TRANSLUCENT;
             }
             final Task task = wc.asTask();
-            if (task != null && task.voiceSession != null) {
-                flags |= FLAG_IS_VOICE_INTERACTION;
-            }
             if (task != null) {
                 final ActivityRecord topActivity = task.getTopNonFinishingActivity();
                 if (topActivity != null && topActivity.mStartingData != null
                         && topActivity.mStartingData.hasImeSurface()) {
                     flags |= FLAG_WILL_IME_SHOWN;
                 }
+                if (task.voiceSession != null) {
+                    flags |= FLAG_IS_VOICE_INTERACTION;
+                }
             }
             Task parentTask = null;
             final ActivityRecord record = wc.asActivityRecord();
@@ -1910,20 +1914,26 @@
                     // Whether the container fills its parent Task bounds.
                     flags |= FLAG_FILLS_TASK;
                 }
-            }
-            final DisplayContent dc = wc.asDisplayContent();
-            if (dc != null) {
-                flags |= FLAG_IS_DISPLAY;
-                if (dc.hasAlertWindowSurfaces()) {
-                    flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+            } else {
+                final DisplayContent dc = wc.asDisplayContent();
+                if (dc != null) {
+                    flags |= FLAG_IS_DISPLAY;
+                    if (dc.hasAlertWindowSurfaces()) {
+                        flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS;
+                    }
+                } else if (isWallpaper(wc)) {
+                    flags |= FLAG_IS_WALLPAPER;
+                } else if (isInputMethod(wc)) {
+                    flags |= FLAG_IS_INPUT_METHOD;
+                } else {
+                    // In this condition, the wc can only be WindowToken or DisplayArea.
+                    final int type = wc.getWindowType();
+                    if (type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
+                            && type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
+                        flags |= TransitionInfo.FLAG_IS_SYSTEM_WINDOW;
+                    }
                 }
             }
-            if (isWallpaper(wc)) {
-                flags |= FLAG_IS_WALLPAPER;
-            }
-            if (isInputMethod(wc)) {
-                flags |= FLAG_IS_INPUT_METHOD;
-            }
             if (occludesKeyguard(wc)) {
                 flags |= FLAG_OCCLUDES_KEYGUARD;
             }
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index e8682f7..26ce4ae 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -126,19 +126,27 @@
         mTransitionTracer = transitionTracer;
         mTransitionPlayerDeath = () -> {
             synchronized (mAtm.mGlobalLock) {
-                // Clean-up/finish any playing transitions.
-                for (int i = 0; i < mPlayingTransitions.size(); ++i) {
-                    mPlayingTransitions.get(i).cleanUpOnFailure();
-                }
-                mPlayingTransitions.clear();
-                mTransitionPlayer = null;
-                mTransitionPlayerProc = null;
-                mRemotePlayer.clear();
-                mRunningLock.doNotifyLocked();
+                detachPlayer();
             }
         };
     }
 
+    private void detachPlayer() {
+        if (mTransitionPlayer == null) return;
+        // Clean-up/finish any playing transitions.
+        for (int i = 0; i < mPlayingTransitions.size(); ++i) {
+            mPlayingTransitions.get(i).cleanUpOnFailure();
+        }
+        mPlayingTransitions.clear();
+        if (mCollectingTransition != null) {
+            mCollectingTransition.abort();
+        }
+        mTransitionPlayer = null;
+        mTransitionPlayerProc = null;
+        mRemotePlayer.clear();
+        mRunningLock.doNotifyLocked();
+    }
+
     /** @see #createTransition(int, int) */
     @NonNull
     Transition createTransition(int type) {
@@ -193,7 +201,7 @@
                 if (mTransitionPlayer.asBinder() != null) {
                     mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0);
                 }
-                mTransitionPlayer = null;
+                detachPlayer();
             }
             if (player.asBinder() != null) {
                 player.asBinder().linkToDeath(mTransitionPlayerDeath, 0);
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 949fa96..32a110e 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1136,10 +1136,13 @@
                 final LauncherAppsServiceInternal launcherApps = LocalServices.getService(
                         LauncherAppsServiceInternal.class);
 
-                launcherApps.startShortcut(caller.mUid, caller.mPid, callingPackage,
-                        hop.getShortcutInfo().getPackage(), null /* default featureId */,
+                final boolean success = launcherApps.startShortcut(caller.mUid, caller.mPid,
+                        callingPackage, hop.getShortcutInfo().getPackage(), null /* featureId */,
                         hop.getShortcutInfo().getId(), null /* sourceBounds */, launchOpts,
                         hop.getShortcutInfo().getUserId());
+                if (success) {
+                    effects |= TRANSACT_EFFECTS_LIFECYCLE;
+                }
                 break;
             }
             case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
index 10f0a5c..68c9ce4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -50,6 +51,9 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
 @Presubmit
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -93,7 +97,7 @@
         mSensorEventListenerCaptor.getValue().onSensorChanged(
                 new SensorEvent(mLightSensor, 1, 2, new float[]{value}));
 
-        assertThat(mProbe.getCurrentLux()).isEqualTo(value);
+        assertThat(mProbe.getMostRecentLux()).isEqualTo(value);
     }
 
     @Test
@@ -121,13 +125,17 @@
         mProbe.destroy();
         mProbe.enable();
 
+        AtomicInteger lux = new AtomicInteger(10);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
         verify(mSensorManager, never()).registerListener(any(), any(), anyInt());
         verifyNoMoreInteractions(mSensorManager);
+        assertThat(lux.get()).isLessThan(0);
     }
 
     @Test
     public void testDisabledReportsNegativeValue() {
-        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
 
         mProbe.enable();
         verify(mSensorManager).registerListener(
@@ -136,7 +144,7 @@
                 new SensorEvent(mLightSensor, 1, 1, new float[]{4.0f}));
         mProbe.disable();
 
-        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
     }
 
     @Test
@@ -150,7 +158,7 @@
 
         verify(mSensorManager).unregisterListener(eq(mSensorEventListenerCaptor.getValue()));
         verifyNoMoreInteractions(mSensorManager);
-        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
     }
 
     @Test
@@ -166,7 +174,148 @@
 
         verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
         verifyNoMoreInteractions(mSensorManager);
-        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+        assertThat(mProbe.getMostRecentLux()).isLessThan(0f);
+    }
+
+    @Test
+    public void testNextLuxWhenAlreadyEnabledAndNotAvailable() {
+        testNextLuxWhenAlreadyEnabled(false /* dataIsAvailable */);
+    }
+
+    @Test
+    public void testNextLuxWhenAlreadyEnabledAndAvailable() {
+        testNextLuxWhenAlreadyEnabled(true /* dataIsAvailable */);
+    }
+
+    private void testNextLuxWhenAlreadyEnabled(boolean dataIsAvailable) {
+        final List<Integer> values = List.of(1, 2, 3, 4, 6);
+        mProbe.enable();
+
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+        if (dataIsAvailable) {
+            for (int v : values) {
+                mSensorEventListenerCaptor.getValue().onSensorChanged(
+                        new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
+            }
+        }
+        AtomicInteger lux = new AtomicInteger(-1);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+        if (!dataIsAvailable) {
+            for (int v : values) {
+                mSensorEventListenerCaptor.getValue().onSensorChanged(
+                        new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
+            }
+        }
+
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{200f}));
+
+        // should remain enabled
+        assertThat(lux.get()).isEqualTo(values.get(dataIsAvailable ? values.size() - 1 : 0));
+        verify(mSensorManager, never()).unregisterListener(any(SensorEventListener.class));
+        verifyNoMoreInteractions(mSensorManager);
+
+        final int anotherValue = 12;
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{12}));
+        assertThat(mProbe.getMostRecentLux()).isEqualTo(anotherValue);
+    }
+
+    @Test
+    public void testNextLuxWhenNotEnabled() {
+        testNextLuxWhenNotEnabled(false /* enableWhileWaiting */);
+    }
+
+    @Test
+    public void testNextLuxWhenNotEnabledButEnabledLater() {
+        testNextLuxWhenNotEnabled(true /* enableWhileWaiting */);
+    }
+
+    private void testNextLuxWhenNotEnabled(boolean enableWhileWaiting) {
+        final List<Integer> values = List.of(1, 2, 3, 4, 6);
+        mProbe.disable();
+
+        AtomicInteger lux = new AtomicInteger(-1);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+        if (enableWhileWaiting) {
+            mProbe.enable();
+        }
+
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+        for (int v : values) {
+            mSensorEventListenerCaptor.getValue().onSensorChanged(
+                    new SensorEvent(mLightSensor, 1, 1, new float[]{v}));
+        }
+
+        // should restore the disabled state
+        assertThat(lux.get()).isEqualTo(values.get(0));
+        verify(mSensorManager, enableWhileWaiting ? never() : times(1)).unregisterListener(
+                any(SensorEventListener.class));
+        verifyNoMoreInteractions(mSensorManager);
+    }
+
+    @Test
+    public void testNextLuxIsNotCanceledByDisableOrDestroy() {
+        final int value = 7;
+        AtomicInteger lux = new AtomicInteger(-1);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+        mProbe.destroy();
+        mProbe.disable();
+
+        assertThat(lux.get()).isEqualTo(-1);
+
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{value}));
+
+        assertThat(lux.get()).isEqualTo(value);
+
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{value + 1}));
+
+        // should remain destroyed
+        mProbe.enable();
+
+        assertThat(lux.get()).isEqualTo(value);
+        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+        verifyNoMoreInteractions(mSensorManager);
+    }
+
+    @Test
+    public void testMultipleNextConsumers() {
+        final int value = 7;
+        AtomicInteger lux = new AtomicInteger(-1);
+        AtomicInteger lux2 = new AtomicInteger(-1);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+        mProbe.awaitNextLux((v) -> lux2.set(Math.round(v)), null /* handler */);
+
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{value}));
+
+        assertThat(lux.get()).isEqualTo(value);
+        assertThat(lux2.get()).isEqualTo(value);
+    }
+
+    @Test
+    public void testNoNextLuxWhenDestroyed() {
+        mProbe.destroy();
+
+        AtomicInteger lux = new AtomicInteger(-20);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+        assertThat(lux.get()).isEqualTo(-1);
+        verify(mSensorManager, never()).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+        verifyNoMoreInteractions(mSensorManager);
     }
 
     private void moveTimeBy(long millis) {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
index 60dc2eb..88a9646 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
@@ -121,7 +121,7 @@
         verify(mSink).authenticate(eq(mOpContext),
                 eq(DEFAULT_MODALITY), eq(DEFAULT_ACTION), eq(DEFAULT_CLIENT), anyBoolean(),
                 anyLong(), anyInt(), eq(requireConfirmation),
-                eq(targetUserId), anyFloat());
+                eq(targetUserId), any());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index dea4d4f..a5c181d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -215,7 +216,7 @@
 
     @Test
     public void luxProbeWhenAwake() throws RemoteException {
-        when(mBiometricContext.isAwake()).thenReturn(false, true, false);
+        when(mBiometricContext.isAwake()).thenReturn(false);
         when(mBiometricContext.isAod()).thenReturn(false);
         final FingerprintAuthenticationClient client = createClient();
         client.start(mCallback);
@@ -228,15 +229,38 @@
         verify(mLuxProbe, never()).enable();
 
         reset(mLuxProbe);
+        when(mBiometricContext.isAwake()).thenReturn(true);
+
         mContextInjector.getValue().accept(opContext);
         verify(mLuxProbe).enable();
         verify(mLuxProbe, never()).disable();
 
+        when(mBiometricContext.isAwake()).thenReturn(false);
+
         mContextInjector.getValue().accept(opContext);
         verify(mLuxProbe).disable();
     }
 
     @Test
+    public void luxProbeEnabledOnStartWhenWake() throws RemoteException {
+        luxProbeEnabledOnStart(true /* isAwake */);
+    }
+
+    @Test
+    public void luxProbeNotEnabledOnStartWhenNotWake() throws RemoteException {
+        luxProbeEnabledOnStart(false /* isAwake */);
+    }
+
+    private void luxProbeEnabledOnStart(boolean isAwake) throws RemoteException {
+        when(mBiometricContext.isAwake()).thenReturn(isAwake);
+        when(mBiometricContext.isAod()).thenReturn(false);
+        final FingerprintAuthenticationClient client = createClient();
+        client.start(mCallback);
+
+        verify(mLuxProbe, isAwake ? times(1) : never()).enable();
+    }
+
+    @Test
     public void luxProbeDisabledOnAod() throws RemoteException {
         when(mBiometricContext.isAwake()).thenReturn(false);
         when(mBiometricContext.isAod()).thenReturn(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index d544744..462957a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2319,6 +2319,22 @@
         assertTrue(activity1.getTask().getTaskInfo().launchCookies.contains(launchCookie));
     }
 
+    @Test
+    public void testOrientationForScreenOrientationBehind() {
+        final Task task = createTask(mDisplayContent);
+        // Activity below
+        new ActivityBuilder(mAtm)
+                .setTask(task)
+                .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+                .build();
+        final ActivityRecord activityTop = new ActivityBuilder(mAtm)
+                .setTask(task)
+                .setScreenOrientation(SCREEN_ORIENTATION_BEHIND)
+                .build();
+        final int topOrientation = activityTop.getRequestedConfigurationOrientation();
+        assertEquals(SCREEN_ORIENTATION_PORTRAIT, topOrientation);
+    }
+
     private void verifyProcessInfoUpdate(ActivityRecord activity, State state,
             boolean shouldUpdate, boolean activityChange) {
         reset(activity.app);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
new file mode 100644
index 0000000..7830e90
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -0,0 +1,148 @@
+/*
+ * 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.server.wm;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
+
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS;
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY;
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_DONE;
+import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.LaunchParamsController.LaunchParamsModifier.Result;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for desktop mode task bounds.
+ *
+ * Build/Install/Run:
+ * atest WmTests:DesktopModeLaunchParamsModifierTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase {
+
+    private ActivityRecord mActivity;
+
+    private DesktopModeLaunchParamsModifier mTarget;
+
+    private LaunchParamsController.LaunchParams mCurrent;
+    private LaunchParamsController.LaunchParams mResult;
+
+    @Before
+    public void setUp() throws Exception {
+        mActivity = new ActivityBuilder(mAtm).build();
+        mTarget = new DesktopModeLaunchParamsModifier();
+        mCurrent = new LaunchParamsController.LaunchParams();
+        mCurrent.reset();
+        mResult = new LaunchParamsController.LaunchParams();
+        mResult.reset();
+    }
+
+    @Test
+    public void testReturnsSkipIfTaskIsNull() {
+        assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(null).calculate());
+    }
+
+    @Test
+    public void testReturnsSkipIfNotBoundsPhase() {
+        final Task task = new TaskBuilder(mSupervisor).build();
+        assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).setPhase(
+                PHASE_DISPLAY).calculate());
+    }
+
+    @Test
+    public void testReturnsSkipIfTaskNotInFreeform() {
+        final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+                WINDOWING_MODE_FULLSCREEN).build();
+        assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).calculate());
+    }
+
+    @Test
+    public void testReturnsSkipIfCurrentParamsHasBounds() {
+        final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+                WINDOWING_MODE_FREEFORM).build();
+        mCurrent.mBounds.set(/* left */ 0, /* top */ 0, /* right */ 100, /* bottom */ 100);
+        assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).calculate());
+    }
+
+    @Test
+    public void testUsesDefaultBounds() {
+        final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+                WINDOWING_MODE_FREEFORM).build();
+        assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(dpiToPx(task, 840), mResult.mBounds.width());
+        assertEquals(dpiToPx(task, 630), mResult.mBounds.height());
+    }
+
+    @Test
+    public void testUsesDisplayAreaAndWindowingModeFromSource() {
+        final Task task = new TaskBuilder(mSupervisor).setWindowingMode(
+                WINDOWING_MODE_FREEFORM).build();
+        TaskDisplayArea mockTaskDisplayArea = mock(TaskDisplayArea.class);
+        mCurrent.mPreferredTaskDisplayArea = mockTaskDisplayArea;
+        mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM;
+
+        assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(mockTaskDisplayArea, mResult.mPreferredTaskDisplayArea);
+        assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode);
+    }
+
+    private int dpiToPx(Task task, int dpi) {
+        float density = (float) task.getConfiguration().densityDpi / DENSITY_DEFAULT;
+        return (int) (dpi * density + 0.5f);
+    }
+
+    private class CalculateRequestBuilder {
+        private Task mTask;
+        private int mPhase = PHASE_BOUNDS;
+        private final ActivityRecord mActivity =
+                DesktopModeLaunchParamsModifierTests.this.mActivity;
+        private final LaunchParamsController.LaunchParams mCurrentParams = mCurrent;
+        private final LaunchParamsController.LaunchParams mOutParams = mResult;
+
+        private CalculateRequestBuilder setTask(Task task) {
+            mTask = task;
+            return this;
+        }
+
+        private CalculateRequestBuilder setPhase(int phase) {
+            mPhase = phase;
+            return this;
+        }
+
+        @Result
+        private int calculate() {
+            return mTarget.onCalculate(mTask, /* layout*/ null, mActivity, /* source */
+                    null, /* options */ null, /* request */ null, mPhase, mCurrentParams,
+                    mOutParams);
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
new file mode 100644
index 0000000..86732c9
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
@@ -0,0 +1,176 @@
+/*
+ * 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.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link DeviceStateController}.
+ *
+ * Build/Install/Run:
+ *  atest WmTests:DeviceStateControllerTests
+ */
+@SmallTest
+@Presubmit
+public class DeviceStateControllerTests {
+
+    private DeviceStateController.FoldStateListener mFoldStateListener;
+    private DeviceStateController mTarget;
+    private DeviceStateControllerBuilder mBuilder;
+
+    private Context mMockContext;
+    private Handler mMockHandler;
+    private Resources mMockRes;
+    private DeviceStateManager mMockDeviceStateManager;
+
+    private Consumer<DeviceStateController.FoldState> mDelegate;
+    private DeviceStateController.FoldState mCurrentState = DeviceStateController.FoldState.UNKNOWN;
+
+    @Before
+    public void setUp() {
+        mBuilder = new DeviceStateControllerBuilder();
+        mCurrentState = DeviceStateController.FoldState.UNKNOWN;
+    }
+
+    private void initialize(boolean supportFold, boolean supportHalfFold) throws Exception {
+        mBuilder.setSupportFold(supportFold, supportHalfFold);
+        mDelegate = (newFoldState) -> {
+            mCurrentState = newFoldState;
+        };
+        mBuilder.setDelegate(mDelegate);
+        mBuilder.build();
+        verifyFoldStateListenerRegistration(1);
+    }
+
+    @Test
+    public void testInitialization() throws Exception {
+        initialize(true /* supportFold */, true /* supportHalfFolded */);
+        mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
+        assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+    }
+
+    @Test
+    public void testInitializationWithNoFoldSupport() throws Exception {
+        initialize(false /* supportFold */, false /* supportHalfFolded */);
+        mFoldStateListener.onStateChanged(mFoldedStates[0]);
+        // Note that the folded state is ignored.
+        assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+    }
+
+    @Test
+    public void testWithFoldSupported() throws Exception {
+        initialize(true /* supportFold */, false /* supportHalfFolded */);
+        mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
+        assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+        mFoldStateListener.onStateChanged(mFoldedStates[0]);
+        assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED);
+        mFoldStateListener.onStateChanged(mHalfFoldedStates[0]);
+        assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); // Ignored
+    }
+
+    @Test
+    public void testWithHalfFoldSupported() throws Exception {
+        initialize(true /* supportFold */, true /* supportHalfFolded */);
+        mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
+        assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+        mFoldStateListener.onStateChanged(mFoldedStates[0]);
+        assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED);
+        mFoldStateListener.onStateChanged(mHalfFoldedStates[0]);
+        assertEquals(mCurrentState, DeviceStateController.FoldState.HALF_FOLDED);
+    }
+
+
+    private final int[] mFoldedStates = {0};
+    private final int[] mUnfoldedStates = {1};
+    private final int[] mHalfFoldedStates = {2};
+
+
+    private void verifyFoldStateListenerRegistration(int numOfInvocation) {
+        final ArgumentCaptor<DeviceStateController.FoldStateListener> listenerCaptor =
+                ArgumentCaptor.forClass(DeviceStateController.FoldStateListener.class);
+        verify(mMockDeviceStateManager, times(numOfInvocation)).registerCallback(
+                any(),
+                listenerCaptor.capture());
+        if (numOfInvocation > 0) {
+            mFoldStateListener = listenerCaptor.getValue();
+        }
+    }
+
+    private class DeviceStateControllerBuilder {
+        private boolean mSupportFold = false;
+        private boolean mSupportHalfFold = false;
+        private Consumer<DeviceStateController.FoldState> mDelegate;
+
+        DeviceStateControllerBuilder setSupportFold(
+                boolean supportFold, boolean supportHalfFold) {
+            mSupportFold = supportFold;
+            mSupportHalfFold = supportHalfFold;
+            return this;
+        }
+
+        DeviceStateControllerBuilder setDelegate(
+                Consumer<DeviceStateController.FoldState> delegate) {
+            mDelegate = delegate;
+            return this;
+        }
+
+        private void mockFold(boolean enableFold, boolean enableHalfFold) {
+            if (enableFold) {
+                when(mMockContext.getResources().getIntArray(
+                        com.android.internal.R.array.config_foldedDeviceStates))
+                        .thenReturn(mFoldedStates);
+            }
+            if (enableHalfFold) {
+                when(mMockContext.getResources().getIntArray(
+                        com.android.internal.R.array.config_halfFoldedDeviceStates))
+                        .thenReturn(mHalfFoldedStates);
+            }
+        }
+
+        private void build() throws Exception {
+            mMockContext = mock(Context.class);
+            mMockRes = mock(Resources.class);
+            when(mMockContext.getResources()).thenReturn((mMockRes));
+            mMockDeviceStateManager = mock(DeviceStateManager.class);
+            when(mMockContext.getSystemService(DeviceStateManager.class))
+                    .thenReturn(mMockDeviceStateManager);
+            mockFold(mSupportFold, mSupportHalfFold);
+            mMockHandler = mock(Handler.class);
+            mTarget = new DeviceStateController(mMockContext, mMockHandler, mDelegate);
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index 89f7111..b45c37f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -28,6 +28,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.atMost;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
@@ -103,7 +104,7 @@
     private Context mMockContext;
     private Resources mMockRes;
     private SensorManager mMockSensorManager;
-    private Sensor mFakeSensor;
+    private Sensor mFakeOrientationSensor;
     private DisplayWindowSettings mMockDisplayWindowSettings;
     private ContentResolver mMockResolver;
     private FakeSettingsProvider mFakeSettingsProvider;
@@ -323,7 +324,7 @@
         waitForUiHandler();
         verify(mMockSensorManager, times(numOfInvocation)).registerListener(
                 listenerCaptor.capture(),
-                same(mFakeSensor),
+                same(mFakeOrientationSensor),
                 anyInt(),
                 any());
         if (numOfInvocation > 0) {
@@ -460,7 +461,7 @@
                 SensorEvent.class.getDeclaredConstructor(int.class);
         constructor.setAccessible(true);
         final SensorEvent event = constructor.newInstance(1);
-        event.sensor = mFakeSensor;
+        event.sensor = mFakeOrientationSensor;
         event.values[0] = rotation;
         event.timestamp = SystemClock.elapsedRealtimeNanos();
         return event;
@@ -691,6 +692,43 @@
                 SCREEN_ORIENTATION_SENSOR, Surface.ROTATION_0));
     }
 
+    // ====================================================
+    // Tests for half-fold auto-rotate override of rotation
+    // ====================================================
+    @Test
+    public void testUpdatesRotationWhenSensorUpdates_RotationLocked_HalfFolded() throws Exception {
+        mBuilder.setSupportHalfFoldAutoRotateOverride(true);
+        mBuilder.build();
+        configureDisplayRotation(SCREEN_ORIENTATION_LANDSCAPE, false, false);
+
+        enableOrientationSensor();
+
+        mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN);
+        freezeRotation(Surface.ROTATION_270);
+
+        mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_0));
+        assertTrue(waitForUiHandler());
+        // No rotation...
+        assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation(
+                SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+        // ... until half-fold
+        mTarget.foldStateChanged(DeviceStateController.FoldState.HALF_FOLDED);
+        assertTrue(waitForUiHandler());
+        verify(sMockWm).updateRotation(false, false);
+        assertTrue(waitForUiHandler());
+        assertEquals(Surface.ROTATION_0, mTarget.rotationForOrientation(
+                SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+        // ... then transition back to flat
+        mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN);
+        assertTrue(waitForUiHandler());
+        verify(sMockWm, atLeast(1)).updateRotation(false, false);
+        assertTrue(waitForUiHandler());
+        assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation(
+                SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+    }
+
     // =================================
     // Tests for Policy based Rotation
     // =================================
@@ -884,6 +922,7 @@
     private class DisplayRotationBuilder {
         private boolean mIsDefaultDisplay = true;
         private boolean mSupportAutoRotation = true;
+        private boolean mSupportHalfFoldAutoRotateOverride = false;
 
         private int mLidOpenRotation = WindowManagerPolicy.WindowManagerFuncs.LID_ABSENT;
         private int mCarDockRotation;
@@ -920,6 +959,12 @@
             return this;
         }
 
+        private DisplayRotationBuilder setSupportHalfFoldAutoRotateOverride(
+                boolean supportHalfFoldAutoRotateOverride) {
+            mSupportHalfFoldAutoRotateOverride = supportHalfFoldAutoRotateOverride;
+            return this;
+        }
+
         private void captureObservers() {
             ArgumentCaptor<ContentObserver> captor = ArgumentCaptor.forClass(
                     ContentObserver.class);
@@ -1032,9 +1077,13 @@
             mMockSensorManager = mock(SensorManager.class);
             when(mMockContext.getSystemService(Context.SENSOR_SERVICE))
                     .thenReturn(mMockSensorManager);
-            mFakeSensor = createSensor(Sensor.TYPE_DEVICE_ORIENTATION);
+            mFakeOrientationSensor = createSensor(Sensor.TYPE_DEVICE_ORIENTATION);
             when(mMockSensorManager.getSensorList(Sensor.TYPE_DEVICE_ORIENTATION)).thenReturn(
-                    Collections.singletonList(mFakeSensor));
+                    Collections.singletonList(mFakeOrientationSensor));
+
+            when(mMockContext.getResources().getBoolean(
+                    com.android.internal.R.bool.config_windowManagerHalfFoldAutoRotateOverride))
+                    .thenReturn(mSupportHalfFoldAutoRotateOverride);
 
             mMockResolver = mock(ContentResolver.class);
             when(mMockContext.getContentResolver()).thenReturn(mMockResolver);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 601cf15..64c1e05 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -72,6 +72,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.graphics.Rect;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
 import android.util.MergedConfiguration;
@@ -377,6 +378,33 @@
         assertEquals(WINDOWING_MODE_FULLSCREEN, fullscreenTask.getWindowingMode());
     }
 
+    @Test
+    public void testMovingEmbeddedActivityToPip() {
+        final Rect taskBounds = new Rect(0, 0, 800, 1000);
+        final Rect taskFragmentBounds = new Rect(0, 0, 400, 1000);
+        final Task task = mRootWindowContainer.getDefaultTaskDisplayArea().createRootTask(
+                WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+        task.setBounds(taskBounds);
+        assertEquals(taskBounds, task.getBounds());
+        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .createActivityCount(2)
+                .setBounds(taskFragmentBounds)
+                .build();
+        assertEquals(taskFragmentBounds, taskFragment.getBounds());
+        final ActivityRecord topActivity = taskFragment.getTopMostActivity();
+
+        // Move the top activity to pinned root task.
+        mRootWindowContainer.moveActivityToPinnedRootTask(topActivity,
+                null /* launchIntoPipHostActivity */, "test");
+
+        final Task pinnedRootTask = task.getDisplayArea().getRootPinnedTask();
+
+        // Ensure the initial bounds of the PiP Task is the same as the TaskFragment.
+        ensureTaskPlacement(pinnedRootTask, topActivity);
+        assertEquals(taskFragmentBounds, pinnedRootTask.getBounds());
+    }
+
     private static void ensureTaskPlacement(Task task, ActivityRecord... activities) {
         final ArrayList<ActivityRecord> taskActivities = new ArrayList<>();
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 7342c49..9fd0850 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1322,6 +1322,35 @@
     }
 
     @Test
+    public void testReparentChangeLastParent() {
+        final Transition transition = createTestTransition(TRANSIT_CHANGE);
+        final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+        final ArraySet<WindowContainer> participants = transition.mParticipants;
+
+        // Reparent activity in transition.
+        final Task lastParent = createTask(mDisplayContent);
+        final Task newParent = createTask(mDisplayContent);
+        final ActivityRecord activity = createActivityRecord(lastParent);
+        activity.mVisibleRequested = true;
+        // Skip manipulate the SurfaceControl.
+        doNothing().when(activity).setDropInputMode(anyInt());
+        changes.put(activity, new Transition.ChangeInfo(activity));
+        activity.reparent(newParent, POSITION_TOP);
+        activity.mVisibleRequested = false;
+
+        participants.add(activity);
+        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+                participants, changes);
+        final TransitionInfo info = Transition.calculateTransitionInfo(
+                transition.mType, 0 /* flags */, targets, changes, mMockT);
+
+        // Change contains last parent info.
+        assertEquals(1, info.getChanges().size());
+        assertEquals(lastParent.mRemoteToken.toWindowContainerToken(),
+                info.getChanges().get(0).getLastParent());
+    }
+
+    @Test
     public void testIncludeEmbeddedActivityReparent() {
         final Transition transition = createTestTransition(TRANSIT_OPEN);
         final Task task = createTask(mDisplayContent);
diff --git a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java
index eafcef2..1e74451 100644
--- a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java
+++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java
@@ -210,21 +210,15 @@
         final int translatedAppUid =
                 getAppUidByComponentName(getContext(), componentName, getUserId());
         final String packageName = componentName.getPackageName();
-        if (activityDestroyed) {
-            // In the Activity destroy case, we only calls onTranslationFinished() in
-            // non-finisTranslation() state. If there is a finisTranslation() calls by apps, we
-            // should remove the waiting callback to avoid callback twice.
+        // In the Activity destroyed case, we only call onTranslationFinished() in
+        // non-finishTranslation() state. If there is a finishTranslation() call by apps, we
+        // should remove the waiting callback to avoid invoking callbacks twice.
+        if (activityDestroyed || mWaitingFinishedCallbackActivities.contains(token)) {
             invokeCallbacks(STATE_UI_TRANSLATION_FINISHED,
                     /* sourceSpec= */ null, /* targetSpec= */ null,
                     packageName, translatedAppUid);
             mWaitingFinishedCallbackActivities.remove(token);
-        } else {
-            if (mWaitingFinishedCallbackActivities.contains(token)) {
-                invokeCallbacks(STATE_UI_TRANSLATION_FINISHED,
-                        /* sourceSpec= */ null, /* targetSpec= */ null,
-                        packageName, translatedAppUid);
-                mWaitingFinishedCallbackActivities.remove(token);
-            }
+            mActiveTranslations.remove(token);
         }
     }
 
@@ -237,6 +231,9 @@
         // Activity is the new Activity, the original Activity is paused in the same task.
         // To make sure the operation still work, we use the token to find the target Activity in
         // this task, not the top Activity only.
+        //
+        // Note: getAttachedNonFinishingActivityForTask() takes the shareable activity token. We
+        // call this method so that we can get the regular activity token below.
         ActivityTokens candidateActivityTokens =
                 mActivityTaskManagerInternal.getAttachedNonFinishingActivityForTask(taskId, token);
         if (candidateActivityTokens == null) {
@@ -263,27 +260,27 @@
                 getAppUidByComponentName(getContext(), componentName, getUserId());
         String packageName = componentName.getPackageName();
 
-        invokeCallbacksIfNecessaryLocked(state, sourceSpec, targetSpec, packageName, activityToken,
+        invokeCallbacksIfNecessaryLocked(state, sourceSpec, targetSpec, packageName, token,
                 translatedAppUid);
-        updateActiveTranslationsLocked(state, sourceSpec, targetSpec, packageName, activityToken,
+        updateActiveTranslationsLocked(state, sourceSpec, targetSpec, packageName, token,
                 translatedAppUid);
     }
 
     @GuardedBy("mLock")
     private void updateActiveTranslationsLocked(int state, TranslationSpec sourceSpec,
-            TranslationSpec targetSpec, String packageName, IBinder activityToken,
+            TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken,
             int translatedAppUid) {
         // We keep track of active translations and their state so that we can:
         // 1. Trigger callbacks that are registered after translation has started.
         //    See registerUiTranslationStateCallbackLocked().
         // 2. NOT trigger callbacks when the state didn't change.
         //    See invokeCallbacksIfNecessaryLocked().
-        ActiveTranslation activeTranslation = mActiveTranslations.get(activityToken);
+        ActiveTranslation activeTranslation = mActiveTranslations.get(shareableActivityToken);
         switch (state) {
             case STATE_UI_TRANSLATION_STARTED: {
                 if (activeTranslation == null) {
                     try {
-                        activityToken.linkToDeath(this, /* flags= */ 0);
+                        shareableActivityToken.linkToDeath(this, /* flags= */ 0);
                     } catch (RemoteException e) {
                         Slog.w(TAG, "Failed to call linkToDeath for translated app with uid="
                                 + translatedAppUid + "; activity is already dead", e);
@@ -294,7 +291,7 @@
                                 packageName, translatedAppUid);
                         return;
                     }
-                    mActiveTranslations.put(activityToken,
+                    mActiveTranslations.put(shareableActivityToken,
                             new ActiveTranslation(sourceSpec, targetSpec, translatedAppUid,
                                     packageName));
                 }
@@ -317,7 +314,7 @@
 
             case STATE_UI_TRANSLATION_FINISHED: {
                 if (activeTranslation != null) {
-                    mActiveTranslations.remove(activityToken);
+                    mActiveTranslations.remove(shareableActivityToken);
                 }
                 break;
             }
@@ -332,12 +329,12 @@
 
     @GuardedBy("mLock")
     private void invokeCallbacksIfNecessaryLocked(int state, TranslationSpec sourceSpec,
-            TranslationSpec targetSpec, String packageName, IBinder activityToken,
+            TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken,
             int translatedAppUid) {
         boolean shouldInvokeCallbacks = true;
         int stateForCallbackInvocation = state;
 
-        ActiveTranslation activeTranslation = mActiveTranslations.get(activityToken);
+        ActiveTranslation activeTranslation = mActiveTranslations.get(shareableActivityToken);
         if (activeTranslation == null) {
             if (state != STATE_UI_TRANSLATION_STARTED) {
                 shouldInvokeCallbacks = false;
@@ -403,14 +400,6 @@
             }
         }
 
-        if (DEBUG) {
-            Slog.d(TAG,
-                    (shouldInvokeCallbacks ? "" : "NOT ")
-                            + "Invoking callbacks for translation state="
-                            + stateForCallbackInvocation + " for app with uid=" + translatedAppUid
-                            + " packageName=" + packageName);
-        }
-
         if (shouldInvokeCallbacks) {
             invokeCallbacks(stateForCallbackInvocation, sourceSpec, targetSpec, packageName,
                     translatedAppUid);
@@ -448,7 +437,7 @@
             pw.println(waitingFinishCallbackSize);
             for (IBinder activityToken : mWaitingFinishedCallbackActivities) {
                 pw.print(prefix);
-                pw.print("activityToken: ");
+                pw.print("shareableActivityToken: ");
                 pw.println(activityToken);
             }
         }
@@ -458,7 +447,14 @@
             int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName,
             int translatedAppUid) {
         Bundle result = createResultForCallback(state, sourceSpec, targetSpec, packageName);
-        if (mCallbacks.getRegisteredCallbackCount() == 0) {
+        int registeredCallbackCount = mCallbacks.getRegisteredCallbackCount();
+        if (DEBUG) {
+            Slog.d(TAG, "Invoking " + registeredCallbackCount + " callbacks for translation state="
+                    + state + " for app with uid=" + translatedAppUid
+                    + " packageName=" + packageName);
+        }
+
+        if (registeredCallbackCount == 0) {
             return;
         }
         List<InputMethodInfo> enabledInputMethods = getEnabledInputMethods();
@@ -521,8 +517,10 @@
     @GuardedBy("mLock")
     public void registerUiTranslationStateCallbackLocked(IRemoteCallback callback, int sourceUid) {
         mCallbacks.register(callback, sourceUid);
-
-        if (mActiveTranslations.size() == 0) {
+        int numActiveTranslations = mActiveTranslations.size();
+        Slog.i(TAG, "New registered callback for sourceUid=" + sourceUid + " with currently "
+                + numActiveTranslations + " active translations");
+        if (numActiveTranslations == 0) {
             return;
         }