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