Merge changes I61bfb975,I2cb907c5,I13c60b3b into tm-qpr-dev

* changes:
  [Media TTT] Animate the sender chip out.
  [Chipbar] Define #shouldIgnoreViewRemoval as a specific API on the TemporaryViewDisplayController, instead of having subclasses just override #removeView.
  [Media TTT] Add the background back to the receiver chip.
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
index dc2c6356..1b7e26b 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -361,13 +361,17 @@
          *
          * The end state of the animation is controlled by [destination]. This value can be any of
          * the four corners, any of the four edges, or the center of the view.
+         *
+         * @param onAnimationEnd an optional runnable that will be run once the animation finishes
+         *    successfully. Will not be run if the animation is cancelled.
          */
         @JvmOverloads
         fun animateRemoval(
             rootView: View,
             destination: Hotspot = Hotspot.CENTER,
             interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR,
-            duration: Long = DEFAULT_DURATION
+            duration: Long = DEFAULT_DURATION,
+            onAnimationEnd: Runnable? = null,
         ): Boolean {
             if (
                 !occupiesSpace(
@@ -391,13 +395,28 @@
                 addListener(child, listener, recursive = false)
             }
 
-            // Remove the view so that a layout update is triggered for the siblings and they
-            // animate to their next position while the view's removal is also animating.
-            parent.removeView(rootView)
-            // By adding the view to the overlay, we can animate it while it isn't part of the view
-            // hierarchy. It is correctly positioned because we have its previous bounds, and we set
-            // them manually during the animation.
-            parent.overlay.add(rootView)
+            val viewHasSiblings = parent.childCount > 1
+            if (viewHasSiblings) {
+                // Remove the view so that a layout update is triggered for the siblings and they
+                // animate to their next position while the view's removal is also animating.
+                parent.removeView(rootView)
+                // By adding the view to the overlay, we can animate it while it isn't part of the
+                // view hierarchy. It is correctly positioned because we have its previous bounds,
+                // and we set them manually during the animation.
+                parent.overlay.add(rootView)
+            }
+            // If this view has no siblings, the parent view may shrink to (0,0) size and mess
+            // up the animation if we immediately remove the view. So instead, we just leave the
+            // view in the real hierarchy until the animation finishes.
+
+            val endRunnable = Runnable {
+                if (viewHasSiblings) {
+                    parent.overlay.remove(rootView)
+                } else {
+                    parent.removeView(rootView)
+                }
+                onAnimationEnd?.run()
+            }
 
             val startValues =
                 mapOf(
@@ -430,7 +449,8 @@
                 endValues,
                 interpolator,
                 duration,
-                ephemeral = true
+                ephemeral = true,
+                endRunnable,
             )
 
             if (rootView is ViewGroup) {
@@ -463,7 +483,6 @@
                                 .alpha(0f)
                                 .setInterpolator(Interpolators.ALPHA_OUT)
                                 .setDuration(duration / 2)
-                                .withEndAction { parent.overlay.remove(rootView) }
                                 .start()
                         }
                     }
@@ -477,7 +496,6 @@
                     .setInterpolator(Interpolators.ALPHA_OUT)
                     .setDuration(duration / 2)
                     .setStartDelay(duration / 2)
-                    .withEndAction { parent.overlay.remove(rootView) }
                     .start()
             }
 
diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
index e079fd3..21d12c2 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
@@ -29,6 +29,7 @@
 
     <com.android.internal.widget.CachingIconView
         android:id="@+id/app_icon"
+        android:background="@drawable/media_ttt_chip_background_receiver"
         android:layout_width="@dimen/media_ttt_icon_size_receiver"
         android:layout_height="@dimen/media_ttt_icon_size_receiver"
         android:layout_gravity="center|bottom"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f7019dc..8fdfc89 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1056,9 +1056,8 @@
     <!-- Media tap-to-transfer chip for receiver device -->
     <dimen name="media_ttt_chip_size_receiver">100dp</dimen>
     <dimen name="media_ttt_icon_size_receiver">95dp</dimen>
-    <!-- Since the generic icon isn't circular, we need to scale it down so it still fits within
-         the circular chip. -->
-    <dimen name="media_ttt_generic_icon_size_receiver">70dp</dimen>
+    <!-- Add some padding for the generic icon so it doesn't go all the way to the border. -->
+    <dimen name="media_ttt_generic_icon_padding">12dp</dimen>
     <dimen name="media_ttt_receiver_vert_translation">20dp</dimen>
 
     <!-- Window magnification -->
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index 792ae7c..c3de94f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
@@ -19,7 +19,6 @@
 import android.content.Context
 import android.content.pm.PackageManager
 import android.graphics.drawable.Drawable
