Merge "Fix TV pip menu edu text hidden too soon"
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
index afd3aac..70755e6 100644
--- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
@@ -119,7 +119,7 @@
 
     <!-- Temporarily extending the background to show an edu text hint for opening the menu -->
     <FrameLayout
-        android:id="@+id/tv_pip_menu_edu_text_container"
+        android:id="@+id/tv_pip_menu_edu_text_drawer_placeholder"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_below="@+id/tv_pip"
@@ -127,23 +127,8 @@
         android:layout_alignStart="@+id/tv_pip"
         android:layout_alignEnd="@+id/tv_pip"
         android:background="@color/tv_pip_menu_background"
-        android:clipChildren="true">
-
-        <TextView
-            android:id="@+id/tv_pip_menu_edu_text"
-            android:layout_width="wrap_content"
-            android:layout_height="@dimen/pip_menu_edu_text_view_height"
-            android:layout_gravity="bottom|center"
-            android:gravity="center"
-            android:clickable="false"
-            android:paddingBottom="@dimen/pip_menu_border_width"
-            android:text="@string/pip_edu_text"
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:marqueeRepeatLimit="1"
-            android:scrollHorizontally="true"
-            android:textAppearance="@style/TvPipEduText"/>
-    </FrameLayout>
+        android:paddingBottom="@dimen/pip_menu_border_width"
+        android:paddingTop="@dimen/pip_menu_border_width"/>
 
     <!-- Frame around the PiP content + edu text hint - used to highlight open menu -->
     <View
diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
index b45b9ec..9833a88 100644
--- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
+++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
@@ -41,8 +41,10 @@
     <dimen name="pip_menu_edu_text_view_height">24dp</dimen>
     <dimen name="pip_menu_edu_text_home_icon">9sp</dimen>
     <dimen name="pip_menu_edu_text_home_icon_outline">14sp</dimen>
-    <integer name="pip_edu_text_show_duration_ms">10500</integer>
-    <integer name="pip_edu_text_window_exit_animation_duration_ms">1000</integer>
-    <integer name="pip_edu_text_view_exit_animation_duration_ms">300</integer>
+    <integer name="pip_edu_text_scroll_times">2</integer>
+    <integer name="pip_edu_text_non_scroll_show_duration">10500</integer>
+    <integer name="pip_edu_text_start_scroll_delay">2000</integer>
+    <integer name="pip_edu_text_window_exit_animation_duration">1000</integer>
+    <integer name="pip_edu_text_view_exit_animation_duration">300</integer>
 </resources>
 
diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml
index 2b7a13e..8f806cf 100644
--- a/libs/WindowManager/Shell/res/values/strings_tv.xml
+++ b/libs/WindowManager/Shell/res/values/strings_tv.xml
@@ -42,8 +42,8 @@
 
     <!-- Educative text instructing the user to double press the HOME button to access the pip
         controls menu [CHAR LIMIT=50] -->
-    <string name="pip_edu_text"> Double press <annotation icon="home_icon"> HOME </annotation> for
-        controls </string>
+    <string name="pip_edu_text">Double press <annotation icon="home_icon">HOME</annotation> for
+        controls</string>
 
     <!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] -->
     <string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 4c26224..3e8de45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -127,7 +127,7 @@
     private int mPipForceCloseDelay;
 
     private int mResizeAnimationDuration;
-    private int mEduTextWindowExitAnimationDurationMs;
+    private int mEduTextWindowExitAnimationDuration;
 
     public static Pip create(
             Context context,
@@ -371,10 +371,10 @@
     }
 
     @Override
-    public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) {
-        mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds,
+    public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
+        mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
                 animationDuration, rect -> mTvPipMenuController.updateExpansionState());
