Merge "Fix unable to drag notification to enter split screen" into sc-v2-dev
diff --git a/apct-tests/perftests/autofill/AndroidManifest.xml b/apct-tests/perftests/autofill/AndroidManifest.xml
index 57595a2..de2a3f2 100644
--- a/apct-tests/perftests/autofill/AndroidManifest.xml
+++ b/apct-tests/perftests/autofill/AndroidManifest.xml
@@ -16,6 +16,16 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.perftests.autofill">
 
+    <uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
+    <uses-permission android:name="android.permission.DEVICE_POWER" />
+    <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+    <uses-permission android:name="android.permission.MANAGE_USERS" />
+    <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
     <application>
         <uses-library android:name="android.test.runner" />
         <activity android:name="android.perftests.utils.PerfTestActivity"
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index bd9b6e9..ddde272 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -368,7 +368,8 @@
                 && Objects.equals(taskDescription, that.taskDescription)
                 && isFocused == that.isFocused
                 && isVisible == that.isVisible
-                && isSleeping == that.isSleeping;
+                && isSleeping == that.isSleeping
+                && Objects.equals(mTopActivityLocusId, that.mTopActivityLocusId);
     }
 
     /**
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index b570ae6..11c01e6 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -586,12 +586,12 @@
 
             Rect dimensions = null;
             synchronized (this) {
+                ParcelFileDescriptor pfd = null;
                 try {
                     Bundle params = new Bundle();
+                    pfd = mService.getWallpaperWithFeature(context.getOpPackageName(),
+                            context.getAttributionTag(), this, FLAG_SYSTEM, params, userId);
                     // Let's peek user wallpaper first.
-                    ParcelFileDescriptor pfd = mService.getWallpaperWithFeature(
-                            context.getOpPackageName(), context.getAttributionTag(), this,
-                            FLAG_SYSTEM, params, userId);
                     if (pfd != null) {
                         BitmapFactory.Options options = new BitmapFactory.Options();
                         options.inJustDecodeBounds = true;
@@ -600,6 +600,13 @@
                     }
                 } catch (RemoteException ex) {
                     Log.w(TAG, "peek wallpaper dimensions failed", ex);
+                } finally {
+                    if (pfd != null) {
+                        try {
+                            pfd.close();
+                        } catch (IOException ignored) {
+                        }
+                    }
                 }
             }
             // If user wallpaper is unavailable, may be the default one instead.
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index 0686104..02b2c5d 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -124,7 +124,7 @@
     public void setControl(@Nullable InsetsSourceControl control, int[] showTypes,
             int[] hideTypes) {
         super.setControl(control, showTypes, hideTypes);
-        if (control == null && !mIsRequestedVisibleAwaitingControl) {
+        if (control == null && !isRequestedVisibleAwaitingControl()) {
             hide();
             removeSurface();
         }
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 7d8d653..805727c 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -284,8 +284,8 @@
                     mShownOnFinish, mCurrentAlpha, mCurrentInsets));
             mController.notifyFinished(this, mShownOnFinish);
             releaseLeashes();
+            if (DEBUG) Log.d(TAG, "Animation finished abruptly.");
         }
-        if (DEBUG) Log.d(TAG, "Animation finished abruptly.");
         return mFinished;
     }
 
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 51cd95e..8764ccc 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -2441,6 +2441,20 @@
         public static final int PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY = 0x00100000;
 
         /**
+         * Flag to indicate that this window will be excluded while computing the magnifiable region
+         * on the un-scaled screen coordinate, which could avoid the cutout on the magnification
+         * border. It should be used for unmagnifiable overlays.
+         *
+         * </p><p>
+         * Note unlike {@link #PRIVATE_FLAG_NOT_MAGNIFIABLE}, this flag doesn't affect the ability
+         * of magnification. If you want to the window to be unmagnifiable and doesn't lead to the
+         * cutout, you need to combine both of them.
+         * </p><p>
+         * @hide
+         */
+        public static final int PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION = 0x00200000;
+
+        /**
          * Flag to prevent the window from being magnified by the accessibility magnifier.
          *
          * TODO(b/190623172): This is a temporary solution and need to find out another way instead.
@@ -2551,6 +2565,7 @@
                 PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE,
                 SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
                 PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY,
+                PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION,
                 PRIVATE_FLAG_NOT_MAGNIFIABLE,
                 PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION,
                 PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC,
@@ -2632,6 +2647,10 @@
                         equals = PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY,
                         name = "IS_ROUNDED_CORNERS_OVERLAY"),
                 @ViewDebug.FlagToString(
+                        mask = PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION,
+                        equals = PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION,
+                        name = "EXCLUDE_FROM_SCREEN_MAGNIFICATION"),
+                @ViewDebug.FlagToString(
                         mask = PRIVATE_FLAG_NOT_MAGNIFIABLE,
                         equals = PRIVATE_FLAG_NOT_MAGNIFIABLE,
                         name = "NOT_MAGNIFIABLE"),
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 2357d13..57b7d61 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -5746,11 +5746,9 @@
         // persisted across change, and has the RemoteViews re-applied in a different situation
         // (orientation or size), we throw an exception, since the layouts may be completely
         // unrelated.
-        if (hasMultipleLayouts()) {
-            if (!rvToApply.canRecycleView(v)) {
-                throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
-                        " that does not share the same root layout id.");
-            }
+        if (!rvToApply.canRecycleView(v)) {
+            throw new RuntimeException("Attempting to re-apply RemoteViews to a view that"
+                    + " that does not share the same root layout id.");
         }
 
         rvToApply.performApply(v, (ViewGroup) v.getParent(), handler, colorResources);
@@ -5794,11 +5792,9 @@
         // In the case that a view has this RemoteViews applied in one orientation, is persisted
         // across orientation change, and has the RemoteViews re-applied in the new orientation,
         // we throw an exception, since the layouts may be completely unrelated.
-        if (hasMultipleLayouts()) {
-            if (!rvToApply.canRecycleView(v)) {
-                throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
-                        " that does not share the same root layout id.");
-            }
+        if (!rvToApply.canRecycleView(v)) {
+            throw new RuntimeException("Attempting to re-apply RemoteViews to a view that"
+                    + " that does not share the same root layout id.");
         }
 
         return new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(),
diff --git a/core/java/android/window/SplashScreenView.java b/core/java/android/window/SplashScreenView.java
index f748d4b..f04155d 100644
--- a/core/java/android/window/SplashScreenView.java
+++ b/core/java/android/window/SplashScreenView.java
@@ -20,6 +20,10 @@
 import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
 import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
 
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLASHSCREEN_AVD;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.annotation.ColorInt;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -54,6 +58,7 @@
 import android.widget.ImageView;
 
 import com.android.internal.R;
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.policy.DecorView;
 import com.android.internal.util.ContrastColorUtil;
 
@@ -487,6 +492,23 @@
         }
         IconAnimateListener aniDrawable = (IconAnimateListener) iconDrawable;
         aniDrawable.prepareAnimate(duration, this::animationStartCallback);
+        aniDrawable.setAnimationJankMonitoring(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                InteractionJankMonitor.getInstance().cancel(CUJ_SPLASHSCREEN_AVD);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                InteractionJankMonitor.getInstance().end(CUJ_SPLASHSCREEN_AVD);
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                InteractionJankMonitor.getInstance().begin(
+                        SplashScreenView.this, CUJ_SPLASHSCREEN_AVD);
+            }
+        });
     }
 
     private void animationStartCallback() {
@@ -669,6 +691,12 @@
          * Stop animation.
          */
         void stopAnimation();
+
+        /**
+         * Provides a chance to start interaction jank monitoring in avd animation.
+         * @param listener a listener to start jank monitoring
+         */
+        default void setAnimationJankMonitoring(AnimatorListenerAdapter listener) {}
     }
 
     /**
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index c863292..d14054d 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -99,6 +99,7 @@
     private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback;
     private final Handler mHandler;
     private final ChoreographerWrapper mChoreographer;
+    private final Object mLock = InteractionJankMonitor.getInstance().getLock();
 
     @VisibleForTesting
     public final boolean mSurfaceOnly;
@@ -181,7 +182,7 @@
             mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() {
                 @Override
                 public void surfaceCreated(SurfaceControl.Transaction t) {
-                    synchronized (FrameTracker.this) {
+                    synchronized (mLock) {
                         if (mSurfaceControl == null) {
                             mSurfaceControl = mViewRoot.getSurfaceControl();
                             if (mBeginVsyncId != INVALID_ID) {
@@ -203,12 +204,12 @@
                     // Wait a while to give the system a chance for the remaining
                     // frames to arrive, then force finish the session.
                     mHandler.postDelayed(() -> {
-                        synchronized (FrameTracker.this) {
+                        synchronized (mLock) {
                             if (DEBUG) {
                                 Log.d(TAG, "surfaceDestroyed: " + mSession.getName()
                                         + ", finalized=" + mMetricsFinalized
                                         + ", info=" + mJankInfos.size()
-                                        + ", vsync=" + mBeginVsyncId + "-" + mEndVsyncId);
+                                        + ", vsync=" + mBeginVsyncId);
                             }
                             if (!mMetricsFinalized) {
                                 end(REASON_END_SURFACE_DESTROYED);
@@ -227,20 +228,20 @@
     /**
      * Begin a trace session of the CUJ.
      */
-    public synchronized void begin() {
-        mBeginVsyncId = mChoreographer.getVsyncId() + 1;
-        if (DEBUG) {
-            Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId);
-        }
-        if (mSurfaceControl != null) {
-            postTraceStartMarker();
-            mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
-        }
-        if (!mSurfaceOnly) {
-            mRendererWrapper.addObserver(mObserver);
-        }
-        if (mListener != null) {
-            mListener.onCujEvents(mSession, ACTION_SESSION_BEGIN);
+    public void begin() {
+        synchronized (mLock) {
+            mBeginVsyncId = mChoreographer.getVsyncId() + 1;
+            if (DEBUG) {
+                Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId);
+            }
+            if (mSurfaceControl != null) {
+                postTraceStartMarker();
+                mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
+            }
+            if (!mSurfaceOnly) {
+                mRendererWrapper.addObserver(mObserver);
+            }
+            notifyCujEvent(ACTION_SESSION_BEGIN);
         }
     }
 
@@ -250,7 +251,7 @@
     @VisibleForTesting
     public void postTraceStartMarker() {
         mChoreographer.mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, () -> {
-            synchronized (FrameTracker.this) {
+            synchronized (mLock) {
                 if (mCancelled || mEndVsyncId != INVALID_ID) {
                     return;
                 }
@@ -263,88 +264,98 @@
     /**
      * End the trace session of the CUJ.
      */
-    public synchronized void end(@Reasons int reason) {
-        if (mEndVsyncId != INVALID_ID) return;
-        mEndVsyncId = mChoreographer.getVsyncId();
+    public boolean end(@Reasons int reason) {
+        synchronized (mLock) {
+            if (mCancelled || mEndVsyncId != INVALID_ID) return false;
+            mEndVsyncId = mChoreographer.getVsyncId();
+            // Cancel the session if:
+            // 1. The session begins and ends at the same vsync id.
+            // 2. The session never begun.
+            if (mBeginVsyncId == INVALID_ID) {
+                return cancel(REASON_CANCEL_NOT_BEGUN);
+            } else if (mEndVsyncId <= mBeginVsyncId) {
+                return cancel(REASON_CANCEL_SAME_VSYNC);
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, "end: " + mSession.getName()
+                            + ", end=" + mEndVsyncId + ", reason=" + reason);
+                }
+                Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
+                mSession.setReason(reason);
 
-        // Cancel the session if:
-        // 1. The session begins and ends at the same vsync id.
-        // 2. The session never begun.
-        if (mBeginVsyncId == INVALID_ID) {
-            cancel(REASON_CANCEL_NOT_BEGUN);
-        } else if (mEndVsyncId <= mBeginVsyncId) {
-            cancel(REASON_CANCEL_SAME_VSYNC);
-        } else {
-            if (DEBUG) {
-                Log.d(TAG, "end: " + mSession.getName()
-                        + ", end=" + mEndVsyncId + ", reason=" + reason);
+                // We don't remove observer here,
+                // will remove it when all the frame metrics in this duration are called back.
+                // See onFrameMetricsAvailable for the logic of removing the observer.
+                // Waiting at most 10 seconds for all callbacks to finish.
+                mWaitForFinishTimedOut = () -> {
+                    Log.e(TAG, "force finish cuj because of time out:" + mSession.getName());
+                    finish(mJankInfos.size() - 1);
+                };
+                mHandler.postDelayed(mWaitForFinishTimedOut, TimeUnit.SECONDS.toMillis(10));
+                notifyCujEvent(ACTION_SESSION_END);
+                return true;
             }
-            Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
-            mSession.setReason(reason);
-            if (mListener != null) {
-                mListener.onCujEvents(mSession, ACTION_SESSION_END);
-            }
-
-            // We don't remove observer here,
-            // will remove it when all the frame metrics in this duration are called back.
-            // See onFrameMetricsAvailable for the logic of removing the observer.
-            // Waiting at most 10 seconds for all callbacks to finish.
-            mWaitForFinishTimedOut = () -> {
-                Log.e(TAG, "force finish cuj because of time out:" + mSession.getName());
-                finish(mJankInfos.size() - 1);
-            };
-            mHandler.postDelayed(mWaitForFinishTimedOut, TimeUnit.SECONDS.toMillis(10));
         }
     }
 
     /**
      * Cancel the trace session of the CUJ.
      */
-    public synchronized void cancel(@Reasons int reason) {
-        mCancelled = true;
+    public boolean cancel(@Reasons int reason) {
+        synchronized (mLock) {
+            final boolean cancelFromEnd =
+                    reason == REASON_CANCEL_NOT_BEGUN || reason == REASON_CANCEL_SAME_VSYNC;
+            if (mCancelled || (mEndVsyncId != INVALID_ID && !cancelFromEnd)) return false;
+            mCancelled = true;
+            // We don't need to end the trace section if it never begun.
+            if (mTracingStarted) {
+                Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
+            }
 
-        // We don't need to end the trace section if it never begun.
-        if (mTracingStarted) {
-            Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
-        }
+            // Always remove the observers in cancel call to avoid leakage.
+            removeObservers();
 
-        // Always remove the observers in cancel call to avoid leakage.
-        removeObservers();
+            if (DEBUG) {
+                Log.d(TAG, "cancel: " + mSession.getName() + ", begin=" + mBeginVsyncId
+                        + ", end=" + mEndVsyncId + ", reason=" + reason);
+            }
 
-        if (DEBUG) {
-            Log.d(TAG, "cancel: " + mSession.getName()
-                    + ", begin=" + mBeginVsyncId + ", end=" + mEndVsyncId + ", reason=" + reason);
-        }
-
-        mSession.setReason(reason);
-        // Notify the listener the session has been cancelled.
-        // We don't notify the listeners if the session never begun.
-        if (mListener != null) {
-            mListener.onCujEvents(mSession, ACTION_SESSION_CANCEL);
+            mSession.setReason(reason);
+            // Notify the listener the session has been cancelled.
+            // We don't notify the listeners if the session never begun.
+            notifyCujEvent(ACTION_SESSION_CANCEL);
+            return true;
         }
     }
 
-    @Override
-    public synchronized void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
-        if (mCancelled) {
-            return;
-        }
+    private void notifyCujEvent(String action) {
+        if (mListener == null) return;
+        mListener.onCujEvents(mSession, action);
+    }
 
-        for (SurfaceControl.JankData jankStat : jankData) {
-            if (!isInRange(jankStat.frameVsyncId)) {
-                continue;
+    @Override
+    public void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
+        synchronized (mLock) {
+            if (mCancelled) {
+                return;
             }
-            JankInfo info = findJankInfo(jankStat.frameVsyncId);
-            if (info != null) {
-                info.surfaceControlCallbackFired = true;
-                info.jankType = jankStat.jankType;
-            } else {
-                mJankInfos.put((int) jankStat.frameVsyncId,
-                        JankInfo.createFromSurfaceControlCallback(
-                                jankStat.frameVsyncId, jankStat.jankType));
+
+            for (SurfaceControl.JankData jankStat : jankData) {
+                if (!isInRange(jankStat.frameVsyncId)) {
+                    continue;
+                }
+                JankInfo info = findJankInfo(jankStat.frameVsyncId);
+                if (info != null) {
+                    info.surfaceControlCallbackFired = true;
+                    info.jankType = jankStat.jankType;
+                } else {
+                    mJankInfos.put((int) jankStat.frameVsyncId,
+                            JankInfo.createFromSurfaceControlCallback(
+                                    jankStat.frameVsyncId, jankStat.jankType));
+                }
             }
+            processJankInfos();
         }
-        processJankInfos();
     }
 
     private @Nullable JankInfo findJankInfo(long frameVsyncId) {
@@ -359,31 +370,34 @@
     }
 
     @Override
-    public synchronized void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
-        if (mCancelled) {
-            return;
-        }
+    public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
+        synchronized (mLock) {
+            if (mCancelled) {
+                return;
+            }
 
-        // Since this callback might come a little bit late after the end() call.
-        // We should keep tracking the begin / end timestamp.
-        // Then compare with vsync timestamp to check if the frame is in the duration of the CUJ.
-        long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION);
-        boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1;
-        long frameVsyncId = mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];
+            // Since this callback might come a little bit late after the end() call.
+            // We should keep tracking the begin / end timestamp that we can compare with
+            // vsync timestamp to check if the frame is in the duration of the CUJ.
+            long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION);
+            boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1;
+            long frameVsyncId =
+                    mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];
 
-        if (!isInRange(frameVsyncId)) {
-            return;
+            if (!isInRange(frameVsyncId)) {
+                return;
+            }
+            JankInfo info = findJankInfo(frameVsyncId);
+            if (info != null) {
+                info.hwuiCallbackFired = true;
+                info.totalDurationNanos = totalDurationNanos;
+                info.isFirstFrame = isFirstFrame;
+            } else {
+                mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback(
+                        frameVsyncId, totalDurationNanos, isFirstFrame));
+            }
+            processJankInfos();
         }
-        JankInfo info = findJankInfo(frameVsyncId);
-        if (info != null) {
-            info.hwuiCallbackFired = true;
-            info.totalDurationNanos = totalDurationNanos;
-            info.isFirstFrame = isFirstFrame;
-        } else {
-            mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback(
-                    frameVsyncId, totalDurationNanos, isFirstFrame));
-        }
-        processJankInfos();
     }
 
     /**
@@ -497,11 +511,7 @@
                 (int) (maxFrameTimeNanos / NANOS_IN_MILLISECOND));
 
         // Trigger perfetto if necessary.
-        boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
-                && missedFramesCount >= mTraceThresholdMissedFrames;
-        boolean overFrameTimeThreshold = !mSurfaceOnly && mTraceThresholdFrameTimeMillis != -1
-                && maxFrameTimeNanos >= mTraceThresholdFrameTimeMillis * NANOS_IN_MILLISECOND;
-        if (overMissedFramesThreshold || overFrameTimeThreshold) {
+        if (shouldTriggerPerfetto(missedFramesCount, (int) maxFrameTimeNanos)) {
             triggerPerfetto();
         }
         if (mSession.logToStatsd()) {
@@ -513,9 +523,7 @@
                     maxFrameTimeNanos, /* will be 0 if mSurfaceOnly == true */
                     missedSfFramesCount,
                     missedAppFramesCount);
-            if (mListener != null) {
-                mListener.onCujEvents(mSession, ACTION_METRICS_LOGGED);
-            }
+            notifyCujEvent(ACTION_METRICS_LOGGED);
         }
         if (DEBUG) {
             Log.i(TAG, "finish: CUJ=" + mSession.getName()
@@ -528,6 +536,14 @@
         }
     }
 
+    private boolean shouldTriggerPerfetto(int missedFramesCount, int maxFrameTimeNanos) {
+        boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
+                && missedFramesCount >= mTraceThresholdMissedFrames;
+        boolean overFrameTimeThreshold = !mSurfaceOnly && mTraceThresholdFrameTimeMillis != -1
+                && maxFrameTimeNanos >= mTraceThresholdFrameTimeMillis * NANOS_IN_MILLISECOND;
+        return overMissedFramesThreshold || overFrameTimeThreshold;
+    }
+
     /**
      * Remove all the registered listeners, observers and callbacks.
      */
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index f8eb95c..0ba5a39 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -58,6 +58,8 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_ROW_EXPAND;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_ROW_SWIPE;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_SCROLL_FLING;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SPLASHSCREEN_AVD;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SPLASHSCREEN_EXIT_ANIM;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION;
@@ -173,6 +175,8 @@
     public static final int CUJ_PIP_TRANSITION = 35;
     public static final int CUJ_WALLPAPER_TRANSITION = 36;
     public static final int CUJ_USER_SWITCH = 37;
+    public static final int CUJ_SPLASHSCREEN_AVD = 38;
+    public static final int CUJ_SPLASHSCREEN_EXIT_ANIM = 39;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -219,6 +223,8 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SPLASHSCREEN_AVD,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SPLASHSCREEN_EXIT_ANIM,
     };
 
     private static volatile InteractionJankMonitor sInstance;
@@ -230,6 +236,7 @@
     private final SparseArray<FrameTracker> mRunningTrackers;
     private final SparseArray<Runnable> mTimeoutActions;
     private final HandlerThread mWorker;
+    private final Object mLock = new Object();
 
     private boolean mEnabled = DEFAULT_ENABLED;
     private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
@@ -276,6 +283,8 @@
             CUJ_PIP_TRANSITION,
             CUJ_WALLPAPER_TRANSITION,
             CUJ_USER_SWITCH,
+            CUJ_SPLASHSCREEN_AVD,
+            CUJ_SPLASHSCREEN_EXIT_ANIM,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -325,6 +334,10 @@
                 mPropertiesChangedListener);
     }
 