-import com.android.internal.widget.CachingIconView
 import com.android.settingslib.Utils
 import com.android.systemui.R
 
@@ -76,29 +75,6 @@
                 isAppIcon = false
             )
         }
-
-        /**
-         * Sets an icon to be displayed by the given view.
-         *
-         * @param iconSize the size in pixels that the icon should be. If null, the size of
-         * [appIconView] will not be adjusted.
-         */
-        fun setIcon(
-            appIconView: CachingIconView,
-            icon: Drawable,
-            iconContentDescription: CharSequence,
-            iconSize: Int? = null,
-        ) {
-            iconSize?.let { size ->
-                val lp = appIconView.layoutParams
-                lp.width = size
-                lp.height = size
-                appIconView.layoutParams = lp
-            }
-
-            appIconView.contentDescription = iconContentDescription
-            appIconView.setImageDrawable(icon)
-        }
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index dfd9e22..8fc5519 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -30,6 +30,7 @@
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
+import com.android.internal.widget.CachingIconView
 import com.android.settingslib.Utils
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
@@ -146,20 +147,17 @@
         )
         val iconDrawable = newInfo.appIconDrawableOverride ?: iconInfo.drawable
         val iconContentDescription = newInfo.appNameOverride ?: iconInfo.contentDescription
-        val iconSize = context.resources.getDimensionPixelSize(
+        val iconPadding =
             if (iconInfo.isAppIcon) {
-                R.dimen.media_ttt_icon_size_receiver
+                0
             } else {
-                R.dimen.media_ttt_generic_icon_size_receiver
+                context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding)
             }
-        )
 
-        MediaTttUtils.setIcon(
-            currentView.requireViewById(R.id.app_icon),
-            iconDrawable,
-            iconContentDescription,
-            iconSize,
-        )
+        val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon)
+        iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding)
+        iconView.setImageDrawable(iconDrawable)
+        iconView.contentDescription = iconContentDescription
     }
 
     override fun animateViewIn(view: ViewGroup) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index 007eb8f..11c5528 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -29,6 +29,7 @@
 import android.view.accessibility.AccessibilityManager
 import android.widget.TextView
 import com.android.internal.statusbar.IUndoMediaTransferCallback
+import com.android.internal.widget.CachingIconView
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
@@ -53,7 +54,7 @@
  * chip is shown when a user is transferring media to/from this device and a receiver device.
  */
 @SysUISingleton
-class MediaTttChipControllerSender @Inject constructor(
+open class MediaTttChipControllerSender @Inject constructor(
         commandQueue: CommandQueue,
         context: Context,
         @MediaTttSenderLogger logger: MediaTttLogger,
@@ -145,11 +146,9 @@
         val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
             context, newInfo.routeInfo.clientPackageName, logger
         )
-        MediaTttUtils.setIcon(
-            currentView.requireViewById(R.id.app_icon),
-            iconInfo.drawable,
-            iconInfo.contentDescription
-        )
+        val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon)
+        iconView.setImageDrawable(iconInfo.drawable)
+        iconView.contentDescription = iconInfo.contentDescription
 
         // Text
         val otherDeviceName = newInfo.routeInfo.name.toString()
@@ -196,7 +195,19 @@
         )
     }
 
-    override fun removeView(removalReason: String) {
+    override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+        ViewHierarchyAnimator.animateRemoval(
+            view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner),
+            ViewHierarchyAnimator.Hotspot.TOP,
+            Interpolators.EMPHASIZED_ACCELERATE,
+            ANIMATION_DURATION,
+            onAnimationEnd,
+        )
+        // TODO(b/203800644): Add includeMargins as an option to ViewHierarchyAnimator so that the
+        //   animateChipOut matches the animateChipIn.
+    }
+
+    override fun shouldIgnoreViewRemoval(removalReason: String): Boolean {
         // Don't remove the chip if we're in progress or succeeded, since the user should still be
         // able to see the status of the transfer. (But do remove it if it's finally timed out.)
         val transferStatus = info?.state?.transferStatus
@@ -208,9 +219,9 @@
             logger.logRemovalBypass(
                 removalReason, bypassReason = "transferStatus=${transferStatus.name}"
             )
-            return
+            return true
         }