-        mTvPipMenuController.onPipTransitionStarted(newTargetBounds);
+        mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
     }
 
     /**
@@ -411,7 +411,7 @@
 
     @Override
     public void closeEduText() {
-        updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
+        updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false);
     }
 
     private void registerSessionListenerForCurrentUser() {
@@ -453,27 +453,30 @@
     }
 
     @Override
-    public void onPipTransitionStarted(int direction, Rect pipBounds) {
+    public void onPipTransitionStarted(int direction, Rect currentPipBounds) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState));
-        mTvPipMenuController.notifyPipAnimating(true);
+                "%s: onPipTransition_Started(), state=%s, direction=%d",
+                TAG, stateToName(mState), direction);
     }
 
     @Override
     public void onPipTransitionCanceled(int direction) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
-        mTvPipMenuController.notifyPipAnimating(false);
+        mTvPipMenuController.onPipTransitionFinished(
+                PipAnimationController.isInPipDirection(direction));
     }
 
     @Override
     public void onPipTransitionFinished(int direction) {
-        if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) {
+        final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction);
+        if (enterPipTransition && mState == STATE_NO_PIP) {
             setState(STATE_PIP);
         }
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState));
-        mTvPipMenuController.notifyPipAnimating(false);
+                "%s: onPipTransition_Finished(), state=%s, direction=%d",
+                TAG, stateToName(mState), direction);
+        mTvPipMenuController.onPipTransitionFinished(enterPipTransition);
     }
 
     private void setState(@State int state) {
@@ -487,8 +490,8 @@
         final Resources res = mContext.getResources();
         mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
         mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
-        mEduTextWindowExitAnimationDurationMs =
-                res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
+        mEduTextWindowExitAnimationDuration =
+                res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration);
     }
 
     private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index 176fdfe..ab7edbf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -60,9 +60,6 @@
     private final SystemWindows mSystemWindows;
     private final TvPipBoundsState mTvPipBoundsState;
     private final Handler mMainHandler;
-    private final int mPipMenuBorderWidth;
-    private final int mPipEduTextShowDurationMs;
-    private final int mPipEduTextHeight;
 
     private Delegate mDelegate;
     private SurfaceControl mLeash;
@@ -85,8 +82,6 @@
     RectF mTmpDestinationRectF = new RectF();
     Matrix mMoveTransform = new Matrix();
 
-    private final Runnable mCloseEduTextRunnable = this::closeEduText;
-
     public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState,
             SystemWindows systemWindows, PipMediaController pipMediaController,
             Handler mainHandler) {
@@ -109,12 +104,6 @@
 
         pipMediaController.addActionListener(this::onMediaActionsChanged);
 
-        mPipEduTextShowDurationMs = context.getResources()
-                .getInteger(R.integer.pip_edu_text_show_duration_ms);
-        mPipEduTextHeight = context.getResources()
-                .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
-        mPipMenuBorderWidth = context.getResources()
-                .getDimensionPixelSize(R.dimen.pip_menu_border_width);
     }
 
     void setDelegate(Delegate delegate) {
@@ -152,15 +141,17 @@
         attachPipBackgroundView();
         attachPipMenuView();
 
-        mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth,
-                -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth));
-        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight));
-        mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs);
+        int pipEduTextHeight = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
+        int pipMenuBorderWidth = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.pip_menu_border_width);
+        mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth,
+                    -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth));
+        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight));
     }
 
     private void attachPipMenuView() {
-        mPipMenuView = new TvPipMenuView(mContext);
-        mPipMenuView.setListener(this);
+        mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this);
         setUpViewSurfaceZOrder(mPipMenuView, 1);
         addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE);
         maybeUpdateMenuViewActions();
@@ -192,11 +183,15 @@
                 0 /* height */), 0 /* displayId */, SHELL_ROOT_LAYER_PIP);
     }
 
