TV Pip fixes after menu migration to Window

Moving TV Pip menu from an Activity to a Window caused NPE in
TvPipMenuController when referencing PipMenuView. This change fixes the
NPE as well as does initial refactoring in TV Pip implementation, such
as removing .tv.PipController.Listener and
.tv.PipContoller.suspend(resume)PipResizing methods.

Bug: 174818743
Bug: 171956642
Test: atest WMShellFlickerTests:TvPipBasicTest
Change-Id: Ifcd6c777a561421b45703300e3eccb44c13e2172
This commit is contained in:
Sergey Nikolaienkov
2020-12-04 06:26:24 +00:00
parent bbf75b5aa4
commit 919cce15f4
4 changed files with 97 additions and 248 deletions

View File

@@ -37,7 +37,6 @@ import android.content.IntentFilter;
import android.content.pm.ParceledListSlice;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Debug;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -56,8 +55,6 @@ import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipTaskOrganizer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
@@ -87,42 +84,21 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
private static final int TASK_ID_NO_PIP = -1;
private static final int INVALID_RESOURCE_TYPE = -1;
public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH = 0x1;
/**
* PIPed activity is playing a media and it can be paused.
*/
static final int PLAYBACK_STATE_PLAYING = 0;
/**
* PIPed activity has a paused media and it can be played.
*/
static final int PLAYBACK_STATE_PAUSED = 1;
/**
* Users are unable to control PIPed activity's media playback.
*/
static final int PLAYBACK_STATE_UNAVAILABLE = 2;
private static final int CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS = 3000;
private int mSuspendPipResizingReason;
private final Context mContext;
private final PipBoundsState mPipBoundsState;
private final PipBoundsAlgorithm mPipBoundsAlgorithm;
private final PipTaskOrganizer mPipTaskOrganizer;
private final PipMediaController mPipMediaController;
private final TvPipMenuController mTvPipMenuController;
private final PipNotification mPipNotification;
private IActivityTaskManager mActivityTaskManager;
private int mState = STATE_NO_PIP;
private int mResumeResizePinnedStackRunnableState = STATE_NO_PIP;
private final Handler mHandler = new Handler();
private List<Listener> mListeners = new ArrayList<>();
private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED;
private int mPipTaskId = TASK_ID_NO_PIP;
private int mPinnedStackId = INVALID_STACK_ID;
private String[] mLastPackagesResourceGranted;
private PipNotification mPipNotification;
private ParceledListSlice<RemoteAction> mCustomActions;
private WindowManagerShellWrapper mWindowManagerShellWrapper;
private int mResizeAnimationDuration;
@@ -135,9 +111,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
private boolean mImeVisible;
private int mImeHeightAdjustment;
private final Runnable mResizePinnedStackRunnable =
() -> resizePinnedStack(mResumeResizePinnedStackRunnableState);
private final Runnable mClosePipRunnable = () -> closePip();
private final Runnable mClosePipRunnable = this::closePip;
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -244,8 +218,6 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
mPipTaskOrganizer.registerPipTransitionCallback(this);
mActivityTaskManager = ActivityTaskManager.getService();
addListener(mPipNotification);
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_CLOSE);
intentFilter.addAction(ACTION_MENU);
@@ -347,9 +319,8 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
mPinnedStackId = INVALID_STACK_ID;
}
}
for (int i = mListeners.size() - 1; i >= 0; --i) {
mListeners.get(i).onPipActivityClosed();
}
mPipNotification.dismiss();
mTvPipMenuController.hideMenu();
mHandler.removeCallbacks(mClosePipRunnable);
}
@@ -360,9 +331,9 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
if (DEBUG) Log.d(TAG, "movePipToFullscreen(), current state=" + getStateDescription());
mPipTaskId = TASK_ID_NO_PIP;
for (int i = mListeners.size() - 1; i >= 0; --i) {
mListeners.get(i).onMoveToFullscreen();
}
mTvPipMenuController.hideMenu();
mPipNotification.dismiss();
resizePinnedStack(STATE_NO_PIP);
}
@@ -386,9 +357,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
// Set state to STATE_PIP so we show it when the pinned stack animation ends.
mState = STATE_PIP;
mPipMediaController.onActivityPinned();
for (int i = mListeners.size() - 1; i >= 0; i--) {
mListeners.get(i).onPipEntered(packageName);
}
mPipNotification.show(packageName);
}
private void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
@@ -434,62 +403,18 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
}
}
/**
* Suspends resizing operation on the Pip until {@link #resumePipResizing} is called
*
* @param reason The reason for suspending resizing operations on the Pip.
*/
public void suspendPipResizing(int reason) {
if (DEBUG) {
Log.d(TAG,
"suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
}
mSuspendPipResizingReason |= reason;
}
/**
* Resumes resizing operation on the Pip that was previously suspended.
*
* @param reason The reason resizing operations on the Pip was suspended.
*/
public void resumePipResizing(int reason) {
if ((mSuspendPipResizingReason & reason) == 0) {
return;
}
if (DEBUG) {
Log.d(TAG,
"resumePipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
}
mSuspendPipResizingReason &= ~reason;
mHandler.post(mResizePinnedStackRunnable);
}
/**
* Resize the Pip to the appropriate size for the input state.
*
* @param state In Pip state also used to determine the new size for the Pip.
*/
public void resizePinnedStack(int state) {
if (DEBUG) {
Log.d(TAG, "resizePinnedStack() state=" + stateToName(state) + ", current state="
+ getStateDescription(), new Exception());
}
boolean wasStateNoPip = (mState == STATE_NO_PIP);
for (int i = mListeners.size() - 1; i >= 0; --i) {
mListeners.get(i).onPipResizeAboutToStart();
}
if (mSuspendPipResizingReason != 0) {
mResumeResizePinnedStackRunnableState = state;
if (DEBUG) {
Log.d(TAG, "resizePinnedStack() deferring"
+ " mSuspendPipResizingReason=" + mSuspendPipResizingReason
+ " mResumeResizePinnedStackRunnableState="
+ stateToName(mResumeResizePinnedStackRunnableState));
}
return;
}
final boolean wasStateNoPip = (mState == STATE_NO_PIP);
mTvPipMenuController.hideMenu();
mState = state;
final Rect newBounds;
switch (mState) {
@@ -517,44 +442,19 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
}
/**
* @return the current state, or the pending state if the state change was previously suspended.
* @return the current state.
*/
private int getState() {
if (mSuspendPipResizingReason != 0) {
return mResumeResizePinnedStackRunnableState;
}
return mState;
}
/**
* Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned
* stack to the centered PIP bound {@link R.config_centeredPictureInPictureBounds}.
*/
private void showPipMenu() {
if (DEBUG) Log.d(TAG, "showPipMenu(), current state=" + getStateDescription());
mState = STATE_PIP_MENU;
for (int i = mListeners.size() - 1; i >= 0; --i) {
mListeners.get(i).onShowPipMenu();
}
mTvPipMenuController.showMenu();
}
/**
* Adds a {@link Listener} to PipController.
*/
void addListener(Listener listener) {
mListeners.add(listener);
}
/**
* Removes a {@link Listener} from PipController.
*/
void removeListener(Listener listener) {
mListeners.remove(listener);
}
/**
* Returns {@code true} if PIP is shown.
*/
@@ -626,33 +526,8 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
}
}
/**
* A listener interface to receive notification on changes in PIP.
*/
public interface Listener {
/**
* Invoked when an activity is pinned and PIP manager is set corresponding information.
* Classes must use this instead of {@link android.app.ITaskStackListener.onActivityPinned}
* because there's no guarantee for the PIP manager be return relavent information
* correctly. (e.g. {@link Pip.isPipShown}).
*/
void onPipEntered(String packageName);
/** Invoked when a PIPed activity is closed. */
void onPipActivityClosed();
/** Invoked when the PIP menu gets shown. */
void onShowPipMenu();
/** Invoked when the PIPed activity is about to return back to the fullscreen. */
void onMoveToFullscreen();
/** Invoked when we are above to start resizing the Pip. */
void onPipResizeAboutToStart();
}
private String getStateDescription() {
if (mSuspendPipResizingReason == 0) {
return stateToName(mState);
}
return stateToName(mResumeResizePinnedStackRunnableState) + " (while " + stateToName(mState)
+ " is suspended)";
return stateToName(mState);
}
private static String stateToName(int state) {

View File

@@ -16,6 +16,9 @@
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;
@@ -36,25 +39,22 @@ import java.util.Collections;
/**
* The Menu View that shows controls of the PiP. Always fullscreen.
*/
public class PipMenuView extends FrameLayout implements PipController.Listener {
public class PipMenuView extends FrameLayout {
private static final String TAG = "PipMenuView";
private static final boolean DEBUG = PipController.DEBUG;
private final PipController mPipController;
private final Animator mFadeInAnimation;
private final Animator mFadeOutAnimation;
private final PipControlsViewController mPipControlsViewController;
private boolean mRestorePipSizeWhenClose;
@Nullable
private OnBackPressListener mOnBackPressListener;
public PipMenuView(Context context, PipController pipController) {
super(context, null, 0);
mPipController = pipController;
inflate(context, R.layout.tv_pip_menu, this);
mPipControlsViewController = new PipControlsViewController(
findViewById(R.id.pip_controls), mPipController);
mRestorePipSizeWhenClose = true;
findViewById(R.id.pip_controls), pipController);
mFadeInAnimation = AnimatorInflater.loadAnimator(
mContext, R.anim.tv_pip_menu_fade_in_animation);
mFadeInAnimation.setTarget(mPipControlsViewController.getView());
@@ -63,16 +63,6 @@ public class PipMenuView extends FrameLayout implements PipController.Listener {
mFadeOutAnimation.setTarget(mPipControlsViewController.getView());
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK
&& event.getAction() == KeyEvent.ACTION_UP) {
restorePipAndFinish();
return true;
}
return super.dispatchKeyEvent(event);
}
@Nullable
SurfaceControl getWindowSurfaceControl() {
final ViewRootImpl root = getViewRootImpl();
@@ -87,53 +77,39 @@ public class PipMenuView extends FrameLayout implements PipController.Listener {
}
void showMenu() {
mPipController.addListener(this);
mFadeInAnimation.start();
setAlpha(1.0f);
try {
WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */,
getViewRootImpl().getInputToken(), true /* grantFocus */);
} catch (Exception e) {
Log.e(TAG, "Unable to update focus as menu appears", e);
}
grantWindowFocus(true);
}
void hideMenu() {
mPipController.removeListener(this);
mPipController.resumePipResizing(
PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH);
mFadeOutAnimation.start();
setAlpha(0.0f);
grantWindowFocus(false);
}
private void grantWindowFocus(boolean grantFocus) {
try {
WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */,
getViewRootImpl().getInputToken(), false /* grantFocus */);
getViewRootImpl().getInputToken(), grantFocus);
} catch (Exception e) {
Log.e(TAG, "Unable to update focus as menu disappears", e);
}
}
private void restorePipAndFinish() {
if (DEBUG) Log.d(TAG, "restorePipAndFinish()");
void setOnBackPressListener(OnBackPressListener onBackPressListener) {
mOnBackPressListener = onBackPressListener;
}
if (mRestorePipSizeWhenClose) {
if (DEBUG) Log.d(TAG, " > restoring to the default position");
// When PIP menu activity is closed, restore to the default position.
mPipController.resizePinnedStack(PipController.STATE_PIP);
@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);
}
hideMenu();
}
@Override
public void onPipEntered(String packageName) {
if (DEBUG) Log.d(TAG, "onPipEntered(), packageName=" + packageName);
}
@Override
public void onPipActivityClosed() {
if (DEBUG) Log.d(TAG, "onPipActivityClosed()");
hideMenu();
}
void setAppActions(ParceledListSlice<RemoteAction> actions) {
@@ -144,27 +120,7 @@ public class PipMenuView extends FrameLayout implements PipController.Listener {
hasCustomActions ? actions.getList() : Collections.emptyList());
}
@Override
public void onShowPipMenu() {
if (DEBUG) Log.d(TAG, "onShowPipMenu()");
}
@Override
public void onMoveToFullscreen() {
if (DEBUG) Log.d(TAG, "onMoveToFullscreen()");
// Moving PIP to fullscreen is implemented by resizing PINNED_STACK with null bounds.
// This conflicts with restoring PIP position, so disable it.
mRestorePipSizeWhenClose = false;
hideMenu();
}
@Override
public void onPipResizeAboutToStart() {
if (DEBUG) Log.d(TAG, "onPipResizeAboutToStart()");
hideMenu();
mPipController.suspendPipResizing(
PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH);
interface OnBackPressListener {
void onBackPress();
}
}