+    Object getLock() {
+        return mLock;
+    }
+
     /**
      * Creates a {@link FrameTracker} instance.
      *
@@ -344,7 +357,7 @@
         final ChoreographerWrapper choreographer =
                 new ChoreographerWrapper(Choreographer.getInstance());
 
-        synchronized (this) {
+        synchronized (mLock) {
             FrameTrackerListener eventsListener =
                     (s, act) -> handleCujEvents(config.getContext(), act, s);
             return new FrameTracker(session, mWorker.getThreadHandler(),
@@ -372,11 +385,16 @@
         final boolean badEnd = action.equals(ACTION_SESSION_END)
                 && session.getReason() != REASON_END_NORMAL;
         final boolean badCancel = action.equals(ACTION_SESSION_CANCEL)
-                && session.getReason() != REASON_CANCEL_NORMAL;
+                && !(session.getReason() == REASON_CANCEL_NORMAL
+                || session.getReason() == REASON_CANCEL_TIMEOUT);
         return badEnd || badCancel;
     }
 
-    private void notifyEvents(Context context, String action, Session session) {
+    /**
+     * Notifies who may interest in some CUJ events.
+     */
+    @VisibleForTesting
+    public void notifyEvents(Context context, String action, Session session) {
         if (action.equals(ACTION_SESSION_CANCEL)
                 && session.getReason() == REASON_CANCEL_NOT_BEGUN) {
             return;
@@ -389,7 +407,7 @@
     }
 
     private void removeTimeout(@CujType int cujType) {
-        synchronized (this) {
+        synchronized (mLock) {
             Runnable timeout = mTimeoutActions.get(cujType);
             if (timeout != null) {
                 mWorker.getThreadHandler().removeCallbacks(timeout);
@@ -432,17 +450,9 @@
     }
 
     private boolean beginInternal(@NonNull Configuration conf) {
-        synchronized (this) {
+        synchronized (mLock) {
             int cujType = conf.mCujType;
-            boolean shouldSample = ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
-            if (!mEnabled || !shouldSample) {
-                if (DEBUG) {
-                    Log.d(TAG, "Skip monitoring cuj: " + getNameOfCuj(cujType)
-                            + ", enable=" + mEnabled + ", debuggable=" + DEFAULT_ENABLED
-                            + ", sample=" + shouldSample + ", interval=" + mSamplingInterval);
-                }
-                return false;
-            }
+            if (!shouldMonitor(cujType)) return false;
             FrameTracker tracker = getTracker(cujType);
             // Skip subsequent calls if we already have an ongoing tracing.
             if (tracker != null) return false;
@@ -460,6 +470,24 @@
     }
 
     /**
+     * Check if the monitoring is enabled and if it should be sampled.
+     */
+    @SuppressWarnings("RandomModInteger")
+    @VisibleForTesting
+    public boolean shouldMonitor(@CujType int cujType) {
+        boolean shouldSample = ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
+        if (!mEnabled || !shouldSample) {
+            if (DEBUG) {
+                Log.d(TAG, "Skip monitoring cuj: " + getNameOfCuj(cujType)
+                        + ", enable=" + mEnabled + ", debuggable=" + DEFAULT_ENABLED
+                        + ", sample=" + shouldSample + ", interval=" + mSamplingInterval);
+            }
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * Schedules a timeout action.
      * @param cuj cuj type
      * @param timeout duration to timeout
@@ -478,14 +506,16 @@
      * @return boolean true if the tracker is ended successfully, false otherwise.
      */
     public boolean end(@CujType int cujType) {
-        synchronized (this) {
+        synchronized (mLock) {
             // remove the timeout action first.
             removeTimeout(cujType);
             FrameTracker tracker = getTracker(cujType);
             // Skip this call since we haven't started a trace yet.
             if (tracker == null) return false;
-            tracker.end(REASON_END_NORMAL);
-            removeTracker(cujType);
+            // if the end call doesn't return true, another thread is handling end of the cuj.
+            if (tracker.end(REASON_END_NORMAL)) {
+                removeTracker(cujType);
+            }
             return true;
         }
     }
@@ -499,33 +529,37 @@
         return cancel(cujType, REASON_CANCEL_NORMAL);
     }
 
-    boolean cancel(@CujType int cujType, @Reasons int reason) {
-        synchronized (this) {
+    /**
+     * Cancels the trace session.
+     *
+     * @return boolean true if the tracker is cancelled successfully, false otherwise.
+     */
+    @VisibleForTesting
+    public boolean cancel(@CujType int cujType, @Reasons int reason) {
+        synchronized (mLock) {
             // remove the timeout action first.
             removeTimeout(cujType);
             FrameTracker tracker = getTracker(cujType);
             // Skip this call since we haven't started a trace yet.
             if (tracker == null) return false;
-            tracker.cancel(reason);
-            removeTracker(cujType);
+            // if the cancel call doesn't return true, another thread is handling cancel of the cuj.
+            if (tracker.cancel(reason)) {
+                removeTracker(cujType);
+            }
             return true;
         }
     }
 
     private FrameTracker getTracker(@CujType int cuj) {
-        synchronized (this) {
-            return mRunningTrackers.get(cuj);
-        }
+        return mRunningTrackers.get(cuj);
     }
 
     private void removeTracker(@CujType int cuj) {
-        synchronized (this) {
-            mRunningTrackers.remove(cuj);
-        }
+        mRunningTrackers.remove(cuj);
     }
 
     private void updateProperties(DeviceConfig.Properties properties) {
-        synchronized (this) {
+        synchronized (mLock) {
             mSamplingInterval = properties.getInt(SETTINGS_SAMPLING_INTERVAL_KEY,
                     DEFAULT_SAMPLING_INTERVAL);
             mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED);
@@ -547,10 +581,8 @@
      */
     @VisibleForTesting
     public void trigger(Session session) {
-        synchronized (this) {
-            mWorker.getThreadHandler().post(
-                    () -> PerfettoTrigger.trigger(session.getPerfettoTrigger()));
-        }
+        mWorker.getThreadHandler().post(
+                () -> PerfettoTrigger.trigger(session.getPerfettoTrigger()));
     }
 
     /**
@@ -648,6 +680,10 @@
                 return "WALLPAPER_TRANSITION";
             case CUJ_USER_SWITCH:
                 return "USER_SWITCH";
+            case CUJ_SPLASHSCREEN_AVD:
+                return "SPLASHSCREEN_AVD";
+            case CUJ_SPLASHSCREEN_EXIT_ANIM:
+                return "SPLASHSCREEN_EXIT_ANIM";
         }
         return "UNKNOWN";
     }
diff --git a/core/res/res/layout/alert_dialog.xml b/core/res/res/layout/alert_dialog.xml
index 59e56af..6869c5f 100644
--- a/core/res/res/layout/alert_dialog.xml
+++ b/core/res/res/layout/alert_dialog.xml
@@ -124,21 +124,21 @@
                 android:layout_width="0dip"
                 android:layout_gravity="start"
                 android:layout_weight="1"
-                style="?android:attr/buttonBarButtonStyle"
+                style="?android:attr/buttonBarPositiveButtonStyle"
                 android:maxLines="2"
                 android:layout_height="wrap_content" />
             <Button android:id="@+id/button3"
                 android:layout_width="0dip"
                 android:layout_gravity="center_horizontal"
                 android:layout_weight="1"
-                style="?android:attr/buttonBarButtonStyle"
+                style="?android:attr/buttonBarNeutralButtonStyle"
                 android:maxLines="2"
                 android:layout_height="wrap_content" />
             <Button android:id="@+id/button2"
                 android:layout_width="0dip"
                 android:layout_gravity="end"
                 android:layout_weight="1"
-                style="?android:attr/buttonBarButtonStyle"
+                style="?android:attr/buttonBarNegativeButtonStyle"
                 android:maxLines="2"
                 android:layout_height="wrap_content" />
             <LinearLayout android:id="@+id/rightSpacer"
diff --git a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
index d7a5e26..0d2d047 100644
--- a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.jank;
 
+import static com.android.internal.jank.FrameTracker.REASON_CANCEL_TIMEOUT;
+import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_TO_STATSD_INTERACTION_TYPE;
 
@@ -34,6 +36,7 @@
 
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.SystemClock;
 import android.provider.DeviceConfig;
 import android.view.View;
 import android.view.ViewAttachTestActivity;
@@ -82,36 +85,23 @@
 
         Handler handler = spy(new Handler(mActivity.getMainLooper()));
         doReturn(true).when(handler).sendMessageAtTime(any(), anyLong());
-        mWorker = spy(new HandlerThread("Interaction-jank-monitor-test"));
-        doNothing().when(mWorker).start();
+        mWorker = mock(HandlerThread.class);
         doReturn(handler).when(mWorker).getThreadHandler();
     }
 
     @Test
     public void testBeginEnd() {
-        // Should return false if the view is not attached.
-        InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
-        verify(mWorker).start();
-
-        Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX);
-        Configuration config = mock(Configuration.class);
-        when(config.isSurfaceOnly()).thenReturn(false);
-        FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
-                new ThreadedRendererWrapper(mView.getThreadedRenderer()),
-                new ViewRootWrapper(mView.getViewRootImpl()),
-                new SurfaceControlWrapper(), mock(ChoreographerWrapper.class),
-                new FrameMetricsWrapper(),
-                /* traceThresholdMissedFrames= */ 1, /* traceThresholdFrameTimeMillis= */ -1,
-                /* FrameTrackerListener */ null, config));
+        InteractionJankMonitor monitor = createMockedInteractionJankMonitor();
+        FrameTracker tracker = createMockedFrameTracker(null);
         doReturn(tracker).when(monitor).createFrameTracker(any(), any());
-        doNothing().when(tracker).triggerPerfetto();
-        doNothing().when(tracker).postTraceStartMarker();
+        doNothing().when(tracker).begin();
+        doReturn(true).when(tracker).end(anyInt());
 
         // Simulate a trace session and see if begin / end are invoked.
-        assertThat(monitor.begin(mView, session.getCuj())).isTrue();
+        assertThat(monitor.begin(mView, CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
         verify(tracker).begin();
-        assertThat(monitor.end(session.getCuj())).isTrue();
-        verify(tracker).end(FrameTracker.REASON_END_NORMAL);
+        assertThat(monitor.end(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
+        verify(tracker).end(REASON_END_NORMAL);
     }
 
     @Test
@@ -140,33 +130,23 @@
     }
 
     @Test
-    public void testBeginCancel() {
-        InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
-
+    public void testBeginTimeout() {
         ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
-
-        Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX);
-        Configuration config = mock(Configuration.class);
-        when(config.isSurfaceOnly()).thenReturn(false);
-        FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
-                new ThreadedRendererWrapper(mView.getThreadedRenderer()),
-                new ViewRootWrapper(mView.getViewRootImpl()),
-                new SurfaceControlWrapper(), mock(FrameTracker.ChoreographerWrapper.class),
-                new FrameMetricsWrapper(),
-                /* traceThresholdMissedFrames= */ 1, /* traceThresholdFrameTimeMillis= */ -1,
-                /* FrameTrackerListener */ null, config));
+        InteractionJankMonitor monitor = createMockedInteractionJankMonitor();
+        FrameTracker tracker = createMockedFrameTracker(null);
         doReturn(tracker).when(monitor).createFrameTracker(any(), any());
-        doNothing().when(tracker).triggerPerfetto();
-        doNothing().when(tracker).postTraceStartMarker();
+        doNothing().when(tracker).begin();
+        doReturn(true).when(tracker).cancel(anyInt());
 
-        assertThat(monitor.begin(mView, session.getCuj())).isTrue();
+        assertThat(monitor.begin(mView, CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
         verify(tracker).begin();
         verify(monitor).scheduleTimeoutAction(anyInt(), anyLong(), captor.capture());
         Runnable runnable = captor.getValue();
         assertThat(runnable).isNotNull();
         mWorker.getThreadHandler().removeCallbacks(runnable);
         runnable.run();
-        verify(tracker).cancel(FrameTracker.REASON_CANCEL_TIMEOUT);
+        verify(monitor).cancel(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, REASON_CANCEL_TIMEOUT);
+        verify(tracker).cancel(REASON_CANCEL_TIMEOUT);
     }
 
     @Test
@@ -192,4 +172,43 @@
                     .isTrue();
         }
     }
+
+    private InteractionJankMonitor createMockedInteractionJankMonitor() {
+        InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
+        doReturn(true).when(monitor).shouldMonitor(anyInt());
+        doNothing().when(monitor).notifyEvents(any(), any(), any());
+        return monitor;
+    }
+
+    private FrameTracker createMockedFrameTracker(FrameTracker.FrameTrackerListener listener) {
+        Session session = spy(new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX));
+        doReturn(false).when(session).logToStatsd();
+
+        ThreadedRendererWrapper threadedRenderer = mock(ThreadedRendererWrapper.class);
+        doNothing().when(threadedRenderer).addObserver(any());
+        doNothing().when(threadedRenderer).removeObserver(any());
+
+        ViewRootWrapper viewRoot = spy(new ViewRootWrapper(mView.getViewRootImpl()));
+        doNothing().when(viewRoot).addSurfaceChangedCallback(any());
+
+        SurfaceControlWrapper surfaceControl = mock(SurfaceControlWrapper.class);
+        doNothing().when(surfaceControl).addJankStatsListener(any(), any());
+        doNothing().when(surfaceControl).removeJankStatsListener(any());
+
+        final ChoreographerWrapper choreographer = mock(ChoreographerWrapper.class);
+        doReturn(SystemClock.elapsedRealtime()).when(choreographer).getVsyncId();
+
+        Configuration configuration = mock(Configuration.class);
+        when(configuration.isSurfaceOnly()).thenReturn(false);
+
+        FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
+                threadedRenderer, viewRoot, surfaceControl, choreographer,
+                new FrameMetricsWrapper(), /* traceThresholdMissedFrames= */ 1,
+                /* traceThresholdFrameTimeMillis= */ -1, listener, configuration));
+
+        doNothing().when(tracker).postTraceStartMarker();
+        doNothing().when(tracker).triggerPerfetto();
+
+        return tracker;
+    }
 }
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_hint_bubble.xml b/libs/WindowManager/Shell/res/drawable/size_compat_hint_bubble.xml
new file mode 100644
index 0000000..94165a1
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/size_compat_hint_bubble.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="@color/size_compat_hint_bubble"/>
+    <corners android:radius="@dimen/size_compat_hint_corner_radius"/>
+</shape>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml b/libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml
new file mode 100644
index 0000000..a8f0f76
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/size_compat_hint_point.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="@dimen/size_compat_hint_point_width"
+        android:height="8dp"
+        android:viewportWidth="10"
+        android:viewportHeight="8">
+    <path
+        android:fillColor="@color/size_compat_hint_bubble"
+        android:pathData="M10,0 l-4.1875,6.6875 a1,1 0 0,1 -1.625,0 l-4.1875,-6.6875z"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
index 73a48d3..3e486df 100644
--- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
+++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml
@@ -15,14 +15,21 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="48"
+        android:viewportHeight="48">
     <path
-        android:fillColor="#aa000000"
-        android:pathData="M0,12 a12,12 0 1,0 24,0 a12,12 0 1,0 -24,0" />
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M17.65,6.35c-1.63,-1.63 -3.94,-2.57 -6.48,-2.31c-3.67,0.37 -6.69,3.35 -7.1,7.02C3.52,15.91 7.27,20 12,20c3.19,0 5.93,-1.87 7.21,-4.57c0.31,-0.66 -0.16,-1.43 -0.89,-1.43h-0.01c-0.37,0 -0.72,0.2 -0.88,0.53c-1.13,2.43 -3.84,3.97 -6.81,3.32c-2.22,-0.49 -4.01,-2.3 -4.49,-4.52C5.31,9.44 8.26,6 12,6c1.66,0 3.14,0.69 4.22,1.78l-2.37,2.37C13.54,10.46 13.76,11 14.21,11H19c0.55,0 1,-0.45 1,-1V5.21c0,-0.45 -0.54,-0.67 -0.85,-0.35L17.65,6.35z"/>
+        android:fillColor="#53534D"
+        android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0" />
+    <group
+        android:translateX="12"
+        android:translateY="12">
+        <path
+            android:fillColor="#E4E3DA"
+            android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/>
+        <path
+            android:fillColor="#E4E3DA"
+            android:pathData="M20,13c0,-4.42 -3.58,-8 -8,-8c-0.06,0 -0.12,0.01 -0.18,0.01v0l1.09,-1.09L11.5,2.5L8,6l3.5,3.5l1.41,-1.41l-1.08,-1.08C11.89,7.01 11.95,7 12,7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02C16.95,20.44 20,17.08 20,13z"/>
+    </group>
 </vector>
diff --git a/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml
index 0dea87c..17347f6 100644
--- a/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml
+++ b/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml
@@ -22,41 +22,34 @@
     <FrameLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:gravity="center"
         android:clipToPadding="false"
-        android:padding="@dimen/bubble_elevation">
+        android:paddingBottom="5dp">
 
         <LinearLayout
+            android:id="@+id/size_compat_hint_popup"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:background="@android:color/background_light"
-            android:elevation="@dimen/bubble_elevation"
-            android:orientation="vertical">
+            android:orientation="vertical"
+            android:clickable="true">
 
             <TextView
-                android:layout_width="180dp"
+                android:layout_width="188dp"
                 android:layout_height="wrap_content"
-                android:paddingLeft="10dp"
-                android:paddingRight="10dp"
-                android:paddingTop="10dp"
+                android:lineSpacingExtra="4sp"
+                android:background="@drawable/size_compat_hint_bubble"
+                android:padding="16dp"
                 android:text="@string/restart_button_description"
                 android:textAlignment="viewStart"
-                android:textColor="@android:color/primary_text_light"
-                android:textSize="16sp"/>
+                android:textColor="#E4E3DA"
+                android:textSize="14sp"/>
 
-            <Button
-                android:id="@+id/got_it"
+            <ImageView
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:includeFontPadding="false"
                 android:layout_gravity="end"
-                android:minHeight="36dp"
-                android:background="?android:attr/selectableItemBackground"
-                android:text="@string/got_it"
-                android:textAllCaps="true"
-                android:textColor="#3c78d8"
-                android:textSize="16sp"
-                android:textStyle="bold"/>
+                android:src="@drawable/size_compat_hint_point"
+                android:paddingHorizontal="@dimen/size_compat_hint_corner_radius"
+                android:contentDescription="@null"/>
 
         </LinearLayout>
 
diff --git a/libs/WindowManager/Shell/res/layout/size_compat_ui.xml b/libs/WindowManager/Shell/res/layout/size_compat_ui.xml
index cd31531..47e76f0 100644
--- a/libs/WindowManager/Shell/res/layout/size_compat_ui.xml
+++ b/libs/WindowManager/Shell/res/layout/size_compat_ui.xml
@@ -19,12 +19,21 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content">
 
-    <ImageButton
-        android:id="@+id/size_compat_restart_button"
-        android:layout_width="@dimen/size_compat_button_size"
-        android:layout_height="@dimen/size_compat_button_size"
-        android:layout_gravity="center"
-        android:src="@drawable/size_compat_restart_button"
-        android:contentDescription="@string/restart_button_description"/>
+    <FrameLayout
+        android:layout_width="@dimen/size_compat_button_width"
+        android:layout_height="@dimen/size_compat_button_height"
+        android:clipToPadding="false"
+        android:paddingBottom="16dp">
+
+        <ImageButton
+            android:id="@+id/size_compat_restart_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:src="@drawable/size_compat_restart_button"
+            android:background="@android:color/transparent"
+            android:contentDescription="@string/restart_button_description"/>
+
+    </FrameLayout>
 
 </com.android.wm.shell.sizecompatui.SizeCompatRestartButton>
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index 93c0352..b25a218 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -29,6 +29,7 @@
     <color name="bubbles_light">#FFFFFF</color>
     <color name="bubbles_dark">@color/GM2_grey_800</color>
     <color name="bubbles_icon_tint">@color/GM2_grey_700</color>
+    <color name="size_compat_hint_bubble">#30312B</color>
 
     <!-- GM2 colors -->
     <color name="GM2_grey_200">#E8EAED</color>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index f857664..11bb4e9 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -194,8 +194,17 @@
     <!-- Size of user education views on large screens (phone is just match parent). -->
     <dimen name="bubbles_user_education_width_large_screen">400dp</dimen>
 
-    <!-- The width/height of the size compat restart button. -->
-    <dimen name="size_compat_button_size">48dp</dimen>
+    <!-- The width of the size compat restart button including padding. -->
+    <dimen name="size_compat_button_width">80dp</dimen>
+
+    <!-- The height of the size compat restart button including padding. -->
+    <dimen name="size_compat_button_height">64dp</dimen>
+
+    <!-- The radius of the corners of the size compat hint bubble. -->
+    <dimen name="size_compat_hint_corner_radius">28dp</dimen>
+
+    <!-- The width of the size compat hint point. -->
+    <dimen name="size_compat_hint_point_width">10dp</dimen>
 
     <!-- The width of the brand image on staring surface. -->
     <dimen name="starting_surface_brand_image_width">200dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index e512698..764854a 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -155,7 +155,4 @@
 
     <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] -->
     <string name="restart_button_description">Tap to restart this app and go full screen.</string>
-
-    <!-- Generic "got it" acceptance of dialog or cling [CHAR LIMIT=NONE] -->
-    <string name="got_it">Got it</string>
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java
new file mode 100644
index 0000000..b77ac8a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java
@@ -0,0 +1,123 @@
+/*
+ * 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.wm.shell.common;
+
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import androidx.annotation.BinderThread;
+
+import java.util.function.Consumer;
+
+/**
+ * Manages the lifecycle of a single instance of a remote listener, including the clean up if the
+ * remote process dies.  All calls on this class should happen on the main shell thread.
+ *
+ * @param <C> The controller (must be RemoteCallable)
+ * @param <L> The remote listener interface type
+ */
+public class SingleInstanceRemoteListener<C extends RemoteCallable, L extends IInterface> {
+    private static final String TAG = SingleInstanceRemoteListener.class.getSimpleName();
+
+    /**
+     * Simple callable interface that throws a remote exception.
+     */
+    public interface RemoteCall<L> {
+        void accept(L l) throws RemoteException;
+    }
+
+    private final C mCallableController;
+    private final Consumer<C> mOnRegisterCallback;
+    private final Consumer<C> mOnUnregisterCallback;
+
+    L mListener;
+
+    private final IBinder.DeathRecipient mListenerDeathRecipient =
+            new IBinder.DeathRecipient() {
+                @Override
+                @BinderThread
+                public void binderDied() {
+                    final C callableController = mCallableController;
+                    mCallableController.getRemoteCallExecutor().execute(() -> {
+                        mListener = null;
+                        mOnUnregisterCallback.accept(callableController);
+                    });
+                }
+            };
+
+    /**
+     * @param onRegisterCallback Callback when register() is called (same thread)
+     * @param onUnregisterCallback Callback when unregister() is called (same thread as unregister()
+     *                             or the callableController.getRemoteCallbackExecutor() thread)
+     */
+    public SingleInstanceRemoteListener(C callableController,
+            Consumer<C> onRegisterCallback,
+            Consumer<C> onUnregisterCallback) {
+        mCallableController = callableController;
+        mOnRegisterCallback = onRegisterCallback;
+        mOnUnregisterCallback = onUnregisterCallback;
+    }
+
+    /**
+     * Registers this listener, storing a reference to it and calls the provided method in the
+     * constructor.
+     */
+    public void register(L listener) {
+        if (mListener != null) {
+            mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, 0 /* flags */);
+        }
+        if (listener != null) {
+            try {
+                listener.asBinder().linkToDeath(mListenerDeathRecipient, 0 /* flags */);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to link to death");
+                return;
+            }
+        }
+        mListener = listener;
+        mOnRegisterCallback.accept(mCallableController);
+    }
+
+    /**
+     * Unregisters this listener, removing all references to it and calls the provided method in the
+     * constructor.
+     */
+    public void unregister() {
+        if (mListener != null) {
+            mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, 0 /* flags */);
+        }
+        mListener = null;
+        mOnUnregisterCallback.accept(mCallableController);
+    }
+
+    /**
+     * Safely wraps a call to the remote listener.
+     */
+    public void call(RemoteCall<L> handler) {
+        if (mListener == null) {
+            Slog.e(TAG, "Failed remote call on null listener");
+            return;
+        }
+        try {
+            handler.accept(mListener);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed remote call", e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 5fb3297..8a8d7c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.freeform;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
 import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
 
@@ -27,6 +28,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.SurfaceControl;
+import android.window.WindowContainerTransaction;
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -83,6 +85,13 @@
             Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
             return;
         }
+
+        // Clears windowing mode and window bounds to let the task inherits from its new parent.
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        wct.setBounds(taskInfo.token, null)
+                .setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+        mSyncQueue.queue(wct);
+
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d",
                 taskInfo.taskId);
         mTasks.remove(taskInfo.taskId);
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 8e5c5c5..51eea37 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
@@ -43,7 +43,6 @@
 import android.content.pm.ParceledListSlice;
 import android.content.res.Configuration;
 import android.graphics.Rect;
-import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -69,6 +68,7 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SingleInstanceRemoteListener;
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.onehanded.OneHandedController;
@@ -117,13 +117,28 @@
     private final Rect mTmpInsetBounds = new Rect();
 
     private boolean mIsInFixedRotation;
-    private IPipAnimationListener mPinnedStackAnimationRecentsCallback;
+    private PipAnimationListener mPinnedStackAnimationRecentsCallback;
 
     protected PhonePipMenuController mMenuController;
     protected PipTaskOrganizer mPipTaskOrganizer;
     protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener =
             new PipControllerPinnedTaskListener();
 
+    private interface PipAnimationListener {
+        /**
+         * Notifies the listener that the Pip animation is started.
+         */
+        void onPipAnimationStarted();
+
+        /**
+         * Notifies the listener about PiP round corner radius changes.
+         * Listener can expect an immediate callback the first time they attach.
+         *
+         * @param cornerRadius the pixel value of the corner radius, zero means it's disabled.
+         */
+        void onPipCornerRadiusChanged(int cornerRadius);
+    }
+
     /**
      * Handler for display rotation changes.
      */
@@ -551,7 +566,7 @@
                 animationType == PipAnimationController.ANIM_TYPE_BOUNDS);
     }
 
-    private void setPinnedStackAnimationListener(IPipAnimationListener callback) {
+    private void setPinnedStackAnimationListener(PipAnimationListener callback) {
         mPinnedStackAnimationRecentsCallback = callback;
         onPipCornerRadiusChanged();
     }
@@ -560,11 +575,7 @@
         if (mPinnedStackAnimationRecentsCallback != null) {
             final int cornerRadius =
                     mContext.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius);
-            try {
-                mPinnedStackAnimationRecentsCallback.onPipCornerRadiusChanged(cornerRadius);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to call onPipCornerRadiusChanged", e);
-            }
+            mPinnedStackAnimationRecentsCallback.onPipCornerRadiusChanged(cornerRadius);
         }
     }
 
@@ -623,11 +634,7 @@
         // Disable touches while the animation is running
         mTouchHandler.setTouchEnabled(false);
         if (mPinnedStackAnimationRecentsCallback != null) {
-            try {
-                mPinnedStackAnimationRecentsCallback.onPipAnimationStarted();
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to call onPinnedStackAnimationStarted()", e);
-            }
+            mPinnedStackAnimationRecentsCallback.onPipAnimationStarted();
         }
     }
 
@@ -866,22 +873,25 @@
     @BinderThread
     private static class IPipImpl extends IPip.Stub {
         private PipController mController;
-        private IPipAnimationListener mListener;
-        private final IBinder.DeathRecipient mListenerDeathRecipient =
-                new IBinder.DeathRecipient() {
-                    @Override
-                    @BinderThread
-                    public void binderDied() {
-                        final PipController controller = mController;
-                        controller.getRemoteCallExecutor().execute(() -> {
-                            mListener = null;
-                            controller.setPinnedStackAnimationListener(null);
-                        });
-                    }
-                };
+        private final SingleInstanceRemoteListener<PipController,
+                IPipAnimationListener> mListener;
+        private final PipAnimationListener mPipAnimationListener = new PipAnimationListener() {
+            @Override
+            public void onPipAnimationStarted() {
+                mListener.call(l -> l.onPipAnimationStarted());
+            }
+
+            @Override
+            public void onPipCornerRadiusChanged(int cornerRadius) {
+                mListener.call(l -> l.onPipCornerRadiusChanged(cornerRadius));
+            }
+        };
 
         IPipImpl(PipController controller) {
             mController = controller;
+            mListener = new SingleInstanceRemoteListener<>(mController,
+                    c -> c.setPinnedStackAnimationListener(mPipAnimationListener),
+                    c -> c.setPinnedStackAnimationListener(null));
         }
 
         /**
@@ -925,23 +935,11 @@
         public void setPinnedStackAnimationListener(IPipAnimationListener listener) {
             executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener",
                     (controller) -> {
-                        if (mListener != null) {
-                            // Reset the old death recipient
-                            mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
-                                    0 /* flags */);
-                        }
                         if (listener != null) {
-                            // Register the death recipient for the new listener to clear the listener
-                            try {
-                                listener.asBinder().linkToDeath(mListenerDeathRecipient,
-                                        0 /* flags */);
-                            } catch (RemoteException e) {
-                                Slog.e(TAG, "Failed to link to death");
-                                return;
-                            }
+                            mListener.register(listener);
+                        } else {
+                            mListener.unregister();
                         }
