Re-implement TvPipController
Re-working TvPipController (formerly tv.PipController) taking into account recent migration to WmShell. Bug: 153784643 Test: atest WMShellFlickerTests Change-Id: I3657efccece24e12783b582db26ebe93468c6fa5
This commit is contained in:
@@ -1,540 +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.app.ActivityTaskManager.INVALID_STACK_ID;
|
||||
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
|
||||
import static android.content.Intent.ACTION_MEDIA_RESOURCE_GRANTED;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityTaskManager;
|
||||
import android.app.ActivityTaskManager.RootTaskInfo;
|
||||
import android.app.IActivityTaskManager;
|
||||
import android.app.RemoteAction;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ParceledListSlice;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.DisplayInfo;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.WindowManagerShellWrapper;
|
||||
import com.android.wm.shell.common.TaskStackListenerCallback;
|
||||
import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
|
||||
import com.android.wm.shell.pip.Pip;
|
||||
import com.android.wm.shell.pip.PipBoundsAlgorithm;
|
||||
import com.android.wm.shell.pip.PipBoundsState;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipTaskOrganizer;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Manages the picture-in-picture (PIP) UI and states.
|
||||
*/
|
||||
public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallback,
|
||||
TvPipMenuController.Delegate, TvPipNotificationController.Delegate {
|
||||
private static final String TAG = "TvPipController";
|
||||
static final boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* Unknown or invalid state
|
||||
*/
|
||||
public static final int STATE_UNKNOWN = -1;
|
||||
/**
|
||||
* State when there's no PIP.
|
||||
*/
|
||||
public static final int STATE_NO_PIP = 0;
|
||||
/**
|
||||
* State when PIP is shown. This is used as default PIP state.
|
||||
*/
|
||||
public static final int STATE_PIP = 1;
|
||||
/**
|
||||
* State when PIP menu dialog is shown.
|
||||
*/
|
||||
public static final int STATE_PIP_MENU = 2;
|
||||
|
||||
private static final int TASK_ID_NO_PIP = -1;
|
||||
private static final int INVALID_RESOURCE_TYPE = -1;
|
||||
|
||||
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 TvPipNotificationController mPipNotificationController;
|
||||
|
||||
private IActivityTaskManager mActivityTaskManager;
|
||||
private int mState = STATE_NO_PIP;
|
||||
private final Handler mHandler = new Handler();
|
||||
private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED;
|
||||
private int mPipTaskId = TASK_ID_NO_PIP;
|
||||
private int mPinnedStackId = INVALID_STACK_ID;
|
||||
private String[] mLastPackagesResourceGranted;
|
||||
private ParceledListSlice<RemoteAction> mCustomActions;
|
||||
private WindowManagerShellWrapper mWindowManagerShellWrapper;
|
||||
private int mResizeAnimationDuration;
|
||||
|
||||
// Used to calculate the movement bounds
|
||||
private final DisplayInfo mTmpDisplayInfo = new DisplayInfo();
|
||||
private final Rect mTmpInsetBounds = new Rect();
|
||||
|
||||
// Keeps track of the IME visibility to adjust the PiP when the IME is visible
|
||||
private boolean mImeVisible;
|
||||
private int mImeHeightAdjustment;
|
||||
|
||||
private final Runnable mClosePipRunnable = this::closePip;
|
||||
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "mBroadcastReceiver, action: " + intent.getAction());
|
||||
}
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_MEDIA_RESOURCE_GRANTED:
|
||||
String[] packageNames = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
|
||||
int resourceType = intent.getIntExtra(Intent.EXTRA_MEDIA_RESOURCE_TYPE,
|
||||
INVALID_RESOURCE_TYPE);
|
||||
if (packageNames != null && packageNames.length > 0
|
||||
&& resourceType == Intent.EXTRA_MEDIA_RESOURCE_TYPE_VIDEO_CODEC) {
|
||||
handleMediaResourceGranted(packageNames);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final PinnedStackListenerForwarder.PinnedStackListener mPinnedStackListener =
|
||||
new PipControllerPinnedStackListener();
|
||||
|
||||
@Override
|
||||
public void registerSessionListenerForCurrentUser() {
|
||||
mPipMediaController.registerSessionListenerForCurrentUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for messages from the PIP controller.
|
||||
*/
|
||||
private class PipControllerPinnedStackListener extends
|
||||
PinnedStackListenerForwarder.PinnedStackListener {
|
||||
@Override
|
||||
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
|
||||
mPipBoundsState.setImeVisibility(imeVisible, imeHeight);
|
||||
if (mState == STATE_PIP) {
|
||||
if (mImeVisible != imeVisible) {
|
||||
if (imeVisible) {
|
||||
// Save the IME height adjustment, and offset to not occlude the IME
|
||||
mPipBoundsState.getNormalBounds().offset(0, -imeHeight);
|
||||
mImeHeightAdjustment = imeHeight;
|
||||
} else {
|
||||
// Apply the inverse adjustment when the IME is hidden
|
||||
mPipBoundsState.getNormalBounds().offset(0, mImeHeightAdjustment);
|
||||
}
|
||||
mImeVisible = imeVisible;
|
||||
resizePinnedStack(STATE_PIP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMovementBoundsChanged(boolean fromImeAdjustment) {
|
||||
mTmpDisplayInfo.copyFrom(mPipBoundsState.getDisplayInfo());
|
||||
mPipBoundsAlgorithm.getInsetBounds(mTmpInsetBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {
|
||||
mCustomActions = actions;
|
||||
mTvPipMenuController.setAppActions(mCustomActions);
|
||||
}
|
||||
}
|
||||
|
||||
public PipController(Context context,
|
||||
PipBoundsState pipBoundsState,
|
||||
PipBoundsAlgorithm pipBoundsAlgorithm,
|
||||
PipTaskOrganizer pipTaskOrganizer,
|
||||
TvPipMenuController tvPipMenuController,
|
||||
PipMediaController pipMediaController,
|
||||
TvPipNotificationController tvPipNotificationController,
|
||||
TaskStackListenerImpl taskStackListener,
|
||||
WindowManagerShellWrapper windowManagerShellWrapper) {
|
||||
mContext = context;
|
||||
mPipBoundsState = pipBoundsState;
|
||||
mPipNotificationController = tvPipNotificationController;
|
||||
mPipNotificationController.setDelegate(this);
|
||||
mPipBoundsAlgorithm = pipBoundsAlgorithm;
|
||||
mPipMediaController = pipMediaController;
|
||||
mTvPipMenuController = tvPipMenuController;
|
||||
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();
|
||||
context.getDisplay().getDisplayInfo(displayInfo);
|
||||
mPipBoundsState.setDisplayInfo(displayInfo);
|
||||
|
||||
mResizeAnimationDuration = context.getResources()
|
||||
.getInteger(R.integer.config_pipResizeAnimationDuration);
|
||||
mPipTaskOrganizer = pipTaskOrganizer;
|
||||
mPipTaskOrganizer.registerPipTransitionCallback(this);
|
||||
mActivityTaskManager = ActivityTaskManager.getService();
|
||||
|
||||
final IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(ACTION_MEDIA_RESOURCE_GRANTED);
|
||||
mContext.registerReceiver(mBroadcastReceiver, intentFilter, UserHandle.USER_ALL);
|
||||
|
||||
// Initialize the last orientation and apply the current configuration
|
||||
Configuration initialConfig = mContext.getResources().getConfiguration();
|
||||
mLastOrientation = initialConfig.orientation;
|
||||
loadConfigurationsAndApply(initialConfig);
|
||||
|
||||
mWindowManagerShellWrapper = windowManagerShellWrapper;
|
||||
try {
|
||||
mWindowManagerShellWrapper.addPinnedStackListener(mPinnedStackListener);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Failed to register pinned stack listener", e);
|
||||
}
|
||||
|
||||
// Handle for system task stack changes.
|
||||
taskStackListener.addListener(
|
||||
new TaskStackListenerCallback() {
|
||||
@Override
|
||||
public void onTaskStackChanged() {
|
||||
PipController.this.onTaskStackChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPinned(String packageName, int userId, int taskId,
|
||||
int stackId) {
|
||||
PipController.this.onActivityPinned(packageName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
|
||||
boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
|
||||
PipController.this.onActivityRestartAttempt(task, clearedTask);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadConfigurationsAndApply(Configuration newConfig) {
|
||||
if (mLastOrientation != newConfig.orientation) {
|
||||
// Don't resize the pinned stack on orientation change. TV does not care about this case
|
||||
// and this could clobber the existing animation to the new bounds calculated by WM.
|
||||
mLastOrientation = newConfig.orientation;
|
||||
return;
|
||||
}
|
||||
|
||||
final Rect menuBounds = Rect.unflattenFromString(
|
||||
mContext.getResources().getString(R.string.pip_menu_bounds));
|
||||
mPipBoundsState.setExpandedBounds(menuBounds);
|
||||
|
||||
resizePinnedStack(getPinnedTaskInfo() == null ? STATE_NO_PIP : STATE_PIP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the PIP per configuration changed.
|
||||
*/
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
loadConfigurationsAndApply(newConfig);
|
||||
mPipNotificationController.onConfigurationChanged(mContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the picture-in-picture menu if an activity is in picture-in-picture mode.
|
||||
*/
|
||||
public void showPictureInPictureMenu() {
|
||||
if (DEBUG) Log.d(TAG, "showPictureInPictureMenu(), current state=" + getStateDescription());
|
||||
|
||||
if (getState() == STATE_PIP) {
|
||||
resizePinnedStack(STATE_PIP_MENU);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes PIP (PIPed activity and PIP system UI).
|
||||
*/
|
||||
@Override
|
||||
public void closePip() {
|
||||
if (DEBUG) Log.d(TAG, "closePip(), current state=" + getStateDescription());
|
||||
|
||||
closePipInternal(true);
|
||||
}
|
||||
|
||||
private void closePipInternal(boolean removePipStack) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG,
|
||||
"closePipInternal() removePipStack=" + removePipStack + ", current state="
|
||||
+ getStateDescription());
|
||||
}
|
||||
|
||||
mState = STATE_NO_PIP;
|
||||
mPipTaskId = TASK_ID_NO_PIP;
|
||||
if (removePipStack) {
|
||||
try {
|
||||
mActivityTaskManager.removeTask(mPinnedStackId);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "removeTask failed", e);
|
||||
} finally {
|
||||
mPinnedStackId = INVALID_STACK_ID;
|
||||
}
|
||||
}
|
||||
mPipNotificationController.dismiss();
|
||||
mTvPipMenuController.hideMenu();
|
||||
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());
|
||||
|
||||
mPipTaskId = TASK_ID_NO_PIP;
|
||||
mTvPipMenuController.hideMenu();
|
||||
mPipNotificationController.dismiss();
|
||||
|
||||
resizePinnedStack(STATE_NO_PIP);
|
||||
}
|
||||
|
||||
private void onActivityPinned(String packageName) {
|
||||
final RootTaskInfo taskInfo = getPinnedTaskInfo();
|
||||
if (DEBUG) Log.d(TAG, "onActivityPinned, task=" + taskInfo);
|
||||
if (taskInfo == null) {
|
||||
Log.w(TAG, "Cannot find pinned stack");
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point PipBoundsState knows the correct aspect ratio for this pinned task, so we
|
||||
// use PipBoundsAlgorithm to calculate the normal bounds for the task (PipBoundsAlgorithm
|
||||
// will query PipBoundsState for the aspect ratio) and pass the bounds over to the
|
||||
// PipBoundsState.
|
||||
mPipBoundsState.setNormalBounds(mPipBoundsAlgorithm.getNormalBounds());
|
||||
|
||||
mPinnedStackId = taskInfo.taskId;
|
||||
mPipTaskId = taskInfo.childTaskIds[taskInfo.childTaskIds.length - 1];
|
||||
|
||||
// Set state to STATE_PIP so we show it when the pinned stack animation ends.
|
||||
mState = STATE_PIP;
|
||||
mPipMediaController.onActivityPinned();
|
||||
mPipNotificationController.show(packageName);
|
||||
}
|
||||
|
||||
private void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
|
||||
boolean clearedTask) {
|
||||
if (task.getWindowingMode() != WINDOWING_MODE_PINNED) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()");
|
||||
|
||||
// If PIPed activity is launched again by Launcher or intent, make it fullscreen.
|
||||
movePipToFullscreen();
|
||||
}
|
||||
|
||||
private void onTaskStackChanged() {
|
||||
if (DEBUG) Log.d(TAG, "onTaskStackChanged()");
|
||||
|
||||
if (getState() != STATE_NO_PIP) {
|
||||
boolean hasPip = false;
|
||||
|
||||
RootTaskInfo taskInfo = getPinnedTaskInfo();
|
||||
if (taskInfo == null || taskInfo.childTaskIds == null) {
|
||||
Log.w(TAG, "There is nothing in pinned stack");
|
||||
closePipInternal(false);
|
||||
return;
|
||||
}
|
||||
for (int i = taskInfo.childTaskIds.length - 1; i >= 0; --i) {
|
||||
if (taskInfo.childTaskIds[i] == mPipTaskId) {
|
||||
// PIP task is still alive.
|
||||
hasPip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasPip) {
|
||||
// PIP task doesn't exist anymore in PINNED_STACK.
|
||||
closePipInternal(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (getState() == STATE_PIP) {
|
||||
if (!Objects.equals(mPipBoundsState.getBounds(), mPipBoundsState.getNormalBounds())) {
|
||||
resizePinnedStack(STATE_PIP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
final boolean wasStateNoPip = (mState == STATE_NO_PIP);
|
||||
mTvPipMenuController.hideMenu();
|
||||
mState = state;
|
||||
final Rect newBounds;
|
||||
switch (mState) {
|
||||
case STATE_NO_PIP:
|
||||
newBounds = null;
|
||||
// If the state was already STATE_NO_PIP, then do not resize the stack below as it
|
||||
// will not exist
|
||||
if (wasStateNoPip) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case STATE_PIP_MENU:
|
||||
newBounds = mPipBoundsState.getExpandedBounds();
|
||||
break;
|
||||
case STATE_PIP: // fallthrough
|
||||
default:
|
||||
newBounds = mPipBoundsState.getNormalBounds();
|
||||
break;
|
||||
}
|
||||
if (newBounds != null) {
|
||||
mPipTaskOrganizer.scheduleAnimateResizePip(newBounds, mResizeAnimationDuration, null);
|
||||
} else {
|
||||
mPipTaskOrganizer.exitPip(mResizeAnimationDuration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current state.
|
||||
*/
|
||||
private int getState() {
|
||||
return mState;
|
||||
}
|
||||
|
||||
private void showPipMenu() {
|
||||
if (DEBUG) Log.d(TAG, "showPipMenu(), current state=" + getStateDescription());
|
||||
|
||||
mState = STATE_PIP_MENU;
|
||||
mTvPipMenuController.showMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if PIP is shown.
|
||||
*/
|
||||
public boolean isPipShown() {
|
||||
return mState != STATE_NO_PIP;
|
||||
}
|
||||
|
||||
private RootTaskInfo getPinnedTaskInfo() {
|
||||
RootTaskInfo taskInfo = null;
|
||||
try {
|
||||
taskInfo = ActivityTaskManager.getService().getRootTaskInfo(
|
||||
WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "getRootTaskInfo failed", e);
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "getPinnedTaskInfo(), taskInfo=" + taskInfo);
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
private void handleMediaResourceGranted(String[] packageNames) {
|
||||
if (getState() == STATE_NO_PIP) {
|
||||
mLastPackagesResourceGranted = packageNames;
|
||||
} else {
|
||||
boolean requestedFromLastPackages = false;
|
||||
if (mLastPackagesResourceGranted != null) {
|
||||
for (String packageName : mLastPackagesResourceGranted) {
|
||||
for (String newPackageName : packageNames) {
|
||||
if (TextUtils.equals(newPackageName, packageName)) {
|
||||
requestedFromLastPackages = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mLastPackagesResourceGranted = packageNames;
|
||||
if (!requestedFromLastPackages) {
|
||||
closePip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) {
|
||||
}
|
||||
|
||||
PipMediaController getPipMediaController() {
|
||||
return mPipMediaController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPipTransitionFinished(ComponentName activity, int direction) {
|
||||
onPipTransitionFinishedOrCanceled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPipTransitionCanceled(ComponentName activity, int direction) {
|
||||
onPipTransitionFinishedOrCanceled();
|
||||
}
|
||||
|
||||
private void onPipTransitionFinishedOrCanceled() {
|
||||
if (DEBUG) Log.d(TAG, "onPipTransitionFinishedOrCanceled()");
|
||||
|
||||
if (getState() == STATE_PIP_MENU) {
|
||||
showPipMenu();
|
||||
}
|
||||
}
|
||||
|
||||
private String getStateDescription() {
|
||||
return stateToName(mState);
|
||||
}
|
||||
|
||||
private static String stateToName(int state) {
|
||||
switch (state) {
|
||||
case STATE_NO_PIP:
|
||||
return "NO_PIP";
|
||||
|
||||
case STATE_PIP:
|
||||
return "PIP";
|
||||
|
||||
case STATE_PIP_MENU:
|
||||
return "PIP_MENU";
|
||||
|
||||
default:
|
||||
return "UNKNOWN(" + state + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* 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.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityTaskManager;
|
||||
import android.app.RemoteAction;
|
||||
import android.app.TaskInfo;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ParceledListSlice;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.view.DisplayInfo;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.WindowManagerShellWrapper;
|
||||
import com.android.wm.shell.common.TaskStackListenerCallback;
|
||||
import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
|
||||
import com.android.wm.shell.pip.Pip;
|
||||
import com.android.wm.shell.pip.PipBoundsAlgorithm;
|
||||
import com.android.wm.shell.pip.PipBoundsState;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipTaskOrganizer;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Manages the picture-in-picture (PIP) UI and states.
|
||||
*/
|
||||
public class TvPipController implements Pip, PipTaskOrganizer.PipTransitionCallback,
|
||||
TvPipMenuController.Delegate, TvPipNotificationController.Delegate {
|
||||
private static final String TAG = "TvPipController";
|
||||
static final boolean DEBUG = true;
|
||||
|
||||
private static final int NONEXISTENT_TASK_ID = -1;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(prefix = { "STATE_" }, value = {
|
||||
STATE_NO_PIP,
|
||||
STATE_PIP,
|
||||
STATE_PIP_MENU
|
||||
})
|
||||
public @interface State {}
|
||||
|
||||
/**
|
||||
* State when there is no applications in Pip.
|
||||
*/
|
||||
private static final int STATE_NO_PIP = 0;
|
||||
/**
|
||||
* State when there is an applications in Pip and the Pip window located at its "normal" place
|
||||
* (usually the bottom right corner).
|
||||
*/
|
||||
private static final int STATE_PIP = 1;
|
||||
/**
|
||||
* State when there is an applications in Pip and the Pip menu is open. In this state Pip window
|
||||
* is usually moved from its "normal" position on the screen to the "menu" position - which is
|
||||
* often at the middle of the screen, and gets slightly scaled up.
|
||||
*/
|
||||
private static final int STATE_PIP_MENU = 2;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private final PipBoundsState mPipBoundsState;
|
||||
private final PipBoundsAlgorithm mPipBoundsAlgorithm;
|
||||
private final PipTaskOrganizer mPipTaskOrganizer;
|
||||
private final PipMediaController mPipMediaController;
|
||||
private final TvPipNotificationController mPipNotificationController;
|
||||
private final TvPipMenuController mTvPipMenuController;
|
||||
|
||||
private @State int mState = STATE_NO_PIP;
|
||||
private int mPinnedTaskId = NONEXISTENT_TASK_ID;
|
||||
|
||||
private int mResizeAnimationDuration;
|
||||
|
||||
public TvPipController(
|
||||
Context context,
|
||||
PipBoundsState pipBoundsState,
|
||||
PipBoundsAlgorithm pipBoundsAlgorithm,
|
||||
PipTaskOrganizer pipTaskOrganizer,
|
||||
TvPipMenuController tvPipMenuController,
|
||||
PipMediaController pipMediaController,
|
||||
TvPipNotificationController pipNotificationController,
|
||||
TaskStackListenerImpl taskStackListener,
|
||||
WindowManagerShellWrapper wmShell) {
|
||||
mContext = context;
|
||||
|
||||
mPipBoundsState = pipBoundsState;
|
||||
mPipBoundsState.setDisplayInfo(getDisplayInfo());
|
||||
mPipBoundsAlgorithm = pipBoundsAlgorithm;
|
||||
|
||||
mPipMediaController = pipMediaController;
|
||||
|
||||
mPipNotificationController = pipNotificationController;
|
||||
mPipNotificationController.setDelegate(this);
|
||||
|
||||
mTvPipMenuController = tvPipMenuController;
|
||||
mTvPipMenuController.setDelegate(this);
|
||||
|
||||
mPipTaskOrganizer = pipTaskOrganizer;
|
||||
mPipTaskOrganizer.registerPipTransitionCallback(this);
|
||||
|
||||
loadConfigurations();
|
||||
|
||||
registerTaskStackListenerCallback(taskStackListener);
|
||||
registerWmShellPinnedStackListener(wmShell);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
if (DEBUG) Log.d(TAG, "onConfigurationChanged(), state=" + stateToName(mState));
|
||||
|
||||
if (isPipShown()) {
|
||||
if (DEBUG) Log.d(TAG, " > closing Pip.");
|
||||
closePip();
|
||||
}
|
||||
|
||||
loadConfigurations();
|
||||
mPipNotificationController.onConfigurationChanged(mContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if Pip is shown.
|
||||
*/
|
||||
@Override
|
||||
public boolean isPipShown() {
|
||||
return mState != STATE_NO_PIP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the process if bringing up the Pip menu if by issuing a command to move Pip
|
||||
* task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip
|
||||
* task/window is properly positioned in {@link #onPipTransitionFinished(ComponentName, int)}.
|
||||
*/
|
||||
@Override
|
||||
public void showPictureInPictureMenu() {
|
||||
if (DEBUG) Log.d(TAG, "showPictureInPictureMenu(), state=" + stateToName(mState));
|
||||
|
||||
if (mState != STATE_PIP) {
|
||||
if (DEBUG) Log.d(TAG, " > cannot open Menu from the current state.");
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_PIP_MENU);
|
||||
resizePinnedStack(STATE_PIP_MENU);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves Pip window to its "normal" position.
|
||||
*/
|
||||
@Override
|
||||
public void movePipToNormalPosition() {
|
||||
if (DEBUG) Log.d(TAG, "movePipToNormalPosition(), state=" + stateToName(mState));
|
||||
|
||||
setState(STATE_PIP);
|
||||
resizePinnedStack(STATE_PIP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the "Pip-ed" Activity fullscreen.
|
||||
*/
|
||||
@Override
|
||||
public void movePipToFullscreen() {
|
||||
if (DEBUG) Log.d(TAG, "movePipToFullscreen(), state=" + stateToName(mState));
|
||||
|
||||
mPipTaskOrganizer.exitPip(mResizeAnimationDuration);
|
||||
onPipDisappeared();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes Pip window.
|
||||
*/
|
||||
@Override
|
||||
public void closePip() {
|
||||
if (DEBUG) Log.d(TAG, "closePip(), state=" + stateToName(mState));
|
||||
|
||||
removeTask(mPinnedTaskId);
|
||||
onPipDisappeared();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the Pip task/window to the appropriate size for the given state.
|
||||
* This is a legacy API. Now we expect that the state argument passed to it should always match
|
||||
* the current state of the Controller. If it does not match an {@link IllegalArgumentException}
|
||||
* will be thrown. However, if the passed state does match - we'll determine the right bounds
|
||||
* to the state and will move Pip task/window there.
|
||||
*
|
||||
* @param state the to determine the Pip bounds. IMPORTANT: should always match the current
|
||||
* state of the Controller.
|
||||
*/
|
||||
@Override
|
||||
public void resizePinnedStack(@State int state) {
|
||||
if (state != mState) {
|
||||
throw new IllegalArgumentException("The passed state should match the current state!");
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + stateToName(mState));
|
||||
|
||||
final Rect newBounds;
|
||||
switch (mState) {
|
||||
case STATE_PIP_MENU:
|
||||
newBounds = mPipBoundsState.getExpandedBounds();
|
||||
break;
|
||||
|
||||
case STATE_PIP:
|
||||
// Let PipBoundsAlgorithm figure out what the correct bounds are at the moment.
|
||||
// Internally, it will get the "default" bounds from PipBoundsState and adjust them
|
||||
// as needed to account for things like IME state (will query PipBoundsState for
|
||||
// this information as well, so it's important to keep PipBoundsState up to date).
|
||||
newBounds = mPipBoundsAlgorithm.getNormalBounds();
|
||||
break;
|
||||
|
||||
case STATE_NO_PIP:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
mPipTaskOrganizer.scheduleAnimateResizePip(newBounds, mResizeAnimationDuration, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerSessionListenerForCurrentUser() {
|
||||
mPipMediaController.registerSessionListenerForCurrentUser();
|
||||
}
|
||||
|
||||
private void checkIfPinnedTaskAppeared() {
|
||||
final TaskInfo pinnedTask = getPinnedTaskInfo();
|
||||
if (DEBUG) Log.d(TAG, "checkIfPinnedTaskAppeared(), task=" + pinnedTask);
|
||||
if (pinnedTask == null) return;
|
||||
mPinnedTaskId = pinnedTask.taskId;
|
||||
setState(STATE_PIP);
|
||||
|
||||
mPipMediaController.onActivityPinned();
|
||||
mPipNotificationController.show(pinnedTask.topActivity.getPackageName());
|
||||
}
|
||||
|
||||
private void checkIfPinnedTaskIsGone() {
|
||||
if (DEBUG) Log.d(TAG, "onTaskStackChanged()");
|
||||
|
||||
if (isPipShown() && getPinnedTaskInfo() == null) {
|
||||
Log.w(TAG, "Pinned task is gone.");
|
||||
onPipDisappeared();
|
||||
}
|
||||
}
|
||||
|
||||
private void onPipDisappeared() {
|
||||
if (DEBUG) Log.d(TAG, "onPipDisappeared() state=" + stateToName(mState));
|
||||
|
||||
mPipNotificationController.dismiss();
|
||||
mTvPipMenuController.hideMenu();
|
||||
setState(STATE_NO_PIP);
|
||||
mPinnedTaskId = NONEXISTENT_TASK_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) {
|
||||
if (DEBUG) Log.d(TAG, "onPipTransition_Started(), state=" + stateToName(mState));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPipTransitionCanceled(ComponentName activity, int direction) {
|
||||
if (DEBUG) Log.d(TAG, "onPipTransition_Canceled(), state=" + stateToName(mState));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPipTransitionFinished(ComponentName activity, int direction) {
|
||||
if (DEBUG) Log.d(TAG, "onPipTransition_Finished(), state=" + stateToName(mState));
|
||||
|
||||
if (mState == STATE_PIP_MENU) {
|
||||
if (DEBUG) Log.d(TAG, " > show menu");
|
||||
mTvPipMenuController.showMenu();
|
||||
}
|
||||
}
|
||||
|
||||
private void setState(@State int state) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setState(), state=" + stateToName(state) + ", prev="
|
||||
+ stateToName(mState));
|
||||
}
|
||||
mState = state;
|
||||
}
|
||||
|
||||
private void loadConfigurations() {
|
||||
final Resources res = mContext.getResources();
|
||||
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
|
||||
// "Cache" bounds for the Pip menu as "expanded" bounds in PipBoundsState. We'll refer back
|
||||
// to this value in resizePinnedStack(), when we are adjusting Pip task/window position for
|
||||
// the menu.
|
||||
mPipBoundsState.setExpandedBounds(
|
||||
Rect.unflattenFromString(res.getString(R.string.pip_menu_bounds)));
|
||||
}
|
||||
|
||||
private DisplayInfo getDisplayInfo() {
|
||||
final DisplayInfo displayInfo = new DisplayInfo();
|
||||
mContext.getDisplay().getDisplayInfo(displayInfo);
|
||||
return displayInfo;
|
||||
}
|
||||
|
||||
private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
|
||||
taskStackListener.addListener(new TaskStackListenerCallback() {
|
||||
@Override
|
||||
public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
|
||||
checkIfPinnedTaskAppeared();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskStackChanged() {
|
||||
checkIfPinnedTaskIsGone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
|
||||
boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
|
||||
if (task.getWindowingMode() == WINDOWING_MODE_PINNED) {
|
||||
if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()");
|
||||
|
||||
// If the "Pip-ed" Activity is launched again by Launcher or intent, make it
|
||||
// fullscreen.
|
||||
movePipToFullscreen();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void registerWmShellPinnedStackListener(WindowManagerShellWrapper wmShell) {
|
||||
try {
|
||||
wmShell.addPinnedStackListener(new PinnedStackListenerForwarder.PinnedStackListener() {
|
||||
@Override
|
||||
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onImeVisibilityChanged(), visible=" + imeVisible
|
||||
+ ", height=" + imeHeight);
|
||||
}
|
||||
|
||||
if (imeVisible == mPipBoundsState.isImeShowing()
|
||||
&& (!imeVisible || imeHeight == mPipBoundsState.getImeHeight())) {
|
||||
// Nothing changed: either IME has been and remains invisible, or remains
|
||||
// visible with the same height.
|
||||
return;
|
||||
}
|
||||
mPipBoundsState.setImeVisibility(imeVisible, imeHeight);
|
||||
// "Normal" Pip bounds may have changed, so if we are in the "normal" state,
|
||||
// let's update the bounds.
|
||||
if (mState == STATE_PIP) {
|
||||
resizePinnedStack(STATE_PIP);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMovementBoundsChanged(boolean fromImeAdjustment) {}
|
||||
|
||||
@Override
|
||||
public void onActionsChanged(ParceledListSlice<RemoteAction> actions) {
|
||||
if (DEBUG) Log.d(TAG, "onActionsChanged()");
|
||||
|
||||
mTvPipMenuController.setAppActions(actions);
|
||||
}
|
||||
});
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Failed to register pinned stack listener", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static TaskInfo getPinnedTaskInfo() {
|
||||
if (DEBUG) Log.d(TAG, "getPinnedTaskInfo()");
|
||||
try {
|
||||
final TaskInfo taskInfo = ActivityTaskManager.getService().getRootTaskInfo(
|
||||
WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
|
||||
if (DEBUG) Log.d(TAG, " > taskInfo=" + taskInfo);
|
||||
return taskInfo;
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "getRootTaskInfo() failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void removeTask(int taskId) {
|
||||
if (DEBUG) Log.d(TAG, "removeTask(), taskId=" + taskId);
|
||||
try {
|
||||
ActivityTaskManager.getService().removeTask(taskId);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Atm.removeTask() failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String stateToName(@State int state) {
|
||||
switch (state) {
|
||||
case STATE_NO_PIP:
|
||||
return "NO_PIP";
|
||||
case STATE_PIP:
|
||||
return "PIP";
|
||||
case STATE_PIP_MENU:
|
||||
return "PIP_MENU";
|
||||
default:
|
||||
// This can't happen.
|
||||
throw new IllegalArgumentException("Unknown state " + state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ import java.util.List;
|
||||
*/
|
||||
public class TvPipMenuController implements PipMenuController, TvPipMenuView.Listener {
|
||||
private static final String TAG = "TvPipMenuController";
|
||||
private static final boolean DEBUG = PipController.DEBUG;
|
||||
private static final boolean DEBUG = TvPipController.DEBUG;
|
||||
|
||||
private final Context mContext;
|
||||
private final SystemWindows mSystemWindows;
|
||||
@@ -134,10 +134,18 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
|
||||
}
|
||||
|
||||
void hideMenu() {
|
||||
if (DEBUG) Log.d(TAG, "hideMenu()");
|
||||
hideMenu(true);
|
||||
}
|
||||
|
||||
if (isMenuVisible()) {
|
||||
mMenuView.hide();
|
||||
void hideMenu(boolean movePipWindow) {
|
||||
if (DEBUG) Log.d(TAG, "hideMenu(), movePipWindow=" + movePipWindow);
|
||||
|
||||
if (!isMenuVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mMenuView.hide();
|
||||
if (movePipWindow) {
|
||||
mDelegate.movePipToNormalPosition();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ import java.util.List;
|
||||
*/
|
||||
public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
|
||||
private static final String TAG = "TvPipMenuView";
|
||||
private static final boolean DEBUG = PipController.DEBUG;
|
||||
private static final boolean DEBUG = TvPipController.DEBUG;
|
||||
|
||||
private static final float DISABLED_ACTION_ALPHA = 0.54f;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ import java.util.Objects;
|
||||
*/
|
||||
public class TvPipNotificationController {
|
||||
private static final String TAG = "TvPipNotification";
|
||||
private static final boolean DEBUG = PipController.DEBUG;
|
||||
private static final boolean DEBUG = TvPipController.DEBUG;
|
||||
|
||||
// Referenced in com.android.systemui.util.NotificationChannels.
|
||||
public static final String NOTIFICATION_CHANNEL = "TVPIP";
|
||||
|
||||
@@ -32,7 +32,7 @@ import com.android.wm.shell.pip.PipMediaController;
|
||||
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.TvPipController;
|
||||
import com.android.wm.shell.pip.tv.TvPipMenuController;
|
||||
import com.android.wm.shell.pip.tv.TvPipNotificationController;
|
||||
|
||||
@@ -59,7 +59,7 @@ public abstract class TvPipModule {
|
||||
TaskStackListenerImpl taskStackListener,
|
||||
WindowManagerShellWrapper windowManagerShellWrapper) {
|
||||
return Optional.of(
|
||||
new PipController(
|
||||
new TvPipController(
|
||||
context,
|
||||
pipBoundsState,
|
||||
pipBoundsAlgorithm,
|
||||
|
||||
Reference in New Issue
Block a user