-    void notifyPipAnimating(boolean animating) {
-        mPipMenuView.setEduTextActive(!animating);
-        if (!animating) {
-            mPipMenuView.onPipTransitionFinished(mTvPipBoundsState.isTvPipExpanded());
-        }
+    void onPipTransitionFinished(boolean enterTransition) {
+        // There is a race between when this is called and when the last frame of the pip transition
+        // is drawn. To ensure that view updates are applied only when the animation has fully drawn
+        // and the menu view has been fully remeasured and relaid out, we add a small delay here by
+        // posting on the handler.
+        mMainHandler.post(() -> {
+            mPipMenuView.onPipTransitionFinished(
+                    enterTransition, mTvPipBoundsState.isTvPipExpanded());
+        });
     }
 
     void showMovementMenuOnly() {
@@ -219,7 +214,6 @@
         if (mPipMenuView == null) {
             return;
         }
-        maybeCloseEduText();
         maybeUpdateMenuViewActions();
         updateExpansionState();
 
@@ -232,25 +226,12 @@
         mPipMenuView.updateBounds(mTvPipBoundsState.getBounds());
     }
 
-    void onPipTransitionStarted(Rect finishBounds) {
+    void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
         if (mPipMenuView != null) {
-            mPipMenuView.onPipTransitionStarted(finishBounds);
+            mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds);
         }
     }
 
-    private void maybeCloseEduText() {
-        if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) {
-            mMainHandler.removeCallbacks(mCloseEduTextRunnable);
-            mCloseEduTextRunnable.run();
-        }
-    }
-
-    private void closeEduText() {
-        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
-        mPipMenuView.hideEduText();
-        mDelegate.closeEduText();
-    }
-
     void updateGravity(int gravity) {
         mPipMenuView.showMovementHints(gravity);
     }
@@ -332,7 +313,6 @@
     @Override
     public void detach() {
         closeMenu();
-        mMainHandler.removeCallbacks(mCloseEduTextRunnable);
         detachPipMenu();
         mLeash = null;
     }
@@ -578,6 +558,12 @@
         mDelegate.togglePipExpansion();
     }
 