-                        mListener = listener;
-                        controller.setPinnedStackAnimationListener(listener);
                     });
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java
index 78af9df..ff6f913 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java
@@ -17,13 +17,10 @@
 package com.android.wm.shell.sizecompatui;
 
 import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.drawable.RippleDrawable;
 import android.util.AttributeSet;
 import android.view.View;
-import android.widget.Button;
 import android.widget.FrameLayout;
+import android.widget.LinearLayout;
 
 import androidx.annotation.Nullable;
 
@@ -58,10 +55,8 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        final Button gotItButton = findViewById(R.id.got_it);
-        gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY),
-                null /* content */, null /* mask */));
-        gotItButton.setOnClickListener(this);
+        final LinearLayout hintPopup = findViewById(R.id.size_compat_hint_popup);
+        hintPopup.setOnClickListener(this);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java
index 08a8402..d75fe51 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java
@@ -17,10 +17,6 @@
 package com.android.wm.shell.sizecompatui;
 
 import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.RippleDrawable;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.FrameLayout;
@@ -63,11 +59,6 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         final ImageButton restartButton = findViewById(R.id.size_compat_restart_button);
-        final ColorStateList color = ColorStateList.valueOf(Color.LTGRAY);
-        final GradientDrawable mask = new GradientDrawable();
-        mask.setShape(GradientDrawable.OVAL);
-        mask.setColor(color);
-        restartButton.setBackground(new RippleDrawable(color, null /* content */, mask));
         restartButton.setOnClickListener(this);
         restartButton.setOnLongClickListener(this);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
index 7cf9559..bebb6d3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java
@@ -25,6 +25,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.os.Binder;
@@ -54,6 +55,10 @@
     private final int mTaskId;
     private ShellTaskOrganizer.TaskListener mTaskListener;
     private DisplayLayout mDisplayLayout;
+    private final int mButtonWidth;
+    private final int mButtonHeight;
+    private final int mPopupOffsetX;
+    private final int mPopupOffsetY;
 
     @VisibleForTesting
     final SizeCompatUIWindowManager mButtonWindowManager;
@@ -66,9 +71,7 @@
     @VisibleForTesting
     @Nullable
     SizeCompatHintPopup mHint;
-    final int mButtonSize;
-    final int mPopupOffsetX;
-    final int mPopupOffsetY;
+    @VisibleForTesting
     boolean mShouldShowHint;
 
     SizeCompatUILayout(SyncTransactionQueue syncQueue,
@@ -86,10 +89,13 @@
         mShouldShowHint = !hasShownHint;
         mButtonWindowManager = new SizeCompatUIWindowManager(mContext, taskConfig, this);
 
-        mButtonSize =
-                mContext.getResources().getDimensionPixelSize(R.dimen.size_compat_button_size);
-        mPopupOffsetX = mButtonSize / 4;
-        mPopupOffsetY = mButtonSize;
+        final Resources resources = mContext.getResources();
+        mButtonWidth = resources.getDimensionPixelSize(R.dimen.size_compat_button_width);
+        mButtonHeight = resources.getDimensionPixelSize(R.dimen.size_compat_button_height);
+        mPopupOffsetX = (mButtonWidth / 2) - resources.getDimensionPixelSize(
+                R.dimen.size_compat_hint_corner_radius) - (resources.getDimensionPixelSize(
+                R.dimen.size_compat_hint_point_width) / 2);
+        mPopupOffsetY = mButtonHeight;
     }
 
     /** Creates the activity restart button window. */
@@ -222,7 +228,7 @@
     WindowManager.LayoutParams getButtonWindowLayoutParams() {
         final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams(
                 // Cannot be wrap_content as this determines the actual window size
-                mButtonSize, mButtonSize,
+                mButtonWidth, mButtonHeight,
                 TYPE_APPLICATION_OVERLAY,
                 FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL,
                 PixelFormat.TRANSLUCENT);
@@ -278,8 +284,8 @@
         // Position of the button in the container coordinate.
         final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
                 ? stableBounds.left - taskBounds.left
-                : stableBounds.right - taskBounds.left - mButtonSize;
-        final int positionY = stableBounds.bottom - taskBounds.top - mButtonSize;
+                : stableBounds.right - taskBounds.left - mButtonWidth;
+        final int positionY = stableBounds.bottom - taskBounds.top - mButtonHeight;
 
         updateSurfacePosition(leash, positionX, positionY);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 36f1406..14a6574 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -34,7 +34,6 @@
 import android.content.pm.LauncherApps;
 import android.graphics.Rect;
 import android.os.Bundle;
-import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.ArrayMap;
@@ -61,6 +60,7 @@
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SingleInstanceRemoteListener;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ExternalThread;
@@ -433,46 +433,26 @@
     @BinderThread
     private static class ISplitScreenImpl extends ISplitScreen.Stub {
         private SplitScreenController mController;
-        private ISplitScreenListener mListener;
+        private final SingleInstanceRemoteListener<SplitScreenController,
+                ISplitScreenListener> mListener;
         private final SplitScreen.SplitScreenListener mSplitScreenListener =
                 new SplitScreen.SplitScreenListener() {
                     @Override
                     public void onStagePositionChanged(int stage, int position) {
-                        try {
-                            if (mListener != null) {
-                                mListener.onStagePositionChanged(stage, position);
-                            }
-                        } catch (RemoteException e) {
-                            Slog.e(TAG, "onStagePositionChanged", e);
-                        }
+                        mListener.call(l -> l.onStagePositionChanged(stage, position));
                     }
 
                     @Override
                     public void onTaskStageChanged(int taskId, int stage, boolean visible) {
-                        try {
-                            if (mListener != null) {
-                                mListener.onTaskStageChanged(taskId, stage, visible);
-                            }
-                        } catch (RemoteException e) {
-                            Slog.e(TAG, "onTaskStageChanged", e);
-                        }
-                    }
-                };
-        private final IBinder.DeathRecipient mListenerDeathRecipient =
-                new IBinder.DeathRecipient() {
-                    @Override
-                    @BinderThread
-                    public void binderDied() {
-                        final SplitScreenController controller = mController;
-                        controller.getRemoteCallExecutor().execute(() -> {
-                            mListener = null;
-                            controller.unregisterSplitScreenListener(mSplitScreenListener);
-                        });
+                        mListener.call(l -> l.onTaskStageChanged(taskId, stage, visible));
                     }
                 };
 
         public ISplitScreenImpl(SplitScreenController controller) {
             mController = controller;
+            mListener = new SingleInstanceRemoteListener<>(controller,
+                    c -> c.registerSplitScreenListener(mSplitScreenListener),
+                    c -> c.unregisterSplitScreenListener(mSplitScreenListener));
         }
 
         /**
@@ -485,36 +465,13 @@
         @Override
         public void registerSplitScreenListener(ISplitScreenListener listener) {
             executeRemoteCallWithTaskPermission(mController, "registerSplitScreenListener",
-                    (controller) -> {
-                        if (mListener != null) {
-                            mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
-                                    0 /* flags */);
-                        }
-                        if (listener != null) {
-                            try {
-                                listener.asBinder().linkToDeath(mListenerDeathRecipient,
-                                        0 /* flags */);
-                            } catch (RemoteException e) {
-                                Slog.e(TAG, "Failed to link to death");
-                                return;
-                            }
-                        }
-                        mListener = listener;
-                        controller.registerSplitScreenListener(mSplitScreenListener);
-                    });
+                    (controller) -> mListener.register(listener));
         }
 
         @Override
         public void unregisterSplitScreenListener(ISplitScreenListener listener) {
             executeRemoteCallWithTaskPermission(mController, "unregisterSplitScreenListener",
-                    (controller) -> {
-                        if (mListener != null) {
-                            mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
-                                    0 /* flags */);
-                        }
-                        mListener = null;
-                        controller.unregisterSplitScreenListener(mSplitScreenListener);
-                    });
+                    (controller) -> mListener.unregister());
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java
index 4e477ca..003d8a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java
@@ -18,6 +18,8 @@
 import static android.view.Choreographer.CALLBACK_COMMIT;
 import static android.view.View.GONE;
 
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLASHSCREEN_EXIT_ANIM;
+
 import android.animation.Animator;
 import android.animation.ValueAnimator;
 import android.content.Context;
@@ -42,6 +44,7 @@
 import android.view.animation.PathInterpolator;
 import android.window.SplashScreenView;
 
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.Interpolators;
 import com.android.wm.shell.common.TransactionPool;
@@ -311,17 +314,19 @@
 
     @Override
     public void onAnimationStart(Animator animation) {
-        // ignore
+        InteractionJankMonitor.getInstance().begin(mSplashScreenView, CUJ_SPLASHSCREEN_EXIT_ANIM);
     }
 
     @Override
     public void onAnimationEnd(Animator animation) {
         reset();
+        InteractionJankMonitor.getInstance().end(CUJ_SPLASHSCREEN_EXIT_ANIM);
     }
 
     @Override
     public void onAnimationCancel(Animator animation) {
         reset();
+        InteractionJankMonitor.getInstance().cancel(CUJ_SPLASHSCREEN_EXIT_ANIM);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
index e2a72bd..709e221 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
@@ -19,6 +19,7 @@
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.ColorInt;
 import android.annotation.NonNull;
@@ -263,11 +264,12 @@
      * A lightweight AdaptiveIconDrawable which support foreground to be Animatable, and keep this
      * drawable masked by config_icon_mask.
      */
-    private static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable
+    public static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable
             implements SplashScreenView.IconAnimateListener {
         private Animatable mAnimatableIcon;
         private Animator mIconAnimator;
         private boolean mAnimationTriggered;
+        private AnimatorListenerAdapter mJankMonitoringListener;
 
         AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable) {
             super(foregroundDrawable);
@@ -275,6 +277,11 @@
         }
 
         @Override
+        public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) {
+            mJankMonitoringListener = listener;
+        }
+
+        @Override
         public boolean prepareAnimate(long duration, Runnable startListener) {
             mAnimatableIcon = (Animatable) mForegroundDrawable;
             mIconAnimator = ValueAnimator.ofInt(0, 1);
@@ -286,6 +293,9 @@
                         startListener.run();
                     }
                     try {
+                        if (mJankMonitoringListener != null) {
+                            mJankMonitoringListener.onAnimationStart(animation);
+                        }
                         mAnimatableIcon.start();
                     } catch (Exception ex) {
                         Log.e(TAG, "Error while running the splash screen animated icon", ex);
@@ -296,11 +306,17 @@
                 @Override
                 public void onAnimationEnd(Animator animation) {
                     mAnimatableIcon.stop();
+                    if (mJankMonitoringListener != null) {
+                        mJankMonitoringListener.onAnimationEnd(animation);
+                    }
                 }
 
                 @Override
                 public void onAnimationCancel(Animator animation) {
                     mAnimatableIcon.stop();
+                    if (mJankMonitoringListener != null) {
+                        mJankMonitoringListener.onAnimationCancel(animation);
+                    }
                 }
 
                 @Override
@@ -316,6 +332,7 @@
         public void stopAnimation() {
             if (mIconAnimator != null && mIconAnimator.isRunning()) {
                 mIconAnimator.end();
+                mJankMonitoringListener = null;
             }
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
index a86e07a..e98a3e8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SingleInstanceRemoteListener;
 import com.android.wm.shell.common.TransactionPool;
 
 /**
@@ -237,24 +238,19 @@
     @BinderThread
     private static class IStartingWindowImpl extends IStartingWindow.Stub {
         private StartingWindowController mController;
-        private IStartingWindowListener mListener;
+        private SingleInstanceRemoteListener<StartingWindowController,
+                IStartingWindowListener> mListener;
         private final TriConsumer<Integer, Integer, Integer> mStartingWindowListener =
-                this::notifyIStartingWindowListener;
-        private final IBinder.DeathRecipient mListenerDeathRecipient =
-                new IBinder.DeathRecipient() {
-                    @Override
-                    @BinderThread
-                    public void binderDied() {
-                        final StartingWindowController controller = mController;
-                        controller.getRemoteCallExecutor().execute(() -> {
-                            mListener = null;
-                            controller.setStartingWindowListener(null);
-                        });
-                    }
+                (taskId, supportedType, startingWindowBackgroundColor) -> {
+                    mListener.call(l -> l.onTaskLaunching(taskId, supportedType,
+                            startingWindowBackgroundColor));
                 };
 
         public IStartingWindowImpl(StartingWindowController controller) {
             mController = controller;
+            mListener = new SingleInstanceRemoteListener<>(controller,
+                    c -> c.setStartingWindowListener(mStartingWindowListener),
+                    c -> c.setStartingWindowListener(null));
         }
 
         /**
@@ -268,36 +264,12 @@
         public void setStartingWindowListener(IStartingWindowListener listener) {
             executeRemoteCallWithTaskPermission(mController, "setStartingWindowListener",
                     (controller) -> {
-                        if (mListener != null) {
-                            // Reset the old death recipient
-                            mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
-                                    0 /* flags */);
-                        }
                         if (listener != null) {
-                            try {
-                                listener.asBinder().linkToDeath(mListenerDeathRecipient,
-                                        0 /* flags */);
-                            } catch (RemoteException e) {
-                                Slog.e(TAG, "Failed to link to death");
-                                return;
-                            }
+                            mListener.register(listener);
+                        } else {
+                            mListener.unregister();
                         }
-                        mListener = listener;
-                        controller.setStartingWindowListener(mStartingWindowListener);
                     });
         }
-
-        private void notifyIStartingWindowListener(int taskId, int supportedType,
-                int startingWindowBackgroundColor) {
-            if (mListener == null) {
-                return;
-            }
-
-            try {
-                mListener.onTaskLaunching(taskId, supportedType, startingWindowBackgroundColor);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Failed to notify task launching", e);
-            }
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
index 802d25f..b34049d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
@@ -38,11 +38,11 @@
     /**
      * Registers a remote transition.
      */
-    void registerRemote(@NonNull TransitionFilter filter,
-            @NonNull RemoteTransition remoteTransition);
+    default void registerRemote(@NonNull TransitionFilter filter,
+            @NonNull RemoteTransition remoteTransition) {}
 
     /**
      * Unregisters a remote transition.
      */
-    void unregisterRemote(@NonNull RemoteTransition remoteTransition);
+    default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {}
 }
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 c369831..804e449 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
@@ -171,24 +171,6 @@
         }
     }
 
-    /** Create an empty/non-registering transitions object for system-ui tests. */
-    @VisibleForTesting
-    public static ShellTransitions createEmptyForTesting() {
-        return new ShellTransitions() {
-            @Override
-            public void registerRemote(@androidx.annotation.NonNull TransitionFilter filter,
-                    @androidx.annotation.NonNull RemoteTransition remoteTransition) {
-                // Do nothing
-            }
-
-            @Override
-            public void unregisterRemote(
-                    @androidx.annotation.NonNull RemoteTransition remoteTransition) {
-                // Do nothing
-            }
-        };
-    }
-
     /** Register this transition handler with Core */
     public void register(ShellTaskOrganizer taskOrganizer) {
         if (mPlayerImpl == null) return;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java
index 10fd7d7..3a14a33 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java
@@ -24,7 +24,7 @@
 import android.content.res.Configuration;
 import android.testing.AndroidTestingRunner;
 import android.view.LayoutInflater;
-import android.widget.Button;
+import android.widget.LinearLayout;
 
 import androidx.test.filters.SmallTest;
 
@@ -77,8 +77,8 @@
     public void testOnClick() {
         doNothing().when(mLayout).dismissHint();
 
-        final Button button = mHint.findViewById(R.id.got_it);
-        button.performClick();
+        final LinearLayout hintPopup = mHint.findViewById(R.id.size_compat_hint_popup);
+        hintPopup.performClick();
 
         verify(mLayout).dismissHint();
     }
diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
index f4faa62..86e2661 100644
--- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
+++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
@@ -329,7 +329,8 @@
                 android:layout_height="wrap_content"
                 android:paddingBottom="4dp"
                 android:clickable="false"
-                android:focusable="false">
+                android:focusable="false"
+                android:visibility="gone">
 
                 <LinearLayout
                     android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 9bdd572..3f855c7 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -419,6 +419,9 @@
 
     <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog">
         <item name="android:buttonCornerRadius">28dp</item>
+        <item name="android:buttonBarPositiveButtonStyle">@style/Widget.QSDialog.Button</item>
+        <item name="android:buttonBarNegativeButtonStyle">@style/Widget.QSDialog.Button.BorderButton</item>
+        <item name="android:buttonBarNeutralButtonStyle">@style/Widget.QSDialog.Button.BorderButton</item>
     </style>
 
     <style name="Theme.SystemUI.Dialog.Alert" parent="@*android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
@@ -980,12 +983,15 @@
     <style name="InternetDialog.Network">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">88dp</item>
+        <item name="android:layout_marginStart">@dimen/internet_dialog_network_layout_margin</item>
         <item name="android:layout_marginEnd">@dimen/internet_dialog_network_layout_margin</item>
+        <item name="android:layout_gravity">center_vertical|start</item>
         <item name="android:paddingStart">22dp</item>
         <item name="android:paddingEnd">22dp</item>
         <item name="android:orientation">horizontal</item>
         <item name="android:focusable">true</item>
         <item name="android:clickable">true</item>
+        <item name="android:background">?android:attr/selectableItemBackground</item>
     </style>
 
     <style name="InternetDialog.NetworkTitle">
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index db729da..fcf1b2c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -20,11 +20,14 @@
 import android.view.View;
 import android.view.ViewRootImpl;
 
+import androidx.annotation.Nullable;
+
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationPanelViewController;
 import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 
 /**
  *  Interface to control Keyguard View. It should be implemented by KeyguardViewManagers, which
@@ -184,14 +187,10 @@
 
     /**
      * Registers the StatusBar to which this Keyguard View is mounted.
-     * @param statusBar
-     * @param notificationPanelViewController
-     * @param biometricUnlockController
-     * @param notificationContainer
-     * @param bypassController
      */
     void registerStatusBar(StatusBar statusBar,
             NotificationPanelViewController notificationPanelViewController,
+            @Nullable PanelExpansionStateManager panelExpansionStateManager,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer,
             KeyguardBypassController bypassController);
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
index c64f416..f4f99b7 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
@@ -30,6 +30,7 @@
 import com.android.systemui.dagger.WMComponent;
 import com.android.systemui.navigationbar.gestural.BackGestureTfClassifierProvider;
 import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider;
+import com.android.wm.shell.transition.ShellTransitions;
 import com.android.wm.shell.transition.Transitions;
 
 import java.util.Optional;
@@ -133,7 +134,7 @@
                     .setShellCommandHandler(Optional.ofNullable(null))
                     .setAppPairs(Optional.ofNullable(null))
                     .setTaskViewFactory(Optional.ofNullable(null))
-                    .setTransitions(Transitions.createEmptyForTesting())
+                    .setTransitions(new ShellTransitions() {})
                     .setDisplayAreaHelper(Optional.ofNullable(null))
                     .setStartingSurface(Optional.ofNullable(null))
                     .setTaskSurfaceHelper(Optional.ofNullable(null));
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
index 59d9aff..d2703f5 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
@@ -22,6 +22,7 @@
 import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.systemBars;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
 
 import static java.util.Objects.requireNonNull;
 
@@ -659,6 +660,7 @@
                         | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
                 PixelFormat.TRANSLUCENT);
         params.receiveInsetsIgnoringZOrder = true;
+        params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
         params.windowAnimations = android.R.style.Animation_Translucent;
         params.gravity = Gravity.START | Gravity.TOP;
         params.x = (mAlignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 19ee50a..f438181 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -82,6 +82,8 @@
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.jank.InteractionJankMonitor.Configuration;
 import com.android.internal.policy.IKeyguardDismissCallback;
@@ -117,6 +119,7 @@
 import com.android.systemui.statusbar.phone.NotificationPanelViewController;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
+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.unfold.UnfoldLightRevealOverlayAnimation;
@@ -2654,10 +2657,16 @@
      */
     public KeyguardViewController registerStatusBar(StatusBar statusBar,
             NotificationPanelViewController panelView,
+            @Nullable PanelExpansionStateManager panelExpansionStateManager,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer, KeyguardBypassController bypassController) {
-        mKeyguardViewControllerLazy.get().registerStatusBar(statusBar, panelView,
-                biometricUnlockController, notificationContainer, bypassController);
+        mKeyguardViewControllerLazy.get().registerStatusBar(
+                statusBar,
+                panelView,
+                panelExpansionStateManager,
+                biometricUnlockController,
+                notificationContainer,
+                bypassController);
         return mKeyguardViewControllerLazy.get();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt b/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt
index 6de8370..48546009 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt
@@ -34,7 +34,11 @@
      * Change the height of all tiles and repositions their siblings.
      */
     private fun updateSquishiness() {
-        // Start by updating the height of all tiles
+        // Update tile positions in the layout
+        val tileLayout = quickQSPanelController.tileLayout as TileLayout
+        tileLayout.setSquishinessFraction(squishiness)
+
+        // Adjust their heights as well
         for (tile in qsTileHost.tiles) {
             val tileView = quickQSPanelController.getTileView(tile)
             (tileView as? HeightOverrideable)?.let {
@@ -42,10 +46,6 @@
             }
         }
 
-        // Update tile positions in the layout
-        val tileLayout = quickQSPanelController.tileLayout as TileLayout
-        tileLayout.setSquishinessFraction(squishiness)
-
         // Calculate how much we should move the footer
         val tileHeightOffset = tileLayout.height - tileLayout.tilesHeight
         val footerTopMargin = (qqsFooterActionsView.layoutParams as ViewGroup.MarginLayoutParams)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
index ee5d5ff..58c0508 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
@@ -212,7 +212,7 @@
         return mMaxCellHeight;
     }
 
-    private void layoutTileRecords(int numRecords) {
+    private void layoutTileRecords(int numRecords, boolean forLayout) {
         final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
         int row = 0;
         int column = 0;
@@ -232,14 +232,18 @@
             final int left = getColumnStart(isRtl ? mColumns - column - 1 : column);
             final int right = left + mCellWidth;
             final int bottom = top + record.tileView.getMeasuredHeight();
-            record.tileView.layout(left, top, right, bottom);
+            if (forLayout) {
+                record.tileView.layout(left, top, right, bottom);
+            } else {
+                record.tileView.setLeftTopRightBottom(left, top, right, bottom);
+            }
             mLastTileBottom = bottom;
         }
     }
 
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        layoutTileRecords(mRecords.size());
+        layoutTileRecords(mRecords.size(), true /* forLayout */);
     }
 
     protected int getRowTop(int row) {
@@ -280,6 +284,6 @@
             return;
         }
         mSquishinessFraction = squishinessFraction;
-        layoutTileRecords(mRecords.size());
+        layoutTileRecords(mRecords.size(), false /* forLayout */);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
index 1dab263..f6dbb0b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
@@ -19,6 +19,7 @@
 
 import android.app.AlertDialog;
 import android.content.Context;
+import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -122,6 +123,7 @@
     private Switch mWiFiToggle;
     private FrameLayout mDoneLayout;
     private Drawable mBackgroundOn;
+    private Drawable mBackgroundOff = null;
     private int mDefaultDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     private boolean mCanConfigMobileData;
 
@@ -209,6 +211,14 @@
         mInternetDialogTitle.setText(getDialogTitleText());
         mInternetDialogTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
 
+        TypedArray typedArray = mContext.obtainStyledAttributes(
+                new int[]{android.R.attr.selectableItemBackground});
+        try {
+            mBackgroundOff = typedArray.getDrawable(0 /* index */);
+        } finally {
+            typedArray.recycle();
+        }
+
         setOnClickListener();
         mTurnWifiOnLayout.setBackground(null);
         mWifiRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
@@ -364,7 +374,8 @@
             mMobileSummaryText.setTextAppearance(isCarrierNetworkConnected
                     ? R.style.TextAppearance_InternetDialog_Secondary_Active
                     : R.style.TextAppearance_InternetDialog_Secondary);
-            mMobileNetworkLayout.setBackground(isCarrierNetworkConnected ? mBackgroundOn : null);
+            mMobileNetworkLayout.setBackground(
+                    isCarrierNetworkConnected ? mBackgroundOn : mBackgroundOff);
 
             mMobileDataToggle.setVisibility(mCanConfigMobileData ? View.VISIBLE : View.INVISIBLE);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 18a3d86..1ce7f03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -15,21 +15,16 @@
  */
 package com.android.systemui.statusbar;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.RemoteInput;
-import android.app.RemoteInputHistoryItem;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.UserInfo;
-import android.net.Uri;
 import android.os.Handler;
-import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
@@ -48,6 +43,9 @@
 import android.widget.RemoteViews.InteractionHandler;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.NotificationVisibility;
@@ -55,6 +53,7 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.dagger.StatusBarDependenciesModule;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
@@ -70,12 +69,10 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
-import java.util.stream.Stream;
 
 import dagger.Lazy;
 
@@ -93,27 +90,7 @@
     private static final boolean DEBUG = false;
     private static final String TAG = "NotifRemoteInputManager";
 
-    /**
-     * How long to wait before auto-dismissing a notification that was kept for remote input, and
-     * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
-     * these given that they technically don't exist anymore. We wait a bit in case the app issues
-     * an update.
-     */
-    private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
-
-    /**
-     * Notifications that are already removed but are kept around because we want to show the
-     * remote input history. See {@link RemoteInputHistoryExtender} and
-     * {@link SmartReplyHistoryExtender}.
-     */
-    protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
-
-    /**
-     * Notifications that are already removed but are kept around because the remote input is
-     * actively being used (i.e. user is typing in it).  See {@link RemoteInputActiveExtender}.
-     */
-    protected final ArraySet<NotificationEntry> mEntriesKeptForRemoteInputActive =
-            new ArraySet<>();
+    private RemoteInputListener mRemoteInputListener;
 
     // Dependencies:
     private final NotificationLockscreenUserManager mLockscreenUserManager;
@@ -125,18 +102,17 @@
     private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
 
     protected final Context mContext;
+    protected final FeatureFlags mFeatureFlags;
     private final UserManager mUserManager;
     private final KeyguardManager mKeyguardManager;
+    private final RemoteInputNotificationRebuilder mRebuilder;
     private final StatusBarStateController mStatusBarStateController;
     private final RemoteInputUriController mRemoteInputUriController;
     private final NotificationClickNotifier mClickNotifier;
 
     protected RemoteInputController mRemoteInputController;
-    protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
-            mNotificationLifetimeFinishedCallback;
     protected IStatusBarService mBarService;
     protected Callback mCallback;
-    protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
 
     private final List<RemoteInputController.Callback> mControllerCallbacks = new ArrayList<>();
 
@@ -226,6 +202,7 @@
                 ViewGroup actionGroup = (ViewGroup) parent;
                 buttonIndex = actionGroup.indexOfChild(view);
             }