-        super.removeView(removalReason)
+        return false
     }
 
     private fun Boolean.visibleIfTrue(): Int {
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index a52e2af..91e20ee 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -167,11 +167,19 @@
      * @param removalReason a short string describing why the view was removed (timeout, state
      *     change, etc.)
      */
-    open fun removeView(removalReason: String) {
-        if (view == null) { return }
+    fun removeView(removalReason: String) {
+        if (shouldIgnoreViewRemoval(removalReason)) {
+            return
+        }
+        val currentView = view ?: return
+
+        animateViewOut(currentView) { windowManager.removeView(currentView) }
+
         logger.logChipRemoval(removalReason)
         configurationController.removeCallback(displayScaleListener)
-        windowManager.removeView(view)
+        // Re-set the view to null immediately (instead as part of the animation end runnable) so
+        // that if a new view event comes in while this view is animating out, we still display the
+        // new view appropriately.
         view = null
         info = null
         // No need to time the view out since it's already gone
@@ -179,6 +187,13 @@
     }
 
     /**
+     * Returns true if a view removal request should be ignored and false otherwise.
+     *
+     * Allows subclasses to keep the view visible for longer in certain circumstances.
+     */
+    open fun shouldIgnoreViewRemoval(removalReason: String): Boolean = false
+
+    /**
      * A method implemented by subclasses to update [currentView] based on [newInfo].
      */
     @CallSuper
@@ -190,7 +205,17 @@
      * A method that can be implemented by subclasses to do custom animations for when the view
      * appears.
      */
-    open fun animateViewIn(view: ViewGroup) {}
+    internal open fun animateViewIn(view: ViewGroup) {}
+
+    /**
+     * A method that can be implemented by subclasses to do custom animations for when the view
+     * disappears.
+     *
+     * @param onAnimationEnd an action that *must* be run once the animation finishes successfully.
+     */
+    internal open fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+        onAnimationEnd.run()
+    }
 }
 
 object TemporaryDisplayRemovalReason {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
index 8fc0489..986e7cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
@@ -718,7 +718,7 @@
     }
 
     @Test
