Merge "Mark existing Launcher CUJs as Platinum tests." into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 3e1a6ae..a9d50b9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -40,6 +40,8 @@
 
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * A view that displays a recent task during a keyboard quick switch.
  */
@@ -96,17 +98,18 @@
         Resources resources = mContext.getResources();
 
         Preconditions.assertNotNull(mContent);
-        mBorderAnimator = new BorderAnimator(
+        mBorderAnimator = BorderAnimator.createScalingBorderAnimator(
                 /* borderRadiusPx= */ resources.getDimensionPixelSize(
                         R.dimen.keyboard_quick_switch_task_view_radius),
-                /* borderColor= */ mBorderColor,
-                /* borderAnimationParams= */ new BorderAnimator.ScalingParams(
-                        /* borderWidthPx= */ resources.getDimensionPixelSize(
+                /* borderWidthPx= */ resources.getDimensionPixelSize(
                                 R.dimen.keyboard_quick_switch_border_width),
-                        /* boundsBuilder= */ bounds -> bounds.set(
-                                0, 0, getWidth(), getHeight()),
-                        /* targetView= */ this,
-                        /* contentView= */ mContent));
+                /* boundsBuilder= */ bounds -> {
+                    bounds.set(0, 0, getWidth(), getHeight());
+                    return Unit.INSTANCE;
+                },
+                /* targetView= */ this,
+                /* contentView= */ mContent,
+                /* borderColor= */ mBorderColor);
     }
 
     @Nullable
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.java b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
deleted file mode 100644
index 7563187..0000000
--- a/quickstep/src/com/android/quickstep/util/BorderAnimator.java
+++ /dev/null
@@ -1,352 +0,0 @@
-/*
- * Copyright (C) 2023 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.quickstep.util;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.annotation.ColorInt;
-import android.annotation.Nullable;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.view.View;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Px;
-
-import com.android.app.animation.Interpolators;
-import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.anim.AnimatorListeners;
-
-/**
- * Utility class for drawing a rounded-rect border around a view.
- * <p>
- * To use this class:
- * 1. Create an instance in the target view. NOTE: The border will animate outwards from the
- *      provided border bounds. See {@link SimpleParams} and {@link ScalingParams} to determine
- *      which would be best for your target view.
- * 2. Override the target view's {@link android.view.View#draw(Canvas)} method and call
- *      {@link BorderAnimator#drawBorder(Canvas)} after {@code super.draw(canvas)}.
- * 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation or call
- *      {@link BorderAnimator#setBorderVisible(boolean)} where appropriate.
- */
-public final class BorderAnimator {
-
-    public static final int DEFAULT_BORDER_COLOR = Color.WHITE;
-
-    private static final long DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300;
-    private static final long DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133;
-    private static final Interpolator DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE;
-
-    @NonNull private final AnimatedFloat mBorderAnimationProgress = new AnimatedFloat(
-            this::updateOutline);
-    @Px private final int mBorderRadiusPx;
-    @NonNull private final BorderAnimationParams mBorderAnimationParams;
-    private final long mAppearanceDurationMs;
-    private final long mDisappearanceDurationMs;
-    @NonNull private final Interpolator mInterpolator;
-    @NonNull private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
-    @Nullable private Animator mRunningBorderAnimation;
-
-    public BorderAnimator(
-            @Px int borderRadiusPx,
-            @ColorInt int borderColor,
-            @NonNull BorderAnimationParams borderAnimationParams) {
-        this(borderRadiusPx,
-                borderColor,
-                borderAnimationParams,
-                DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
-                DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
-                DEFAULT_INTERPOLATOR);
-    }
-
-    /**
-     * @param borderRadiusPx the radius of the border's corners, in pixels
-     * @param borderColor the border's color
-     * @param borderAnimationParams params for handling different target view layout situation.
-     * @param appearanceDurationMs appearance animation duration, in milliseconds
-     * @param disappearanceDurationMs disappearance animation duration, in milliseconds
-     * @param interpolator animation interpolator
-     */
-    public BorderAnimator(
-            @Px int borderRadiusPx,
-            @ColorInt int borderColor,
-            @NonNull BorderAnimationParams borderAnimationParams,
-            long appearanceDurationMs,
-            long disappearanceDurationMs,
-            @NonNull Interpolator interpolator) {
-        mBorderRadiusPx = borderRadiusPx;
-        mBorderAnimationParams = borderAnimationParams;
-        mAppearanceDurationMs = appearanceDurationMs;
-        mDisappearanceDurationMs = disappearanceDurationMs;
-        mInterpolator = interpolator;
-
-        mBorderPaint.setColor(borderColor);
-        mBorderPaint.setStyle(Paint.Style.STROKE);
-        mBorderPaint.setAlpha(0);
-    }
-
-    private void updateOutline() {
-        float interpolatedProgress = mInterpolator.getInterpolation(
-                mBorderAnimationProgress.value);
-
-        mBorderAnimationParams.setProgress(interpolatedProgress);
-        mBorderPaint.setAlpha(Math.round(255 * interpolatedProgress));
-        mBorderPaint.setStrokeWidth(mBorderAnimationParams.getBorderWidth());
-        mBorderAnimationParams.mTargetView.invalidate();
-    }
-
-    /**
-     * Draws the border on the given canvas.
-     * <p>
-     * Call this method in the target view's {@link android.view.View#draw(Canvas)} method after
-     * calling super.
-     */
-    public void drawBorder(Canvas canvas) {
-        float alignmentAdjustment = mBorderAnimationParams.getAlignmentAdjustment();
-        canvas.drawRoundRect(
-                /* left= */ mBorderAnimationParams.mBorderBounds.left + alignmentAdjustment,
-                /* top= */ mBorderAnimationParams.mBorderBounds.top + alignmentAdjustment,
-                /* right= */ mBorderAnimationParams.mBorderBounds.right - alignmentAdjustment,
-                /* bottom= */ mBorderAnimationParams.mBorderBounds.bottom - alignmentAdjustment,
-                /* rx= */ mBorderRadiusPx + mBorderAnimationParams.getRadiusAdjustment(),
-                /* ry= */ mBorderRadiusPx + mBorderAnimationParams.getRadiusAdjustment(),
-                /* paint= */ mBorderPaint);
-    }
-
-    /**
-     * Builds the border appearance/disappearance animation.
-     */
-    @NonNull
-    public Animator buildAnimator(boolean isAppearing) {
-        mRunningBorderAnimation = mBorderAnimationProgress.animateToValue(isAppearing ? 1f : 0f);
-        mRunningBorderAnimation.setDuration(
-                isAppearing ? mAppearanceDurationMs : mDisappearanceDurationMs);
-
-        mRunningBorderAnimation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                mBorderAnimationParams.onShowBorder();
-            }
-        });
-        mRunningBorderAnimation.addListener(
-                AnimatorListeners.forEndCallback(() -> {
-                    mRunningBorderAnimation = null;
-                    if (isAppearing) {
-                        return;
-                    }
-                    mBorderAnimationParams.onHideBorder();
-                }));
-
-        return mRunningBorderAnimation;
-    }
-
-    /**
-     * Immediately shows/hides the border without an animation.
-     * <p>
-     * To animate the appearance/disappearance, see {@link BorderAnimator#buildAnimator(boolean)}
-     */
-    public void setBorderVisible(boolean visible) {
-        if (mRunningBorderAnimation != null) {
-            mRunningBorderAnimation.end();
-        }
-        if (visible) {
-            mBorderAnimationParams.onShowBorder();
-        }
-        mBorderAnimationProgress.updateValue(visible ? 1f : 0f);
-        if (!visible) {
-            mBorderAnimationParams.onHideBorder();
-        }
-    }
-
-    /**
-     * Callback to update the border bounds when building this animation.
-     */
-    public interface BorderBoundsBuilder {
-
-        /**
-         * Sets the given rect to the most up-to-date bounds.
-         */
-        void updateBorderBounds(Rect rect);
-    }
-
-    /**
-     * Params for handling different target view layout situation.
-     */
-    private abstract static class BorderAnimationParams {
-
-        @NonNull private final Rect mBorderBounds = new Rect();
-        @NonNull private final BorderBoundsBuilder mBoundsBuilder;
-
-        @NonNull final View mTargetView;
-        @Px final int mBorderWidthPx;
-
-        private float mAnimationProgress = 0f;
-        @Nullable private View.OnLayoutChangeListener mLayoutChangeListener;
-
-        /**
-         * @param borderWidthPx the width of the border, in pixels
-         * @param boundsBuilder callback to update the border bounds
-         * @param targetView the view that will be drawing the border
-         */
-        private BorderAnimationParams(
-                @Px int borderWidthPx,
-                @NonNull BorderBoundsBuilder boundsBuilder,
-                @NonNull View targetView) {
-            mBorderWidthPx = borderWidthPx;
-            mBoundsBuilder = boundsBuilder;
-            mTargetView = targetView;
-        }
-
-        private void setProgress(float progress) {
-            mAnimationProgress = progress;
-        }
-
-        private float getBorderWidth() {
-            return mBorderWidthPx * mAnimationProgress;
-        }
-
-        float getAlignmentAdjustment() {
-            // Outset the border by half the width to create an outwards-growth animation
-            return (-getBorderWidth() / 2f) + getAlignmentAdjustmentInset();
-        }
-
-
-        void onShowBorder() {
-            if (mLayoutChangeListener == null) {
-                mLayoutChangeListener =
-                        (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                            onShowBorder();
-                            mTargetView.invalidate();
-                        };
-                mTargetView.addOnLayoutChangeListener(mLayoutChangeListener);
-            }
-            mBoundsBuilder.updateBorderBounds(mBorderBounds);
-        }
-
-        void onHideBorder() {
-            if (mLayoutChangeListener != null) {
-                mTargetView.removeOnLayoutChangeListener(mLayoutChangeListener);
-                mLayoutChangeListener = null;
-            }
-        }
-
-        abstract int getAlignmentAdjustmentInset();
-
-        abstract float getRadiusAdjustment();
-    }
-
-    /**
-     * Use an instance of this {@link BorderAnimationParams} if the border can be drawn outside the
-     * target view's bounds without any additional logic.
-     */
-    public static final class SimpleParams extends BorderAnimationParams {
-
-        public SimpleParams(
-                @Px int borderWidthPx,
-                @NonNull BorderBoundsBuilder boundsBuilder,
-                @NonNull View targetView) {
-            super(borderWidthPx, boundsBuilder, targetView);
-        }
-
-        @Override
-        int getAlignmentAdjustmentInset() {
-            return 0;
-        }
-
-        @Override
-        float getRadiusAdjustment() {
-            return -getAlignmentAdjustment();
-        }
-    }
-
-    /**
-     * Use an instance of this {@link BorderAnimationParams} if the border would other be clipped by
-     * the target view's bound.
-     * <p>
-     * Note: using these params will set the scales and pivots of the
-     * container and content views, however will only reset the scales back to 1.
-     */
-    public static final class ScalingParams extends BorderAnimationParams {
-
-        @NonNull private final View mContentView;
-
-        /**
-         * @param targetView the view that will be drawing the border. this view will be scaled up
-         *                   to make room for the border
-         * @param contentView the view around which the border will be drawn. this view will be
-         *                    scaled down reciprocally to keep its original size and location.
-         */
-        public ScalingParams(
-                @Px int borderWidthPx,
-                @NonNull BorderBoundsBuilder boundsBuilder,
-                @NonNull View targetView,
-                @NonNull View contentView) {
-            super(borderWidthPx, boundsBuilder, targetView);
-            mContentView = contentView;
-        }
-
-        @Override
-        void onShowBorder() {
-            super.onShowBorder();
-            float width = mTargetView.getWidth();
-            float height = mTargetView.getHeight();
-            // Scale up just enough to make room for the border. Fail fast and fix the scaling
-            // onLayout.
-            float scaleX = width == 0 ? 1f : 1f + ((2 * mBorderWidthPx) / width);
-            float scaleY = height == 0 ? 1f : 1f + ((2 * mBorderWidthPx) / height);
-
-            mTargetView.setPivotX(width / 2);
-            mTargetView.setPivotY(height / 2);
-            mTargetView.setScaleX(scaleX);
-            mTargetView.setScaleY(scaleY);
-
-            mContentView.setPivotX(mContentView.getWidth() / 2f);
-            mContentView.setPivotY(mContentView.getHeight() / 2f);
-            mContentView.setScaleX(1f / scaleX);
-            mContentView.setScaleY(1f / scaleY);
-        }
-
-        @Override
-        void onHideBorder() {
-            super.onHideBorder();
-            mTargetView.setPivotX(mTargetView.getWidth());
-            mTargetView.setPivotY(mTargetView.getHeight());
-            mTargetView.setScaleX(1f);
-            mTargetView.setScaleY(1f);
-
-            mContentView.setPivotX(mContentView.getWidth() / 2f);
-            mContentView.setPivotY(mContentView.getHeight() / 2f);
-            mContentView.setScaleX(1f);
-            mContentView.setScaleY(1f);
-        }
-
-        @Override
-        int getAlignmentAdjustmentInset() {
-            // Inset the border since we are scaling the container up
-            return mBorderWidthPx;
-        }
-
-        @Override
-        float getRadiusAdjustment() {
-            // Increase the radius since we are scaling the container up
-            return getAlignmentAdjustment();
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
new file mode 100644
index 0000000..44eb070
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2023 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.quickstep.util
+
+import android.animation.Animator
+import android.annotation.ColorInt
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.view.View
+import android.view.View.OnLayoutChangeListener
+import android.view.animation.Interpolator
+import androidx.annotation.Px
+import androidx.core.animation.doOnEnd
+import androidx.core.animation.doOnStart
+import com.android.app.animation.Interpolators
+import com.android.launcher3.anim.AnimatedFloat
+import kotlin.math.roundToInt
+
+/**
+ * Utility class for drawing a rounded-rect border around a view.
+ *
+ * To use this class:
+ * 1. Create an instance in the target view. NOTE: The border will animate outwards from the
+ *    provided border bounds.
+ * 2. Override the target view's [View.draw] method and call [drawBorder] after
+ *    `super.draw(canvas)`.
+ * 3. Call [buildAnimator] and start the animation or call [setBorderVisibility] where appropriate.
+ */
+class BorderAnimator
+private constructor(
+    @field:Px @param:Px private val borderRadiusPx: Int,
+    @ColorInt borderColor: Int,
+    private val borderAnimationParams: BorderAnimationParams,
+    private val appearanceDurationMs: Long,
+    private val disappearanceDurationMs: Long,
+    private val interpolator: Interpolator,
+) {
+    private val borderAnimationProgress = AnimatedFloat { updateOutline() }
+    private val borderPaint =
+        Paint(Paint.ANTI_ALIAS_FLAG).apply {
+            color = borderColor
+            style = Paint.Style.STROKE
+            alpha = 0
+        }
+    private var runningBorderAnimation: Animator? = null
+
+    companion object {
+        const val DEFAULT_BORDER_COLOR = Color.WHITE
+        private const val DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300L
+        private const val DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133L
+        private val DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE
+
+        /**
+         * Creates a BorderAnimator that simply draws the border outside the bound of the target
+         * view.
+         *
+         * Use this method if the border can be drawn outside the target view's bounds without any
+         * additional logic.
+         *
+         * @param borderRadiusPx the radius of the border's corners, in pixels
+         * @param borderWidthPx the width of the border, in pixels
+         * @param boundsBuilder callback to update the border bounds
+         * @param targetView the view that will be drawing the border
+         * @param borderColor the border's color
+         * @param appearanceDurationMs appearance animation duration, in milliseconds
+         * @param disappearanceDurationMs disappearance animation duration, in milliseconds
+         * @param interpolator animation interpolator
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun createSimpleBorderAnimator(
+            @Px borderRadiusPx: Int,
+            @Px borderWidthPx: Int,
+            boundsBuilder: (rect: Rect?) -> Unit,
+            targetView: View,
+            @ColorInt borderColor: Int = DEFAULT_BORDER_COLOR,
+            appearanceDurationMs: Long = DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
+            disappearanceDurationMs: Long = DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
+            interpolator: Interpolator = DEFAULT_INTERPOLATOR,
+        ): BorderAnimator {
+            return BorderAnimator(
+                borderRadiusPx,
+                borderColor,
+                SimpleParams(borderWidthPx, boundsBuilder, targetView),
+                appearanceDurationMs,
+                disappearanceDurationMs,
+                interpolator,
+            )
+        }
+
+        /**
+         * Creates a BorderAnimator that scales the target and content views to draw the border
+         * within the target's bounds without obscuring the content.
+         *
+         * Use this method if the border would otherwise be clipped by the target view's bound.
+         *
+         * Note: using this method will set the scales and pivots of the container and content
+         * views, however will only reset the scales back to 1.
+         *
+         * @param borderRadiusPx the radius of the border's corners, in pixels
+         * @param borderWidthPx the width of the border, in pixels
+         * @param boundsBuilder callback to update the border bounds
+         * @param targetView the view that will be drawing the border
+         * @param contentView the view around which the border will be drawn. this view will be
+         *   scaled down reciprocally to keep its original size and location.
+         * @param borderColor the border's color
+         * @param appearanceDurationMs appearance animation duration, in milliseconds
+         * @param disappearanceDurationMs disappearance animation duration, in milliseconds
+         * @param interpolator animation interpolator
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun createScalingBorderAnimator(
+            @Px borderRadiusPx: Int,
+            @Px borderWidthPx: Int,
+            boundsBuilder: (rect: Rect?) -> Unit,
+            targetView: View,
+            contentView: View,
+            @ColorInt borderColor: Int = DEFAULT_BORDER_COLOR,
+            appearanceDurationMs: Long = DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
+            disappearanceDurationMs: Long = DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
+            interpolator: Interpolator = DEFAULT_INTERPOLATOR,
+        ): BorderAnimator {
+            return BorderAnimator(
+                borderRadiusPx,
+                borderColor,
+                ScalingParams(borderWidthPx, boundsBuilder, targetView, contentView),
+                appearanceDurationMs,
+                disappearanceDurationMs,
+                interpolator,
+            )
+        }
+    }
+
+    private fun updateOutline() {
+        val interpolatedProgress = interpolator.getInterpolation(borderAnimationProgress.value)
+        borderAnimationParams.animationProgress = interpolatedProgress
+        borderPaint.alpha = (255 * interpolatedProgress).roundToInt()
+        borderPaint.strokeWidth = borderAnimationParams.borderWidth
+        borderAnimationParams.targetView.invalidate()
+    }
+
+    /**
+     * Draws the border on the given canvas.
+     *
+     * Call this method in the target view's [View.draw] method after calling super.
+     */
+    fun drawBorder(canvas: Canvas) {
+        with(borderAnimationParams) {
+            val radius = borderRadiusPx + radiusAdjustment
+            canvas.drawRoundRect(
+                /* left= */ borderBounds.left + alignmentAdjustment,
+                /* top= */ borderBounds.top + alignmentAdjustment,
+                /* right= */ borderBounds.right - alignmentAdjustment,
+                /* bottom= */ borderBounds.bottom - alignmentAdjustment,
+                /* rx= */ radius,
+                /* ry= */ radius,
+                /* paint= */ borderPaint
+            )
+        }
+    }
+
+    /** Builds the border appearance/disappearance animation. */
+    fun buildAnimator(isAppearing: Boolean): Animator {
+        return borderAnimationProgress.animateToValue(if (isAppearing) 1f else 0f).apply {
+            duration = if (isAppearing) appearanceDurationMs else disappearanceDurationMs
+            doOnStart {
+                runningBorderAnimation?.cancel()
+                runningBorderAnimation = this
+                borderAnimationParams.onShowBorder()
+            }
+            doOnEnd {
+                runningBorderAnimation = null
+                if (!isAppearing) {
+                    borderAnimationParams.onHideBorder()
+                }
+            }
+        }
+    }
+
+    /** Shows/hides the border, optionally with an animation. */
+    fun setBorderVisibility(visible: Boolean, animated: Boolean) {
+        if (animated) {
+            buildAnimator(visible).start()
+            return
+        }
+        runningBorderAnimation?.end()
+        if (visible) {
+            borderAnimationParams.onShowBorder()
+        }
+        borderAnimationProgress.updateValue(if (visible) 1f else 0f)
+        if (!visible) {
+            borderAnimationParams.onHideBorder()
+        }
+    }
+
+    /** Params for handling different target view layout situations. */
+    private abstract class BorderAnimationParams(
+        @field:Px @param:Px val borderWidthPx: Int,
+        private val boundsBuilder: (rect: Rect) -> Unit,
+        val targetView: View,
+    ) {
+        val borderBounds = Rect()
+        var animationProgress = 0f
+        private var layoutChangeListener: OnLayoutChangeListener? = null
+
+        abstract val alignmentAdjustmentInset: Int
+        abstract val radiusAdjustment: Float
+
+        val borderWidth: Float
+            get() = borderWidthPx * animationProgress
+        val alignmentAdjustment: Float
+            // Outset the border by half the width to create an outwards-growth animation
+            get() = -borderWidth / 2f + alignmentAdjustmentInset
+
+        open fun onShowBorder() {
+            if (layoutChangeListener == null) {
+                layoutChangeListener = OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+                    onShowBorder()
+                    targetView.invalidate()
+                }
+                targetView.addOnLayoutChangeListener(layoutChangeListener)
+            }
+            boundsBuilder(borderBounds)
+        }
+
+        open fun onHideBorder() {
+            if (layoutChangeListener != null) {
+                targetView.removeOnLayoutChangeListener(layoutChangeListener)
+                layoutChangeListener = null
+            }
+        }
+    }
+
+    /** BorderAnimationParams that simply draws the border outside the bounds of the target view. */
+    private class SimpleParams(
+        @Px borderWidthPx: Int,
+        boundsBuilder: (rect: Rect?) -> Unit,
+        targetView: View,
+    ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) {
+        override val alignmentAdjustmentInset = 0
+        override val radiusAdjustment: Float
+            get() = -alignmentAdjustment
+    }
+
+    /**
+     * BorderAnimationParams that scales the target and content views to draw the border within the
+     * target's bounds without obscuring the content.
+     */
+    private class ScalingParams(
+        @Px borderWidthPx: Int,
+        boundsBuilder: (rect: Rect?) -> Unit,
+        targetView: View,
+        private val contentView: View,
+    ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) {
+        // Inset the border since we are scaling the container up
+        override val alignmentAdjustmentInset = borderWidthPx
+        override val radiusAdjustment: Float
+            // Increase the radius since we are scaling the container up
+            get() = alignmentAdjustment
+
+        override fun onShowBorder() {
+            super.onShowBorder()
+            val tvWidth = targetView.width.toFloat()
+            val tvHeight = targetView.height.toFloat()
+            // Scale up just enough to make room for the border. Fail fast and fix the scaling
+            // onLayout.
+            val newScaleX = if (tvWidth == 0f) 1f else 1f + 2 * borderWidthPx / tvWidth
+            val newScaleY = if (tvHeight == 0f) 1f else 1f + 2 * borderWidthPx / tvHeight
+            with(targetView) {
+                pivotX = width / 2f
+                pivotY = height / 2f
+                scaleX = newScaleX
+                scaleY = newScaleY
+            }
+            with(contentView) {
+                pivotX = width / 2f
+                pivotY = height / 2f
+                scaleX = 1f / newScaleX
+                scaleY = 1f / newScaleY
+            }
+        }
+
+        override fun onHideBorder() {
+            super.onHideBorder()
+            with(targetView) {
+                pivotX = width.toFloat()
+                pivotY = height.toFloat()
+                scaleX = 1f
+                scaleY = 1f
+            }
+            with(contentView) {
+                pivotX = width / 2f
+                pivotY = height / 2f
+                scaleX = 1f
+                scaleY = 1f
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 32d6582..8a6c197 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -62,6 +62,8 @@
 import java.util.List;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * TaskView that contains all tasks that are part of the desktop.
  */
@@ -142,9 +144,10 @@
     }
 
     @Override
-    protected void updateBorderBounds(Rect bounds) {
+    protected Unit updateBorderBounds(@NonNull Rect bounds) {
         bounds.set(mBackgroundView.getLeft(), mBackgroundView.getTop(), mBackgroundView.getRight(),
                 mBackgroundView.getBottom());
+        return Unit.INSTANCE;
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 7e58763..2e347ba 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -37,6 +37,8 @@
 import java.util.HashMap;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
  *
@@ -76,10 +78,10 @@
     }
 
     @Override
-    protected void updateBorderBounds(Rect bounds) {
+    protected Unit updateBorderBounds(@NonNull Rect bounds) {
         if (mSplitBoundsConfig == null) {
             super.updateBorderBounds(bounds);
-            return;
+            return Unit.INSTANCE;
         }
         bounds.set(
                 Math.min(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()),
@@ -90,6 +92,7 @@
                         mSnapshotView2.getRight() + Math.round(mSnapshotView2.getTranslationX())),
                 Math.max(mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()),
                         mSnapshotView2.getBottom() + Math.round(mSnapshotView2.getTranslationY())));
+        return Unit.INSTANCE;
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index d5b43a8..50b38ea 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -120,6 +120,8 @@
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
+import kotlin.Unit;
+
 /**
  * A task in the Recents view.
  */
@@ -440,48 +442,44 @@
 
         boolean keyboardFocusHighlightEnabled = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
                 || DesktopTaskView.DESKTOP_MODE_SUPPORTED;
+        boolean cursorHoverStatesEnabled = FeatureFlags.enableCursorHoverStates();
 
-        boolean willDrawBorder =
-                keyboardFocusHighlightEnabled || FeatureFlags.enableCursorHoverStates();
-        setWillNotDraw(!willDrawBorder);
+        setWillNotDraw(!keyboardFocusHighlightEnabled && !cursorHoverStatesEnabled);
 
-        if (willDrawBorder) {
-            TypedArray styledAttrs = context.obtainStyledAttributes(
-                    attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
+        TypedArray styledAttrs = context.obtainStyledAttributes(
+                attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
 
-            mFocusBorderAnimator = keyboardFocusHighlightEnabled ? new BorderAnimator(
-                    /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
-                    /* borderColor= */ styledAttrs.getColor(
-                            R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR),
-                    /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
-                            /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
-                                    R.dimen.keyboard_quick_switch_border_width),
-                            /* boundsBuilder= */ this::updateBorderBounds,
-                            /* targetView= */ this)) : null;
+        mFocusBorderAnimator = keyboardFocusHighlightEnabled
+                ? BorderAnimator.createSimpleBorderAnimator(
+                        /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                        /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+                                R.dimen.keyboard_quick_switch_border_width),
+                        /* boundsBuilder= */ this::updateBorderBounds,
+                        /* targetView= */ this,
+                        /* borderColor= */ styledAttrs.getColor(
+                                R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR))
+                : null;
 
-            mHoverBorderAnimator =
-                    FeatureFlags.enableCursorHoverStates() ? new BorderAnimator(
-                            /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
-                            /* borderColor= */ styledAttrs.getColor(
-                                    R.styleable.TaskView_hoverBorderColor, DEFAULT_BORDER_COLOR),
-                            /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
-                                    /* borderWidthPx= */ context.getResources()
-                                            .getDimensionPixelSize(R.dimen.task_hover_border_width),
-                                    /* boundsBuilder= */ this::updateBorderBounds,
-                                    /* targetView= */ this)) : null;
+        mHoverBorderAnimator = cursorHoverStatesEnabled
+                ? BorderAnimator.createSimpleBorderAnimator(
+                        /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                        /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+                                R.dimen.task_hover_border_width),
+                        /* boundsBuilder= */ this::updateBorderBounds,
+                        /* targetView= */ this,
+                        /* borderColor= */ styledAttrs.getColor(
+                                R.styleable.TaskView_hoverBorderColor, DEFAULT_BORDER_COLOR))
+                : null;
 
-            styledAttrs.recycle();
-        } else {
-            mFocusBorderAnimator = null;
-            mHoverBorderAnimator = null;
-        }
+        styledAttrs.recycle();
     }
 
-    protected void updateBorderBounds(Rect bounds) {
+    protected Unit updateBorderBounds(@NonNull Rect bounds) {
         bounds.set(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()),
                 mSnapshotView.getTop() + Math.round(mSnapshotView.getTranslationY()),
                 mSnapshotView.getRight() + Math.round(mSnapshotView.getTranslationX()),
                 mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()));
+        return Unit.INSTANCE;
     }
 
     public void setTaskViewId(int id) {
@@ -529,19 +527,21 @@
     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
         if (mFocusBorderAnimator != null) {
-            mFocusBorderAnimator.buildAnimator(gainFocus).start();
+            mFocusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true);
         }
     }
 
     @Override
     public boolean onHoverEvent(MotionEvent event) {
-        if (FeatureFlags.enableCursorHoverStates()) {
+        if (mHoverBorderAnimator != null) {
             switch (event.getAction()) {
                 case MotionEvent.ACTION_HOVER_ENTER:
-                    mHoverBorderAnimator.buildAnimator(/* isAppearing= */ true).start();
+                    mHoverBorderAnimator.setBorderVisibility(
+                            /* visible= */ true, /* animated= */ true);
                     break;
                 case MotionEvent.ACTION_HOVER_EXIT:
-                    mHoverBorderAnimator.buildAnimator(/* isAppearing= */ false).start();
+                    mHoverBorderAnimator.setBorderVisibility(
+                            /* visible= */ false, /* animated= */ true);
                     break;
                 default:
                     break;
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 202c078..130bd3a 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -995,6 +995,25 @@
     }
 
     /**
+     * Goes to home from immersive fullscreen app by first swiping up to bring navbar, and then
+     * performing {@code goHome()} action.
+     * Currently only supports gesture navigation mode.
+     *
+     * @return the Workspace object.
+     */
+    public Workspace goHomeFromImmersiveFullscreenApp() {
+        assertTrue("expected gesture navigation mode",
+                getNavigationModel() == NavigationModel.ZERO_BUTTON);
+        final Point displaySize = getRealDisplaySize();
+        linearGesture(
+                displaySize.x / 2, displaySize.y - 1,
+                displaySize.x / 2, 0,
+                ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
+                false, GestureScope.EXPECT_PILFER);
+        return goHome();
+    }
+
+    /**
      * Goes to home by swiping up in zero-button mode or pressing Home button.
      * Calling it after another TAPL call is safe because all TAPL methods wait for the animations
      * to finish.