Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 69079b46 authored by Galia Peycheva's avatar Galia Peycheva Committed by Android (Google) Code Review
Browse files

Merge "Fix TV pip menu edu text hidden too soon"

parents fa42e5b6 832fafb0
Loading
Loading
Loading
Loading
+3 −18
Original line number Diff line number Diff line
@@ -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:paddingTop="@dimen/pip_menu_border_width"/>

    <!-- Frame around the PiP content + edu text hint - used to highlight open menu -->
    <View
+5 −3
Original line number Diff line number Diff line
@@ -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>
+17 −14
Original line number Diff line number Diff line
@@ -127,7 +127,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
    private int mPipForceCloseDelay;

    private int mResizeAnimationDuration;
    private int mEduTextWindowExitAnimationDurationMs;
    private int mEduTextWindowExitAnimationDuration;

    public static Pip create(
            Context context,
@@ -371,10 +371,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
    }

    @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 @@ public class TvPipController implements PipTransitionController.PipTransitionCal

    @Override
    public void closeEduText() {
        updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
        updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false);
    }

    private void registerSessionListenerForCurrentUser() {
@@ -453,27 +453,30 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
    }

    @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 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
        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) {
+25 −39
Original line number Diff line number Diff line
@@ -60,9 +60,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
    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 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
    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 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis

        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 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
        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 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
                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 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
        if (mPipMenuView == null) {
            return;
        }
        maybeCloseEduText();
        maybeUpdateMenuViewActions();
        updateExpansionState();

@@ -232,25 +226,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
        mPipMenuView.updateBounds(mTvPipBoundsState.getBounds());
    }

    void onPipTransitionStarted(Rect finishBounds) {
    void onPipTransitionToTargetBoundsStarted(Rect targetBounds) {
        if (mPipMenuView != null) {
            mPipMenuView.onPipTransitionStarted(finishBounds);
        }
    }

    private void maybeCloseEduText() {
        if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) {
            mMainHandler.removeCallbacks(mCloseEduTextRunnable);
            mCloseEduTextRunnable.run();
            mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds);
        }
    }

    private void closeEduText() {
        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
        mPipMenuView.hideEduText();
        mDelegate.closeEduText();
    }

    void updateGravity(int gravity) {
        mPipMenuView.showMovementHints(gravity);
    }
@@ -332,7 +313,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
    @Override
    public void detach() {
        closeMenu();
        mMainHandler.removeCallbacks(mCloseEduTextRunnable);
        detachPipMenu();
        mLeash = null;
    }
@@ -578,6 +558,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
        mDelegate.togglePipExpansion();
    }

    @Override
    public void onCloseEduText() {
        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
        mDelegate.closeEduText();
    }

    interface Delegate {
        void movePipToFullscreen();

+280 −0
Original line number Diff line number Diff line
/*
 * 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();
    }

}
Loading