diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 70755e6cc3cfc..dcce4698c2524 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -44,67 +44,15 @@ android:background="@color/tv_pip_menu_dim_layer" android:alpha="0"/> - - - - - - - - - - - - - - - - - - - - - - + diff --git a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml index c4dbd39c729a6..b2ac85b018be9 100644 --- a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml +++ b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml @@ -21,6 +21,7 @@ android:layout_width="@dimen/tv_window_menu_button_size" android:layout_height="@dimen/tv_window_menu_button_size" android:padding="@dimen/tv_window_menu_button_margin" + android:duplicateParentState="true" android:stateListAnimator="@animator/tv_window_menu_action_button_animator" android:focusable="true"> diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index 9833a88a1c0a2..0b61d7a85d9ee 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -28,7 +28,7 @@ 6dp 4dp 24dp - 26dp + 30dp 20dp diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java index 39b0b5500ceaf..8ba785a1f03ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java @@ -19,6 +19,8 @@ package com.android.wm.shell.common; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Handler; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -30,11 +32,11 @@ import com.android.wm.shell.R; /** * A common action button for TV window menu layouts. */ -public class TvWindowMenuActionButton extends RelativeLayout implements View.OnClickListener { +public class TvWindowMenuActionButton extends RelativeLayout { private final ImageView mIconImageView; private final View mButtonBackgroundView; - private final View mButtonView; - private OnClickListener mOnClickListener; + + private Icon mCurrentIcon; public TvWindowMenuActionButton(Context context) { this(context, null, 0, 0); @@ -56,7 +58,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC inflater.inflate(R.layout.tv_window_menu_action_button, this); mIconImageView = findViewById(R.id.icon); - mButtonView = findViewById(R.id.button); mButtonBackgroundView = findViewById(R.id.background); final int[] values = new int[]{android.R.attr.src, android.R.attr.text}; @@ -71,23 +72,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC typedArray.recycle(); } - @Override - public void setOnClickListener(OnClickListener listener) { - // We do not want to set an OnClickListener to the TvWindowMenuActionButton itself, but only - // to the ImageView. So let's "cash" the listener we've been passed here and set a "proxy" - // listener to the ImageView. - mOnClickListener = listener; - mButtonView.setOnClickListener(listener != null ? this : null); - } - - @Override - public void onClick(View v) { - if (mOnClickListener != null) { - // Pass the correct view - this. - mOnClickListener.onClick(this); - } - } - /** * Sets the drawable for the button with the given drawable. */ @@ -104,11 +88,24 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC } } + public void setImageIconAsync(Icon icon, Handler handler) { + mCurrentIcon = icon; + // Remove old image while waiting for the new one to load. + mIconImageView.setImageDrawable(null); + icon.loadDrawableAsync(mContext, d -> { + // The image hasn't been set any other way and the drawable belongs to the most + // recently set Icon. + if (mIconImageView.getDrawable() == null && mCurrentIcon == icon) { + mIconImageView.setImageDrawable(d); + } + }, handler); + } + /** * Sets the text for description the with the given string. */ public void setTextAndDescription(CharSequence text) { - mButtonView.setContentDescription(text); + setContentDescription(text); } /** @@ -118,16 +115,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC setTextAndDescription(getContext().getString(resId)); } - @Override - public void setEnabled(boolean enabled) { - mButtonView.setEnabled(enabled); - } - - @Override - public boolean isEnabled() { - return mButtonView.isEnabled(); - } - /** * Marks this button as a custom close action button. * This changes the style of the action button to highlight that this action finishes the @@ -147,10 +134,10 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC @Override public String toString() { - if (mButtonView.getContentDescription() == null) { + if (getContentDescription() == null) { return TvWindowMenuActionButton.class.getSimpleName(); } - return mButtonView.getContentDescription().toString(); + return getContentDescription().toString(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java new file mode 100644 index 0000000000000..9f8f95c93fec2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java @@ -0,0 +1,84 @@ +/* + * 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 android.annotation.IntDef; +import android.annotation.NonNull; +import android.app.PendingIntent; +import android.os.Handler; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.TvWindowMenuActionButton; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +abstract class TvPipAction { + + private static final String TAG = TvPipAction.class.getSimpleName(); + + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"ACTION_"}, value = { + ACTION_FULLSCREEN, + ACTION_CLOSE, + ACTION_MOVE, + ACTION_EXPAND_COLLAPSE, + ACTION_CUSTOM, + ACTION_CUSTOM_CLOSE + }) + public @interface ActionType { + } + + public static final int ACTION_FULLSCREEN = 0; + public static final int ACTION_CLOSE = 1; + public static final int ACTION_MOVE = 2; + public static final int ACTION_EXPAND_COLLAPSE = 3; + public static final int ACTION_CUSTOM = 4; + public static final int ACTION_CUSTOM_CLOSE = 5; + + @ActionType + private final int mActionType; + + TvPipAction(@ActionType int actionType) { + mActionType = actionType; + } + + boolean isCloseAction() { + return mActionType == ACTION_CLOSE || mActionType == ACTION_CUSTOM_CLOSE; + } + + @ActionType + int getActionType() { + return mActionType; + } + + abstract void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler); + + abstract PendingIntent getPendingIntent(); + + void executePendingIntent() { + if (getPendingIntent() == null) return; + try { + getPendingIntent().send(); + } catch (PendingIntent.CanceledException e) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to send action, %s", TAG, e); + } + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java new file mode 100644 index 0000000000000..5c0b1b3aa383d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java @@ -0,0 +1,59 @@ +/* + * 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 android.annotation.NonNull; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.os.Handler; + +import com.android.wm.shell.common.TvWindowMenuActionButton; + +import java.util.List; +import java.util.Objects; + +/** + * A TvPipAction for actions that the app provides via {@link + * android.app.PictureInPictureParams.Builder#setCloseAction(RemoteAction)} or {@link + * android.app.PictureInPictureParams.Builder#setActions(List)}. + */ +public class TvPipCustomAction extends TvPipAction { + + private final RemoteAction mRemoteAction; + + TvPipCustomAction(@ActionType int actionType, @NonNull RemoteAction remoteAction) { + super(actionType); + Objects.requireNonNull(remoteAction); + mRemoteAction = remoteAction; + } + + void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) { + if (button == null || mainHandler == null) return; + if (mRemoteAction.getContentDescription().length() > 0) { + button.setTextAndDescription(mRemoteAction.getContentDescription()); + } else { + button.setTextAndDescription(mRemoteAction.getTitle()); + } + button.setImageIconAsync(mRemoteAction.getIcon(), mainHandler); + button.setEnabled(isCloseAction() || mRemoteAction.isEnabled()); + } + + PendingIntent getPendingIntent() { + return mRemoteAction.getActionIntent(); + } + +} 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 c39400ab61e3f..e80602ea0cd92 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 @@ -146,7 +146,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis int pipMenuBorderWidth = mContext.getResources() .getDimensionPixelSize(R.dimen.pip_menu_border_width); mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth, - -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth)); + -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth)); mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight)); } @@ -221,7 +221,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mInMoveMode) { mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); } else { - mPipMenuView.showButtonsMenu(); + mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ false); } mPipMenuView.updateBounds(mTvPipBoundsState.getBounds()); } @@ -294,7 +294,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } if (mInMoveMode) { setInMoveMode(false); - mPipMenuView.showButtonsMenu(); + mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ true); return true; } return false; @@ -360,9 +360,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis return; } if (!mAppActions.isEmpty()) { - mPipMenuView.setAdditionalActions(mAppActions, mCloseAction, mMainHandler); + mPipMenuView.setAdditionalActions(mAppActions, mCloseAction); } else { - mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction, mMainHandler); + mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction); } } 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 57e95c416b3c4..d8dc022ad59c2 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,28 +25,30 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; import static android.view.KeyEvent.KEYCODE_DPAD_UP; import static android.view.KeyEvent.KEYCODE_ENTER; -import android.app.PendingIntent; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; + import android.app.RemoteAction; import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.view.Gravity; import android.view.KeyEvent; -import android.view.SurfaceControl; import android.view.View; import android.view.ViewGroup; -import android.view.ViewRootImpl; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; -import android.widget.HorizontalScrollView; import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ScrollView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.widget.LinearLayoutManager; +import com.android.internal.widget.RecyclerView; import com.android.wm.shell.R; import com.android.wm.shell.common.TvWindowMenuActionButton; import com.android.wm.shell.pip.PipUtils; @@ -60,84 +62,92 @@ import java.util.List; * actions: Fullscreen, Move and Close, but could also display "additional" actions, that may be set * via a {@link #setAdditionalActions(List, RemoteAction, Handler)} call. */ -public class TvPipMenuView extends FrameLayout implements View.OnClickListener { +public class TvPipMenuView extends FrameLayout { private static final String TAG = "TvPipMenuView"; - private static final int FIRST_CUSTOM_ACTION_POSITION = 3; + private static final int CLOSE_ACTION_INDEX = 1; + private static final int FIRST_CUSTOM_ACTION_INDEX = 2; - private final Listener mListener; + private final TvPipMenuView.Listener mListener; + + private final List mActionsList; + private final TvPipSystemAction mDefaultCloseAction; + private final TvPipSystemAction mExpandCollapseAction; + + private final RecyclerView mActionButtonsRecyclerView; + private final LinearLayoutManager mButtonLayoutManager; + private final RecyclerViewAdapter mRecyclerViewAdapter; - private final LinearLayout mActionButtonsContainer; - private final View mMenuFrameView; - private final List mAdditionalButtons = new ArrayList<>(); private final View mPipFrameView; + private final View mMenuFrameView; private final View mPipView; + + private final View mPipBackground; + private final View mDimLayer; + private final TvPipMenuEduTextDrawer mEduTextDrawer; + private final int mPipMenuOuterSpace; private final int mPipMenuBorderWidth; + private final int mPipMenuFadeAnimationDuration; + private final int mResizeAnimationDuration; + private final ImageView mArrowUp; private final ImageView mArrowRight; private final ImageView mArrowDown; private final ImageView mArrowLeft; private final TvWindowMenuActionButton mA11yDoneButton; - private final View mPipBackground; - private final View mDimLayer; - - private final ScrollView mScrollView; - private final HorizontalScrollView mHorizontalScrollView; - private View mFocusedButton; - private Rect mCurrentPipBounds; private boolean mMoveMenuIsVisible; private boolean mButtonMenuIsVisible; - - private final TvWindowMenuActionButton mExpandButton; - private final TvWindowMenuActionButton mCloseButton; - private boolean mSwitchingOrientation; - private final int mPipMenuFadeAnimationDuration; - private final int mResizeAnimationDuration; - private final AccessibilityManager mA11yManager; private final Handler mMainHandler; 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); - mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button) - .setOnClickListener(this); + mActionButtonsRecyclerView = findViewById(R.id.tv_pip_menu_action_buttons); + mButtonLayoutManager = new LinearLayoutManager(mContext); + mActionButtonsRecyclerView.setLayoutManager(mButtonLayoutManager); + mActionButtonsRecyclerView.setPreserveFocusAfterLayout(true); - mCloseButton = mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button); - mCloseButton.setOnClickListener(this); - mCloseButton.setIsCustomCloseAction(true); + mDefaultCloseAction = + new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close, + R.drawable.pip_ic_close_white); + mExpandCollapseAction = + new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse, + R.drawable.pip_ic_collapse); - mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button) - .setOnClickListener(this); - mExpandButton = findViewById(R.id.tv_pip_menu_expand_button); - mExpandButton.setOnClickListener(this); + mActionsList = new ArrayList<>(); + mActionsList.add( + new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen, + R.drawable.pip_ic_fullscreen_white)); + mActionsList.add(mDefaultCloseAction); + mActionsList.add( + new TvPipSystemAction(ACTION_MOVE, R.string.pip_move, + R.drawable.pip_ic_move_white)); + mActionsList.add(mExpandCollapseAction); - mPipBackground = findViewById(R.id.tv_pip_menu_background); - mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer); - - mScrollView = findViewById(R.id.tv_pip_menu_scroll); - mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll); + mRecyclerViewAdapter = new RecyclerViewAdapter(mActionsList); + mActionButtonsRecyclerView.setAdapter(mRecyclerViewAdapter); mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); mPipFrameView = findViewById(R.id.tv_pip_border); mPipView = findViewById(R.id.tv_pip); + mPipBackground = findViewById(R.id.tv_pip_menu_background); + mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer); + mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up); mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right); mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down); @@ -160,8 +170,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } void onPipTransitionToTargetBoundsStarted(Rect targetBounds) { + if (targetBounds == null) { + return; + } + // Fade out content by fading in view on top. - if (mCurrentPipBounds != null && targetBounds != null) { + if (mCurrentPipBounds != null) { boolean ratioChanged = PipUtils.aspectRatioChanged( mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(), targetBounds.width() / (float) targetBounds.height()); @@ -177,7 +191,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { // Update buttons. final boolean vertical = targetBounds.height() > targetBounds.width(); final boolean orientationChanged = - vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL); + vertical != (mButtonLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b", TAG, orientationChanged); @@ -187,19 +201,19 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { if (mButtonMenuIsVisible) { mSwitchingOrientation = true; - mActionButtonsContainer.animate() + mActionButtonsRecyclerView.animate() .alpha(0) .setInterpolator(TvPipInterpolators.EXIT) .setDuration(mResizeAnimationDuration / 2) .withEndAction(() -> { - changeButtonScrollOrientation(targetBounds); - updateButtonGravity(targetBounds); + mButtonLayoutManager.setOrientation(vertical + ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL); // Only make buttons visible again in onPipTransitionFinished to keep in // sync with PiP content alpha animation. }); } else { - changeButtonScrollOrientation(targetBounds); - updateButtonGravity(targetBounds); + mButtonLayoutManager.setOrientation(vertical + ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL); } } @@ -207,7 +221,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionFinished()", TAG); - // Fade in content by fading out view on top. + // Fade in content by fading out view on top (faded out at every aspect ratio change). mPipBackground.animate() .alpha(0f) .setDuration(mResizeAnimationDuration / 2) @@ -218,16 +232,14 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mEduTextDrawer.init(); } + // Update buttons. setIsExpanded(isTvPipExpanded); - // Update buttons. if (mSwitchingOrientation) { - mActionButtonsContainer.animate() + mActionButtonsRecyclerView.animate() .alpha(1) .setInterpolator(TvPipInterpolators.ENTER) .setDuration(mResizeAnimationDuration / 2); - } else { - refocusPreviousButton(); } mSwitchingOrientation = false; } @@ -240,107 +252,9 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { "%s: updateLayout, width: %s, height: %s", TAG, updatedBounds.width(), updatedBounds.height()); mCurrentPipBounds = updatedBounds; - if (!mSwitchingOrientation) { - updateButtonGravity(mCurrentPipBounds); - } - updatePipFrameBounds(); } - private void changeButtonScrollOrientation(Rect bounds) { - final boolean vertical = bounds.height() > bounds.width(); - - final ViewGroup oldScrollView = vertical ? mHorizontalScrollView : mScrollView; - final ViewGroup newScrollView = vertical ? mScrollView : mHorizontalScrollView; - - if (oldScrollView.getChildCount() == 1) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: orientation changed", TAG); - oldScrollView.removeView(mActionButtonsContainer); - oldScrollView.setVisibility(GONE); - mActionButtonsContainer.setOrientation(vertical ? LinearLayout.VERTICAL - : LinearLayout.HORIZONTAL); - newScrollView.addView(mActionButtonsContainer); - newScrollView.setVisibility(VISIBLE); - if (mFocusedButton != null) { - mFocusedButton.requestFocus(); - } - } - } - - /** - * Change button gravity based on new dimensions - */ - private void updateButtonGravity(Rect bounds) { - final boolean vertical = bounds.height() > bounds.width(); - // Use Math.max since the possible orientation change might not have been applied yet. - final int buttonsSize = Math.max(mActionButtonsContainer.getHeight(), - mActionButtonsContainer.getWidth()); - - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: buttons container width: %s, height: %s", TAG, - mActionButtonsContainer.getWidth(), mActionButtonsContainer.getHeight()); - - final boolean buttonsFit = - vertical ? buttonsSize < bounds.height() - : buttonsSize < bounds.width(); - final int buttonGravity = buttonsFit ? Gravity.CENTER - : (vertical ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL); - - final LayoutParams params = (LayoutParams) mActionButtonsContainer.getLayoutParams(); - params.gravity = buttonGravity; - mActionButtonsContainer.setLayoutParams(params); - - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: vertical: %b, buttonsFit: %b, gravity: %s", TAG, vertical, buttonsFit, - Gravity.toString(buttonGravity)); - } - - private void refocusPreviousButton() { - if (mMoveMenuIsVisible || mCurrentPipBounds == null || mFocusedButton == null) { - return; - } - final boolean vertical = mCurrentPipBounds.height() > mCurrentPipBounds.width(); - - if (!mFocusedButton.hasFocus()) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: request focus from: %s", TAG, mFocusedButton); - mFocusedButton.requestFocus(); - } else { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: already focused: %s", TAG, mFocusedButton); - } - - // Do we need to scroll? - final Rect buttonBounds = new Rect(); - final Rect scrollBounds = new Rect(); - if (vertical) { - mScrollView.getDrawingRect(scrollBounds); - } else { - mHorizontalScrollView.getDrawingRect(scrollBounds); - } - mFocusedButton.getHitRect(buttonBounds); - - if (scrollBounds.contains(buttonBounds)) { - // Button is already completely visible, don't scroll - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: not scrolling", TAG); - return; - } - - // Scrolling so the button is visible to the user. - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: scrolling to focused button", TAG); - - if (vertical) { - mScrollView.smoothScrollTo((int) mFocusedButton.getX(), - (int) mFocusedButton.getY()); - } else { - mHorizontalScrollView.smoothScrollTo((int) mFocusedButton.getX(), - (int) mFocusedButton.getY()); - } - } - Rect getPipMenuContainerBounds(Rect pipBounds) { final Rect menuUiBounds = new Rect(pipBounds); menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); @@ -370,20 +284,34 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mPipView.setLayoutParams(pipViewParams); } - + // Keep focused button within the visible area while the PiP is changing size. Otherwise, + // the button would lose focus which would cause a need for scrolling and re-focusing after + // the animation finishes, which does not look good. + View focusedChild = mActionButtonsRecyclerView.getFocusedChild(); + if (focusedChild != null) { + mActionButtonsRecyclerView.scrollToPosition( + mActionButtonsRecyclerView.getChildLayoutPosition(focusedChild)); + } } void setExpandedModeEnabled(boolean enabled) { - mExpandButton.setVisibility(enabled ? VISIBLE : GONE); + int actionIndex = mActionsList.indexOf(mExpandCollapseAction); + boolean actionInList = actionIndex != -1; + if (enabled && !actionInList) { + mActionsList.add(mExpandCollapseAction); + mRecyclerViewAdapter.notifyItemInserted(mActionsList.size() - 1); + } else if (!enabled && actionInList) { + mActionsList.remove(actionIndex); + mRecyclerViewAdapter.notifyItemRemoved(actionIndex); + } } void setIsExpanded(boolean expanded) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setIsExpanded, expanded: %b", TAG, expanded); - mExpandButton.setImageResource( + mExpandCollapseAction.update(expanded ? R.string.pip_collapse : R.string.pip_expand, expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); - mExpandButton.setTextAndDescription( - expanded ? R.string.pip_collapse : R.string.pip_expand); + mRecyclerViewAdapter.notifyItemChanged(mActionsList.indexOf(mExpandCollapseAction)); } /** @@ -391,48 +319,75 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { */ void showMoveMenu(int gravity) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); - showButtonsMenu(false); showMovementHints(gravity); + setMenuButtonsVisible(false); setFrameHighlighted(true); - mHorizontalScrollView.setFocusable(false); - mScrollView.setFocusable(false); + animateAlphaTo(mA11yManager.isEnabled() ? 1f : 0f, mDimLayer); mEduTextDrawer.closeIfNeeded(); } - void showButtonsMenu() { + + void showButtonsMenu(boolean exitingMoveMode) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showButtonsMenu()", TAG); - showButtonsMenu(true); + "%s: showButtonsMenu(), exitingMoveMode %b", TAG, exitingMoveMode); + setMenuButtonsVisible(true); hideMovementHints(); setFrameHighlighted(true); + animateAlphaTo(1f, mDimLayer); + mEduTextDrawer.closeIfNeeded(); - mHorizontalScrollView.setFocusable(true); - mScrollView.setFocusable(true); - - // Always focus on the first button when opening the menu, except directly after moving. - if (mFocusedButton == null) { - // Focus on first button (there is a Space at position 0) - mFocusedButton = mActionButtonsContainer.getChildAt(1); - // Reset scroll position. - mScrollView.scrollTo(0, 0); - mHorizontalScrollView.scrollTo( - isLayoutRtl() ? mActionButtonsContainer.getWidth() : 0, 0); + if (exitingMoveMode) { + scrollAndRefocusButton(getFirstIndexOfAction(ACTION_MOVE), + /* alwaysScroll= */ false); + } else { + scrollAndRefocusButton(0, /* alwaysScroll= */ true); + } + } + + private void scrollAndRefocusButton(int position, boolean alwaysScroll) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: scrollAndRefocusButton, target: %d", TAG, position); + + if (alwaysScroll || !refocusButton(position)) { + mButtonLayoutManager.scrollToPositionWithOffset(position, 0); + mActionButtonsRecyclerView.post(() -> refocusButton(position)); } - refocusPreviousButton(); } /** - * Hides all menu views, including the menu frame. + * @return true if focus was requested, false if focus request could not be carried out due to + * the view for the position not being available (scrolling beforehand will be necessary). */ + private boolean refocusButton(int position) { + View itemToFocus = mButtonLayoutManager.findViewByPosition(position); + if (itemToFocus != null) { + itemToFocus.requestFocus(); + itemToFocus.requestAccessibilityFocus(); + } + return itemToFocus != null; + } + + /** + * Returns the position of the first action of the given action type or -1 if none can be found. + */ + private int getFirstIndexOfAction(@TvPipAction.ActionType int actionType) { + for (int i = 0; i < mActionsList.size(); i++) { + if (mActionsList.get(i).getActionType() == actionType) { + return i; + } + } + return -1; + } + void hideAllUserControls() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAllUserControls()", TAG); - mFocusedButton = null; - showButtonsMenu(false); + setMenuButtonsVisible(false); hideMovementHints(); setFrameHighlighted(false); + animateAlphaTo(0f, mDimLayer); } @Override @@ -463,134 +418,63 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { }); } - /** - * Button order: - * - Fullscreen - * - Close - * - Custom actions (app or media actions) - * - System actions - */ - void setAdditionalActions(List actions, RemoteAction closeAction, - Handler mainHandler) { + void setAdditionalActions(List actions, RemoteAction closeAction) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions()", TAG); + "%s: setAdditionalActions(), %d actions", TAG, actions.size()); - // Replace system close action with custom close action if available - if (closeAction != null) { - setActionForButton(closeAction, mCloseButton, mainHandler); - } else { - mCloseButton.setTextAndDescription(R.string.pip_close); - mCloseButton.setImageResource(R.drawable.pip_ic_close_white); - } - mCloseButton.setIsCustomCloseAction(closeAction != null); - // Make sure the close action is always enabled - mCloseButton.setEnabled(true); - - // Make sure we exactly as many additional buttons as we have actions to display. - final int actionsNumber = actions.size(); - int buttonsNumber = mAdditionalButtons.size(); - if (actionsNumber > buttonsNumber) { - // Add buttons until we have enough to display all the actions. - while (actionsNumber > buttonsNumber) { - TvWindowMenuActionButton button = new TvWindowMenuActionButton(mContext); - button.setOnClickListener(this); - - mActionButtonsContainer.addView(button, - FIRST_CUSTOM_ACTION_POSITION + buttonsNumber); - mAdditionalButtons.add(button); - - buttonsNumber++; - } - } else if (actionsNumber < buttonsNumber) { - // Hide buttons until we as many as the actions. - while (actionsNumber < buttonsNumber) { - final View button = mAdditionalButtons.get(buttonsNumber - 1); - button.setVisibility(View.GONE); - button.setTag(null); - - buttonsNumber--; + int oldCustomActionCount = 0; + for (TvPipAction action : mActionsList) { + if (action.getActionType() == ACTION_CUSTOM) { + oldCustomActionCount++; } } - // "Assign" actions to the buttons. - for (int index = 0; index < actionsNumber; index++) { - final RemoteAction action = actions.get(index); - final TvWindowMenuActionButton button = mAdditionalButtons.get(index); + // Update close action. + mActionsList.set(CLOSE_ACTION_INDEX, + closeAction == null ? mDefaultCloseAction + : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction)); + mRecyclerViewAdapter.notifyItemChanged(CLOSE_ACTION_INDEX); - // Remove action if it matches the custom close action. - if (PipUtils.remoteActionsMatch(action, closeAction)) { - button.setVisibility(GONE); + // Replace custom actions with new ones. + mActionsList.removeIf(tvPipAction -> tvPipAction.getActionType() == ACTION_CUSTOM); + List customActions = new ArrayList<>(actions.size()); + int newCustomActionCount = 0; + for (RemoteAction action : actions) { + if (action == null || PipUtils.remoteActionsMatch(action, closeAction)) { + // Don't show an action if it is the same as the custom close action continue; } - setActionForButton(action, button, mainHandler); + customActions.add(new TvPipCustomAction(ACTION_CUSTOM, action)); + newCustomActionCount++; } + mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, customActions); - if (mCurrentPipBounds != null) { - updateButtonGravity(mCurrentPipBounds); - refocusPreviousButton(); - } - } + mRecyclerViewAdapter.notifyItemRangeChanged( + FIRST_CUSTOM_ACTION_INDEX, Math.min(oldCustomActionCount, newCustomActionCount)); - private void setActionForButton(RemoteAction action, TvWindowMenuActionButton button, - Handler mainHandler) { - button.setVisibility(View.VISIBLE); // Ensure the button is visible. - if (action.getContentDescription().length() > 0) { - button.setTextAndDescription(action.getContentDescription()); - } else { - button.setTextAndDescription(action.getTitle()); - } - button.setEnabled(action.isEnabled()); - button.setTag(action); - action.getIcon().loadDrawableAsync(mContext, button::setImageDrawable, mainHandler); - } + if (newCustomActionCount > oldCustomActionCount) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setAdditionalActions(), %d inserted starting at %d", + TAG, newCustomActionCount - oldCustomActionCount, + FIRST_CUSTOM_ACTION_INDEX + oldCustomActionCount); + mRecyclerViewAdapter.notifyItemRangeInserted( + FIRST_CUSTOM_ACTION_INDEX + oldCustomActionCount, + newCustomActionCount - oldCustomActionCount); - @Nullable - SurfaceControl getWindowSurfaceControl() { - final ViewRootImpl root = getViewRootImpl(); - if (root == null) { - return null; - } - final SurfaceControl out = root.getSurfaceControl(); - if (out != null && out.isValid()) { - return out; - } - return null; - } - - @Override - public void onClick(View v) { - final int id = v.getId(); - if (id == R.id.tv_pip_menu_fullscreen_button) { - mListener.onFullscreenButtonClick(); - } else if (id == R.id.tv_pip_menu_move_button) { - mListener.onEnterMoveMode(); - } else if (id == R.id.tv_pip_menu_close_button) { - mListener.onCloseButtonClick(); - } else if (id == R.id.tv_pip_menu_expand_button) { - mListener.onToggleExpandedMode(); - } else { - // This should be an "additional action" - final RemoteAction action = (RemoteAction) v.getTag(); - if (action != null) { - try { - action.getActionIntent().send(); - } catch (PendingIntent.CanceledException e) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Failed to send action, %s", TAG, e); - } - } else { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: RemoteAction is null", TAG); - } + } else if (oldCustomActionCount > newCustomActionCount) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: setAdditionalActions(), %d removed starting at %d", + TAG, oldCustomActionCount - newCustomActionCount, + FIRST_CUSTOM_ACTION_INDEX + newCustomActionCount); + mRecyclerViewAdapter.notifyItemRangeRemoved( + FIRST_CUSTOM_ACTION_INDEX + newCustomActionCount, + oldCustomActionCount - newCustomActionCount); } } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == ACTION_UP) { - if (!mMoveMenuIsVisible) { - mFocusedButton = mActionButtonsContainer.getFocusedChild(); - } if (event.getKeyCode() == KEYCODE_BACK) { mListener.onBackPress(); @@ -624,10 +508,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { public void showMovementHints(int gravity) { 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); @@ -643,9 +523,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton); if (a11yEnabled) { + mA11yDoneButton.setVisibility(VISIBLE); mA11yDoneButton.setOnClickListener(v -> { mListener.onExitMoveMode(); }); + mA11yDoneButton.requestFocus(); + mA11yDoneButton.requestAccessibilityFocus(); } } @@ -684,27 +567,81 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { /** * Show or hide the pip buttons menu. */ - public void showButtonsMenu(boolean show) { + private void setMenuButtonsVisible(boolean visible) { 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(); + "%s: showUserActions: %b", TAG, visible); + mButtonMenuIsVisible = visible; + animateAlphaTo(visible ? 1 : 0, mActionButtonsRecyclerView); } private void setFrameHighlighted(boolean highlighted) { mMenuFrameView.setActivated(highlighted); } + private class RecyclerViewAdapter extends + RecyclerView.Adapter { + + private final List mActionList; + + RecyclerViewAdapter(List actionList) { + this.mActionList = actionList; + } + + @NonNull + @Override + public ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ButtonViewHolder(new TvWindowMenuActionButton(mContext)); + } + + @Override + public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) { + TvPipAction action = mActionList.get(position); + action.populateButton(holder.mButton, mMainHandler); + } + + @Override + public int getItemCount() { + return mActionList.size(); + } + + private class ButtonViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + TvWindowMenuActionButton mButton; + + ButtonViewHolder(@NonNull View itemView) { + super(itemView); + mButton = (TvWindowMenuActionButton) itemView; + mButton.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + TvPipAction action = mActionList.get( + mActionButtonsRecyclerView.getChildLayoutPosition(v)); + switch (action.getActionType()) { + case ACTION_FULLSCREEN: + mListener.onFullscreenButtonClick(); + return; + case ACTION_CLOSE: + case ACTION_CUSTOM_CLOSE: + mListener.onCloseButtonClick(); + return; + case ACTION_MOVE: + mListener.onEnterMoveMode(); + return; + case ACTION_EXPAND_COLLAPSE: + mListener.onToggleExpandedMode(); + return; + case ACTION_CUSTOM: + action.executePendingIntent(); + return; + default: + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: No action available", TAG); + } + } + } + } + interface Listener extends TvPipMenuEduTextDrawer.Listener { void onBackPress(); @@ -730,4 +667,4 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { void onToggleExpandedMode(); } -} +} \ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java new file mode 100644 index 0000000000000..83a26c351bcfe --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java @@ -0,0 +1,58 @@ +/* + * 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 android.annotation.DrawableRes; +import android.annotation.NonNull; +import android.annotation.StringRes; +import android.app.PendingIntent; +import android.os.Handler; + +import com.android.wm.shell.common.TvWindowMenuActionButton; + +/** + * A TvPipAction for actions that the system provides, i.e. fullscreen, default close, move, + * expand/collapse. + */ +public class TvPipSystemAction extends TvPipAction { + + @StringRes + private int mTitleResource; + @DrawableRes + private int mIconResource; + + TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon) { + super(actionType); + update(title, icon); + } + + void update(@StringRes int title, @DrawableRes int icon) { + mTitleResource = title; + mIconResource = icon; + } + + void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) { + button.setTextAndDescription(mTitleResource); + button.setImageResource(mIconResource); + button.setEnabled(true); + } + + PendingIntent getPendingIntent() { + return null; + } + +}