+    @Override
+    public void onCloseEduText() {
+        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
+        mDelegate.closeEduText();
+    }
+
     interface Delegate {
         void movePipToFullscreen();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
new file mode 100644
index 0000000..6eef225
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static android.view.Gravity.BOTTOM;
+import static android.view.Gravity.CENTER;
+import static android.view.View.GONE;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.text.Annotation;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+
+import java.util.Arrays;
+
+/**
+ * The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu.
+ * It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same
+ * width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might
+ * not be enough space to fit the whole educational text in the available space. In such cases we
+ * apply a marquee animation to the TextView inside the drawer.
+ *
+ * The drawer is shown temporarily giving the user enough time to read it, after which it slides
+ * shut. We show the text for a duration calculated based on whether the text is marqueed or not.
+ */
+class TvPipMenuEduTextDrawer extends FrameLayout {
+    private static final String TAG = "TvPipMenuEduTextDrawer";
+
+    private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND
+    private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY
+    private final float mMarqueeAnimSpeed; // pixels per ms
+
+    private final Runnable mCloseDrawerRunnable = this::closeDrawer;
+    private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText;
+
+    private final Handler mMainHandler;
+    private final Listener mListener;
+    private final TextView mEduTextView;
+
+    TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) {
+        super(context, null, 0, 0);
+
+        mListener = listener;
+        mMainHandler = mainHandler;
+
+        // Taken from TextView.Marquee calculation
+        mMarqueeAnimSpeed =
+            (MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f;
+
+        mEduTextView = new TextView(mContext);
+        setupDrawer();
+    }
+
+    private void setupDrawer() {
+        final int eduTextHeight = mContext.getResources().getDimensionPixelSize(
+                R.dimen.pip_menu_edu_text_view_height);
+        final int marqueeRepeatLimit = mContext.getResources()
+                .getInteger(R.integer.pip_edu_text_scroll_times);
+
+        mEduTextView.setLayoutParams(
+                new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER));
+        mEduTextView.setGravity(CENTER);
+        mEduTextView.setClickable(false);
+        mEduTextView.setText(createEduTextString());
+        mEduTextView.setSingleLine();
+        mEduTextView.setTextAppearance(R.style.TvPipEduText);
+        mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
+        mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit);
+        mEduTextView.setHorizontallyScrolling(true);
+        mEduTextView.setHorizontalFadingEdgeEnabled(true);
+        mEduTextView.setSelected(false);
+        addView(mEduTextView);
+
+        setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER));
+        setClipChildren(true);
+    }
+
+    /**
+     * Initializes the edu text. Should only be called once when the PiP is entered
+     */
+    void init() {
+        ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG);
+        scheduleLifecycleEvents();
+    }
+
+    private void scheduleLifecycleEvents() {
+        final int startScrollDelay = mContext.getResources().getInteger(
+                R.integer.pip_edu_text_start_scroll_delay);
+        if (isEduTextMarqueed()) {
+            mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay);
+        }
+        mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration());
+        mEduTextView.getViewTreeObserver().addOnWindowAttachListener(
+                    new ViewTreeObserver.OnWindowAttachListener() {
+                @Override
+                public void onWindowAttached() {
+                }
+
+                @Override
+                public void onWindowDetached() {
+                    mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this);
+                    mMainHandler.removeCallbacks(mStartScrollEduTextRunnable);
+                    mMainHandler.removeCallbacks(mCloseDrawerRunnable);
+                }
+            });
+    }
+
+    private int getEduTextShowDuration() {
+        int eduTextShowDuration;
+        if (isEduTextMarqueed()) {
+            // Calculate the time it takes to fully scroll the text once: time = distance / speed
+            final float singleMarqueeDuration =
+                    getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed;
+            // The TextView adds a delay between each marquee repetition. Take that into account
+            final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY;
+            // Finally, multiply by the number of times we repeat the marquee animation
+            eduTextShowDuration =
+                    (int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit();
+        } else {
+            eduTextShowDuration = mContext.getResources()
+                    .getInteger(R.integer.pip_edu_text_non_scroll_show_duration);
+        }
+
+        ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d",
+                TAG, eduTextShowDuration);
+        return eduTextShowDuration;
+    }
+
+    /**
+     * Returns true if the edu text width is bigger than the width of the text view, which indicates
+     * that the edu text will be marqueed
+     */
+    private boolean isEduTextMarqueed() {
+        final int availableWidth = (int) mEduTextView.getWidth()
+                - mEduTextView.getCompoundPaddingLeft()
+                - mEduTextView.getCompoundPaddingRight();
+        return availableWidth < getEduTextWidth();
+    }
+
+    /**
+     * Returns the width of a single marquee repetition of the edu text in pixels.
+     * This is the width from the start of the edu text to the start of the next edu
+     * text when it is marqueed.
+     *
+     * This is calculated based on the TextView.Marquee#start calculations
+     */
+    private float getMarqueeAnimEduTextLineWidth() {
+        // When the TextView has a marquee animation, it puts a gap between the text end and the
+        // start of the next edu text repetition. The space is equal to a third of the TextView
+        // width
+        final float gap = mEduTextView.getWidth() / 3.0f;
+        return getEduTextWidth() + gap;
+    }
+
+    private void startScrollEduText() {
+        ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d",
+                TAG, mEduTextView.getMarqueeRepeatLimit());
+        mEduTextView.setSelected(true);
+    }
+
+    /**
+     * Returns the width of the edu text irrespective of the TextView width
+     */
+    private int getEduTextWidth() {
+        return (int) mEduTextView.getLayout().getLineWidth(0);
+    }
+
+    /**
+     * Closes the edu text drawer if it hasn't been closed yet
+     */
+    void closeIfNeeded() {
+        if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) {
+            ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: close(), closing the edu text drawer because of user action", TAG);
+            mMainHandler.removeCallbacks(mCloseDrawerRunnable);
+            mCloseDrawerRunnable.run();
+        } else {
+            // Do nothing, the drawer has already been closed
+        }
+    }
+
+    private void closeDrawer() {
+        ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG);
+        final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger(
+                R.integer.pip_edu_text_view_exit_animation_duration);
+        final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger(
+                R.integer.pip_edu_text_window_exit_animation_duration);
+
+        // Start fading out the edu text
+        mEduTextView.animate()
+                .alpha(0f)
+                .setInterpolator(TvPipInterpolators.EXIT)
+                .setDuration(eduTextFadeExitAnimationDuration)
+                .start();
+
+        // Start animation to close the drawer by animating its height to 0
+        final ValueAnimator heightAnimation = ValueAnimator.ofInt(getHeight(), 0);
+        heightAnimation.setDuration(eduTextSlideExitAnimationDuration);
+        heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
+        heightAnimation.addUpdateListener(animator -> {
+            final ViewGroup.LayoutParams params = getLayoutParams();
+            params.height = (int) animator.getAnimatedValue();
+            setLayoutParams(params);
+            if (params.height == 0) {
+                setVisibility(GONE);
+            }
+        });
+        heightAnimation.start();
+
+        mListener.onCloseEduText();
+    }
+
+    /**
+     * Creates the educational text that will be displayed to the user. Here we replace the
+     * HOME annotation in the String with an icon
+     */
+    private CharSequence createEduTextString() {
+        final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
+        final SpannableString spannableString = new SpannableString(eduText);
+        Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
+                .ifPresent(annotation -> {
+                    final Drawable icon =
+                            getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
+                    if (icon != null) {
+                        icon.mutate();
+                        icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+                        spannableString.setSpan(new CenteredImageSpan(icon),
+                                eduText.getSpanStart(annotation),
+                                eduText.getSpanEnd(annotation),
+                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    }
+                });
+
+        return spannableString;
+    }
+
+    /**
+     * A listener for edu text drawer event states.
+     */
+    interface Listener {
+        /**
+         *  The edu text closing impacts the size of the Picture-in-Picture window and influences
+         *  how it is positioned on the screen.
+         */
+        void onCloseEduText();
+    }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index 9cd05b0..57e95c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -25,18 +25,11 @@
 import static android.view.KeyEvent.KEYCODE_DPAD_UP;
 import static android.view.KeyEvent.KEYCODE_ENTER;
 
-import android.animation.ValueAnimator;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
 import android.content.Context;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.os.Handler;
-import android.text.Annotation;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.SpannedString;
-import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.SurfaceControl;
@@ -49,7 +42,6 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -61,7 +53,6 @@
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -74,21 +65,16 @@
 
     private static final int FIRST_CUSTOM_ACTION_POSITION = 3;
 
-    @Nullable
-    private Listener mListener;
+    private final Listener mListener;
 
     private final LinearLayout mActionButtonsContainer;
     private final View mMenuFrameView;
     private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>();
     private final View mPipFrameView;
     private final View mPipView;
-    private final TextView mEduTextView;
-    private final View mEduTextContainerView;
+    private final TvPipMenuEduTextDrawer mEduTextDrawer;
     private final int mPipMenuOuterSpace;
     private final int mPipMenuBorderWidth;
-    private final int mEduTextFadeExitAnimationDurationMs;
-    private final int mEduTextSlideExitAnimationDurationMs;
-    private int mEduTextHeight;
 
     private final ImageView mArrowUp;
     private final ImageView mArrowRight;
@@ -116,25 +102,17 @@
     private final int mResizeAnimationDuration;
 
     private final AccessibilityManager mA11yManager;
+    private final Handler mMainHandler;
 
-    public TvPipMenuView(@NonNull Context context) {
-        this(context, null);
-    }
-
-    public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
+    public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler,
+            @NonNull Listener listener) {
+        super(context, null, 0, 0);
 
         inflate(context, R.layout.tv_pip_menu, this);
 
+        mMainHandler = mainHandler;
+        mListener = listener;
+
         mA11yManager = context.getSystemService(AccessibilityManager.class);
 
         mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons);