+            // TODO(b/204183781): get this from the current pipeline
             final int count = mEntryManager.getActiveNotificationsCount();
             final int rank = entry.getRanking().getRank();
 
@@ -283,9 +260,11 @@
      */
     public NotificationRemoteInputManager(
             Context context,
+            FeatureFlags featureFlags,
             NotificationLockscreenUserManager lockscreenUserManager,
             SmartReplyController smartReplyController,
             NotificationEntryManager notificationEntryManager,
+            RemoteInputNotificationRebuilder rebuilder,
             Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             StatusBarStateController statusBarStateController,
             @Main Handler mainHandler,
@@ -294,6 +273,7 @@
             ActionClickLogger logger,
             DumpManager dumpManager) {
         mContext = context;
+        mFeatureFlags = featureFlags;
         mLockscreenUserManager = lockscreenUserManager;
         mSmartReplyController = smartReplyController;
         mEntryManager = notificationEntryManager;
@@ -303,7 +283,11 @@
         mBarService = IStatusBarService.Stub.asInterface(
                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
-        addLifetimeExtenders();
+        mRebuilder = rebuilder;
+        if (!featureFlags.isNewNotifPipelineRenderingEnabled()) {
+            mRemoteInputListener = createLegacyRemoteInputLifetimeExtender(mainHandler,
+                    notificationEntryManager, smartReplyController);
+        }
         mKeyguardManager = context.getSystemService(KeyguardManager.class);
         mStatusBarStateController = statusBarStateController;
         mRemoteInputUriController = remoteInputUriController;
@@ -335,10 +319,35 @@
         });
     }
 
+    /** Add a listener for various remote input events.  Works with NEW pipeline only. */
+    public void setRemoteInputListener(@NonNull RemoteInputListener remoteInputListener) {
+        if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
+            if (mRemoteInputListener != null) {
+                throw new IllegalStateException("mRemoteInputListener is already set");
+            }
+            mRemoteInputListener = remoteInputListener;
+            if (mRemoteInputController != null) {
+                mRemoteInputListener.setRemoteInputController(mRemoteInputController);
+            }
+        }
+    }
+
+    @NonNull
+    @VisibleForTesting
+    protected LegacyRemoteInputLifetimeExtender createLegacyRemoteInputLifetimeExtender(
+            Handler mainHandler,
+            NotificationEntryManager notificationEntryManager,
+            SmartReplyController smartReplyController) {
+        return new LegacyRemoteInputLifetimeExtender();
+    }
+
     /** Initializes this component with the provided dependencies. */
     public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
         mCallback = callback;
         mRemoteInputController = new RemoteInputController(delegate, mRemoteInputUriController);
+        if (mRemoteInputListener != null) {
+            mRemoteInputListener.setRemoteInputController(mRemoteInputController);
+        }
         // Register all stored callbacks from before the Controller was initialized.
         for (RemoteInputController.Callback cb : mControllerCallbacks) {
             mRemoteInputController.addCallback(cb);
@@ -347,19 +356,8 @@
         mRemoteInputController.addCallback(new RemoteInputController.Callback() {
             @Override
             public void onRemoteInputSent(NotificationEntry entry) {
-                if (FORCE_REMOTE_INPUT_HISTORY
-                        && isNotificationKeptForRemoteInputHistory(entry.getKey())) {
-                    mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
-                } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
-                    // We're currently holding onto this notification, but from the apps point of
-                    // view it is already canceled, so we'll need to cancel it on the apps behalf
-                    // after sending - unless the app posts an update in the mean time, so wait a
-                    // bit.
-                    mMainHandler.postDelayed(() -> {
-                        if (mEntriesKeptForRemoteInputActive.remove(entry)) {
-                            mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
-                        }
-                    }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+                if (mRemoteInputListener != null) {
+                    mRemoteInputListener.onRemoteInputSent(entry);
                 }
                 try {
                     mBarService.onNotificationDirectReplied(entry.getSbn().getKey());
@@ -381,12 +379,12 @@
                 }
             }
         });
-        mSmartReplyController.setCallback((entry, reply) -> {
-            StatusBarNotification newSbn =
-                    rebuildNotificationWithRemoteInputInserted(entry, reply, true /* showSpinner */,
-                            null /* mimeType */, null /* uri */);
-            mEntryManager.updateNotification(newSbn, null /* ranking */);
-        });
+        if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
+            mSmartReplyController.setCallback((entry, reply) -> {
+                StatusBarNotification newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply);
+                mEntryManager.updateNotification(newSbn, null /* ranking */);
+            });
+        }
     }
 
     public void addControllerCallback(RemoteInputController.Callback callback) {
@@ -574,51 +572,47 @@
         if (v == null) {
             return null;
         }
-        return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
-    }
-
-    /**
-     * Adds all the notification lifetime extenders. Each extender represents a reason for the
-     * NotificationRemoteInputManager to keep a notification lifetime extended.
-     */
-    protected void addLifetimeExtenders() {
-        mLifetimeExtenders.add(new RemoteInputHistoryExtender());
-        mLifetimeExtenders.add(new SmartReplyHistoryExtender());
-        mLifetimeExtenders.add(new RemoteInputActiveExtender());
+        return v.findViewWithTag(RemoteInputView.VIEW_TAG);
     }
 
     public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
-        return mLifetimeExtenders;
+        // OLD pipeline code ONLY; can assume implementation
+        return ((LegacyRemoteInputLifetimeExtender) mRemoteInputListener).mLifetimeExtenders;
     }
 
     @VisibleForTesting
     void onPerformRemoveNotification(NotificationEntry entry, final String key) {
-        if (mKeysKeptForRemoteInputHistory.contains(key)) {
-            mKeysKeptForRemoteInputHistory.remove(key);
-        }
+        // OLD pipeline code ONLY; can assume implementation
+        ((LegacyRemoteInputLifetimeExtender) mRemoteInputListener)
+                .mKeysKeptForRemoteInputHistory.remove(key);
+        cleanUpRemoteInputForUserRemoval(entry);
+    }
+
+    /**
+     * Disable remote input on the entry and remove the remote input view.
+     * This should be called when a user dismisses a notification that won't be lifetime extended.
+     */
+    public void cleanUpRemoteInputForUserRemoval(NotificationEntry entry) {
         if (isRemoteInputActive(entry)) {
             entry.mRemoteEditImeVisible = false;
             mRemoteInputController.removeRemoteInput(entry, null);
         }
     }
 
+    /** Informs the remote input system that the panel has collapsed */
     public void onPanelCollapsed() {
-        for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
-            NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
-            if (mRemoteInputController != null) {
-                mRemoteInputController.removeRemoteInput(entry, null);
-            }
-            if (mNotificationLifetimeFinishedCallback != null) {
-                mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
-            }
+        if (mRemoteInputListener != null) {
+            mRemoteInputListener.onPanelCollapsed();
         }
-        mEntriesKeptForRemoteInputActive.clear();
     }
 
+    /** Returns whether the given notification is lifetime extended because of remote input */
     public boolean isNotificationKeptForRemoteInputHistory(String key) {
-        return mKeysKeptForRemoteInputHistory.contains(key);
+        return mRemoteInputListener != null
+                && mRemoteInputListener.isNotificationKeptForRemoteInputHistory(key);
     }
 
+    /** Returns whether the notification should be lifetime extended for remote input history */
     public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) {
         if (!FORCE_REMOTE_INPUT_HISTORY) {
             return false;
@@ -636,16 +630,12 @@
         if (entry == null) {
             return;
         }
-        final String key = entry.getKey();
-        if (isNotificationKeptForRemoteInputHistory(key)) {
-            mMainHandler.postDelayed(() -> {
-                if (isNotificationKeptForRemoteInputHistory(key)) {
-                    mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
-                }
-            }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+        if (mRemoteInputListener != null) {
+            mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry);
         }
     }
 
+    /** Returns whether the notification should be lifetime extended for smart reply history */
     public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
         if (!FORCE_REMOTE_INPUT_HISTORY) {
             return false;
@@ -661,64 +651,11 @@
         }
     }
 
-    @VisibleForTesting
-    StatusBarNotification rebuildNotificationForCanceledSmartReplies(
-            NotificationEntry entry) {
-        return rebuildNotificationWithRemoteInputInserted(entry, null /* remoteInputTest */,
-                false /* showSpinner */, null /* mimeType */, null /* uri */);
-    }
-
-    @VisibleForTesting
-    StatusBarNotification rebuildNotificationWithRemoteInputInserted(NotificationEntry entry,
-            CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
-        StatusBarNotification sbn = entry.getSbn();
-
-        Notification.Builder b = Notification.Builder
-                .recoverBuilder(mContext, sbn.getNotification().clone());
-        if (remoteInputText != null || uri != null) {
-            RemoteInputHistoryItem newItem = uri != null
-                    ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
-                    : new RemoteInputHistoryItem(remoteInputText);
-            Parcelable[] oldHistoryItems = sbn.getNotification().extras
-                    .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
-            RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
-                    ? Stream.concat(
-                                Stream.of(newItem),
-                                Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
-                            .toArray(RemoteInputHistoryItem[]::new)
-                    : new RemoteInputHistoryItem[] { newItem };
-            b.setRemoteInputHistory(newHistoryItems);
-        }
-        b.setShowRemoteInputSpinner(showSpinner);
-        b.setHideSmartReplies(true);
-
-        Notification newNotification = b.build();
-
-        // Undo any compatibility view inflation
-        newNotification.contentView = sbn.getNotification().contentView;
-        newNotification.bigContentView = sbn.getNotification().bigContentView;
-        newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
-
-        return new StatusBarNotification(
-                sbn.getPackageName(),
-                sbn.getOpPkg(),
-                sbn.getId(),
-                sbn.getTag(),
-                sbn.getUid(),
-                sbn.getInitialPid(),
-                newNotification,
-                sbn.getUser(),
-                sbn.getOverrideGroupKey(),
-                sbn.getPostTime());
-    }
-
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        pw.println("NotificationRemoteInputManager state:");
-        pw.print("  mKeysKeptForRemoteInputHistory: ");
-        pw.println(mKeysKeptForRemoteInputHistory);
-        pw.print("  mEntriesKeptForRemoteInputActive: ");
-        pw.println(mEntriesKeptForRemoteInputActive);
+        if (mRemoteInputListener instanceof Dumpable) {
+            ((Dumpable) mRemoteInputListener).dump(fd, pw, args);
+        }
     }
 
     public void bindRow(ExpandableNotificationRow row) {
@@ -734,11 +671,6 @@
         return mInteractionHandler;
     }
 
-    @VisibleForTesting
-    public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
-        return mEntriesKeptForRemoteInputActive;
-    }
-
     public boolean isRemoteInputActive() {
         return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive();
     }
@@ -758,131 +690,6 @@
     }
 
     /**
-     * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
-     * so we implement multiple NotificationLifetimeExtenders
-     */
-    protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
-        @Override
-        public void setCallback(NotificationSafeToRemoveCallback callback) {
-            if (mNotificationLifetimeFinishedCallback == null) {
-                mNotificationLifetimeFinishedCallback = callback;
-            }
-        }
-    }
-
-    /**
-     * Notification is kept alive as it was cancelled in response to a remote input interaction.
-     * This allows us to show what you replied and allows you to continue typing into it.
-     */
-    protected class RemoteInputHistoryExtender extends RemoteInputExtender {
-        @Override
-        public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
-            return shouldKeepForRemoteInputHistory(entry);
-        }
-
-        @Override
-        public void setShouldManageLifetime(NotificationEntry entry,
-                boolean shouldExtend) {
-            if (shouldExtend) {
-                CharSequence remoteInputText = entry.remoteInputText;
-                if (TextUtils.isEmpty(remoteInputText)) {
-                    remoteInputText = entry.remoteInputTextWhenReset;
-                }
-                String remoteInputMimeType = entry.remoteInputMimeType;
-                Uri remoteInputUri = entry.remoteInputUri;
-                StatusBarNotification newSbn = rebuildNotificationWithRemoteInputInserted(entry,
-                        remoteInputText, false /* showSpinner */, remoteInputMimeType,
-                        remoteInputUri);
-                entry.onRemoteInputInserted();
-
-                if (newSbn == null) {
-                    return;
-                }
-
-                mEntryManager.updateNotification(newSbn, null);
-
-                // Ensure the entry hasn't already been removed. This can happen if there is an
-                // inflation exception while updating the remote history
-                if (entry.isRemoved()) {
-                    return;
-                }
-
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Keeping notification around after sending remote input "
-                            + entry.getKey());
-                }
-
-                mKeysKeptForRemoteInputHistory.add(entry.getKey());
-            } else {
-                mKeysKeptForRemoteInputHistory.remove(entry.getKey());
-            }
-        }
-    }
-
-    /**
-     * Notification is kept alive for smart reply history.  Similar to REMOTE_INPUT_HISTORY but with
-     * {@link SmartReplyController} specific logic
-     */
-    protected class SmartReplyHistoryExtender extends RemoteInputExtender {
-        @Override
-        public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
-            return shouldKeepForSmartReplyHistory(entry);
-        }
-
-        @Override
-        public void setShouldManageLifetime(NotificationEntry entry,
-                boolean shouldExtend) {
-            if (shouldExtend) {
-                StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
-
-                if (newSbn == null) {
-                    return;
-                }
-
-                mEntryManager.updateNotification(newSbn, null);
-
-                if (entry.isRemoved()) {
-                    return;
-                }
-
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Keeping notification around after sending smart reply "
-                            + entry.getKey());
-                }
-
-                mKeysKeptForRemoteInputHistory.add(entry.getKey());
-            } else {
-                mKeysKeptForRemoteInputHistory.remove(entry.getKey());
-                mSmartReplyController.stopSending(entry);
-            }
-        }
-    }
-
-    /**
-     * Notification is kept alive because the user is still using the remote input
-     */
-    protected class RemoteInputActiveExtender extends RemoteInputExtender {
-        @Override
-        public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
-            return isRemoteInputActive(entry);
-        }
-
-        @Override
-        public void setShouldManageLifetime(NotificationEntry entry,
-                boolean shouldExtend) {
-            if (shouldExtend) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Keeping notification around while remote input active "
-                            + entry.getKey());
-                }
-                mEntriesKeptForRemoteInputActive.add(entry);
-            } else {
-                mEntriesKeptForRemoteInputActive.remove(entry);
-            }
-        }
-    }
-
-    /**
      * Callback for various remote input related events, or for providing information that
      * NotificationRemoteInputManager needs to know to decide what to do.
      */
@@ -975,4 +782,256 @@
          */
         boolean showBouncerIfNecessary();
     }
+
+    /** An interface for listening to remote input events that relate to notification lifetime */
+    public interface RemoteInputListener {
+        /** Called when remote input pending intent has been sent */
+        void onRemoteInputSent(@NonNull NotificationEntry entry);
+
+        /** Called when the notification shade becomes fully closed */
+        void onPanelCollapsed();
+
+        /** @return whether lifetime of a notification is being extended by the listener */
+        boolean isNotificationKeptForRemoteInputHistory(@NonNull String key);
+
+        /** Called on user interaction to end lifetime extension for history */
+        void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry);
+
+        /** Called when the RemoteInputController is attached to the manager */
+        void setRemoteInputController(@NonNull RemoteInputController remoteInputController);
+    }
+
+    @VisibleForTesting
+    protected class LegacyRemoteInputLifetimeExtender implements RemoteInputListener, Dumpable {
+
+        /**
+         * How long to wait before auto-dismissing a notification that was kept for remote input,
+         * and has now sent a remote input. We auto-dismiss, because the app may not see a reason to
+         * cancel these given that they technically don't exist anymore. We wait a bit in case the
+         * app issues an update.
+         */
+        private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
+
+        /**
+         * Notifications that are already removed but are kept around because we want to show the
+         * remote input history. See {@link RemoteInputHistoryExtender} and
+         * {@link SmartReplyHistoryExtender}.
+         */
+        protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
+
+        /**
+         * Notifications that are already removed but are kept around because the remote input is
+         * actively being used (i.e. user is typing in it).  See {@link RemoteInputActiveExtender}.
+         */
+        protected final ArraySet<NotificationEntry> mEntriesKeptForRemoteInputActive =
+                new ArraySet<>();
+
+        protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
+                mNotificationLifetimeFinishedCallback;
+
+        protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders =
+                new ArrayList<>();
+        private RemoteInputController mRemoteInputController;
+
+        LegacyRemoteInputLifetimeExtender() {
+            addLifetimeExtenders();
+        }
+
+        /**
+         * Adds all the notification lifetime extenders. Each extender represents a reason for the
+         * NotificationRemoteInputManager to keep a notification lifetime extended.
+         */
+        protected void addLifetimeExtenders() {
+            mLifetimeExtenders.add(new RemoteInputHistoryExtender());
+            mLifetimeExtenders.add(new SmartReplyHistoryExtender());
+            mLifetimeExtenders.add(new RemoteInputActiveExtender());
+        }
+
+        @Override
+        public void setRemoteInputController(@NonNull RemoteInputController remoteInputController) {
+            mRemoteInputController= remoteInputController;
+        }
+
+        @Override
+        public void onRemoteInputSent(@NonNull NotificationEntry entry) {
+            if (FORCE_REMOTE_INPUT_HISTORY
+                    && isNotificationKeptForRemoteInputHistory(entry.getKey())) {
+                mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
+            } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
+                // We're currently holding onto this notification, but from the apps point of
+                // view it is already canceled, so we'll need to cancel it on the apps behalf
+                // after sending - unless the app posts an update in the mean time, so wait a
+                // bit.
+                mMainHandler.postDelayed(() -> {
+                    if (mEntriesKeptForRemoteInputActive.remove(entry)) {
+                        mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
+                    }
+                }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+            }
+        }
+
+        @Override
+        public void onPanelCollapsed() {
+            for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
+                NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
+                if (mRemoteInputController != null) {
+                    mRemoteInputController.removeRemoteInput(entry, null);
+                }
+                if (mNotificationLifetimeFinishedCallback != null) {
+                    mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
+                }
+            }
+            mEntriesKeptForRemoteInputActive.clear();
+        }
+
+        @Override
+        public boolean isNotificationKeptForRemoteInputHistory(@NonNull String key) {
+            return mKeysKeptForRemoteInputHistory.contains(key);
+        }
+
+        @Override
+        public void releaseNotificationIfKeptForRemoteInputHistory(
+                @NonNull NotificationEntry entry) {
+            final String key = entry.getKey();
+            if (isNotificationKeptForRemoteInputHistory(key)) {
+                mMainHandler.postDelayed(() -> {
+                    if (isNotificationKeptForRemoteInputHistory(key)) {
+                        mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
+                    }
+                }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+            }
+        }
+
+        @VisibleForTesting
+        public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
+            return mEntriesKeptForRemoteInputActive;
+        }
+
+        @Override
+        public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
+                @NonNull String[] args) {
+            pw.println("LegacyRemoteInputLifetimeExtender:");
+            pw.print("  mKeysKeptForRemoteInputHistory: ");
+            pw.println(mKeysKeptForRemoteInputHistory);
+            pw.print("  mEntriesKeptForRemoteInputActive: ");
+            pw.println(mEntriesKeptForRemoteInputActive);
+        }
+
+        /**
+         * NotificationRemoteInputManager has multiple reasons to keep notification lifetime
+         * extended so we implement multiple NotificationLifetimeExtenders
+         */
+        protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
+            @Override
+            public void setCallback(NotificationSafeToRemoveCallback callback) {
+                if (mNotificationLifetimeFinishedCallback == null) {
+                    mNotificationLifetimeFinishedCallback = callback;
+                }
+            }
+        }
+
+        /**
+         * Notification is kept alive as it was cancelled in response to a remote input interaction.
+         * This allows us to show what you replied and allows you to continue typing into it.
+         */
+        protected class RemoteInputHistoryExtender extends RemoteInputExtender {
+            @Override
+            public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
+                return shouldKeepForRemoteInputHistory(entry);
+            }
+
+            @Override
+            public void setShouldManageLifetime(NotificationEntry entry,
+                    boolean shouldExtend) {
+                if (shouldExtend) {
+                    StatusBarNotification newSbn = mRebuilder.rebuildForRemoteInputReply(entry);
+                    entry.onRemoteInputInserted();
+
+                    if (newSbn == null) {
+                        return;
+                    }
+
+                    mEntryManager.updateNotification(newSbn, null);
+
+                    // Ensure the entry hasn't already been removed. This can happen if there is an
+                    // inflation exception while updating the remote history
+                    if (entry.isRemoved()) {
+                        return;
+                    }
+
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Keeping notification around after sending remote input "
+                                + entry.getKey());
+                    }
+
+                    mKeysKeptForRemoteInputHistory.add(entry.getKey());
+                } else {
+                    mKeysKeptForRemoteInputHistory.remove(entry.getKey());
+                }
+            }
+        }
+
+        /**
+         * Notification is kept alive for smart reply history.  Similar to REMOTE_INPUT_HISTORY but
+         * with {@link SmartReplyController} specific logic
+         */
+        protected class SmartReplyHistoryExtender extends RemoteInputExtender {
+            @Override
+            public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
+                return shouldKeepForSmartReplyHistory(entry);
+            }
+
+            @Override
+            public void setShouldManageLifetime(NotificationEntry entry,
+                    boolean shouldExtend) {
+                if (shouldExtend) {
+                    StatusBarNotification newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry);
+
+                    if (newSbn == null) {
+                        return;
+                    }
+
+                    mEntryManager.updateNotification(newSbn, null);
+
+                    if (entry.isRemoved()) {
+                        return;
+                    }
+
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Keeping notification around after sending smart reply "
+                                + entry.getKey());
+                    }
+
+                    mKeysKeptForRemoteInputHistory.add(entry.getKey());
+                } else {
+                    mKeysKeptForRemoteInputHistory.remove(entry.getKey());
+                    mSmartReplyController.stopSending(entry);
+                }
+            }
+        }
+
+        /**
+         * Notification is kept alive because the user is still using the remote input
+         */
+        protected class RemoteInputActiveExtender extends RemoteInputExtender {
+            @Override
+            public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
+                return isRemoteInputActive(entry);
+            }
+
+            @Override
+            public void setShouldManageLifetime(NotificationEntry entry,
+                    boolean shouldExtend) {
+                if (shouldExtend) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Keeping notification around while remote input active "
+                                + entry.getKey());
+                    }
+                    mEntriesKeptForRemoteInputActive.add(entry);
+                } else {
+                    mEntriesKeptForRemoteInputActive.remove(entry);
+                }
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index 5648741e..5f2b28b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -38,8 +38,8 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.statusbar.phone.PanelExpansionListener
 import com.android.systemui.statusbar.phone.ScrimController
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.WallpaperController
 import java.io.FileDescriptor
@@ -329,10 +329,10 @@
     /**
      * Update blurs when pulling down the shade
      */
