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
This commit is contained in:
Sergey Nikolaienkov
2020-12-16 10:30:11 +00:00
parent 9eb96adc00
commit 1991120975
14 changed files with 453 additions and 503 deletions

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<!-- Layout for {@link com.android.wm.shell.pip.tv.PipControlsView}. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<com.android.wm.shell.pip.tv.PipControlButtonView
android:id="@+id/full_button"
android:layout_width="@dimen/picture_in_picture_button_width"
android:layout_height="wrap_content"
android:src="@drawable/pip_ic_fullscreen_white"
android:text="@string/pip_fullscreen" />
<com.android.wm.shell.pip.tv.PipControlButtonView
android:id="@+id/close_button"
android:layout_width="@dimen/picture_in_picture_button_width"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/picture_in_picture_button_start_margin"
android:src="@drawable/pip_ic_close_white"
android:text="@string/pip_close" />
</merge>

View File

@@ -14,19 +14,39 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_pip_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingTop="350dp"
android:background="#CC000000"
android:gravity="top|center_horizontal"
android:clipChildren="false">
<!-- Layout for TvPipMenuView -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_pip_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#CC000000">
<com.android.wm.shell.pip.tv.PipControlsView
android:id="@+id/pip_controls"
<LinearLayout
android:id="@+id/tv_pip_menu_action_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0" />
</LinearLayout>
android:layout_gravity="center_horizontal"
android:layout_marginTop="350dp"
android:orientation="horizontal"
android:alpha="0">
<com.android.wm.shell.pip.tv.TvPipMenuActionButton
android:id="@+id/tv_pip_menu_fullscreen_button"
android:layout_width="@dimen/picture_in_picture_button_width"
android:layout_height="wrap_content"
android:src="@drawable/pip_ic_fullscreen_white"
android:text="@string/pip_fullscreen" />
<com.android.wm.shell.pip.tv.TvPipMenuActionButton
android:id="@+id/tv_pip_menu_close_button"
android:layout_width="@dimen/picture_in_picture_button_width"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/picture_in_picture_button_start_margin"
android:src="@drawable/pip_ic_close_white"
android:text="@string/pip_close" />
<!-- More TvPipMenuActionButtons may be added here at runtime. -->
</LinearLayout>
</FrameLayout>

View File

@@ -14,7 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Layout for {@link com.android.wm.shell.pip.tv.PipControlButtonView}. -->
<!-- Layout for TvPipMenuActionButton -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView android:id="@+id/button"

View File

@@ -14,7 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<com.android.wm.shell.pip.tv.PipControlButtonView
<com.android.wm.shell.pip.tv.TvPipMenuActionButton
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/picture_in_picture_button_width"
android:layout_height="wrap_content"

View File

@@ -60,7 +60,8 @@ import java.util.Objects;
/**
* Manages the picture-in-picture (PIP) UI and states.
*/
public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallback {
public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallback,
TvPipMenuController.Delegate {
private static final String TAG = "TvPipController";
static final boolean DEBUG = false;
@@ -198,7 +199,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
mPipBoundsAlgorithm = pipBoundsAlgorithm;
mPipMediaController = pipMediaController;
mTvPipMenuController = tvPipMenuController;
mTvPipMenuController.attachPipController(this);
mTvPipMenuController.setDelegate(this);
// Ensure that we have the display info in case we get calls to update the bounds
// before the listener calls back
final DisplayInfo displayInfo = new DisplayInfo();
@@ -289,6 +290,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
/**
* Closes PIP (PIPed activity and PIP system UI).
*/
@Override
public void closePip() {
if (DEBUG) Log.d(TAG, "closePip(), current state=" + getStateDescription());
@@ -318,9 +320,15 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
mHandler.removeCallbacks(mClosePipRunnable);
}
@Override
public void movePipToNormalPosition() {
resizePinnedStack(PipController.STATE_PIP);
}
/**
* Moves the PIPed activity to the fullscreen and closes PIP system UI.
*/
@Override
public void movePipToFullscreen() {
if (DEBUG) Log.d(TAG, "movePipToFullscreen(), current state=" + getStateDescription());

View File

@@ -1,61 +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 android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import com.android.wm.shell.R;
/**
* A view containing PIP controls including fullscreen, close, and media controls.
*/
public class PipControlsView extends LinearLayout {
public PipControlsView(Context context) {
this(context, null);
}
public PipControlsView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
layoutInflater.inflate(R.layout.tv_pip_controls, this);
setOrientation(LinearLayout.HORIZONTAL);
setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
}
PipControlButtonView getFullscreenButton() {
return findViewById(R.id.full_button);
}
PipControlButtonView getCloseButton() {
return findViewById(R.id.close_button);
}
}

View File

@@ -1,158 +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 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.Log;
import android.view.LayoutInflater;
import android.view.View;
import com.android.wm.shell.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Controller for {@link PipControlsView}.
*/
public class PipControlsViewController {
private static final String TAG = PipControlsViewController.class.getSimpleName();
private static final float DISABLED_ACTION_ALPHA = 0.54f;
private final PipController mPipController;
private final Context mContext;
private final Handler mUiThreadHandler;
private final PipControlsView mView;
private final List<PipControlButtonView> mAdditionalButtons = new ArrayList<>();
private final List<RemoteAction> mCustomActions = new ArrayList<>();
private final List<RemoteAction> 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<RemoteAction> 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<RemoteAction> 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);
}
}
}

View File

@@ -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<RemoteAction> 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();
}
}

View File

@@ -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();
}
}

View File

@@ -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<RemoteAction> mMediaActions = new ArrayList<>();
private final List<RemoteAction> 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<RemoteAction> appActions) {
if (DEBUG) Log.d(TAG, "setAppActions(), actions=" + appActions);
public void setAppActions(ParceledListSlice<RemoteAction> actions) {
if (DEBUG) Log.d(TAG, "setAppActions()");
updateAdditionalActionsList(mAppActions, actions.getList());
}
if (mMenuView != null) {
mMenuView.setAppActions(appActions);
private void onMediaActionsChanged(List<RemoteAction> actions) {
if (DEBUG) Log.d(TAG, "onMediaActionsChanged()");
updateAdditionalActionsList(mMediaActions, actions);
}
private void updateAdditionalActionsList(
List<RemoteAction> destination, @Nullable List<RemoteAction> 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();
}
}

View File

@@ -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<TvPipMenuActionButton> 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<RemoteAction> 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();
}
}

View File

@@ -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...

View File

@@ -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)

View File

@@ -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