@@ -166,9 +144,6 @@
         mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left);
         mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button);
 
-        mEduTextView = findViewById(R.id.tv_pip_menu_edu_text);
-        mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container);
-
         mResizeAnimationDuration = context.getResources().getInteger(
                 R.integer.config_pipResizeAnimationDuration);
         mPipMenuFadeAnimationDuration = context.getResources()
@@ -178,63 +153,18 @@
                 .getDimensionPixelSize(R.dimen.pip_menu_outer_space);
         mPipMenuBorderWidth = context.getResources()
                 .getDimensionPixelSize(R.dimen.pip_menu_border_width);
-        mEduTextHeight = context.getResources()
-                .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
-        mEduTextFadeExitAnimationDurationMs = context.getResources()
-                .getInteger(R.integer.pip_edu_text_view_exit_animation_duration_ms);
-        mEduTextSlideExitAnimationDurationMs = context.getResources()
-                .getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
 
-        initEduText();
+        mEduTextDrawer = new TvPipMenuEduTextDrawer(mContext, mainHandler, mListener);
+        ((FrameLayout) findViewById(R.id.tv_pip_menu_edu_text_drawer_placeholder))
+                .addView(mEduTextDrawer);
     }
 
-    void initEduText() {
-        final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
-        final SpannableString spannableString = new SpannableString(eduText);
-        Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
-                .ifPresent(annotation -> {
-                    final Drawable icon =
-                            getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
-                    if (icon != null) {
-                        icon.mutate();
-                        icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
-                        spannableString.setSpan(new CenteredImageSpan(icon),
-                                eduText.getSpanStart(annotation),
-                                eduText.getSpanEnd(annotation),
-                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-                    }
-                });
-
-        mEduTextView.setText(spannableString);
-    }
-
-    void setEduTextActive(boolean active) {
-        mEduTextView.setSelected(active);
-    }
-
-    void hideEduText() {
-        final ValueAnimator heightAnimation = ValueAnimator.ofInt(mEduTextHeight, 0);
-        heightAnimation.setDuration(mEduTextSlideExitAnimationDurationMs);
-        heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
-        heightAnimation.addUpdateListener(animator -> {
-            mEduTextHeight = (int) animator.getAnimatedValue();
-        });
-        mEduTextView.animate()
-                .alpha(0f)
-                .setInterpolator(TvPipInterpolators.EXIT)
-                .setDuration(mEduTextFadeExitAnimationDurationMs)
-                .withEndAction(() -> {
-                    mEduTextContainerView.setVisibility(GONE);
-                }).start();
-        heightAnimation.start();
-    }
-
-    void onPipTransitionStarted(Rect finishBounds) {
+    void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
         // Fade out content by fading in view on top.
-        if (mCurrentPipBounds != null && finishBounds != null) {
+        if (mCurrentPipBounds != null && targetBounds != null) {
             boolean ratioChanged = PipUtils.aspectRatioChanged(
                     mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(),
-                    finishBounds.width() / (float) finishBounds.height());
+                    targetBounds.width() / (float) targetBounds.height());
             if (ratioChanged) {
                 mPipBackground.animate()
                         .alpha(1f)
@@ -245,11 +175,12 @@
         }
 
         // Update buttons.
-        final boolean vertical = finishBounds.height() > finishBounds.width();
+        final boolean vertical = targetBounds.height() > targetBounds.width();
         final boolean orientationChanged =
                 vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL);
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: onPipTransitionStarted(), orientation changed %b", TAG, orientationChanged);
+                "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b",
+                TAG, orientationChanged);
         if (!orientationChanged) {
             return;
         }
@@ -261,18 +192,18 @@
                     .setInterpolator(TvPipInterpolators.EXIT)
                     .setDuration(mResizeAnimationDuration / 2)
                     .withEndAction(() -> {
-                        changeButtonScrollOrientation(finishBounds);
-                        updateButtonGravity(finishBounds);
+                        changeButtonScrollOrientation(targetBounds);
+                        updateButtonGravity(targetBounds);
                         // Only make buttons visible again in onPipTransitionFinished to keep in
                         // sync with PiP content alpha animation.
                     });
         } else {
-            changeButtonScrollOrientation(finishBounds);
-            updateButtonGravity(finishBounds);
+            changeButtonScrollOrientation(targetBounds);
+            updateButtonGravity(targetBounds);
         }
     }
 