-    override fun onPanelExpansionChanged(rawExpansion: Float, tracking: Boolean) {
+    override fun onPanelExpansionChanged(rawFraction: Float, tracking: Boolean) {
         val timestamp = SystemClock.elapsedRealtimeNanos()
         val expansion = MathUtils.saturate(
-                (rawExpansion - panelPullDownMinFraction) / (1f - panelPullDownMinFraction))
+                (rawFraction - panelPullDownMinFraction) / (1f - panelPullDownMinFraction))
 
         if (shadeExpansion == expansion && prevTracking == tracking) {
             prevTimestamp = timestamp
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
index 83701a0..cde3b0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputController.java
@@ -299,6 +299,9 @@
         default void onRemoteInputSent(NotificationEntry entry) {}
     }
 
+    /**
+     * This is a delegate which implements some view controller pieces of the remote input process
+     */
     public interface Delegate {
         /**
          * Activate remote input if necessary.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
new file mode 100644
index 0000000..90abec1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
@@ -0,0 +1,141 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.RemoteInputHistoryItem;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Parcelable;
+import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+
+/**
+ * A helper class which will augment the notifications using arguments and other information
+ * accessible to the entry in order to provide intermediate remote input states.
+ */
+@SysUISingleton
+public class RemoteInputNotificationRebuilder {
+
+    private final Context mContext;
+
+    @Inject
+    RemoteInputNotificationRebuilder(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * When a smart reply is sent off to the app, we insert the text into the remote input history,
+     * and show a spinner to indicate that the app has yet to respond.
+     */
+    @NonNull
+    public StatusBarNotification rebuildForSendingSmartReply(NotificationEntry entry,
+            CharSequence reply) {
+        return rebuildWithRemoteInputInserted(entry, reply,
+                true /* showSpinner */,
+                null /* mimeType */, null /* uri */);
+    }
+
+    /**
+     * When the app cancels a notification in response to a smart reply, we remove the spinner
+     * and leave the previously-added reply.  This is the lifetime-extended appearance of the
+     * notification.
+     */
+    @NonNull
+    public StatusBarNotification rebuildForCanceledSmartReplies(
+            NotificationEntry entry) {
+        return rebuildWithRemoteInputInserted(entry, null /* remoteInputTest */,
+                false /* showSpinner */, null /* mimeType */, null /* uri */);
+    }
+
+    /**
+     * When the app cancels a notification in response to a remote input reply, we update the
+     * notification with the reply text and/or attachment. This is the lifetime-extended
+     * appearance of the notification.
+     */
+    @NonNull
+    public StatusBarNotification rebuildForRemoteInputReply(NotificationEntry entry) {
+        CharSequence remoteInputText = entry.remoteInputText;
+        if (TextUtils.isEmpty(remoteInputText)) {
+            remoteInputText = entry.remoteInputTextWhenReset;
+        }
+        String remoteInputMimeType = entry.remoteInputMimeType;
+        Uri remoteInputUri = entry.remoteInputUri;
+        StatusBarNotification newSbn = rebuildWithRemoteInputInserted(entry,
+                remoteInputText, false /* showSpinner */, remoteInputMimeType,
+                remoteInputUri);
+        return newSbn;
+    }
+
+    /** Inner method for generating the SBN */
+    @VisibleForTesting
+    @NonNull
+    StatusBarNotification rebuildWithRemoteInputInserted(NotificationEntry entry,
+            CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
+        StatusBarNotification sbn = entry.getSbn();
+
+        Notification.Builder b = Notification.Builder
+                .recoverBuilder(mContext, sbn.getNotification().clone());
+        if (remoteInputText != null || uri != null) {
+            RemoteInputHistoryItem newItem = uri != null
+                    ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
+                    : new RemoteInputHistoryItem(remoteInputText);
+            Parcelable[] oldHistoryItems = sbn.getNotification().extras
+                    .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+            RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
+                    ? Stream.concat(
+                    Stream.of(newItem),
+                    Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
+                    .toArray(RemoteInputHistoryItem[]::new)
+                    : new RemoteInputHistoryItem[] { newItem };
+            b.setRemoteInputHistory(newHistoryItems);
+        }
+        b.setShowRemoteInputSpinner(showSpinner);
+        b.setHideSmartReplies(true);
+
+        Notification newNotification = b.build();
+
+        // Undo any compatibility view inflation
+        newNotification.contentView = sbn.getNotification().contentView;
+        newNotification.bigContentView = sbn.getNotification().bigContentView;
+        newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
+
+        return new StatusBarNotification(
+                sbn.getPackageName(),
+                sbn.getOpPkg(),
+                sbn.getId(),
+                sbn.getTag(),
+                sbn.getUid(),
+                sbn.getInitialPid(),
+                newNotification,
+                sbn.getUser(),
+                sbn.getOverrideGroupKey(),
+                sbn.getPostTime());
+    }
+
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java
index 7fc18b7..e288b15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java
@@ -19,35 +19,44 @@
 import android.os.RemoteException;
 import android.util.ArraySet;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.Dumpable;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.dagger.StatusBarModule;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.util.Set;
 
 /**
  * Handles when smart replies are added to a notification
  * and clicked upon.
  */
-public class SmartReplyController {
+public class SmartReplyController implements Dumpable {
     private final IStatusBarService mBarService;
     private final NotificationEntryManager mEntryManager;
     private final NotificationClickNotifier mClickNotifier;
-    private Set<String> mSendingKeys = new ArraySet<>();
+    private final Set<String> mSendingKeys = new ArraySet<>();
     private Callback mCallback;
 
     /**
      * Injected constructor. See {@link StatusBarModule}.
      */
-    public SmartReplyController(NotificationEntryManager entryManager,
+    public SmartReplyController(
+            DumpManager dumpManager,
+            NotificationEntryManager entryManager,
             IStatusBarService statusBarService,
             NotificationClickNotifier clickNotifier) {
         mBarService = statusBarService;
         mEntryManager = entryManager;
         mClickNotifier = clickNotifier;
+        dumpManager.registerDumpable(this);
     }
 
     public void setCallback(Callback callback) {
@@ -75,6 +84,7 @@
     public void smartActionClicked(
             NotificationEntry entry, int actionIndex, Notification.Action action,
             boolean generatedByAssistant) {
+        // TODO(b/204183781): get this from the current pipeline
         final int count = mEntryManager.getActiveNotificationsCount();
         final int rank = entry.getRanking().getRank();
         NotificationVisibility.NotificationLocation location =
@@ -112,6 +122,14 @@
         }
     }
 
+    @Override
+    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
+        pw.println("mSendingKeys: " + mSendingKeys.size());
+        for (String key : mSendingKeys) {
+            pw.println(" * " + key);
+        }
+    }
+
     /**
      * Callback for any class that needs to do something in response to a smart reply being sent.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index 1c9174a..bb697c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
+import com.android.systemui.statusbar.RemoteInputNotificationRebuilder;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -96,9 +97,11 @@
     @Provides
     static NotificationRemoteInputManager provideNotificationRemoteInputManager(
             Context context,
+            FeatureFlags featureFlags,
             NotificationLockscreenUserManager lockscreenUserManager,
             SmartReplyController smartReplyController,
             NotificationEntryManager notificationEntryManager,
+            RemoteInputNotificationRebuilder rebuilder,
             Lazy<Optional<StatusBar>> statusBarOptionalLazy,
             StatusBarStateController statusBarStateController,
             Handler mainHandler,
@@ -108,9 +111,11 @@
             DumpManager dumpManager) {
         return new NotificationRemoteInputManager(
                 context,
+                featureFlags,
                 lockscreenUserManager,
                 smartReplyController,
                 notificationEntryManager,
+                rebuilder,
                 statusBarOptionalLazy,
                 statusBarStateController,
                 mainHandler,
@@ -166,10 +171,11 @@
     @SysUISingleton
     @Provides
     static SmartReplyController provideSmartReplyController(
+            DumpManager dumpManager,
             NotificationEntryManager entryManager,
             IStatusBarService statusBarService,
             NotificationClickNotifier clickNotifier) {
-        return new SmartReplyController(entryManager, statusBarService, clickNotifier);
+        return new SmartReplyController(dumpManager, entryManager, statusBarService, clickNotifier);
     }
 
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index 60f44a0d..8bc41c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -689,8 +689,9 @@
         for (NotificationEntryListener listener : mNotificationEntryListeners) {
             listener.onPreEntryUpdated(entry);
         }
+        final boolean fromSystem = ranking != null;
         for (NotifCollectionListener listener : mNotifCollectionListeners) {
-            listener.onEntryUpdated(entry);
+            listener.onEntryUpdated(entry, fromSystem);
         }
 
         if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
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 a2c9ffc..3edeec6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -27,8 +27,8 @@
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.phone.PanelExpansionListener
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
+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
@@ -294,8 +294,8 @@
         this.state = newState
     }
 
-    override fun onPanelExpansionChanged(expansion: Float, tracking: Boolean) {
-        val collapsedEnough = expansion <= 0.9f
+    override fun onPanelExpansionChanged(fraction: Float, tracking: Boolean) {
+        val collapsedEnough = fraction <= 0.9f
         if (collapsedEnough != this.collapsedEnoughToHide) {
             val couldShowPulsingHuns = canShowPulsingHuns
             this.collapsedEnoughToHide = collapsedEnough
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index b36b7c9..f36f430 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -47,6 +47,7 @@
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.Notification;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -62,6 +63,7 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.dump.LogBufferEulogizer;
 import com.android.systemui.flags.FeatureFlags;
@@ -76,6 +78,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryRemovedEvent;
 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryUpdatedEvent;
 import com.android.systemui.statusbar.notification.collection.notifcollection.InitEntryEvent;
+import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
@@ -131,6 +134,7 @@
     private final SystemClock mClock;
     private final FeatureFlags mFeatureFlags;
     private final NotifCollectionLogger mLogger;
+    private final Handler mMainHandler;
     private final LogBufferEulogizer mEulogizer;
 
     private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
@@ -154,6 +158,7 @@
             SystemClock clock,
             FeatureFlags featureFlags,
             NotifCollectionLogger logger,
+            @Main Handler mainHandler,
             LogBufferEulogizer logBufferEulogizer,
             DumpManager dumpManager) {
         Assert.isMainThread();
@@ -161,6 +166,7 @@
         mClock = clock;
         mFeatureFlags = featureFlags;
         mLogger = logger;
+        mMainHandler = mainHandler;
         mEulogizer = logBufferEulogizer;
 
         dumpManager.registerDumpable(TAG, this);
@@ -442,7 +448,7 @@
             mEventQueue.add(new BindEntryEvent(entry, sbn));
 
             mLogger.logNotifUpdated(sbn.getKey());
-            mEventQueue.add(new EntryUpdatedEvent(entry));
+            mEventQueue.add(new EntryUpdatedEvent(entry, true /* fromSystem */));
         }
     }
 
@@ -791,6 +797,51 @@
 
     private static final String TAG = "NotifCollection";
 
+    /**
+     * Get an object which can be used to update a notification (internally to the pipeline)
+     * in response to a user action.
+     *
+     * @param name the name of the component that will update notifiations
+     * @return an updater
+     */
+    public InternalNotifUpdater getInternalNotifUpdater(String name) {
+        return (sbn, reason) -> mMainHandler.post(
+                () -> updateNotificationInternally(sbn, name, reason));
+    }
+
+    /**
+     * Provide an updated StatusBarNotification for an existing entry.  If no entry exists for the
+     * given notification key, this method does nothing.
+     *
+     * @param sbn the updated notification
+     * @param name the component which is updating the notification
+     * @param reason the reason the notification is being updated
+     */
+    private void updateNotificationInternally(StatusBarNotification sbn, String name,
+            String reason) {
+        Assert.isMainThread();
+        checkForReentrantCall();
+
+        // Make sure we have the notification to update
+        NotificationEntry entry = mNotificationSet.get(sbn.getKey());
+        if (entry == null) {
+            mLogger.logNotifInternalUpdateFailed(sbn.getKey(), name, reason);
+            return;
+        }
+        mLogger.logNotifInternalUpdate(sbn.getKey(), name, reason);
+
+        // First do the pieces of postNotification which are not about assuming the notification
+        // was sent by the app
+        entry.setSbn(sbn);
+        mEventQueue.add(new BindEntryEvent(entry, sbn));
+
+        mLogger.logNotifUpdated(sbn.getKey());
+        mEventQueue.add(new EntryUpdatedEvent(entry, false /* fromSystem */));
+
+        // Skip the applyRanking step and go straight to dispatching the events
+        dispatchEventsAndRebuildList();
+    }
+
     @IntDef(prefix = { "REASON_" }, value = {
             REASON_NOT_CANCELED,
             REASON_UNKNOWN,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java
index 5777925..27ba4c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.collection;
 
+import android.os.Handler;
+
 import androidx.annotation.Nullable;
 
 import com.android.systemui.dagger.SysUISingleton;
@@ -30,6 +32,7 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
+import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
@@ -223,6 +226,17 @@
     }
 
     /**
+     * Get an object which can be used to update a notification (internally to the pipeline)
+     * in response to a user action.
+     *
+     * @param name the name of the component that will update notifiations
+     * @return an updater
+     */
+    public InternalNotifUpdater getInternalNotifUpdater(String name) {
+        return mNotifCollection.getInternalNotifUpdater(name);
+    }
+
+    /**
      * Returns a read-only view in to the current shade list, i.e. the list of notifications that
      * are currently present in the shade. If this method is called during pipeline execution it
      * will return the current state of the list, which will likely be only partially-generated.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 66290bb..39b1ec4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -48,6 +48,7 @@
     conversationCoordinator: ConversationCoordinator,
     preparationCoordinator: PreparationCoordinator,
     mediaCoordinator: MediaCoordinator,
+    remoteInputCoordinator: RemoteInputCoordinator,
     shadeEventCoordinator: ShadeEventCoordinator,
     smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator,
     viewConfigCoordinator: ViewConfigCoordinator,
@@ -72,6 +73,7 @@
         mCoordinators.add(bubbleCoordinator)
         mCoordinators.add(conversationCoordinator)
         mCoordinators.add(mediaCoordinator)
+        mCoordinators.add(remoteInputCoordinator)
         mCoordinators.add(shadeEventCoordinator)
         mCoordinators.add(viewConfigCoordinator)
         mCoordinators.add(visualStabilityCoordinator)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
new file mode 100644
index 0000000..3397815
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -0,0 +1,225 @@
+/*
+ * 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.notification.collection.coordinator
+
+import android.os.Handler
+import android.service.notification.NotificationListenerService.REASON_CANCEL
+import android.service.notification.NotificationListenerService.REASON_CLICK
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputListener
+import com.android.systemui.statusbar.RemoteInputController
+import com.android.systemui.statusbar.RemoteInputNotificationRebuilder
+import com.android.systemui.statusbar.SmartReplyController
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.notifcollection.SelfTrackingLifetimeExtender
+import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import javax.inject.Inject
+
+private const val TAG = "RemoteInputCoordinator"
+
+/**
+ * How long to wait before auto-dismissing a notification that was kept for active remote input, and
+ * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel
+ * these given that they technically don't exist anymore. We wait a bit in case the app issues
+ * an update, and to also give the other lifetime extenders a beat to decide they want it.
+ */
+private const val REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY: Long = 500
+
+/**
+ * How long to wait before releasing a lifetime extension when requested to do so due to a user
+ * interaction (such as tapping another action).
+ * We wait a bit in case the app issues an update in response to the action, but not too long or we
+ * risk appearing unresponsive to the user.
+ */
+private const val REMOTE_INPUT_EXTENDER_RELEASE_DELAY: Long = 200
+
+/** Whether this class should print spammy debug logs */
+private val DEBUG: Boolean by lazy { Log.isLoggable(TAG, Log.DEBUG) }
+
+@SysUISingleton
+class RemoteInputCoordinator @Inject constructor(
+    dumpManager: DumpManager,
+    private val mRebuilder: RemoteInputNotificationRebuilder,
+    private val mNotificationRemoteInputManager: NotificationRemoteInputManager,
+    @Main private val mMainHandler: Handler,
+    private val mSmartReplyController: SmartReplyController
+) : Coordinator, RemoteInputListener, Dumpable {
+
+    @VisibleForTesting val mRemoteInputHistoryExtender = RemoteInputHistoryExtender()
+    @VisibleForTesting val mSmartReplyHistoryExtender = SmartReplyHistoryExtender()
+    @VisibleForTesting val mRemoteInputActiveExtender = RemoteInputActiveExtender()
+    private val mRemoteInputLifetimeExtenders = listOf(
+            mRemoteInputHistoryExtender,
+            mSmartReplyHistoryExtender,
+            mRemoteInputActiveExtender
+    )
+
+    private lateinit var mNotifUpdater: InternalNotifUpdater
+
+    init {
+        dumpManager.registerDumpable(this)
+    }
+
+    fun getLifetimeExtenders(): List<NotifLifetimeExtender> = mRemoteInputLifetimeExtenders
+
+    override fun attach(pipeline: NotifPipeline) {
+        mNotificationRemoteInputManager.setRemoteInputListener(this)
+        mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) }
+        mNotifUpdater = pipeline.getInternalNotifUpdater(TAG)
+        pipeline.addCollectionListener(mCollectionListener)
+    }
+
+    val mCollectionListener = object : NotifCollectionListener {
+        override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
+            if (DEBUG) {
+                Log.d(TAG, "mCollectionListener.onEntryUpdated(entry=${entry.key}," +
+                        " fromSystem=$fromSystem)")
+            }
+            if (fromSystem) {
+                // Mark smart replies as sent whenever a notification is updated by the app,
+                // otherwise the smart replies are never marked as sent.
+                mSmartReplyController.stopSending(entry)
+            }
+        }
+
+        override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+            if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})")
+            // We're removing the notification, the smart reply controller can forget about it.
+            // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear it.
+            mSmartReplyController.stopSending(entry)
+
+            // When we know the entry will not be lifetime extended, clean up the remote input view
+            // TODO: Share code with NotifCollection.cannotBeLifetimeExtended
+            if (reason == REASON_CANCEL || reason == REASON_CLICK) {
+                mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry)
+            }
+        }
+    }
+
+    override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
+        mRemoteInputLifetimeExtenders.forEach { it.dump(fd, pw, args) }
+    }
+
+    override fun onRemoteInputSent(entry: NotificationEntry) {
+        if (DEBUG) Log.d(TAG, "onRemoteInputSent(entry=${entry.key})")
+        // These calls effectively ensure the freshness of the lifetime extensions.
+        // NOTE: This is some trickery! By removing the lifetime extensions when we know they should
+        // be immediately re-upped, we ensure that the side-effects of the lifetime extenders get to
+        // fire again, thus ensuring that we add subsequent replies to the notification.
+        mRemoteInputHistoryExtender.endLifetimeExtension(entry.key)
+        mSmartReplyHistoryExtender.endLifetimeExtension(entry.key)
+
+        // If we're extending for remote input being active, then from the apps point of
+        // view it is already canceled, so we'll need to cancel it on the apps behalf
+        // now that a reply has been sent. However, delay so that the app has time to posts an
+        // update in the mean time, and to give another lifetime extender time to pick it up.
+        mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
+                REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY)
+    }
+
+    private fun onSmartReplySent(entry: NotificationEntry, reply: CharSequence) {
+        if (DEBUG) Log.d(TAG, "onSmartReplySent(entry=${entry.key})")
+        val newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply)
+        mNotifUpdater.onInternalNotificationUpdate(newSbn,
+                "Adding smart reply spinner for sent")
+
+        // If we're extending for remote input being active, then from the apps point of
+        // view it is already canceled, so we'll need to cancel it on the apps behalf
+        // now that a reply has been sent. However, delay so that the app has time to posts an
+        // update in the mean time, and to give another lifetime extender time to pick it up.
+        mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
+                REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY)
+    }
+
+    override fun onPanelCollapsed() {
+        mRemoteInputActiveExtender.endAllLifetimeExtensions()
+    }
+
+    override fun isNotificationKeptForRemoteInputHistory(key: String) =
+            mRemoteInputHistoryExtender.isExtending(key) ||
+                    mSmartReplyHistoryExtender.isExtending(key)
+
+    override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
+        if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
+        mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
+                REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+        mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
+                REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+        mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
+                REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+    }
+
+    override fun setRemoteInputController(remoteInputController: RemoteInputController) {
+        mSmartReplyController.setCallback(this::onSmartReplySent)
+    }
+
+    @VisibleForTesting
+    inner class RemoteInputHistoryExtender :
+            SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) {
+
+        override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
+                mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry)
+
+        override fun onStartedLifetimeExtension(entry: NotificationEntry) {
+            val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
+            entry.onRemoteInputInserted()
+            mNotifUpdater.onInternalNotificationUpdate(newSbn,
+                    "Extending lifetime of notification with remote input")
+            // TODO: Check if the entry was removed due perhaps to an inflation exception?
+        }
+    }
+
+    @VisibleForTesting
+    inner class SmartReplyHistoryExtender :
+            SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) {
+
+        override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
+                mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry)
+
+        override fun onStartedLifetimeExtension(entry: NotificationEntry) {
+            val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
+            mSmartReplyController.stopSending(entry)
+            mNotifUpdater.onInternalNotificationUpdate(newSbn,
+                    "Extending lifetime of notification with smart reply")
+            // TODO: Check if the entry was removed due perhaps to an inflation exception?
+        }
+
+        override fun onCanceledLifetimeExtension(entry: NotificationEntry) {
+            // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear it.
+            mSmartReplyController.stopSending(entry)
+        }
+    }
+
+    @VisibleForTesting
+    inner class RemoteInputActiveExtender :
+            SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) {
+
+        override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
+                mNotificationRemoteInputManager.isRemoteInputActive(entry)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/InternalNotifUpdater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/InternalNotifUpdater.java
new file mode 100644
index 0000000..5692fb2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/InternalNotifUpdater.java
@@ -0,0 +1,37 @@
+/*
+ * 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.notification.collection.notifcollection;
+
+import android.service.notification.StatusBarNotification;
+
+/**
+ * An object that allows Coordinators to update notifications internally to SystemUI.
+ * This is used when part of the UI involves updating the underlying appearance of a notification
+ * on behalf of an app, such as to add a spinner or remote input history.
+ */
+public interface InternalNotifUpdater {
+    /**
+     * Called when an already-existing notification needs to be updated to a new temporary
+     * appearance.
+     * This update is local to the SystemUI process.
+     * This has no effect if no notification with the given key exists in the pipeline.
+     *
+     * @param sbn a notification to update
+     * @param reason a debug reason for the update
+     */
+    void onInternalNotificationUpdate(StatusBarNotification sbn, String reason);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java
index db0c174..68a346f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java
@@ -56,6 +56,17 @@
     /**
      * Called whenever a notification with the same key as an existing notification is posted. By
      * the time this listener is called, the entry's SBN and Ranking will already have been updated.
+     * This delegates to {@link #onEntryUpdated(NotificationEntry)} by default.
+     * @param fromSystem If true, this update came from the NotificationManagerService.
+     *                   If false, the notification update is an internal change within systemui.
+     */
+    default void onEntryUpdated(@NonNull NotificationEntry entry, boolean fromSystem) {
+        onEntryUpdated(entry);
+    }
+
+    /**
+     * Called whenever a notification with the same key as an existing notification is posted. By
+     * the time this listener is called, the entry's SBN and Ranking will already have been updated.
      */
     default void onEntryUpdated(@NonNull NotificationEntry entry) {
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index f8a778d..1ebc66e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -121,6 +121,26 @@
         })
     }
 
