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 extends RemoteAction> 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