-    void onPipTransitionFinished(boolean isTvPipExpanded) {
+    void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: onPipTransitionFinished()", TAG);
 
@@ -283,6 +214,10 @@
                 .setInterpolator(TvPipInterpolators.ENTER)
                 .start();
 
+        if (enterTransition) {
+            mEduTextDrawer.init();
+        }
+
         setIsExpanded(isTvPipExpanded);
 
         // Update buttons.
@@ -409,7 +344,7 @@
     Rect getPipMenuContainerBounds(Rect pipBounds) {
         final Rect menuUiBounds = new Rect(pipBounds);
         menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
-        menuUiBounds.bottom += mEduTextHeight;
+        menuUiBounds.bottom += mEduTextDrawer.getHeight();
         return menuUiBounds;
     }
 
@@ -438,10 +373,6 @@
 
     }
 
-    void setListener(@Nullable Listener listener) {
-        mListener = listener;
-    }
-
     void setExpandedModeEnabled(boolean enabled) {
         mExpandButton.setVisibility(enabled ? VISIBLE : GONE);
     }
@@ -460,21 +391,19 @@
      */
     void showMoveMenu(int gravity) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
-        mButtonMenuIsVisible = false;
-        mMoveMenuIsVisible = true;
         showButtonsMenu(false);
         showMovementHints(gravity);
         setFrameHighlighted(true);
 
         mHorizontalScrollView.setFocusable(false);
         mScrollView.setFocusable(false);