View File

@@ -39,7 +39,7 @@ import java.util.Objects;
* <p>Once it's created, it will manage the PIP notification UI by itself except for handling
* configuration changes.
*/
public class PipNotification implements PipController.Listener {
public class PipNotification {
private static final boolean DEBUG = PipController.DEBUG;
private static final String TAG = "PipNotification";
@@ -79,38 +79,21 @@ public class PipNotification implements PipController.Listener {
onConfigurationChanged(context);
}
@Override
public void onPipEntered(String packageName) {
void show(String packageName) {
mPackageName = packageName;
notifyPipNotification();
update();
}
@Override
public void onPipActivityClosed() {
dismissPipNotification();
void dismiss() {
mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP);
mNotified = false;
mPackageName = null;
}
@Override
public void onShowPipMenu() {
// no-op.
}
@Override
public void onMoveToFullscreen() {
dismissPipNotification();
mPackageName = null;
}
@Override
public void onPipResizeAboutToStart() {
// no-op.
}
private void onMediaMetadataChanged(MediaMetadata metadata) {
if (updateMediaControllerMetadata(metadata) && mNotified) {
// update notification
notifyPipNotification();
update();
}
}
@@ -123,11 +106,11 @@ public class PipNotification implements PipController.Listener {
mDefaultIconResId = R.drawable.pip_icon;
if (mNotified) {
// update notification
notifyPipNotification();
update();
}
}
private void notifyPipNotification() {
private void update() {
mNotified = true;
mNotificationBuilder
.setShowWhen(true)
@@ -144,11 +127,6 @@ public class PipNotification implements PipController.Listener {
mNotificationBuilder.build());
}
private void dismissPipNotification() {
mNotified = false;
mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP);
}
private boolean updateMediaControllerMetadata(MediaMetadata metadata) {
String title = null;
Bitmap art = null;

View File

@@ -21,6 +21,7 @@ import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP;
import android.app.RemoteAction;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.util.Log;
import android.view.SurfaceControl;
import com.android.wm.shell.common.SystemWindows;
@@ -31,6 +32,8 @@ import com.android.wm.shell.pip.PipMenuController;
* Manages the visibility of the PiP Menu as user interacts with PiP.
*/
public class TvPipMenuController implements PipMenuController {
private static final String TAG = "TvPipMenuController";
private static final boolean DEBUG = PipController.DEBUG;
private final Context mContext;
private final SystemWindows mSystemWindows;
@@ -52,6 +55,8 @@ public class TvPipMenuController implements PipMenuController {
@Override
public void showMenu() {
if (DEBUG) Log.d(TAG, "showMenu()");
if (mMenuView != null) {
mSystemWindows.updateViewLayout(mMenuView, getPipMenuLayoutParams(MENU_WINDOW_TITLE,
mPipBoundsState.getDisplayBounds().width(),
@@ -68,27 +73,62 @@ public class TvPipMenuController implements PipMenuController {
}
}
@Override
public void attach(SurfaceControl leash) {
if (mMenuView == null) {
mMenuView = new PipMenuView(mContext, mPipController);
mSystemWindows.addView(mMenuView,
getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
0, SHELL_ROOT_LAYER_PIP);
mLeash = leash;
void hideMenu() {
if (DEBUG) Log.d(TAG, "hideMenu()");
if (isMenuVisible()) {
mMenuView.hideMenu();
mPipController.resizePinnedStack(PipController.STATE_PIP);
}
}
@Override
public void attach(SurfaceControl leash) {
mLeash = leash;
attachPipMenuView();
}
@Override
public void detach() {
hideMenu();
detachPipMenuView();
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()");
if (mMenuView == null) {
return;
}
mSystemWindows.removeView(mMenuView);
mMenuView = null;
mLeash = null;
}
@Override
public void setAppActions(ParceledListSlice<RemoteAction> appActions) {
mMenuView.setAppActions(appActions);
if (DEBUG) Log.d(TAG, "setAppActions(), actions=" + appActions);
if (mMenuView != null) {
mMenuView.setAppActions(appActions);
} else {
Log.w(TAG, "Cannot set remote actions, there is no View");
}
}
@Override