-    fun animatesViewRemovalFromStartToEnd() {
+    fun animatesViewRemovalFromStartToEnd_viewHasSiblings() {
         setUpRootWithChildren()
 
         val child = rootView.getChildAt(0)
@@ -742,6 +742,35 @@
     }
 
     @Test
+    fun animatesViewRemovalFromStartToEnd_viewHasNoSiblings() {
+        rootView = LinearLayout(mContext)
+        (rootView as LinearLayout).orientation = LinearLayout.HORIZONTAL
+        (rootView as LinearLayout).weightSum = 1f
+
+        val onlyChild = View(mContext)
+        rootView.addView(onlyChild)
+        forceLayout()
+
+        val success = ViewHierarchyAnimator.animateRemoval(
+            onlyChild,
+            destination = ViewHierarchyAnimator.Hotspot.LEFT,
+            interpolator = Interpolators.LINEAR
+        )
+
+        assertTrue(success)
+        assertNotNull(onlyChild.getTag(R.id.tag_animator))
+        checkBounds(onlyChild, l = 0, t = 0, r = 200, b = 100)
+        advanceAnimation(onlyChild, 0.5f)
+        checkBounds(onlyChild, l = 0, t = 0, r = 100, b = 100)
+        advanceAnimation(onlyChild, 1.0f)
+        checkBounds(onlyChild, l = 0, t = 0, r = 0, b = 100)
+        endAnimation(rootView)
+        endAnimation(onlyChild)
+        assertEquals(0, rootView.childCount)
+        assertFalse(onlyChild in rootView.children)
+    }
+
+    @Test
     fun animatesViewRemovalRespectingDestination() {
         // CENTER
         setUpRootWithChildren()
@@ -964,6 +993,60 @@
     }
 
     @Test
+    fun animateRemoval_runnableRunsWhenAnimationEnds() {
+        var runnableRun = false
+        val onAnimationEndRunnable = { runnableRun = true }
+
+        setUpRootWithChildren()
+        forceLayout()
+        val removedView = rootView.getChildAt(0)
+
+        ViewHierarchyAnimator.animateRemoval(
+            removedView,
+            onAnimationEnd = onAnimationEndRunnable
+        )
+        endAnimation(removedView)
+
+        assertEquals(true, runnableRun)
+    }
+
+    @Test
+    fun animateRemoval_runnableDoesNotRunWhenAnimationCancelled() {
+        var runnableRun = false
+        val onAnimationEndRunnable = { runnableRun = true }
+
+        setUpRootWithChildren()
+        forceLayout()
+        val removedView = rootView.getChildAt(0)
+
+        ViewHierarchyAnimator.animateRemoval(
+            removedView,
+            onAnimationEnd = onAnimationEndRunnable
+        )
+        cancelAnimation(removedView)
+
+        assertEquals(false, runnableRun)
+    }
+
+    @Test
+    fun animationRemoval_runnableDoesNotRunWhenOnlyPartwayThroughAnimation() {
+        var runnableRun = false
+        val onAnimationEndRunnable = { runnableRun = true }
+
+        setUpRootWithChildren()
+        forceLayout()
+        val removedView = rootView.getChildAt(0)
+
+        ViewHierarchyAnimator.animateRemoval(
+            removedView,
+            onAnimationEnd = onAnimationEndRunnable
+        )
+        advanceAnimation(removedView, 0.5f)
+
+        assertEquals(false, runnableRun)
+    }
+
+    @Test
     fun cleansUpListenersCorrectly() {
         val firstChild = View(mContext)
         firstChild.layoutParams = LinearLayout.LayoutParams(50 /* width */, 100 /* height */)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index 37f6434..7c83cb7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
@@ -19,9 +19,7 @@
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.graphics.drawable.Drawable
-import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
-import com.android.internal.widget.CachingIconView
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.any
@@ -90,48 +88,6 @@
         assertThat(iconInfo.drawable).isEqualTo(appIconFromPackageName)
         assertThat(iconInfo.contentDescription).isEqualTo(APP_NAME)
     }
-
-    @Test
-    fun setIcon_viewHasIconAndContentDescription() {
-        val view = CachingIconView(context)
-        val icon = context.getDrawable(R.drawable.ic_celebration)!!
-        val contentDescription = "Happy birthday!"
-
-        MediaTttUtils.setIcon(view, icon, contentDescription)
-
-        assertThat(view.drawable).isEqualTo(icon)
-        assertThat(view.contentDescription).isEqualTo(contentDescription)
-    }
-
-    @Test
-    fun setIcon_iconSizeNull_viewSizeDoesNotChange() {
-        val view = CachingIconView(context)
-        val size = 456
-        view.layoutParams = FrameLayout.LayoutParams(size, size)
-
-        MediaTttUtils.setIcon(view, context.getDrawable(R.drawable.ic_cake)!!, "desc")
-
-        assertThat(view.layoutParams.width).isEqualTo(size)
-        assertThat(view.layoutParams.height).isEqualTo(size)
-    }
-
-    @Test
-    fun setIcon_iconSizeProvided_viewSizeUpdates() {
-        val view = CachingIconView(context)
-        val size = 456
-        view.layoutParams = FrameLayout.LayoutParams(size, size)
-
-        val newSize = 40
-        MediaTttUtils.setIcon(
-            view,
-            context.getDrawable(R.drawable.ic_cake)!!,
-            "desc",
-            iconSize = newSize
-        )
-
-        assertThat(view.layoutParams.width).isEqualTo(newSize)
-        assertThat(view.layoutParams.height).isEqualTo(newSize)
-    }
 }
 
 private const val PACKAGE_NAME = "com.android.systemui"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index d41ad48..775dc11 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -212,35 +212,27 @@
     }
 
     @Test
-    fun updateView_isAppIcon_usesAppIconSize() {
+    fun updateView_isAppIcon_usesAppIconPadding() {
         controllerReceiver.displayView(getChipReceiverInfo(packageName = PACKAGE_NAME))
+
         val chipView = getChipView()
-
-        chipView.measure(
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
-        )
-
-        val expectedSize =
-            context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver)
-        assertThat(chipView.getAppIconView().measuredWidth).isEqualTo(expectedSize)
-        assertThat(chipView.getAppIconView().measuredHeight).isEqualTo(expectedSize)
+        assertThat(chipView.getAppIconView().paddingLeft).isEqualTo(0)
+        assertThat(chipView.getAppIconView().paddingRight).isEqualTo(0)
+        assertThat(chipView.getAppIconView().paddingTop).isEqualTo(0)
+        assertThat(chipView.getAppIconView().paddingBottom).isEqualTo(0)
     }
 
     @Test