+    fun logNotifInternalUpdate(key: String, name: String, reason: String) {
+        buffer.log(TAG, INFO, {
+            str1 = key
+            str2 = name
+            str3 = reason
+        }, {
+            "UPDATED INTERNALLY $str1 BY $str2 BECAUSE $str3"
+        })
+    }
+
+    fun logNotifInternalUpdateFailed(key: String, name: String, reason: String) {
+        buffer.log(TAG, INFO, {
+            str1 = key
+            str2 = name
+            str3 = reason
+        }, {
+            "FAILED INTERNAL UPDATE $str1 BY $str2 BECAUSE $str3"
+        })
+    }
+
     fun logNoNotificationToRemoveWithKey(key: String) {
         buffer.log(TAG, ERROR, {
             str1 = key
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt
index 2810b89..179e953 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt
@@ -64,10 +64,11 @@
 }
 
 data class EntryUpdatedEvent(
-    val entry: NotificationEntry
+    val entry: NotificationEntry,
+    val fromSystem: Boolean
 ) : NotifEvent() {
     override fun dispatchToListener(listener: NotifCollectionListener) {
-        listener.onEntryUpdated(entry)
+        listener.onEntryUpdated(entry, fromSystem)
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt
new file mode 100644
index 0000000..145c1e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt
@@ -0,0 +1,113 @@
+package com.android.systemui.statusbar.notification.collection.notifcollection
+
+import android.os.Handler
+import android.util.ArrayMap
+import android.util.Log
+import com.android.systemui.Dumpable
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import java.io.FileDescriptor
+import java.io.PrintWriter
+
+/**
+ * A helpful class that implements the core contract of the lifetime extender internally,
+ * making it easier for coordinators to interact with them
+ */
+abstract class SelfTrackingLifetimeExtender(
+    private val tag: String,
+    private val name: String,
+    private val debug: Boolean,
+    private val mainHandler: Handler
+) : NotifLifetimeExtender, Dumpable {
+    private lateinit var mCallback: NotifLifetimeExtender.OnEndLifetimeExtensionCallback
+    protected val mEntriesExtended = ArrayMap<String, NotificationEntry>()
+    private var mEnding = false
+
+    /**
+     * When debugging, warn if the call is happening during and "end lifetime extension" call.
+     *
+     * Note: this will warn a lot! The pipeline explicitly re-invokes all lifetime extenders
+     * whenever one ends, giving all of them a chance to re-up their lifetime extension.
+     */
+    private fun warnIfEnding() {
+        if (debug && mEnding) Log.w(tag, "reentrant code while ending a lifetime extension")
+    }
+
+    fun endAllLifetimeExtensions() {
+        // clear the map before iterating over a copy of the items, because the pipeline will
+        // always give us another chance to extend the lifetime again, and we don't want
+        // concurrent modification
+        val entries = mEntriesExtended.values.toList()
+        if (debug) Log.d(tag, "$name.endAllLifetimeExtensions() entries=$entries")
+        mEntriesExtended.clear()
+        warnIfEnding()
+        mEnding = true
+        entries.forEach { mCallback.onEndLifetimeExtension(this, it) }
+        mEnding = false
+    }
+
+    fun endLifetimeExtensionAfterDelay(key: String, delayMillis: Long) {
+        if (debug) {
+            Log.d(tag, "$name.endLifetimeExtensionAfterDelay" +
+                    "(key=$key, delayMillis=$delayMillis)" +
+                    " isExtending=${isExtending(key)}")
+        }
+        if (isExtending(key)) {
+            mainHandler.postDelayed({ endLifetimeExtension(key) }, delayMillis)
+        }
+    }
+
+    fun endLifetimeExtension(key: String) {
+        if (debug) {
+            Log.d(tag, "$name.endLifetimeExtension(key=$key)" +
+                    " isExtending=${isExtending(key)}")
+        }
+        warnIfEnding()
+        mEnding = true
+        mEntriesExtended.remove(key)?.let { removedEntry ->
+            mCallback.onEndLifetimeExtension(this, removedEntry)
+        }
+        mEnding = false
+    }
+
+    fun isExtending(key: String) = mEntriesExtended.contains(key)
+
+    final override fun getName(): String = name
+
+    final override fun shouldExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
+        val shouldExtend = queryShouldExtendLifetime(entry)
+        if (debug) {
+            Log.d(tag, "$name.shouldExtendLifetime(key=${entry.key}, reason=$reason)" +
+                    " isExtending=${isExtending(entry.key)}" +
+                    " shouldExtend=$shouldExtend")
+        }
+        warnIfEnding()
+        if (shouldExtend && mEntriesExtended.put(entry.key, entry) == null) {
+            onStartedLifetimeExtension(entry)
+        }
+        return shouldExtend
+    }
+
+    final override fun cancelLifetimeExtension(entry: NotificationEntry) {
+        if (debug) {
+            Log.d(tag, "$name.cancelLifetimeExtension(key=${entry.key})" +
+                    " isExtending=${isExtending(entry.key)}")
+        }
+        warnIfEnding()
+        mEntriesExtended.remove(entry.key)
+        onCanceledLifetimeExtension(entry)
+    }
+
+    abstract fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean
+    open fun onStartedLifetimeExtension(entry: NotificationEntry) {}
+    open fun onCanceledLifetimeExtension(entry: NotificationEntry) {}
+
+    final override fun setCallback(callback: NotifLifetimeExtender.OnEndLifetimeExtensionCallback) {
+        mCallback = callback
+    }
+
+    final override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
+        pw.println("LifetimeExtender: $name:")
+        pw.println("  mEntriesExtended: ${mEntriesExtended.size}")
+        mEntriesExtended.forEach { pw.println("  * ${it.key}") }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 3fe393d..19e8025 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -169,6 +169,7 @@
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
+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;
@@ -686,6 +687,7 @@
             SplitShadeHeaderController splitShadeHeaderController,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             LockscreenGestureLogger lockscreenGestureLogger,
+            PanelExpansionStateManager panelExpansionStateManager,
             NotificationRemoteInputManager remoteInputManager,
             ControlsComponent controlsComponent) {
         super(view,
@@ -699,6 +701,7 @@
                 flingAnimationUtilsBuilder.get(),
                 statusBarTouchableRegionManager,
                 lockscreenGestureLogger,
+                panelExpansionStateManager,
                 ambientState);
         mView = view;
         mVibratorHelper = vibratorHelper;
@@ -2205,10 +2208,6 @@
             mStatusBar.executeRunnableDismissingKeyguard(null, null /* cancelAction */,
                     false /* dismissShade */, true /* afterKeyguardGone */, false /* deferred */);
         }
-        for (int i = 0; i < mExpansionListeners.size(); i++) {
-            mExpansionListeners.get(i).onQsExpansionChanged(
-                    mQsMaxExpansionHeight != 0 ? mQsExpansionHeight / mQsMaxExpansionHeight : 0);
-        }
         if (DEBUG) {
             mView.invalidate();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index 36bd31b..8de0412 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -54,6 +54,7 @@
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.tuner.TunerService;
 
@@ -105,6 +106,7 @@
     private boolean mExpandingBelowNotch;
     private final DockManager mDockManager;
     private final NotificationPanelViewController mNotificationPanelViewController;
+    private final PanelExpansionStateManager mPanelExpansionStateManager;
     private final StatusBarWindowView mStatusBarWindowView;
 
     // Used for determining view / touch intersection
@@ -134,6 +136,7 @@
             NotificationShadeDepthController depthController,
             NotificationShadeWindowView notificationShadeWindowView,
             NotificationPanelViewController notificationPanelViewController,
+            PanelExpansionStateManager panelExpansionStateManager,
             StatusBarWindowView statusBarWindowView,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
@@ -157,6 +160,7 @@
         mShadeController = shadeController;
         mDockManager = dockManager;
         mNotificationPanelViewController = notificationPanelViewController;
+        mPanelExpansionStateManager = panelExpansionStateManager;
         mDepthController = depthController;
         mStatusBarWindowView = statusBarWindowView;
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
@@ -442,7 +446,7 @@
         setDragDownHelper(mLockscreenShadeTransitionController.getTouchHelper());
 
         mDepthController.setRoot(mView);
-        mNotificationPanelViewController.addExpansionListener(mDepthController);
+        mPanelExpansionStateManager.addListener(mDepthController);
     }
 
     public NotificationShadeWindowView getView() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
index e5296af..83f63ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
@@ -59,12 +59,12 @@
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.ArrayList;
 
 public abstract class PanelViewController {
     public static final boolean DEBUG = PanelBar.DEBUG;
@@ -86,7 +86,6 @@
     private boolean mVibrateOnOpening;
     protected boolean mIsLaunchAnimationRunning;
     private int mFixedDuration = NO_FIXED_DURATION;
-    protected ArrayList<PanelExpansionListener> mExpansionListeners = new ArrayList<>();
     protected float mOverExpansion;
 
     /**
@@ -185,6 +184,7 @@
     protected final SysuiStatusBarStateController mStatusBarStateController;
     protected final AmbientState mAmbientState;
     protected final LockscreenGestureLogger mLockscreenGestureLogger;
+    private final PanelExpansionStateManager mPanelExpansionStateManager;
     private final TouchHandler mTouchHandler;
 
     protected abstract void onExpandingFinished();
@@ -211,20 +211,25 @@
         return mAmbientState;
     }
 
-    public PanelViewController(PanelView view,
-            FalsingManager falsingManager, DozeLog dozeLog,
+    public PanelViewController(
+            PanelView view,
+            FalsingManager falsingManager,
+            DozeLog dozeLog,
             KeyguardStateController keyguardStateController,
-            SysuiStatusBarStateController statusBarStateController, VibratorHelper vibratorHelper,
+            SysuiStatusBarStateController statusBarStateController,
+            VibratorHelper vibratorHelper,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             LatencyTracker latencyTracker,
             FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
             LockscreenGestureLogger lockscreenGestureLogger,
+            PanelExpansionStateManager panelExpansionStateManager,
             AmbientState ambientState) {
         mAmbientState = ambientState;
         mView = view;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mLockscreenGestureLogger = lockscreenGestureLogger;
+        mPanelExpansionStateManager = panelExpansionStateManager;
         mTouchHandler = createTouchHandler();
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
@@ -1088,9 +1093,7 @@
             mBar.panelExpansionChanged(mExpandedFraction, isExpanded());
         }
         updateVisibility();
-        for (int i = 0; i < mExpansionListeners.size(); i++) {
-            mExpansionListeners.get(i).onPanelExpansionChanged(mExpandedFraction, mTracking);
-        }
+        mPanelExpansionStateManager.onPanelExpansionChanged(mExpandedFraction, mTracking);
     }
 
     public boolean isExpanded() {
@@ -1102,10 +1105,6 @@
                 && !mIsSpringBackAnimation;
     }
 
-    public void addExpansionListener(PanelExpansionListener panelExpansionListener) {
-        mExpansionListeners.add(panelExpansionListener);
-    }
-
     protected abstract boolean isPanelVisibleBecauseOfHeadsUp();
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 711e941..c9dd983 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -219,6 +219,7 @@
 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
 import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneModule;
 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.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -541,6 +542,7 @@
     private final NotificationGutsManager mGutsManager;
     private final NotificationLogger mNotificationLogger;
     private final NotificationViewHierarchyManager mViewHierarchyManager;
+    private final PanelExpansionStateManager mPanelExpansionStateManager;
     private final KeyguardViewMediator mKeyguardViewMediator;
     protected final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
     private final BrightnessSliderController.Factory mBrightnessSliderFactory;
@@ -726,6 +728,7 @@
             NotificationLogger notificationLogger,
             NotificationInterruptStateProvider notificationInterruptStateProvider,
             NotificationViewHierarchyManager notificationViewHierarchyManager,
+            PanelExpansionStateManager panelExpansionStateManager,
             KeyguardViewMediator keyguardViewMediator,
             DisplayMetrics displayMetrics,
             MetricsLogger metricsLogger,
@@ -832,6 +835,7 @@
         mNotificationLogger = notificationLogger;
         mNotificationInterruptStateProvider = notificationInterruptStateProvider;
         mViewHierarchyManager = notificationViewHierarchyManager;
+        mPanelExpansionStateManager = panelExpansionStateManager;
         mKeyguardViewMediator = keyguardViewMediator;
         mDisplayMetrics = displayMetrics;
         mMetricsLogger = metricsLogger;
@@ -1158,8 +1162,8 @@
         mNotificationLogger.setUpWithContainer(notifListContainer);
 
         mNotificationIconAreaController.setupShelf(mNotificationShelfController);
-        mNotificationPanelViewController.addExpansionListener(mWakeUpCoordinator);
-        mNotificationPanelViewController.addExpansionListener(
+        mPanelExpansionStateManager.addListener(mWakeUpCoordinator);
+        mPanelExpansionStateManager.addListener(
                 this::dispatchPanelExpansionForKeyguardDismiss);
 
         mUserSwitcherController.init(mNotificationShadeWindowView);
@@ -1669,8 +1673,11 @@
                 });
         mStatusBarKeyguardViewManager.registerStatusBar(
                 /* statusBar= */ this,
-                mNotificationPanelViewController, mBiometricUnlockController,
-                mStackScroller, mKeyguardBypassController);
+                mNotificationPanelViewController,
+                mPanelExpansionStateManager,
+                mBiometricUnlockController,
+                mStackScroller,
+                mKeyguardBypassController);
         mKeyguardIndicationController
                 .setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
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 cac66a3..523cf18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -63,6 +63,8 @@
 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.PanelExpansionListener;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -266,6 +268,7 @@
     @Override
     public void registerStatusBar(StatusBar statusBar,
             NotificationPanelViewController notificationPanelViewController,
+            PanelExpansionStateManager panelExpansionStateManager,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer,
             KeyguardBypassController bypassController) {
@@ -275,7 +278,9 @@
         ViewGroup container = mStatusBar.getBouncerContainer();
         mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
         mNotificationPanelViewController = notificationPanelViewController;
-        notificationPanelViewController.addExpansionListener(this);
+        if (panelExpansionStateManager != null) {
+            panelExpansionStateManager.addListener(this);
+        }
         mBypassController = bypassController;
         mNotificationContainer = notificationContainer;
         mKeyguardMessageAreaController = mKeyguardMessageAreaFactory.create(
@@ -323,7 +328,7 @@
     }
 
     @Override
-    public void onPanelExpansionChanged(float expansion, boolean tracking) {
+    public void onPanelExpansionChanged(float fraction, boolean tracking) {
         // We don't want to translate the bounce when:
         // • Keyguard is occluded, because we're in a FLAG_SHOW_WHEN_LOCKED activity and need to
         //   conserve the original animation.
@@ -336,14 +341,14 @@
             mBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
         } else if (mShowing) {
             if (!isWakeAndUnlocking() && !mStatusBar.isInLaunchTransition()) {
-                mBouncer.setExpansion(expansion);
+                mBouncer.setExpansion(fraction);
             }
-            if (expansion != KeyguardBouncer.EXPANSION_HIDDEN && tracking
+            if (fraction != KeyguardBouncer.EXPANSION_HIDDEN && tracking
                     && !mKeyguardStateController.canDismissLockScreen()
                     && !mBouncer.isShowing() && !mBouncer.isAnimatingAway()) {
                 mBouncer.show(false /* resetSecuritySelection */, false /* scrimmed */);
             }
-        } else if (mPulsing && expansion == KeyguardBouncer.EXPANSION_VISIBLE) {
+        } else if (mPulsing && fraction == KeyguardBouncer.EXPANSION_VISIBLE) {
             // Panel expanded while pulsing but didn't translate the bouncer (because we are
             // unlocked.) Let's simply wake-up to dismiss the lock screen.
             mStatusBar.wakeUpIfDozing(SystemClock.uptimeMillis(), mStatusBar.getBouncerContainer(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
index 959c673..076a9a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
@@ -102,6 +102,7 @@
 import com.android.systemui.statusbar.phone.StatusBarWindowView;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 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;
@@ -164,6 +165,7 @@
             NotificationLogger notificationLogger,
             NotificationInterruptStateProvider notificationInterruptStateProvider,
             NotificationViewHierarchyManager notificationViewHierarchyManager,
+            PanelExpansionStateManager panelExpansionStateManager,
             KeyguardViewMediator keyguardViewMediator,
             DisplayMetrics displayMetrics,
             MetricsLogger metricsLogger,
@@ -268,6 +270,7 @@
                 notificationLogger,
                 notificationInterruptStateProvider,
                 notificationViewHierarchyManager,
+                panelExpansionStateManager,
                 keyguardViewMediator,
                 displayMetrics,
                 metricsLogger,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 3806d9a..31cc823 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -86,7 +86,7 @@
         //
         // TODO(b/183229367): Remove this function override when b/178406514 is fixed.
         override fun onEntryAdded(entry: NotificationEntry) {
-            onEntryUpdated(entry)
+            onEntryUpdated(entry, true)
         }
 
         override fun onEntryUpdated(entry: NotificationEntry) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelExpansionListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.java
similarity index 65%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelExpansionListener.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.java
index 655a25d..774609b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelExpansionListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.java
@@ -11,28 +11,20 @@
  * 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
+ * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone;
+package com.android.systemui.statusbar.phone.panelstate;
 
-/**
- * Panel and QS expansion callbacks.
- */
+/** A listener interface to be notified of expansion events for the notification panel. */
 public interface PanelExpansionListener {
     /**
      * 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.
+     * lock screen and swiping to pull down the notification shade.
      *
-     * @param expansion 0 when collapsed, 1 when expanded.
+     * @param fraction 0 when collapsed, 1 when fully expanded.
      * @param tracking {@code true} when the user is actively dragging the panel.
      */
-    void onPanelExpansionChanged(float expansion, boolean tracking);
-
-    /**
-     * Invoked whenever the QS expansion changes, at every animation frame.
-     * @param expansion 0 when collapsed, 1 when expanded.
-     */
-    default void onQsExpansionChanged(float expansion) {};
+    void onPanelExpansionChanged(float fraction, boolean tracking);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
new file mode 100644
index 0000000..1c8b1a8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.annotation.FloatRange
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+/**
+ * A class responsible for managing the notification panel's current state.
+ *
+ * TODO(b/200063118): Move [PanelBar.panelExpansionChanged] logic to this class and make this class
+ *   the one source of truth for the state of panel expansion.
+ */
+@SysUISingleton
+class PanelExpansionStateManager @Inject constructor() {
+
+    private val listeners: MutableList<PanelExpansionListener> = mutableListOf()
+
+    /** Adds a listener that will be notified about panel events. */
+    fun addListener(listener: PanelExpansionListener) {
+        listeners.add(listener)
+    }
+
+    /** Called when the panel expansion has changed. Notifies all listeners of change. */
+    fun onPanelExpansionChanged(
+        @FloatRange(from = 0.0, to = 1.0) fraction: Float,
+        tracking: Boolean
+    ) {
+        listeners.forEach { it.onPanelExpansionChanged(fraction, tracking) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
index 5944e9c..4ed7224 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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;
 
@@ -10,26 +25,25 @@
 import static org.mockito.Mockito.when;
 
 import android.app.Notification;
-import android.app.RemoteInputHistoryItem;
 import android.content.Context;
-import android.net.Uri;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
-import android.service.notification.StatusBarNotification;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputActiveExtender;
-import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputHistoryExtender;
-import com.android.systemui.statusbar.NotificationRemoteInputManager.SmartReplyHistoryExtender;
+import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.RemoteInputActiveExtender;
+import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.RemoteInputHistoryExtender;
+import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.SmartReplyHistoryExtender;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -76,13 +90,19 @@
     private RemoteInputHistoryExtender mRemoteInputHistoryExtender;
     private SmartReplyHistoryExtender mSmartReplyHistoryExtender;
     private RemoteInputActiveExtender mRemoteInputActiveExtender;
+    private TestableNotificationRemoteInputManager.FakeLegacyRemoteInputLifetimeExtender
+            mLegacyRemoteInputLifetimeExtender;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
         mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext,
-                mLockscreenUserManager, mSmartReplyController, mEntryManager,
+                mock(FeatureFlags.class),
+                mLockscreenUserManager,
+                mSmartReplyController,
+                mEntryManager,
+                mock(RemoteInputNotificationRebuilder.class),
                 () -> Optional.of(mock(StatusBar.class)),
                 mStateController,
                 Handler.createAsync(Looper.myLooper()),
@@ -120,6 +140,7 @@
     public void testShouldExtendLifetime_remoteInputActive() {
         when(mController.isRemoteInputActive(mEntry)).thenReturn(true);
 
+        assertTrue(mRemoteInputManager.isRemoteInputActive(mEntry));
         assertTrue(mRemoteInputActiveExtender.shouldExtendLifetime(mEntry));
     }
 
@@ -128,6 +149,7 @@
         NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY = true;
         when(mController.isSpinning(mEntry.getKey())).thenReturn(true);
 
+        assertTrue(mRemoteInputManager.shouldKeepForRemoteInputHistory(mEntry));
         assertTrue(mRemoteInputHistoryExtender.shouldExtendLifetime(mEntry));
     }
 
@@ -136,6 +158,7 @@
         NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY = true;
         mEntry.lastRemoteInputSent = SystemClock.elapsedRealtime();
 
+        assertTrue(mRemoteInputManager.shouldKeepForRemoteInputHistory(mEntry));
         assertTrue(mRemoteInputHistoryExtender.shouldExtendLifetime(mEntry));
     }
 
@@ -144,6 +167,7 @@
         NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY = true;
         when(mSmartReplyController.isSendingSmartReply(mEntry.getKey())).thenReturn(true);
 
+        assertTrue(mRemoteInputManager.shouldKeepForSmartReplyHistory(mEntry));
         assertTrue(mSmartReplyHistoryExtender.shouldExtendLifetime(mEntry));
     }
 
@@ -151,124 +175,24 @@
     public void testNotificationWithRemoteInputActiveIsRemovedOnCollapse() {
         mRemoteInputActiveExtender.setShouldManageLifetime(mEntry, true /* shouldManage */);
 
-        assertEquals(mRemoteInputManager.getEntriesKeptForRemoteInputActive(),
+        assertEquals(mLegacyRemoteInputLifetimeExtender.getEntriesKeptForRemoteInputActive(),
                 Sets.newArraySet(mEntry));
 
         mRemoteInputManager.onPanelCollapsed();
 
-        assertTrue(mRemoteInputManager.getEntriesKeptForRemoteInputActive().isEmpty());
+        assertTrue(
+                mLegacyRemoteInputLifetimeExtender.getEntriesKeptForRemoteInputActive().isEmpty());
     }
 
-    @Test
-    public void testRebuildWithRemoteInput_noExistingInput_image() {
-        Uri uri = mock(Uri.class);
-        String mimeType  = "image/jpeg";
-        String text = "image inserted";
-        StatusBarNotification newSbn =
-                mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
-                        mEntry, text, false, mimeType, uri);
-        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
-                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
-        assertEquals(1, messages.length);
-        assertEquals(text, messages[0].getText());
-        assertEquals(mimeType, messages[0].getMimeType());
-        assertEquals(uri, messages[0].getUri());
-    }
-
-    @Test
-    public void testRebuildWithRemoteInput_noExistingInputNoSpinner() {
-        StatusBarNotification newSbn =
-                mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
-                        mEntry, "A Reply", false, null, null);
-        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
-                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
-        assertEquals(1, messages.length);
-        assertEquals("A Reply", messages[0].getText());
-        assertFalse(newSbn.getNotification().extras
-                .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
-        assertTrue(newSbn.getNotification().extras
-                .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
-    }
-
-    @Test
-    public void testRebuildWithRemoteInput_noExistingInputWithSpinner() {
-        StatusBarNotification newSbn =
-                mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
-                        mEntry, "A Reply", true, null, null);
-        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
-                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
-        assertEquals(1, messages.length);
-        assertEquals("A Reply", messages[0].getText());
-        assertTrue(newSbn.getNotification().extras
-                .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
-        assertTrue(newSbn.getNotification().extras
-                .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
-    }
-
-    @Test
-    public void testRebuildWithRemoteInput_withExistingInput() {
-        // Setup a notification entry with 1 remote input.
-        StatusBarNotification newSbn =
-                mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
-                        mEntry, "A Reply", false, null, null);
-        NotificationEntry entry = new NotificationEntryBuilder()
-                .setSbn(newSbn)
-                .build();
-
-        // Try rebuilding to add another reply.
-        newSbn = mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
-                entry, "Reply 2", true, null, null);
-        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
-                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
-        assertEquals(2, messages.length);
-        assertEquals("Reply 2", messages[0].getText());
-        assertEquals("A Reply", messages[1].getText());
-    }
-
-    @Test
-    public void testRebuildWithRemoteInput_withExistingInput_image() {
-        // Setup a notification entry with 1 remote input.
-        Uri uri = mock(Uri.class);
-        String mimeType  = "image/jpeg";
-        String text = "image inserted";
-        StatusBarNotification newSbn =
-                mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
-                        mEntry, text, false, mimeType, uri);
-        NotificationEntry entry = new NotificationEntryBuilder()
-                .setSbn(newSbn)
-                .build();
-
-        // Try rebuilding to add another reply.
-        newSbn = mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
-                entry, "Reply 2", true, null, null);
-        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
-                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
-        assertEquals(2, messages.length);
-        assertEquals("Reply 2", messages[0].getText());
-        assertEquals(text, messages[1].getText());
-        assertEquals(mimeType, messages[1].getMimeType());
-        assertEquals(uri, messages[1].getUri());
-    }
-
-    @Test
-    public void testRebuildNotificationForCanceledSmartReplies() {
-        // Try rebuilding to remove spinner and hide buttons.
-        StatusBarNotification newSbn =
-                mRemoteInputManager.rebuildNotificationForCanceledSmartReplies(mEntry);
-        assertFalse(newSbn.getNotification().extras
-                .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
-        assertTrue(newSbn.getNotification().extras
-                .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
-    }
-
-
     private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager {
 
         TestableNotificationRemoteInputManager(
                 Context context,
+                FeatureFlags featureFlags,
                 NotificationLockscreenUserManager lockscreenUserManager,
                 SmartReplyController smartReplyController,
                 NotificationEntryManager notificationEntryManager,
+                RemoteInputNotificationRebuilder rebuilder,
                 Lazy<Optional<StatusBar>> statusBarOptionalLazy,
                 StatusBarStateController statusBarStateController,
                 Handler mainHandler,
@@ -278,9 +202,11 @@
                 DumpManager dumpManager) {
             super(
                     context,
+                    featureFlags,
                     lockscreenUserManager,
                     smartReplyController,
                     notificationEntryManager,
+                    rebuilder,
                     statusBarOptionalLazy,
                     statusBarStateController,
                     mainHandler,
@@ -297,14 +223,28 @@
             mRemoteInputController = controller;
         }
 
+        @NonNull
         @Override
-        protected void addLifetimeExtenders() {
-            mRemoteInputActiveExtender = new RemoteInputActiveExtender();
-            mRemoteInputHistoryExtender = new RemoteInputHistoryExtender();
-            mSmartReplyHistoryExtender = new SmartReplyHistoryExtender();
-            mLifetimeExtenders.add(mRemoteInputHistoryExtender);
-            mLifetimeExtenders.add(mSmartReplyHistoryExtender);
-            mLifetimeExtenders.add(mRemoteInputActiveExtender);
+        protected LegacyRemoteInputLifetimeExtender createLegacyRemoteInputLifetimeExtender(
+                Handler mainHandler,
+                NotificationEntryManager notificationEntryManager,
+                SmartReplyController smartReplyController) {
+            mLegacyRemoteInputLifetimeExtender = new FakeLegacyRemoteInputLifetimeExtender();
+            return mLegacyRemoteInputLifetimeExtender;
         }
+
+        class FakeLegacyRemoteInputLifetimeExtender extends LegacyRemoteInputLifetimeExtender {
+
+            @Override
+            protected void addLifetimeExtenders() {
+                mRemoteInputActiveExtender = new RemoteInputActiveExtender();
+                mRemoteInputHistoryExtender = new RemoteInputHistoryExtender();
+                mSmartReplyHistoryExtender = new SmartReplyHistoryExtender();
+                mLifetimeExtenders.add(mRemoteInputHistoryExtender);
+                mLifetimeExtenders.add(mSmartReplyHistoryExtender);
+                mLifetimeExtenders.add(mRemoteInputActiveExtender);
+            }
+        }
+
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilderTest.java
new file mode 100644
index 0000000..ce11d6a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilderTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Mockito.mock;
+
+import android.app.Notification;
+import android.app.RemoteInputHistoryItem;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+
+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)
+@TestableLooper.RunWithLooper
+public class RemoteInputNotificationRebuilderTest extends SysuiTestCase {
+    private static final String TEST_PACKAGE_NAME = "test";
+    private static final int TEST_UID = 0;
+    @Mock
+    private ExpandableNotificationRow mRow;
+
+    private RemoteInputNotificationRebuilder mRebuilder;
+    private NotificationEntry mEntry;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mRebuilder = new RemoteInputNotificationRebuilder(mContext);
+        mEntry = new NotificationEntryBuilder()
+                .setPkg(TEST_PACKAGE_NAME)
+                .setOpPkg(TEST_PACKAGE_NAME)
+                .setUid(TEST_UID)
+                .setNotification(new Notification())
+                .setUser(UserHandle.CURRENT)
+                .build();
+        mEntry.setRow(mRow);
+    }
+
+    @Test
+    public void testRebuildWithRemoteInput_noExistingInput_image() {
+        Uri uri = mock(Uri.class);
+        String mimeType = "image/jpeg";
+        String text = "image inserted";
+        StatusBarNotification newSbn =
+                mRebuilder.rebuildWithRemoteInputInserted(
+                        mEntry, text, false, mimeType, uri);
+        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
+                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+        assertEquals(1, messages.length);
+        assertEquals(text, messages[0].getText());
+        assertEquals(mimeType, messages[0].getMimeType());
+        assertEquals(uri, messages[0].getUri());
+    }
+
+    @Test
+    public void testRebuildWithRemoteInput_noExistingInputNoSpinner() {
+        StatusBarNotification newSbn =
+                mRebuilder.rebuildWithRemoteInputInserted(
+                        mEntry, "A Reply", false, null, null);
+        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
+                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+        assertEquals(1, messages.length);
+        assertEquals("A Reply", messages[0].getText());
+        assertFalse(newSbn.getNotification().extras
+                .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
+        assertTrue(newSbn.getNotification().extras
+                .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
+    }
+
+    @Test
+    public void testRebuildWithRemoteInput_noExistingInputWithSpinner() {
+        StatusBarNotification newSbn =
+                mRebuilder.rebuildWithRemoteInputInserted(
+                        mEntry, "A Reply", true, null, null);
+        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
+                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+        assertEquals(1, messages.length);
+        assertEquals("A Reply", messages[0].getText());
+        assertTrue(newSbn.getNotification().extras
+                .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
+        assertTrue(newSbn.getNotification().extras
+                .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
+    }
+
+    @Test
+    public void testRebuildWithRemoteInput_withExistingInput() {
+        // Setup a notification entry with 1 remote input.
+        StatusBarNotification newSbn =
+                mRebuilder.rebuildWithRemoteInputInserted(
+                        mEntry, "A Reply", false, null, null);
+        NotificationEntry entry = new NotificationEntryBuilder()
+                .setSbn(newSbn)
+                .build();
+
+        // Try rebuilding to add another reply.
+        newSbn = mRebuilder.rebuildWithRemoteInputInserted(
+                entry, "Reply 2", true, null, null);
+        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
+                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+        assertEquals(2, messages.length);
+        assertEquals("Reply 2", messages[0].getText());
+        assertEquals("A Reply", messages[1].getText());
+    }
+
+    @Test
+    public void testRebuildWithRemoteInput_withExistingInput_image() {
+        // Setup a notification entry with 1 remote input.
+        Uri uri = mock(Uri.class);
+        String mimeType = "image/jpeg";
+        String text = "image inserted";
+        StatusBarNotification newSbn =
+                mRebuilder.rebuildWithRemoteInputInserted(
+                        mEntry, text, false, mimeType, uri);
+        NotificationEntry entry = new NotificationEntryBuilder()
+                .setSbn(newSbn)
+                .build();
+
+        // Try rebuilding to add another reply.
+        newSbn = mRebuilder.rebuildWithRemoteInputInserted(
+                entry, "Reply 2", true, null, null);
+        RemoteInputHistoryItem[] messages = (RemoteInputHistoryItem[]) newSbn.getNotification()
+                .extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+        assertEquals(2, messages.length);
+        assertEquals("Reply 2", messages[0].getText());
+        assertEquals(text, messages[1].getText());
+        assertEquals(mimeType, messages[1].getMimeType());
+        assertEquals(uri, messages[1].getUri());
+    }
+
+    @Test
+    public void testRebuildNotificationForCanceledSmartReplies() {
+        // Try rebuilding to remove spinner and hide buttons.
+        StatusBarNotification newSbn =
+                mRebuilder.rebuildForCanceledSmartReplies(mEntry);
+        assertFalse(newSbn.getNotification().extras
+                .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
+        assertTrue(newSbn.getNotification().extras
+                .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
index 837d71f..99c965a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
@@ -39,6 +39,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -86,14 +87,20 @@
         mDependency.injectTestDependency(NotificationEntryManager.class,
                 mNotificationEntryManager);
 
-        mSmartReplyController = new SmartReplyController(mNotificationEntryManager,
-                mIStatusBarService, mClickNotifier);
+        mSmartReplyController = new SmartReplyController(
+                mock(DumpManager.class),
+                mNotificationEntryManager,
+                mIStatusBarService,
+                mClickNotifier);
         mDependency.injectTestDependency(SmartReplyController.class,
                 mSmartReplyController);
 
         mRemoteInputManager = new NotificationRemoteInputManager(mContext,
+                mock(FeatureFlags.class),
                 mock(NotificationLockscreenUserManager.class), mSmartReplyController,
-                mNotificationEntryManager, () -> Optional.of(mock(StatusBar.class)),
+                mNotificationEntryManager,
+                new RemoteInputNotificationRebuilder(mContext),
+                () -> Optional.of(mock(StatusBar.class)),
                 mStatusBarStateController,
                 Handler.createAsync(Looper.myLooper()),
                 mRemoteInputUriController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index ebeb591..f08a74a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -35,6 +35,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
@@ -50,6 +51,7 @@
 
 import android.annotation.Nullable;
 import android.app.Notification;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.service.notification.NotificationListenerService.Ranking;
 import android.service.notification.NotificationListenerService.RankingMap;
@@ -77,6 +79,7 @@
 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
+import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
@@ -107,6 +110,7 @@
     @Mock private FeatureFlags mFeatureFlags;
     @Mock private NotifCollectionLogger mLogger;
     @Mock private LogBufferEulogizer mEulogizer;
+    @Mock private Handler mMainHandler;
 
     @Mock private GroupCoalescer mGroupCoalescer;
     @Spy private RecordingCollectionListener mCollectionListener;
@@ -152,6 +156,7 @@
                 mClock,
                 mFeatureFlags,
                 mLogger,
+                mMainHandler,
                 mEulogizer,
                 mock(DumpManager.class));
         mCollection.attach(mGroupCoalescer);
@@ -1322,6 +1327,78 @@
         verify(mCollectionListener, never()).onEntryRemoved(any(NotificationEntry.class), anyInt());
     }
 
+    private Runnable getInternalNotifUpdateRunnable(StatusBarNotification sbn) {
+        InternalNotifUpdater updater = mCollection.getInternalNotifUpdater("Test");
+        updater.onInternalNotificationUpdate(sbn, "reason");
+        ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mMainHandler).post(runnableCaptor.capture());
+        return runnableCaptor.getValue();
+    }
+
+    @Test
+    public void testGetInternalNotifUpdaterPostsToMainHandler() {
+        InternalNotifUpdater updater = mCollection.getInternalNotifUpdater("Test");
+        updater.onInternalNotificationUpdate(mock(StatusBarNotification.class), "reason");
+        verify(mMainHandler).post(any());
+    }
+
+    @Test
+    public void testSecondPostCallsUpdateWithTrue() {
+        // GIVEN a pipeline with one notification
+        NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
+        NotificationEntry entry = mCollectionListener.getEntry(notifEvent.key);
+
+        // KNOWING that it already called listener methods once
+        verify(mCollectionListener).onEntryAdded(eq(entry));
+        verify(mCollectionListener).onRankingApplied();
+
+        // WHEN we update the notification via the system
+        mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
+
+        // THEN entry updated gets called, added does not, and ranking is called again
+        verify(mCollectionListener).onEntryUpdated(eq(entry));
+        verify(mCollectionListener).onEntryUpdated(eq(entry), eq(true));
+        verify(mCollectionListener).onEntryAdded((entry));
+        verify(mCollectionListener, times(2)).onRankingApplied();
+    }
+
+    @Test
+    public void testInternalNotifUpdaterCallsUpdate() {
+        // GIVEN a pipeline with one notification
+        NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
+        NotificationEntry entry = mCollectionListener.getEntry(notifEvent.key);
+
+        // KNOWING that it will call listener methods once
+        verify(mCollectionListener).onEntryAdded(eq(entry));
+        verify(mCollectionListener).onRankingApplied();
+
+        // WHEN we update that notification internally
+        StatusBarNotification sbn = notifEvent.sbn;
+        getInternalNotifUpdateRunnable(sbn).run();
+
+        // THEN only entry updated gets called a second time
+        verify(mCollectionListener).onEntryAdded(eq(entry));
+        verify(mCollectionListener).onRankingApplied();
+        verify(mCollectionListener).onEntryUpdated(eq(entry));
+        verify(mCollectionListener).onEntryUpdated(eq(entry), eq(false));
+    }
+
+    @Test
+    public void testInternalNotifUpdaterIgnoresNew() {
+        // GIVEN a pipeline without any notifications
+        StatusBarNotification sbn = buildNotif(TEST_PACKAGE, 47, "myTag").build().getSbn();
+
+        // WHEN we internally update an unknown notification
+        getInternalNotifUpdateRunnable(sbn).run();
+
+        // THEN only entry updated gets called a second time
+        verify(mCollectionListener, never()).onEntryAdded(any());
+        verify(mCollectionListener, never()).onRankingUpdate(any());
+        verify(mCollectionListener, never()).onRankingApplied();
+        verify(mCollectionListener, never()).onEntryUpdated(any());
+        verify(mCollectionListener, never()).onEntryUpdated(any(), anyBoolean());
+    }
+
     private static NotificationEntryBuilder buildNotif(String pkg, int id, String tag) {
         return new NotificationEntryBuilder()
                 .setPkg(pkg)
@@ -1372,6 +1449,11 @@
         }
 
         @Override
+        public void onEntryUpdated(NotificationEntry entry, boolean fromSystem) {
+            onEntryUpdated(entry);
+        }
+
+        @Override
         public void onEntryRemoved(NotificationEntry entry, int reason) {
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
new file mode 100644
index 0000000..0ce6ada
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.notification.collection.coordinator
+
+import android.os.Handler
+import android.service.notification.StatusBarNotification
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputListener
+import com.android.systemui.statusbar.RemoteInputNotificationRebuilder
+import com.android.systemui.statusbar.SmartReplyController
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class RemoteInputCoordinatorTest : SysuiTestCase() {
+    private lateinit var coordinator: RemoteInputCoordinator
+    private lateinit var listener: RemoteInputListener
+    private lateinit var collectionListener: NotifCollectionListener
+
+    private lateinit var entry1: NotificationEntry
+    private lateinit var entry2: NotificationEntry
+
+    @Mock private lateinit var lifetimeExtensionCallback: OnEndLifetimeExtensionCallback
+    @Mock private lateinit var rebuilder: RemoteInputNotificationRebuilder
+    @Mock private lateinit var remoteInputManager: NotificationRemoteInputManager
+    @Mock private lateinit var mainHandler: Handler
+    @Mock private lateinit var smartReplyController: SmartReplyController
+    @Mock private lateinit var pipeline: NotifPipeline
+    @Mock private lateinit var notifUpdater: InternalNotifUpdater
+    @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var sbn: StatusBarNotification
+
+    @Before
+    fun setUp() {
+        initMocks(this)
+        coordinator = RemoteInputCoordinator(
+                dumpManager,
+                rebuilder,
+                remoteInputManager,
+                mainHandler,
+                smartReplyController
+        )
+        `when`(pipeline.addNotificationLifetimeExtender(any())).thenAnswer {
+            (it.arguments[0] as NotifLifetimeExtender).setCallback(lifetimeExtensionCallback)
+        }
+        `when`(pipeline.getInternalNotifUpdater(any())).thenReturn(notifUpdater)
+        coordinator.attach(pipeline)
+        listener = withArgCaptor {
+            verify(remoteInputManager).setRemoteInputListener(capture())
+        }
+        collectionListener = withArgCaptor {
+            verify(pipeline).addCollectionListener(capture())
+        }
+        entry1 = NotificationEntryBuilder().setId(1).build()
+        entry2 = NotificationEntryBuilder().setId(2).build()
+        `when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn)
+        `when`(rebuilder.rebuildForRemoteInputReply(any())).thenReturn(sbn)
+        `when`(rebuilder.rebuildForSendingSmartReply(any(), any())).thenReturn(sbn)
+    }
+
+    val remoteInputActiveExtender get() = coordinator.mRemoteInputActiveExtender
+    val remoteInputHistoryExtender get() = coordinator.mRemoteInputHistoryExtender
+    val smartReplyHistoryExtender get() = coordinator.mSmartReplyHistoryExtender
+
+    @Test
+    fun testRemoteInputActive() {
+        `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
+        assertThat(remoteInputActiveExtender.shouldExtendLifetime(entry1, 0)).isTrue()
+        assertThat(remoteInputHistoryExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+        assertThat(smartReplyHistoryExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+        assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isFalse()
+    }
+
+    @Test
+    fun testRemoteInputHistory() {
+        `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry1)).thenReturn(true)
+        assertThat(remoteInputActiveExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+        assertThat(remoteInputHistoryExtender.shouldExtendLifetime(entry1, 0)).isTrue()
+        assertThat(smartReplyHistoryExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+        assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isTrue()
+    }
+
+    @Test
+    fun testSmartReplyHistory() {
+        `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry1)).thenReturn(true)
+        assertThat(remoteInputActiveExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+        assertThat(remoteInputHistoryExtender.shouldExtendLifetime(entry1, 0)).isFalse()
+        assertThat(smartReplyHistoryExtender.shouldExtendLifetime(entry1, 0)).isTrue()
+        assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isTrue()
+    }
+
+    @Test
+    fun testNotificationWithRemoteInputActiveIsRemovedOnCollapse() {
+        `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
+        assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
+
+        // Nothing should happen on panel collapse before we start extending the lifetime
+        listener.onPanelCollapsed()
+        assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
+        verify(lifetimeExtensionCallback, never()).onEndLifetimeExtension(any(), any())
+
+        // Start extending lifetime & validate that the extension is ended
+        assertThat(remoteInputActiveExtender.shouldExtendLifetime(entry1, 0)).isTrue()
+        assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isTrue()
+        listener.onPanelCollapsed()
+        verify(lifetimeExtensionCallback).onEndLifetimeExtension(remoteInputActiveExtender, entry1)
+        assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtenderTest.kt
new file mode 100644
index 0000000..37ad835
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtenderTest.kt
@@ -0,0 +1,230 @@
+/*
+ * 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.notification.collection.notifcollection
+
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+import java.util.function.Consumer
+import java.util.function.Predicate
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class SelfTrackingLifetimeExtenderTest : SysuiTestCase() {
+    private lateinit var extender: TestableSelfTrackingLifetimeExtender
+
+    private lateinit var entry1: NotificationEntry
+    private lateinit var entry2: NotificationEntry
+
+    @Mock
+    private lateinit var callback: OnEndLifetimeExtensionCallback
+    @Mock
+    private lateinit var mainHandler: Handler
+    @Mock
+    private lateinit var shouldExtend: Predicate<NotificationEntry>
+    @Mock
+    private lateinit var onStarted: Consumer<NotificationEntry>
+    @Mock
+    private lateinit var onCanceled: Consumer<NotificationEntry>
+
+    @Before
+    fun setUp() {
+        initMocks(this)
+        extender = TestableSelfTrackingLifetimeExtender()
+        extender.setCallback(callback)
+        entry1 = NotificationEntryBuilder().setId(1).build()
+        entry2 = NotificationEntryBuilder().setId(2).build()
+    }
+
+    @Test
+    fun testName() {
+        assertThat(extender.name).isEqualTo("Testable")
+    }
+
+    @Test
+    fun testNoExtend() {
+        `when`(shouldExtend.test(entry1)).thenReturn(false)
+        assertThat(extender.shouldExtendLifetime(entry1, 0)).isFalse()
+        assertThat(extender.isExtending(entry1.key)).isFalse()
+        verify(onStarted, never()).accept(entry1)
+        verify(onCanceled, never()).accept(entry1)
+    }
+
+    @Test
+    fun testExtendThenCancelForRepost() {
+        `when`(shouldExtend.test(entry1)).thenReturn(true)
+        assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+        verify(onStarted).accept(entry1)
+        verify(onCanceled, never()).accept(entry1)
+        assertThat(extender.isExtending(entry1.key)).isTrue()
+        extender.cancelLifetimeExtension(entry1)
+        verify(onCanceled).accept(entry1)
+    }
+
+    @Test
+    fun testExtendThenCancel_thenEndDoesNothing() {
+        testExtendThenCancelForRepost()
+        assertThat(extender.isExtending(entry1.key)).isFalse()
+
+        extender.endLifetimeExtension(entry1.key)
+        extender.endLifetimeExtensionAfterDelay(entry1.key, 1000)
+        verify(callback, never()).onEndLifetimeExtension(any(), any())
+        verify(mainHandler, never()).postDelayed(any(), anyLong())
+    }
+
+    @Test
+    fun testExtendThenEnd() {
+        `when`(shouldExtend.test(entry1)).thenReturn(true)
+        assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+        verify(onStarted).accept(entry1)
+        assertThat(extender.isExtending(entry1.key)).isTrue()
+        extender.endLifetimeExtension(entry1.key)
+        verify(callback).onEndLifetimeExtension(extender, entry1)
+        verify(onCanceled, never()).accept(entry1)
+    }
+
+    @Test
+    fun testExtendThenEndAfterDelay() {
+        `when`(shouldExtend.test(entry1)).thenReturn(true)
+        assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+        verify(onStarted).accept(entry1)
+        assertThat(extender.isExtending(entry1.key)).isTrue()
+
+        // Call the method and capture the posted runnable
+        extender.endLifetimeExtensionAfterDelay(entry1.key, 1234)
+        val runnable = withArgCaptor<Runnable> {
+            verify(mainHandler).postDelayed(capture(), eq(1234.toLong()))
+        }
+        assertThat(extender.isExtending(entry1.key)).isTrue()
+        verify(callback, never()).onEndLifetimeExtension(any(), any())
+
+        // now run the posted runnable and ensure it works as expected
+        runnable.run()
+        verify(callback).onEndLifetimeExtension(extender, entry1)
+        assertThat(extender.isExtending(entry1.key)).isFalse()
+        verify(onCanceled, never()).accept(entry1)
+    }
+
+    @Test
+    fun testExtendThenEndAll() {
+        `when`(shouldExtend.test(entry1)).thenReturn(true)
+        `when`(shouldExtend.test(entry2)).thenReturn(true)
+        assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+        verify(onStarted).accept(entry1)
+        assertThat(extender.isExtending(entry1.key)).isTrue()
+        assertThat(extender.isExtending(entry2.key)).isFalse()
+        assertThat(extender.shouldExtendLifetime(entry2, 0)).isTrue()
+        verify(onStarted).accept(entry2)
+        assertThat(extender.isExtending(entry1.key)).isTrue()
+        assertThat(extender.isExtending(entry2.key)).isTrue()
+        extender.endAllLifetimeExtensions()
+        verify(callback).onEndLifetimeExtension(extender, entry1)
+        verify(callback).onEndLifetimeExtension(extender, entry2)
+        verify(onCanceled, never()).accept(entry1)
+        verify(onCanceled, never()).accept(entry2)
+    }
+
+    @Test
+    fun testExtendWithinEndCanReExtend() {
+        `when`(shouldExtend.test(entry1)).thenReturn(true)
+        assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+        verify(onStarted, times(1)).accept(entry1)
+
+        `when`(callback.onEndLifetimeExtension(extender, entry1)).thenAnswer {
+            assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+        }
+        extender.endLifetimeExtension(entry1.key)
+        verify(onStarted, times(2)).accept(entry1)
+        assertThat(extender.isExtending(entry1.key)).isTrue()
+    }
+
+    @Test
+    fun testExtendWithinEndCanNotReExtend() {
+        `when`(shouldExtend.test(entry1)).thenReturn(true, false)
+        assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+        verify(onStarted, times(1)).accept(entry1)
+
+        `when`(callback.onEndLifetimeExtension(extender, entry1)).thenAnswer {
+            assertThat(extender.shouldExtendLifetime(entry1, 0)).isFalse()
+        }
+        extender.endLifetimeExtension(entry1.key)
+        verify(onStarted, times(1)).accept(entry1)
+        assertThat(extender.isExtending(entry1.key)).isFalse()
+    }
+
+    @Test
+    fun testExtendWithinEndAllCanReExtend() {
+        `when`(shouldExtend.test(entry1)).thenReturn(true)
+        assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+        verify(onStarted, times(1)).accept(entry1)
+
+        `when`(callback.onEndLifetimeExtension(extender, entry1)).thenAnswer {
+            assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+        }
+        extender.endAllLifetimeExtensions()
+        verify(onStarted, times(2)).accept(entry1)
+        assertThat(extender.isExtending(entry1.key)).isTrue()
+    }
+
+    @Test
+    fun testExtendWithinEndAllCanNotReExtend() {
+        `when`(shouldExtend.test(entry1)).thenReturn(true, false)
+        assertThat(extender.shouldExtendLifetime(entry1, 0)).isTrue()
+        verify(onStarted, times(1)).accept(entry1)
+
+        `when`(callback.onEndLifetimeExtension(extender, entry1)).thenAnswer {
+            assertThat(extender.shouldExtendLifetime(entry1, 0)).isFalse()
+        }
+        extender.endAllLifetimeExtensions()
+        verify(onStarted, times(1)).accept(entry1)
+        assertThat(extender.isExtending(entry1.key)).isFalse()
+    }
+
+    inner class TestableSelfTrackingLifetimeExtender(debug: Boolean = false) :
+            SelfTrackingLifetimeExtender("Test", "Testable", debug, mainHandler) {
+
+        override fun queryShouldExtendLifetime(entry: NotificationEntry) =
+                shouldExtend.test(entry)
+
+        override fun onStartedLifetimeExtension(entry: NotificationEntry) {
+            onStarted.accept(entry)
+        }
+
+        override fun onCanceledLifetimeExtension(entry: NotificationEntry) {
+            onCanceled.accept(entry)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
index ead3291..fabe5a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
@@ -121,6 +121,7 @@
 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+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.FakeExecutor;
@@ -444,6 +445,7 @@
                 mSplitShadeHeaderController,
                 mUnlockedScreenOffAnimationController,
                 mLockscreenGestureLogger,
+                new PanelExpansionStateManager(),
                 mNotificationRemoteInputManager,
                 mControlsComponent);
         mNotificationPanelViewController.initDependencies(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
index 6e9bb2d..a9db1a4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.tuner.TunerService;
 
@@ -135,6 +136,7 @@
                 mNotificationShadeDepthController,
                 mView,
                 mNotificationPanelViewController,
+                new PanelExpansionStateManager(),
                 mStatusBarWindowView,
                 mNotificationStackScrollLayoutController,
                 mStatusBarKeyguardViewManager,
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 2d944aa..fbc4128 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
@@ -51,6 +51,7 @@
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -137,9 +138,13 @@
                 mUnlockedScreenOffAnimationController,
                 mKeyguardMessageAreaFactory,
                 mShadeController);
-        mStatusBarKeyguardViewManager.registerStatusBar(mStatusBar,
-                mNotificationPanelView, mBiometrucUnlockController,
-                mNotificationContainer, mBypassController);
+        mStatusBarKeyguardViewManager.registerStatusBar(
+                mStatusBar,
+                mNotificationPanelView,
+                new PanelExpansionStateManager(),
+                mBiometrucUnlockController,
+                mNotificationContainer,
+                mBypassController);
         mStatusBarKeyguardViewManager.show(null);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index f14b126..d0d3d41 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -133,6 +133,7 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
 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;
@@ -385,6 +386,7 @@
                 notificationLogger,
                 mNotificationInterruptStateProvider,
                 mNotificationViewHierarchyManager,
+                new PanelExpansionStateManager(),
                 mKeyguardViewMediator,
                 new DisplayMetrics(),
                 mMetricsLogger,
@@ -464,9 +466,13 @@
                 mock(DumpManager.class),
                 mActivityLaunchAnimator,
                 mDialogLaunchAnimator);
-        when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class),
-                any(NotificationPanelViewController.class), any(BiometricUnlockController.class),
-                any(ViewGroup.class), any(KeyguardBypassController.class)))
+        when(mKeyguardViewMediator.registerStatusBar(
+                any(StatusBar.class),
+                any(NotificationPanelViewController.class),
+                any(PanelExpansionStateManager.class),
+                any(BiometricUnlockController.class),
+                any(ViewGroup.class),
+                any(KeyguardBypassController.class)))
                 .thenReturn(mStatusBarKeyguardViewManager);
 
         when(mKeyguardViewMediator.getViewMediatorCallback()).thenReturn(
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
new file mode 100644
index 0000000..8a10997
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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_listenersNotified() {
+        val listener = TestPanelExpansionListener()
+        panelExpansionStateManager.addListener(listener)
+        val fraction = 0.6f
+        val tracking = true
+
+        panelExpansionStateManager.onPanelExpansionChanged(fraction, tracking)
+
+        assertThat(listener.fraction).isEqualTo(fraction)
+        assertThat(listener.tracking).isEqualTo(tracking)
+    }
+
+    class TestPanelExpansionListener : PanelExpansionListener {
+        var fraction: Float = 0f
+        var tracking: Boolean = false
+
+        override fun onPanelExpansionChanged(
+            fraction: Float,
+            tracking: Boolean
+        ) {
+            this.fraction = fraction
+            this.tracking = tracking
+        }
+    }
+}
diff --git a/packages/services/CameraExtensionsProxy/AndroidManifest.xml b/packages/services/CameraExtensionsProxy/AndroidManifest.xml
index ef1d581..79c9d13 100644
--- a/packages/services/CameraExtensionsProxy/AndroidManifest.xml
+++ b/packages/services/CameraExtensionsProxy/AndroidManifest.xml
@@ -2,6 +2,12 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.cameraextensions">
 
+    <queries>
+        <intent>
+            <action android:name="androidx.camera.extensions.action.VENDOR_ACTION" />
+        </intent>
+    </queries>
+
     <application
         android:label="@string/app_name"
         android:defaultToDeviceProtectedStorage="true"
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 8a42ddf..71749e7 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -223,7 +223,7 @@
     @Override // from AbstractMasterSystemService
     protected ContentCapturePerUserService newServiceLocked(@UserIdInt int resolvedUserId,
             boolean disabled) {
-        return new ContentCapturePerUserService(this, mLock, disabled, resolvedUserId);
+        return new ContentCapturePerUserService(this, mLock, disabled, resolvedUserId, mHandler);
     }
 
     @Override // from SystemService
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index 6bd1fa6..822a42b 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -46,6 +46,7 @@
 import android.content.pm.ServiceInfo;
 import android.os.Binder;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -75,6 +76,7 @@
 import com.android.server.infra.AbstractPerUserSystemService;
 
 import java.io.PrintWriter;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -88,6 +90,10 @@
 
     private static final String TAG = ContentCapturePerUserService.class.getSimpleName();
 
+    private static final int MAX_REBIND_COUNTS = 5;
+    // 5 minutes
+    private static final long REBIND_DURATION_MS = 5 * 60 * 1_000;
+
     @GuardedBy("mLock")
     private final SparseArray<ContentCaptureServerSession> mSessions = new SparseArray<>();
 
@@ -121,11 +127,18 @@
     @GuardedBy("mLock")
     private ContentCaptureServiceInfo mInfo;
 
+    private Instant mLastRebindTime;
+    private int mRebindCount;
+    private final Handler mHandler;
+
+    private final Runnable mReBindServiceRunnable = new RebindServiceRunnable();
+
     // TODO(b/111276913): add mechanism to prune stale sessions, similar to Autofill's
 
     ContentCapturePerUserService(@NonNull ContentCaptureManagerService master,
-            @NonNull Object lock, boolean disabled, @UserIdInt int userId) {
+            @NonNull Object lock, boolean disabled, @UserIdInt int userId, Handler handler) {
         super(master, lock, userId);
+        mHandler = handler;
         updateRemoteServiceLocked(disabled);
     }
 
@@ -190,9 +203,43 @@
         Slog.w(TAG, "remote service died: " + service);
         synchronized (mLock) {
             mZombie = true;
-            writeServiceEvent(
-                    FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_REMOTE_SERVICE_DIED,
-                    getServiceComponentName());
+            // Reset rebindCount if over 12 hours mLastRebindTime
+            if (mLastRebindTime != null && Instant.now().isAfter(
+                    mLastRebindTime.plusMillis(12 * 60 * 60 * 1000))) {
+                if (mMaster.debug) {
+                    Slog.i(TAG, "The current rebind count " + mRebindCount + " is reset.");
+                }
+                mRebindCount = 0;
+            }
+            if (mRebindCount >= MAX_REBIND_COUNTS) {
+                writeServiceEvent(
+                        FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_REMOTE_SERVICE_DIED,
+                        getServiceComponentName());
+            }
+            if (mRebindCount < MAX_REBIND_COUNTS) {
+                mHandler.removeCallbacks(mReBindServiceRunnable);
+                mHandler.postDelayed(mReBindServiceRunnable, REBIND_DURATION_MS);
+            }
+        }
+    }
+
+    private void updateRemoteServiceAndResurrectSessionsLocked() {
+        boolean disabled = !isEnabledLocked();
+        updateRemoteServiceLocked(disabled);
+        resurrectSessionsLocked();
+    }
+
+    private final class RebindServiceRunnable implements Runnable{
+
+        @Override
+        public void run() {
+            synchronized (mLock) {
+                if (mZombie) {
+                    mLastRebindTime = Instant.now();
+                    mRebindCount++;
+                    updateRemoteServiceAndResurrectSessionsLocked();
+                }
+            }
         }
     }
 
@@ -240,8 +287,8 @@
     }
 
     void onPackageUpdatedLocked() {
-        updateRemoteServiceLocked(!isEnabledLocked());
-        resurrectSessionsLocked();
+        mRebindCount = 0;
+        updateRemoteServiceAndResurrectSessionsLocked();
     }
 
     @GuardedBy("mLock")
@@ -555,6 +602,8 @@
             mInfo.dump(prefix2, pw);
         }
         pw.print(prefix); pw.print("Zombie: "); pw.println(mZombie);
+        pw.print(prefix); pw.print("Rebind count: "); pw.println(mRebindCount);
+        pw.print(prefix); pw.print("Last rebind: "); pw.println(mLastRebindTime);
 
         if (mRemoteService != null) {
             pw.print(prefix); pw.println("remote service:");
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index cf9783f..38a4857 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -20,11 +20,11 @@
 import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOWS_FOR_ACCESSIBILITY_CALLBACK;
 import static android.os.Build.IS_USER;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY;
-import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 
 import static com.android.server.accessibility.AccessibilityTraceFileProto.ENTRY;
 import static com.android.server.accessibility.AccessibilityTraceFileProto.MAGIC_NUMBER;
@@ -1009,6 +1009,8 @@
                     final int windowType = windowState.mAttrs.type;
                     if (isExcludedWindowType(windowType)
                             || ((windowState.mAttrs.privateFlags
+                            & PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION) != 0)
+                            || ((windowState.mAttrs.privateFlags
                             & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0)) {
                         continue;
                     }
@@ -1073,7 +1075,6 @@
                         }
                     }
                 }
-
                 visibleWindows.clear();
 
                 mMagnificationRegion.op(mDrawBorderInset, mDrawBorderInset,
@@ -1110,9 +1111,6 @@
 
             private boolean isExcludedWindowType(int windowType) {
                 return windowType == TYPE_MAGNIFICATION_OVERLAY
-                        // Omit the touch region to avoid the cut out of the magnification
-                        // bounds because nav bar panel is unmagnifiable.
-                        || windowType == TYPE_NAVIGATION_BAR_PANEL
                         // Omit the touch region of window magnification to avoid the cut out of the
                         // magnification and the magnified center of window magnification could be
                         // in the bounds
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index c715c39..d137436 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -190,7 +190,7 @@
 
         @VisibleForTesting
         boolean allDrawn() {
-            return mAssociatedTransitionInfo != null && mAssociatedTransitionInfo.allDrawn();
+            return mAssociatedTransitionInfo != null && mAssociatedTransitionInfo.mIsDrawn;
         }
 
         boolean hasActiveTransitionInfo() {
@@ -224,8 +224,8 @@
         final boolean mProcessRunning;
         /** whether the process of the launching activity didn't have any active activity. */
         final boolean mProcessSwitch;
-        /** The activities that should be drawn. */
-        final ArrayList<ActivityRecord> mPendingDrawActivities = new ArrayList<>(2);
+        /** Whether the last launched activity has reported drawn. */
+        boolean mIsDrawn;
         /** The latest activity to have been launched. */
         @NonNull ActivityRecord mLastLaunchedActivity;
 
@@ -318,10 +318,7 @@
                 mLastLaunchedActivity.mLaunchRootTask = null;
             }
             mLastLaunchedActivity = r;
-            if (!r.noDisplay && !r.isReportedDrawn()) {
-                if (DEBUG_METRICS) Slog.i(TAG, "Add pending draw " + r);
-                mPendingDrawActivities.add(r);
-            }
+            mIsDrawn = r.isReportedDrawn();
         }
 
         /** Returns {@code true} if the incoming activity can belong to this transition. */
@@ -332,29 +329,7 @@
 
         /** @return {@code true} if the activity matches a launched activity in this transition. */
         boolean contains(ActivityRecord r) {
-            return r != null && (r == mLastLaunchedActivity || mPendingDrawActivities.contains(r));
-        }
-
-        /** Called when the activity is drawn or won't be drawn. */
-        void removePendingDrawActivity(ActivityRecord r) {
-            if (DEBUG_METRICS) Slog.i(TAG, "Remove pending draw " + r);
-            mPendingDrawActivities.remove(r);
-        }
-
-        boolean allDrawn() {
-            return mPendingDrawActivities.isEmpty();
-        }
-
-        /** Only keep the records which can be drawn. */
-        void updatePendingDraw(boolean keepInitializing) {
-            for (int i = mPendingDrawActivities.size() - 1; i >= 0; i--) {
-                final ActivityRecord r = mPendingDrawActivities.get(i);
-                if (!r.mVisibleRequested
-                        && !(keepInitializing && r.isState(ActivityRecord.State.INITIALIZING))) {
-                    if (DEBUG_METRICS) Slog.i(TAG, "Discard pending draw " + r);
-                    mPendingDrawActivities.remove(i);
-                }
-            }
+            return r == mLastLaunchedActivity;
         }
 
         /**
@@ -377,7 +352,7 @@
         @Override
         public String toString() {
             return "TransitionInfo{" + Integer.toHexString(System.identityHashCode(this))
-                    + " a=" + mLastLaunchedActivity + " ua=" + mPendingDrawActivities + "}";
+                    + " a=" + mLastLaunchedActivity + " d=" + mIsDrawn + "}";
         }
     }
 
@@ -683,8 +658,7 @@
         // visible such as after the top task is finished.
         for (int i = mTransitionInfoList.size() - 2; i >= 0; i--) {
             final TransitionInfo prevInfo = mTransitionInfoList.get(i);
-            prevInfo.updatePendingDraw(false /* keepInitializing */);
-            if (prevInfo.allDrawn()) {
+            if (prevInfo.mIsDrawn || !prevInfo.mLastLaunchedActivity.mVisibleRequested) {
                 abort(prevInfo, "nothing will be drawn");
             }
         }
@@ -711,17 +685,16 @@
         if (DEBUG_METRICS) Slog.i(TAG, "notifyWindowsDrawn " + r);
 
         final TransitionInfo info = getActiveTransitionInfo(r);
-        if (info == null || info.allDrawn()) {
-            if (DEBUG_METRICS) Slog.i(TAG, "notifyWindowsDrawn no activity to be drawn");
+        if (info == null || info.mIsDrawn) {
+            if (DEBUG_METRICS) Slog.i(TAG, "notifyWindowsDrawn not pending drawn " + info);
             return null;
         }
         // Always calculate the delay because the caller may need to know the individual drawn time.
         info.mWindowsDrawnDelayMs = info.calculateDelay(timestampNs);
-        info.removePendingDrawActivity(r);
-        info.updatePendingDraw(false /* keepInitializing */);
+        info.mIsDrawn = true;
         final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info);
-        if (info.mLoggedTransitionStarting && info.allDrawn()) {
-            done(false /* abort */, info, "notifyWindowsDrawn - all windows drawn", timestampNs);
+        if (info.mLoggedTransitionStarting) {
+            done(false /* abort */, info, "notifyWindowsDrawn", timestampNs);
         }
         if (r.mWmService.isRecentsAnimationTarget(r)) {
             r.mWmService.getRecentsAnimationController().logRecentsAnimationStartTime(
@@ -770,12 +743,8 @@
             info.mCurrentTransitionDelayMs = info.calculateDelay(timestampNs);
             info.mReason = activityToReason.valueAt(index);
             info.mLoggedTransitionStarting = true;
-            // Do not remove activity in initializing state because the transition may be started
-            // by starting window. The initializing activity may be requested to visible soon.
-            info.updatePendingDraw(true /* keepInitializing */);
-            if (info.allDrawn()) {
-                done(false /* abort */, info, "notifyTransitionStarting - all windows drawn",
-                        timestampNs);
+            if (info.mIsDrawn) {
+                done(false /* abort */, info, "notifyTransitionStarting drawn", timestampNs);
             }
         }
     }
@@ -828,12 +797,9 @@
             return;
         }
         if (!r.mVisibleRequested || r.finishing) {
-            info.removePendingDrawActivity(r);
-            if (info.mLastLaunchedActivity == r) {
-                // Check if the tracker can be cancelled because the last launched activity may be
-                // no longer visible.
-                scheduleCheckActivityToBeDrawn(r, 0 /* delay */);
-            }
+            // Check if the tracker can be cancelled because the last launched activity may be
+            // no longer visible.
+            scheduleCheckActivityToBeDrawn(r, 0 /* delay */);
         }
     }
 
@@ -852,17 +818,12 @@
             // If we have an active transition that's waiting on a certain activity that will be
             // invisible now, we'll never get onWindowsDrawn, so abort the transition if necessary.
 
-            // We have no active transitions.
+            // We have no active transitions. Or the notified activity whose visibility changed is
+            // no longer the launched activity, then we can still wait to get onWindowsDrawn.
             if (info == null) {
                 return;
             }
 
-            // The notified activity whose visibility changed is no longer the launched activity.
-            // We can still wait to get onWindowsDrawn.
-            if (info.mLastLaunchedActivity != r) {
-                return;
-            }
-
             // If the task of the launched activity contains any activity to be drawn, then the
             // window drawn event should report later to complete the transition. Otherwise all
             // activities in this task may be finished, invisible or drawn, so the transition event
@@ -945,7 +906,6 @@
             }
             logAppTransitionFinished(info, isHibernating != null ? isHibernating : false);
         }
-        info.mPendingDrawActivities.clear();
         mTransitionInfoList.remove(info);
     }
 
@@ -1122,7 +1082,7 @@
         if (info == null) {
             return null;
         }
-        if (!info.allDrawn() && info.mPendingFullyDrawn == null) {
+        if (!info.mIsDrawn && info.mPendingFullyDrawn == null) {
             // There are still undrawn activities, postpone reporting fully drawn until all of its
             // windows are drawn. So that is closer to an usable state.
             info.mPendingFullyDrawn = () -> {
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index cddb1e7..badb1f5 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -119,8 +119,12 @@
 
                 if (adjacentTaskFragments != null && adjacentTaskFragments.contains(
                         childTaskFragment)) {
-                    // Everything behind two adjacent TaskFragments are occluded.
-                    mBehindFullyOccludedContainer = true;
+                    if (!childTaskFragment.isTranslucent(starting)
+                            && !childTaskFragment.getAdjacentTaskFragment().isTranslucent(
+                                    starting)) {
+                        // Everything behind two adjacent TaskFragments are occluded.
+                        mBehindFullyOccludedContainer = true;
+                    }
                     continue;
                 }
 
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index c69a734..40588f4 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1177,6 +1177,12 @@
             sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
             return;
         }
+        if (!ownerActivity.isResizeable()) {
+            final IllegalArgumentException exception = new IllegalArgumentException("Not allowed"
+                    + " to operate with non-resizable owner Activity");
+            sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
+            return;
+        }
         // The ownerActivity has to belong to the same app as the root Activity of the target Task.
         final ActivityRecord rootActivity = ownerActivity.getTask().getRootActivity();
         if (rootActivity.getUid() != ownerActivity.getUid()) {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 2a5bb18..df975cd 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -40,7 +40,6 @@
 import android.os.IBinder;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
@@ -54,13 +53,16 @@
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 /**
  * APCT tests for {@link AccessibilityManagerService}.
  */
-public class AccessibilityManagerServiceTest extends AndroidTestCase {
+public class AccessibilityManagerServiceTest {
     private static final String TAG = "A11Y_MANAGER_SERVICE_TEST";
     private static final int ACTION_ID = 20;
     private static final String LABEL = "label";
@@ -104,8 +106,8 @@
     private AccessibilityServiceConnection mAccessibilityServiceConnection;
     private AccessibilityManagerService mA11yms;
 
-    @Override
-    protected void setUp() throws Exception {
+    @Before
+    public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         LocalServices.removeServiceForTest(WindowManagerInternal.class);
         LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class);
@@ -167,44 +169,48 @@
     }
 
     @SmallTest
+    @Test
     public void testRegisterSystemActionWithoutPermission() throws Exception {
         doThrow(SecurityException.class).when(mMockSecurityPolicy)
                 .enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY);
 
         try {
             mA11yms.registerSystemAction(TEST_ACTION, ACTION_ID);
-            fail();
+            Assert.fail();
         } catch (SecurityException expected) {
         }
         verify(mMockSystemActionPerformer, never()).registerSystemAction(ACTION_ID, TEST_ACTION);
     }
 
     @SmallTest
+    @Test
     public void testRegisterSystemAction() throws Exception {
         mA11yms.registerSystemAction(TEST_ACTION, ACTION_ID);
         verify(mMockSystemActionPerformer).registerSystemAction(ACTION_ID, TEST_ACTION);
     }
 
-    @SmallTest
+    @Test
     public void testUnregisterSystemActionWithoutPermission() throws Exception {
         doThrow(SecurityException.class).when(mMockSecurityPolicy)
                 .enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY);
 
         try {
             mA11yms.unregisterSystemAction(ACTION_ID);
-            fail();
+            Assert.fail();
         } catch (SecurityException expected) {
         }
         verify(mMockSystemActionPerformer, never()).unregisterSystemAction(ACTION_ID);
     }
 
     @SmallTest
+    @Test
     public void testUnregisterSystemAction() throws Exception {
         mA11yms.unregisterSystemAction(ACTION_ID);
         verify(mMockSystemActionPerformer).unregisterSystemAction(ACTION_ID);
     }
 
     @SmallTest
+    @Test
     public void testOnSystemActionsChanged() throws Exception {
         setupAccessibilityServiceConnection();
         mA11yms.notifySystemActionsChangedLocked(mUserState);
@@ -213,6 +219,7 @@
     }
 
     @SmallTest
+    @Test
     public void testOnMagnificationTransitionFailed_capabilitiesIsAll_fallBackToPreviousMode() {
         final AccessibilityUserState userState = mA11yms.mUserStates.get(
                 mA11yms.getCurrentUserIdLocked());
@@ -223,7 +230,7 @@
 
         mA11yms.onMagnificationTransitionEndedLocked(false);
 
-        assertEquals(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
+        Assert.assertEquals(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW,
                 userState.getMagnificationModeLocked());
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
index 8e2c1f0..761cea7 100644
--- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
@@ -160,6 +160,64 @@
     }
 
     @Test
+    public void create_stateWithCancelStickyRequestFlag() {
+        String configString = "<device-state-config>\n"
+                + "    <device-state>\n"
+                + "        <identifier>1</identifier>\n"
+                + "        <flags>\n"
+                + "            <flag>FLAG_CANCEL_STICKY_REQUESTS</flag>\n"
+                + "        </flags>\n"
+                + "        <conditions/>\n"
+                + "    </device-state>\n"
+                + "    <device-state>\n"
+                + "        <identifier>2</identifier>\n"
+                + "        <conditions/>\n"
+                + "    </device-state>\n"
+                + "</device-state-config>\n";
+        DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString);
+        DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(mContext,
+                config);
+
+        DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
+        provider.setListener(listener);
+
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        final DeviceState[] expectedStates = new DeviceState[]{
+                new DeviceState(1, "", DeviceState.FLAG_CANCEL_STICKY_REQUESTS),
+                new DeviceState(2, "", 0 /* flags */) };
+        assertArrayEquals(expectedStates, mDeviceStateArrayCaptor.getValue());
+    }
+
+    @Test
+    public void create_stateWithInvalidFlag() {
+        String configString = "<device-state-config>\n"
+                + "    <device-state>\n"
+                + "        <identifier>1</identifier>\n"
+                + "        <flags>\n"
+                + "            <flag>INVALID_FLAG</flag>\n"
+                + "        </flags>\n"
+                + "        <conditions/>\n"
+                + "    </device-state>\n"
+                + "    <device-state>\n"
+                + "        <identifier>2</identifier>\n"
+                + "        <conditions/>\n"
+                + "    </device-state>\n"
+                + "</device-state-config>\n";
+        DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString);
+        DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(mContext,
+                config);
+
+        DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
+        provider.setListener(listener);
+
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        final DeviceState[] expectedStates = new DeviceState[]{
+                new DeviceState(1, "", 0 /* flags */),
+                new DeviceState(2, "", 0 /* flags */) };
+        assertArrayEquals(expectedStates, mDeviceStateArrayCaptor.getValue());
+    }
+
+    @Test
     public void create_lidSwitch() {
         String configString = "<device-state-config>\n"
                 + "    <device-state>\n"
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
index 0b91802..d4d8b868 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -477,7 +477,6 @@
 
     @Test
     public void testConsecutiveLaunch() {
-        mTrampolineActivity.setState(ActivityRecord.State.INITIALIZING, "test");
         onActivityLaunched(mTrampolineActivity);
         mActivityMetricsLogger.notifyActivityLaunching(mTopActivity.intent,
                 mTrampolineActivity /* caller */, mTrampolineActivity.getUid());
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index cfc145a..54d3af5 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -963,6 +963,21 @@
             = "carrier_use_ims_first_for_emergency_bool";
 
     /**
+     * When {@code true}, the determination of whether to place a call as an emergency call will be
+     * based on the known {@link android.telephony.emergency.EmergencyNumber}s for the SIM on which
+     * the call is being placed.  In a dual SIM scenario, if Sim A has the emergency numbers
+     * 123, 456 and Sim B has the emergency numbers 789, and the user places a call on SIM A to 789,
+     * it will not be treated as an emergency call in this case.
+     * When {@code false}, the determination is based on the emergency numbers from all device SIMs,
+     * regardless of which SIM the call is being placed on.  If Sim A has the emergency numbers
+     * 123, 456 and Sim B has the emergency numbers 789, and the user places a call on SIM A to 789,
+     * the call will be dialed as an emergency number, but with an unspecified routing.
+     * @hide
+     */
+    public static final String KEY_USE_ONLY_DIALED_SIM_ECC_LIST_BOOL =
+            "use_only_dialed_sim_ecc_list_bool";
+
+    /**
      * When IMS instant lettering is available for a carrier (see
      * {@link #KEY_CARRIER_INSTANT_LETTERING_AVAILABLE_BOOL}), determines the list of characters
      * which may not be contained in messages.  Should be specified as a regular expression suitable
@@ -5265,6 +5280,7 @@
         sDefaults.putBoolean(KEY_CARRIER_IMS_GBA_REQUIRED_BOOL, false);
         sDefaults.putBoolean(KEY_CARRIER_INSTANT_LETTERING_AVAILABLE_BOOL, false);
         sDefaults.putBoolean(KEY_CARRIER_USE_IMS_FIRST_FOR_EMERGENCY_BOOL, true);
+        sDefaults.putBoolean(KEY_USE_ONLY_DIALED_SIM_ECC_LIST_BOOL, false);
         sDefaults.putString(KEY_CARRIER_NETWORK_SERVICE_WWAN_PACKAGE_OVERRIDE_STRING, "");
         sDefaults.putString(KEY_CARRIER_NETWORK_SERVICE_WLAN_PACKAGE_OVERRIDE_STRING, "");
         sDefaults.putString(KEY_CARRIER_QUALIFIED_NETWORKS_SERVICE_PACKAGE_OVERRIDE_STRING, "");