From 19911209758f11f6c02b6880a1419f728ff6ba63 Mon Sep 17 00:00:00 2001 From: Sergey Nikolaienkov Date: Wed, 16 Dec 2020 10:30:11 +0000 Subject: [PATCH] Fix and clean up Tv Pip Menu Fix for Tv Pip Menu not properly setting custom actions provided by an application. Fix Pip Menu not closing on the Home button press. Simply and revise Tv Pip Menu implementation which now consists of 3 classes: - TvPipMenuController - TvPipMenuView (formerly PipMenuView) - TvPipMenuActionButton (formerly PipControlButtonView) and thus spare - PipControlsView - PipControlsViewContoller Bug: 174818743 Test: atest WMShellFlickerTests:TvPipBasicTest Test: atest WMShellFlickerTests:TvPipMenuTests Test: atest WMShellFlickerTests:TvPipNotificationTests Change-Id: I9e08ca142e0407a4ede084323b0201194a7e89b8 --- .../Shell/res/layout/tv_pip_controls.xml | 33 --- .../Shell/res/layout/tv_pip_menu.xml | 46 +++- ...tton.xml => tv_pip_menu_action_button.xml} | 2 +- ... tv_pip_menu_additional_action_button.xml} | 2 +- .../wm/shell/pip/tv/PipController.java | 12 +- .../wm/shell/pip/tv/PipControlsView.java | 61 ----- .../pip/tv/PipControlsViewController.java | 158 ------------ .../android/wm/shell/pip/tv/PipMenuView.java | 126 ---------- ...onView.java => TvPipMenuActionButton.java} | 88 +++---- .../wm/shell/pip/tv/TvPipMenuController.java | 157 +++++++++--- .../wm/shell/pip/tv/TvPipMenuView.java | 235 ++++++++++++++++++ .../wm/shell/flicker/pip/tv/TvPipMenuTests.kt | 2 +- .../wm/shell/flicker/pip/tv/TvUtils.kt | 8 +- .../android/systemui/wmshell/TvPipModule.java | 26 +- 14 files changed, 453 insertions(+), 503 deletions(-) delete mode 100644 libs/WindowManager/Shell/res/layout/tv_pip_controls.xml rename libs/WindowManager/Shell/res/layout/{tv_pip_control_button.xml => tv_pip_menu_action_button.xml} (96%) rename libs/WindowManager/Shell/res/layout/{tv_pip_custom_control.xml => tv_pip_menu_additional_action_button.xml} (94%) delete mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsView.java delete mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlsViewController.java delete mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuView.java rename libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/{PipControlButtonView.java => TvPipMenuActionButton.java} (68%) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_controls.xml b/libs/WindowManager/Shell/res/layout/tv_pip_controls.xml deleted file mode 100644 index 9157f63ce1b31..0000000000000 --- a/libs/WindowManager/Shell/res/layout/tv_pip_controls.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 0d684e8b0ab53..49e2379589a45 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -14,19 +14,39 @@ See the License for the specific language governing permissions and limitations under the License. --> - + + - - + android:layout_gravity="center_horizontal" + android:layout_marginTop="350dp" + android:orientation="horizontal" + android:alpha="0"> + + + + + + + + + + diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_control_button.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml similarity index 96% rename from libs/WindowManager/Shell/res/layout/tv_pip_control_button.xml rename to libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml index 727ac3412a25b..5925008e0d084 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_control_button.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - mAdditionalButtons = new ArrayList<>(); - - private final List mCustomActions = new ArrayList<>(); - private final List mMediaActions = new ArrayList<>(); - - public PipControlsViewController(PipControlsView view, PipController pipController) { - mContext = view.getContext(); - mUiThreadHandler = new Handler(Looper.getMainLooper()); - mPipController = pipController; - mView = view; - - mView.getFullscreenButton().setOnClickListener(v -> mPipController.movePipToFullscreen()); - mView.getCloseButton().setOnClickListener(v -> mPipController.closePip()); - - mPipController.getPipMediaController().addActionListener(this::onMediaActionsChanged); - } - - PipControlsView getView() { - return mView; - } - - /** - * Updates the set of activity-defined actions. - */ - void setCustomActions(List actions) { - if (mCustomActions.isEmpty() && actions.isEmpty()) { - // Nothing changed - return early. - return; - } - mCustomActions.clear(); - mCustomActions.addAll(actions); - updateAdditionalActions(); - } - - private void onMediaActionsChanged(List actions) { - if (mMediaActions.isEmpty() && actions.isEmpty()) { - // Nothing changed - return early. - return; - } - mMediaActions.clear(); - mMediaActions.addAll(actions); - - // Update the view only if there are no custom actions (media actions are only shown when - // there no custom actions). - if (mCustomActions.isEmpty()) { - updateAdditionalActions(); - } - } - - private void updateAdditionalActions() { - final List actionsToDisplay; - if (!mCustomActions.isEmpty()) { - // If there are custom actions: show them. - actionsToDisplay = mCustomActions; - } else if (!mMediaActions.isEmpty()) { - // If there are no custom actions, but there media actions: show them. - actionsToDisplay = mMediaActions; - } else { - // If there no custom actions and no media actions: clean up all the additional buttons. - actionsToDisplay = Collections.emptyList(); - } - - // Make sure we exactly as many additional buttons as we have actions to display. - final int actionsNumber = actionsToDisplay.size(); - int buttonsNumber = mAdditionalButtons.size(); - if (actionsNumber > buttonsNumber) { - final LayoutInflater layoutInflater = LayoutInflater.from(mContext); - // Add buttons until we have enough to display all of the actions. - while (actionsNumber > buttonsNumber) { - final PipControlButtonView button = (PipControlButtonView) layoutInflater.inflate( - R.layout.tv_pip_custom_control, mView, false); - mView.addView(button); - 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.setOnClickListener(null); - - buttonsNumber--; - } - } - - // "Assign" actions to the buttons. - for (int index = 0; index < actionsNumber; index++) { - final RemoteAction action = actionsToDisplay.get(index); - final PipControlButtonView button = mAdditionalButtons.get(index); - button.setVisibility(View.VISIBLE); // Ensure the button is visible. - button.setText(action.getContentDescription()); - button.setEnabled(action.isEnabled()); - button.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); - button.setOnClickListener(v -> { - try { - action.getActionIntent().send(); - } catch (PendingIntent.CanceledException e) { - Log.w(TAG, "Failed to send action", e); - } - }); - - action.getIcon().loadDrawableAsync(mContext, drawable -> { - drawable.setTint(Color.WHITE); - button.setImageDrawable(drawable); - }, mUiThreadHandler); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuView.java deleted file mode 100644 index 83cb7ce8065be..0000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipMenuView.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2020 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.KeyEvent.ACTION_UP; -import static android.view.KeyEvent.KEYCODE_BACK; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.annotation.Nullable; -import android.app.RemoteAction; -import android.content.Context; -import android.content.pm.ParceledListSlice; -import android.util.Log; -import android.view.KeyEvent; -import android.view.SurfaceControl; -import android.view.ViewRootImpl; -import android.view.WindowManagerGlobal; -import android.widget.FrameLayout; - -import com.android.wm.shell.R; - -import java.util.Collections; - -/** - * The Menu View that shows controls of the PiP. Always fullscreen. - */ -public class PipMenuView extends FrameLayout { - private static final String TAG = "PipMenuView"; - private static final boolean DEBUG = PipController.DEBUG; - - private final Animator mFadeInAnimation; - private final Animator mFadeOutAnimation; - private final PipControlsViewController mPipControlsViewController; - @Nullable - private OnBackPressListener mOnBackPressListener; - - public PipMenuView(Context context, PipController pipController) { - super(context, null, 0); - inflate(context, R.layout.tv_pip_menu, this); - - mPipControlsViewController = new PipControlsViewController( - findViewById(R.id.pip_controls), pipController); - mFadeInAnimation = AnimatorInflater.loadAnimator( - mContext, R.anim.tv_pip_menu_fade_in_animation); - mFadeInAnimation.setTarget(mPipControlsViewController.getView()); - mFadeOutAnimation = AnimatorInflater.loadAnimator( - mContext, R.anim.tv_pip_menu_fade_out_animation); - mFadeOutAnimation.setTarget(mPipControlsViewController.getView()); - } - - @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; - } - - void showMenu() { - mFadeInAnimation.start(); - setAlpha(1.0f); - grantWindowFocus(true); - } - - void hideMenu() { - mFadeOutAnimation.start(); - setAlpha(0.0f); - grantWindowFocus(false); - } - - private void grantWindowFocus(boolean grantFocus) { - try { - WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, - getViewRootImpl().getInputToken(), grantFocus); - } catch (Exception e) { - Log.e(TAG, "Unable to update focus as menu disappears", e); - } - } - - void setOnBackPressListener(OnBackPressListener onBackPressListener) { - mOnBackPressListener = onBackPressListener; - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getKeyCode() == KEYCODE_BACK && event.getAction() == ACTION_UP - && mOnBackPressListener != null) { - mOnBackPressListener.onBackPress(); - return true; - } else { - return super.dispatchKeyEvent(event); - } - } - - void setAppActions(ParceledListSlice actions) { - if (DEBUG) Log.d(TAG, "onPipMenuActionsChanged()"); - - boolean hasCustomActions = actions != null && !actions.getList().isEmpty(); - mPipControlsViewController.setCustomActions( - hasCustomActions ? actions.getList() : Collections.emptyList()); - } - - interface OnBackPressListener { - void onBackPress(); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlButtonView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java similarity index 68% rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlButtonView.java rename to libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java index 4e82bb557fb98..6f7cd82f8da01 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipControlButtonView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java @@ -31,64 +31,51 @@ import android.widget.TextView; import com.android.wm.shell.R; /** - * A view containing PIP controls including fullscreen, close, and media controls. + * A View that represents Pip Menu action button, such as "Fullscreen" and "Close" as well custom + * (provided by the application in Pip) and media buttons. */ -public class PipControlButtonView extends RelativeLayout { - - private OnFocusChangeListener mFocusChangeListener; - private ImageView mIconImageView; - ImageView mButtonImageView; - private TextView mDescriptionTextView; +public class TvPipMenuActionButton extends RelativeLayout implements View.OnClickListener { + private final ImageView mIconImageView; + private final ImageView mButtonImageView; + private final TextView mDescriptionTextView; private Animator mTextFocusGainAnimator; private Animator mButtonFocusGainAnimator; private Animator mTextFocusLossAnimator; private Animator mButtonFocusLossAnimator; + private OnClickListener mOnClickListener; - private final OnFocusChangeListener mInternalFocusChangeListener = - new OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - startFocusGainAnimation(); - } else { - startFocusLossAnimation(); - } - - if (mFocusChangeListener != null) { - mFocusChangeListener.onFocusChange(PipControlButtonView.this, hasFocus); - } - } - }; - - public PipControlButtonView(Context context) { + public TvPipMenuActionButton(Context context) { this(context, null, 0, 0); } - public PipControlButtonView(Context context, AttributeSet attrs) { + public TvPipMenuActionButton(Context context, AttributeSet attrs) { this(context, attrs, 0, 0); } - public PipControlButtonView(Context context, AttributeSet attrs, int defStyleAttr) { + public TvPipMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } - public PipControlButtonView( + public TvPipMenuActionButton( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - LayoutInflater inflater = (LayoutInflater) getContext() + final LayoutInflater inflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.tv_pip_control_button, this); + inflater.inflate(R.layout.tv_pip_menu_action_button, this); mIconImageView = findViewById(R.id.icon); mButtonImageView = findViewById(R.id.button); mDescriptionTextView = findViewById(R.id.desc); - int[] values = new int[]{android.R.attr.src, android.R.attr.text}; - TypedArray typedArray = context.obtainStyledAttributes(attrs, values, defStyleAttr, + final int[] values = new int[]{android.R.attr.src, android.R.attr.text}; + final TypedArray typedArray = context.obtainStyledAttributes(attrs, values, defStyleAttr, defStyleRes); setImageResource(typedArray.getResourceId(0, 0)); - setText(typedArray.getResourceId(1, 0)); + final int textResId = typedArray.getResourceId(1, 0); + if (textResId != 0) { + setTextAndDescription(getContext().getString(textResId)); + } typedArray.recycle(); } @@ -96,7 +83,13 @@ public class PipControlButtonView extends RelativeLayout { @Override public void onFinishInflate() { super.onFinishInflate(); - mButtonImageView.setOnFocusChangeListener(mInternalFocusChangeListener); + mButtonImageView.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) { + startFocusGainAnimation(); + } else { + startFocusLossAnimation(); + } + }); mTextFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(), R.anim.tv_pip_controls_focus_gain_animation); @@ -115,12 +108,19 @@ public class PipControlButtonView extends RelativeLayout { @Override public void setOnClickListener(OnClickListener listener) { - mButtonImageView.setOnClickListener(listener); + // We do not want to set an OnClickListener to the TvPipMenuActionButton 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; + mButtonImageView.setOnClickListener(listener != null ? this : null); } @Override - public void setOnFocusChangeListener(OnFocusChangeListener listener) { - mFocusChangeListener = listener; + public void onClick(View v) { + if (mOnClickListener != null) { + // Pass the correct view - this. + mOnClickListener.onClick(this); + } } /** @@ -142,21 +142,11 @@ public class PipControlButtonView extends RelativeLayout { /** * Sets the text for description the with the given string. */ - public void setText(CharSequence text) { + public void setTextAndDescription(CharSequence text) { mButtonImageView.setContentDescription(text); mDescriptionTextView.setText(text); } - /** - * Sets the text for description the with the given resource id. - */ - public void setText(int resId) { - if (resId != 0) { - mButtonImageView.setContentDescription(getContext().getString(resId)); - mDescriptionTextView.setText(resId); - } - } - private static void cancelAnimator(Animator animator) { if (animator.isStarted()) { animator.cancel(); @@ -187,8 +177,8 @@ public class PipControlButtonView extends RelativeLayout { mTextFocusLossAnimator.start(); if (mButtonImageView.hasFocus()) { // Button uses ripple that has the default animation for the focus changes. - // Howevever, it doesn't expose the API to fade out while it is focused, - // so we should manually run the fade out animation when PIP controls row loses focus. + // However, it doesn't expose the API to fade out while it is focused, so we should + // manually run the fade out animation when PIP controls row loses focus. mButtonFocusLossAnimator.start(); } } 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 5d0d761abd932..9192cf14cd9b5 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 @@ -19,38 +19,97 @@ package com.android.wm.shell.pip.tv; import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; import android.app.RemoteAction; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ParceledListSlice; import android.util.Log; import android.view.SurfaceControl; +import androidx.annotation.Nullable; + import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipMenuController; +import java.util.ArrayList; +import java.util.List; + /** * Manages the visibility of the PiP Menu as user interacts with PiP. */ -public class TvPipMenuController implements PipMenuController { +public class TvPipMenuController implements PipMenuController, TvPipMenuView.Listener { private static final String TAG = "TvPipMenuController"; private static final boolean DEBUG = PipController.DEBUG; private final Context mContext; private final SystemWindows mSystemWindows; private final PipBoundsState mPipBoundsState; - private PipMenuView mMenuView; - private PipController mPipController; + + private Delegate mDelegate; private SurfaceControl mLeash; + private TvPipMenuView mMenuView; + + private final List mMediaActions = new ArrayList<>(); + private final List mAppActions = new ArrayList<>(); public TvPipMenuController(Context context, PipBoundsState pipBoundsState, - SystemWindows systemWindows) { + SystemWindows systemWindows, PipMediaController pipMediaController) { mContext = context; mPipBoundsState = pipBoundsState; mSystemWindows = systemWindows; + + // We need to "close" the menu the platform call for all the system dialogs to close (for + // example, on the Home button press). + final BroadcastReceiver closeSystemDialogsBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + hideMenu(); + } + }; + context.registerReceiver(closeSystemDialogsBroadcastReceiver, + new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + + pipMediaController.addActionListener(this::onMediaActionsChanged); } - void attachPipController(PipController pipController) { - mPipController = pipController; + void setDelegate(Delegate delegate) { + if (DEBUG) Log.d(TAG, "setDelegate(), delegate=" + delegate); + if (mDelegate != null) { + throw new IllegalStateException( + "The delegate has already been set and should not change."); + } + if (delegate == null) { + throw new IllegalArgumentException("The delegate must not be null."); + } + + mDelegate = delegate; + } + + @Override + public void attach(SurfaceControl leash) { + if (mDelegate == null) { + throw new IllegalStateException("Delegate is not set."); + } + + mLeash = leash; + attachPipMenuView(); + } + + private void attachPipMenuView() { + if (DEBUG) Log.d(TAG, "attachPipMenuView()"); + + if (mMenuView != null) { + detachPipMenuView(); + } + + mMenuView = new TvPipMenuView(mContext); + mMenuView.setListener(this); + mSystemWindows.addView(mMenuView, + getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), + 0, SHELL_ROOT_LAYER_PIP); } @Override @@ -61,7 +120,8 @@ public class TvPipMenuController implements PipMenuController { mSystemWindows.updateViewLayout(mMenuView, getPipMenuLayoutParams(MENU_WINDOW_TITLE, mPipBoundsState.getDisplayBounds().width(), mPipBoundsState.getDisplayBounds().height())); - mMenuView.showMenu(); + maybeUpdateMenuViewActions(); + mMenuView.show(); // By default, SystemWindows views are above everything else. // Set the relative z-order so the menu is below PiP. @@ -77,17 +137,11 @@ public class TvPipMenuController implements PipMenuController { if (DEBUG) Log.d(TAG, "hideMenu()"); if (isMenuVisible()) { - mMenuView.hideMenu(); - mPipController.resizePinnedStack(PipController.STATE_PIP); + mMenuView.hide(); + mDelegate.movePipToNormalPosition(); } } - @Override - public void attach(SurfaceControl leash) { - mLeash = leash; - attachPipMenuView(); - } - @Override public void detach() { hideMenu(); @@ -95,20 +149,6 @@ public class TvPipMenuController implements PipMenuController { mLeash = null; } - private void attachPipMenuView() { - if (DEBUG) Log.d(TAG, "attachPipMenuView()"); - - if (mMenuView != null) { - detachPipMenuView(); - } - - mMenuView = new PipMenuView(mContext, mPipController); - mMenuView.setOnBackPressListener(this::hideMenu); - mSystemWindows.addView(mMenuView, - getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), - 0, SHELL_ROOT_LAYER_PIP); - } - private void detachPipMenuView() { if (DEBUG) Log.d(TAG, "detachPipMenuView()"); @@ -121,18 +161,65 @@ public class TvPipMenuController implements PipMenuController { } @Override - public void setAppActions(ParceledListSlice appActions) { - if (DEBUG) Log.d(TAG, "setAppActions(), actions=" + appActions); + public void setAppActions(ParceledListSlice actions) { + if (DEBUG) Log.d(TAG, "setAppActions()"); + updateAdditionalActionsList(mAppActions, actions.getList()); + } - if (mMenuView != null) { - mMenuView.setAppActions(appActions); + private void onMediaActionsChanged(List actions) { + if (DEBUG) Log.d(TAG, "onMediaActionsChanged()"); + updateAdditionalActionsList(mMediaActions, actions); + } + + private void updateAdditionalActionsList( + List destination, @Nullable List source) { + final int number = source != null ? source.size() : 0; + if (number == 0 && destination.isEmpty()) { + // Nothing changed. + return; + } + + destination.clear(); + if (number > 0) { + destination.addAll(source); + } + maybeUpdateMenuViewActions(); + } + + private void maybeUpdateMenuViewActions() { + if (mMenuView == null) { + return; + } + if (!mAppActions.isEmpty()) { + mMenuView.setAdditionalActions(mAppActions); } else { - Log.w(TAG, "Cannot set remote actions, there is no View"); + mMenuView.setAdditionalActions(mMediaActions); } } @Override public boolean isMenuVisible() { - return mMenuView != null && mMenuView.getAlpha() == 1.0f; + return mMenuView != null && mMenuView.isVisible(); + } + + @Override + public void onBackPress() { + hideMenu(); + } + + @Override + public void onCloseButtonClick() { + mDelegate.closePip(); + } + + @Override + public void onFullscreenButtonClick() { + mDelegate.movePipToFullscreen(); + } + + interface Delegate { + void movePipToNormalPosition(); + void movePipToFullscreen(); + void closePip(); } } 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 new file mode 100644 index 0000000000000..f7b76c1ec7452 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2020 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.animation.AnimatorInflater.loadAnimator; +import static android.view.KeyEvent.ACTION_UP; +import static android.view.KeyEvent.KEYCODE_BACK; + +import android.animation.Animator; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.Context; +import android.graphics.Color; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowManagerGlobal; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * A View that represents Pip Menu on TV. It's responsible for displaying 2 ever-present Pip Menu + * actions: Fullscreen and Close, but could also display "additional" actions, that may be set via + * a {@link #setAdditionalActions(List)} call. + */ +public class TvPipMenuView extends FrameLayout implements View.OnClickListener { + private static final String TAG = "TvPipMenuView"; + private static final boolean DEBUG = PipController.DEBUG; + + private static final float DISABLED_ACTION_ALPHA = 0.54f; + + private final Handler mUiThreadHandler; + private final Animator mFadeInAnimation; + private final Animator mFadeOutAnimation; + @Nullable private Listener mListener; + + private final LinearLayout mActionButtonsContainer; + private final List mAdditionalButtons = new ArrayList<>(); + + 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); + mUiThreadHandler = new Handler(Looper.getMainLooper()); + + inflate(context, R.layout.tv_pip_menu, this); + + mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons); + mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button) + .setOnClickListener(this); + mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button) + .setOnClickListener(this); + + mFadeInAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_in_animation); + mFadeInAnimation.setTarget(mActionButtonsContainer); + + mFadeOutAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_out_animation); + mFadeOutAnimation.setTarget(mActionButtonsContainer); + } + + void setListener(@Nullable Listener listener) { + mListener = listener; + } + + void show() { + if (DEBUG) Log.d(TAG, "show()"); + + mFadeInAnimation.start(); + setAlpha(1.0f); + grantWindowFocus(true); + } + + void hide() { + if (DEBUG) Log.d(TAG, "hide()"); + + mFadeOutAnimation.start(); + setAlpha(0.0f); + grantWindowFocus(false); + } + + boolean isVisible() { + return getAlpha() == 1.0f; + } + + private void grantWindowFocus(boolean grantFocus) { + if (DEBUG) Log.d(TAG, "grantWindowFocus(" + grantFocus + ")"); + + try { + WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, + getViewRootImpl().getInputToken(), grantFocus); + } catch (Exception e) { + Log.e(TAG, "Unable to update focus", e); + } + } + + void setAdditionalActions(List actions) { + if (DEBUG) Log.d(TAG, "setAdditionalActions()"); + + // 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) { + final LayoutInflater layoutInflater = LayoutInflater.from(mContext); + // Add buttons until we have enough to display all of the actions. + while (actionsNumber > buttonsNumber) { + final TvPipMenuActionButton button = (TvPipMenuActionButton) layoutInflater.inflate( + R.layout.tv_pip_menu_additional_action_button, mActionButtonsContainer, + false); + button.setOnClickListener(this); + + mActionButtonsContainer.addView(button); + 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--; + } + } + + // "Assign" actions to the buttons. + for (int index = 0; index < actionsNumber; index++) { + final RemoteAction action = actions.get(index); + final TvPipMenuActionButton button = mAdditionalButtons.get(index); + button.setVisibility(View.VISIBLE); // Ensure the button is visible. + button.setTextAndDescription(action.getContentDescription()); + button.setEnabled(action.isEnabled()); + button.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); + button.setTag(action); + + action.getIcon().loadDrawableAsync(mContext, drawable -> { + drawable.setTint(Color.WHITE); + button.setImageDrawable(drawable); + }, mUiThreadHandler); + } + } + + @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) { + if (mListener == null) return; + + final int id = v.getId(); + if (id == R.id.tv_pip_menu_fullscreen_button) { + mListener.onFullscreenButtonClick(); + } else if (id == R.id.tv_pip_menu_close_button) { + mListener.onCloseButtonClick(); + } else { + // This should be an "additional action" + final RemoteAction action = (RemoteAction) v.getTag(); + if (action != null) { + try { + action.getActionIntent().send(); + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, "Failed to send action", e); + } + } else { + Log.w(TAG, "RemoteAction is null"); + } + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK + && mListener != null) { + mListener.onBackPress(); + return true; + } + return super.dispatchKeyEvent(event); + } + + interface Listener { + void onBackPress(); + void onCloseButtonClick(); + void onFullscreenButtonClick(); + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt index 66efb5ae3c2da..6105f50562d78 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt @@ -208,8 +208,8 @@ class TvPipMenuTests : TvPipTestBase() { @Test fun pipMenu_customActions_override_mediaControls() { // Start media session before entering PiP with custom actions. - testApp.clickStartMediaSessionButton() testApp.checkWithCustomActionsCheckbox() + testApp.clickStartMediaSessionButton() enterPip_openMenu_assertShown() // PiP menu should contain "No-Op", "Off" and "Clear" buttons for the custom actions... diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt index 587b5510b0b48..4a38b0e94ba95 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt @@ -26,9 +26,9 @@ import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME /** Id of the root view in the com.android.wm.shell.pip.tv.PipMenuActivity */ private const val TV_PIP_MENU_ROOT_ID = "tv_pip_menu" -private const val TV_PIP_MENU_CONTROLS_ID = "pip_controls" -private const val TV_PIP_MENU_CLOSE_BUTTON_ID = "close_button" -private const val TV_PIP_MENU_FULLSCREEN_BUTTON_ID = "full_button" +private const val TV_PIP_MENU_BUTTONS_CONTAINER_ID = "tv_pip_menu_action_buttons" +private const val TV_PIP_MENU_CLOSE_BUTTON_ID = "tv_pip_menu_close_button" +private const val TV_PIP_MENU_FULLSCREEN_BUTTON_ID = "tv_pip_menu_fullscreen_button" private const val FOCUS_ATTEMPTS = 10 private const val WAIT_TIME_MS = 3_000L @@ -49,7 +49,7 @@ fun UiDevice.waitForTvPipMenuToClose(): Boolean = wait(Until.gone(tvPipMenuSelec fun UiDevice.findTvPipMenuControls(): UiObject2? = findObject(tvPipMenuSelector) - ?.findObject(By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CONTROLS_ID)) + ?.findObject(By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_BUTTONS_CONTAINER_ID)) fun UiDevice.findTvPipMenuCloseButton(): UiObject2? = findObject(tvPipMenuSelector)?.findObject(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java index 125b5d4c7b8df..4d3af9c011539 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java @@ -24,6 +24,7 @@ import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; @@ -32,11 +33,8 @@ import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.tv.PipController; -import com.android.wm.shell.pip.tv.PipControlsView; -import com.android.wm.shell.pip.tv.PipControlsViewController; import com.android.wm.shell.pip.tv.PipNotification; import com.android.wm.shell.pip.tv.TvPipMenuController; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import java.util.Optional; @@ -73,19 +71,6 @@ public abstract class TvPipModule { windowManagerShellWrapper)); } - @WMSingleton - @Provides - static PipControlsViewController providePipControlsViewController( - PipControlsView pipControlsView, PipController pipController) { - return new PipControlsViewController(pipControlsView, pipController); - } - - @WMSingleton - @Provides - static PipControlsView providePipControlsView(Context context) { - return new PipControlsView(context, null); - } - @WMSingleton @Provides static PipNotification providePipNotification(Context context, @@ -108,9 +93,12 @@ public abstract class TvPipModule { @WMSingleton @Provides - static TvPipMenuController providesPipTvMenuController(Context context, - PipBoundsState pipBoundsState, SystemWindows systemWindows) { - return new TvPipMenuController(context, pipBoundsState, systemWindows); + static TvPipMenuController providesPipTvMenuController( + Context context, + PipBoundsState pipBoundsState, + SystemWindows systemWindows, + PipMediaController pipMediaController) { + return new TvPipMenuController(context, pipBoundsState, systemWindows, pipMediaController); } @WMSingleton