-    fun updateView_notAppIcon_usesGenericIconSize() {
+    fun updateView_notAppIcon_usesGenericIconPadding() {
         controllerReceiver.displayView(getChipReceiverInfo(packageName = null))
+
         val chipView = getChipView()
-
-        chipView.measure(
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
-        )
-
-        val expectedSize =
-            context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_size_receiver)
-        assertThat(chipView.getAppIconView().measuredWidth).isEqualTo(expectedSize)
-        assertThat(chipView.getAppIconView().measuredHeight).isEqualTo(expectedSize)
+        val expectedPadding =
+            context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding)
+        assertThat(chipView.getAppIconView().paddingLeft).isEqualTo(expectedPadding)
+        assertThat(chipView.getAppIconView().paddingRight).isEqualTo(expectedPadding)
+        assertThat(chipView.getAppIconView().paddingTop).isEqualTo(expectedPadding)
+        assertThat(chipView.getAppIconView().paddingBottom).isEqualTo(expectedPadding)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
index 098086a..eca3bed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media.taptotransfer.sender
 
 import android.app.StatusBarManager
+import android.content.Context
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.graphics.drawable.Drawable
@@ -37,9 +38,11 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -61,7 +64,7 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class MediaTttChipControllerSenderTest : SysuiTestCase() {
-    private lateinit var controllerSender: MediaTttChipControllerSender
+    private lateinit var controllerSender: TestMediaTttChipControllerSender
 
     @Mock
     private lateinit var packageManager: PackageManager
@@ -116,7 +119,7 @@
         whenever(lazyFalsingManager.get()).thenReturn(falsingManager)
         whenever(lazyFalsingCollector.get()).thenReturn(falsingCollector)
 
-        controllerSender = MediaTttChipControllerSender(
+        controllerSender = TestMediaTttChipControllerSender(
             commandQueue,
             context,
             logger,
@@ -821,6 +824,37 @@
     /** Helper method providing default parameters to not clutter up the tests. */
     private fun transferToThisDeviceFailed() =
         ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
+
+    private class TestMediaTttChipControllerSender(
+        commandQueue: CommandQueue,
+        context: Context,
+        @MediaTttReceiverLogger logger: MediaTttLogger,
+        windowManager: WindowManager,
+        mainExecutor: DelayableExecutor,
+        accessibilityManager: AccessibilityManager,
+        configurationController: ConfigurationController,
+        powerManager: PowerManager,
+        uiEventLogger: MediaTttSenderUiEventLogger,
+        falsingManager: Lazy<FalsingManager>,
+        falsingCollector: Lazy<FalsingCollector>,
+    ) : MediaTttChipControllerSender(
+        commandQueue,
+        context,
+        logger,
+        windowManager,
+        mainExecutor,
+        accessibilityManager,
+        configurationController,
+        powerManager,
+        uiEventLogger,
+        falsingManager,
+        falsingCollector,
+    ) {
+        override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+            // Just bypass the animation in tests
+            onAnimationEnd.run()
+        }
+    }
 }
 
 private const val APP_NAME = "Fake app name"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index 921b7ef..7cb2806 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -61,6 +61,8 @@
     @Mock
     private lateinit var powerManager: PowerManager
 
+    private var shouldIgnoreViewRemoval: Boolean = false
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -205,6 +207,26 @@
         verify(windowManager, never()).removeView(any())
     }
 
+    @Test
+    fun removeView_shouldIgnoreRemovalFalse_viewRemoved() {
+        shouldIgnoreViewRemoval = false
+        underTest.displayView(getState())
+
+        underTest.removeView("reason")
+
+        verify(windowManager).removeView(any())
+    }
+
+    @Test
+    fun removeView_shouldIgnoreRemovalTrue_viewNotRemoved() {
+        shouldIgnoreViewRemoval = true
+        underTest.displayView(getState())
+
+        underTest.removeView("reason")
+
+        verify(windowManager, never()).removeView(any())
+    }
+
     private fun getState(name: String = "name") = ViewInfo(name)
 
     private fun getConfigurationListener(): ConfigurationListener {
@@ -240,6 +262,10 @@
             super.updateView(newInfo, currentView)
             mostRecentViewInfo = newInfo
         }
+
+        override fun shouldIgnoreViewRemoval(removalReason: String): Boolean {
+            return shouldIgnoreViewRemoval
+        }
     }
 
     inner class ViewInfo(val name: String) : TemporaryViewInfo {