+
+        mEduTextDrawer.closeIfNeeded();
     }
 
     void showButtonsMenu() {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: showButtonsMenu()", TAG);
-        mButtonMenuIsVisible = true;
-        mMoveMenuIsVisible = false;
         showButtonsMenu(true);
         hideMovementHints();
         setFrameHighlighted(true);
@@ -501,8 +430,6 @@
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: hideAllUserControls()", TAG);
         mFocusedButton = null;
-        mButtonMenuIsVisible = false;
-        mMoveMenuIsVisible = false;
         showButtonsMenu(false);
         hideMovementHints();
         setFrameHighlighted(false);
@@ -632,8 +559,6 @@
 
     @Override
     public void onClick(View v) {
-        if (mListener == null) return;
-
         final int id = v.getId();
         if (id == R.id.tv_pip_menu_fullscreen_button) {
             mListener.onFullscreenButtonClick();
@@ -662,7 +587,7 @@
 
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
-        if (mListener != null && event.getAction() == ACTION_UP) {
+        if (event.getAction() == ACTION_UP) {
             if (!mMoveMenuIsVisible) {
                 mFocusedButton = mActionButtonsContainer.getFocusedChild();
             }
@@ -700,6 +625,11 @@
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity));
 
+        if (mMoveMenuIsVisible) {
+            return;
+        }
+        mMoveMenuIsVisible = true;
+
         animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp);
         animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown);
         animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft);
@@ -714,9 +644,7 @@
         animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton);
         if (a11yEnabled) {
             mA11yDoneButton.setOnClickListener(v -> {
-                if (mListener != null) {
-                    mListener.onExitMoveMode();
-                }
+                mListener.onExitMoveMode();
             });
         }
     }
@@ -725,9 +653,7 @@
         arrowView.setClickable(enabled);
         if (enabled) {
             arrowView.setOnClickListener(v -> {
-                if (mListener != null) {
-                    mListener.onPipMovement(keycode);
-                }
+                mListener.onPipMovement(keycode);
             });
         }
     }
@@ -743,6 +669,11 @@
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: hideMovementHints()", TAG);
 
+        if (!mMoveMenuIsVisible) {
+            return;
+        }
+        mMoveMenuIsVisible = false;
+
         animateAlphaTo(0, mArrowUp);
         animateAlphaTo(0, mArrowRight);
         animateAlphaTo(0, mArrowDown);
@@ -756,19 +687,25 @@
     public void showButtonsMenu(boolean show) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "%s: showUserActions: %b", TAG, show);
+        if (mButtonMenuIsVisible == show) {
+            return;
+        }
+        mButtonMenuIsVisible = show;
+
         if (show) {
             mActionButtonsContainer.setVisibility(VISIBLE);
             refocusPreviousButton();
         }
         animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
         animateAlphaTo(show ? 1 : 0, mDimLayer);
+        mEduTextDrawer.closeIfNeeded();
     }
 
     private void setFrameHighlighted(boolean highlighted) {
         mMenuFrameView.setActivated(highlighted);
     }
 
-    interface Listener {
+    interface Listener extends TvPipMenuEduTextDrawer.Listener {
 
         void onBackPress();