Merge changes I3657efcc,Iabdf5ee9

* changes:
  Re-implement TvPipController
  Refactor .tv.PipNotification
This commit is contained in:
Winson Chung
2021-01-12 05:21:15 +00:00
committed by Android (Google) Code Review
8 changed files with 540 additions and 592 deletions

View File

@@ -1,550 +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 static com.android.wm.shell.pip.tv.PipNotification.ACTION_CLOSE;
import static com.android.wm.shell.pip.tv.PipNotification.ACTION_MENU;
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 {
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 PipNotification mPipNotification;
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_MENU:
showPictureInPictureMenu();
break;
case ACTION_CLOSE:
closePip();
break;
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,
PipNotification pipNotification,
TaskStackListenerImpl taskStackListener,
WindowManagerShellWrapper windowManagerShellWrapper) {
mContext = context;
mPipBoundsState = pipBoundsState;
mPipNotification = pipNotification;
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_CLOSE);
intentFilter.addAction(ACTION_MENU);
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);
mPipNotification.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;
}
}
mPipNotification.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();
mPipNotification.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();
mPipNotification.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 + ")";
}
}
}

View File

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

View File

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

View File

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

View File

@@ -19,14 +19,17 @@ package com.android.wm.shell.pip.tv;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.media.MediaMetadata;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.wm.shell.R;
@@ -39,22 +42,27 @@ 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 {
private static final boolean DEBUG = PipController.DEBUG;
private static final String TAG = "PipNotification";
public class TvPipNotificationController {
private static final String TAG = "TvPipNotification";
private static final boolean DEBUG = TvPipController.DEBUG;
private static final String NOTIFICATION_TAG = PipNotification.class.getSimpleName();
public static final String NOTIFICATION_CHANNEL_TVPIP = "TPP";
// Referenced in com.android.systemui.util.NotificationChannels.
public static final String NOTIFICATION_CHANNEL = "TVPIP";
private static final String NOTIFICATION_TAG = "TvPip";
static final String ACTION_MENU = "PipNotification.menu";
static final String ACTION_CLOSE = "PipNotification.close";
private static final String ACTION_SHOW_PIP_MENU =
"com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU";
private static final String ACTION_CLOSE_PIP =
"com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP";
private final Context mContext;
private final PackageManager mPackageManager;
private final NotificationManager mNotificationManager;
private final Notification.Builder mNotificationBuilder;
private final ActionBroadcastReceiver mActionBroadcastReceiver;
private Delegate mDelegate;
private String mDefaultTitle;
private int mDefaultIconResId;
/** Package name for the application that owns PiP window. */
private String mPackageName;
@@ -62,32 +70,56 @@ public class PipNotification {
private String mMediaTitle;
private Bitmap mArt;
public PipNotification(Context context, PipMediaController pipMediaController) {
public TvPipNotificationController(Context context, PipMediaController pipMediaController) {
mContext = context;
mPackageManager = context.getPackageManager();
mNotificationManager = context.getSystemService(NotificationManager.class);
mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL_TVPIP)
mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL)
.setLocalOnly(true)
.setOngoing(false)
.setCategory(Notification.CATEGORY_SYSTEM)
.setShowWhen(true)
.setSmallIcon(R.drawable.pip_icon)
.extend(new Notification.TvExtender()
.setContentIntent(createPendingIntent(context, ACTION_MENU))
.setDeleteIntent(createPendingIntent(context, ACTION_CLOSE)));
.setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU))
.setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP)));
mActionBroadcastReceiver = new ActionBroadcastReceiver();
pipMediaController.addMetadataListener(this::onMediaMetadataChanged);
onConfigurationChanged(context);
}
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;
}
void show(String packageName) {
if (mDelegate == null) {
throw new IllegalStateException("Delegate is not set.");
}
mPackageName = packageName;
update();
mActionBroadcastReceiver.register();
}
void dismiss() {
mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP);
mNotified = false;
mPackageName = null;
mActionBroadcastReceiver.unregister();
}
private void onMediaMetadataChanged(MediaMetadata metadata) {
@@ -101,11 +133,9 @@ public class PipNotification {
* Called by {@link PipController} when the configuration is changed.
*/
void onConfigurationChanged(Context context) {
Resources res = context.getResources();
mDefaultTitle = res.getString(R.string.pip_notification_unknown_title);
mDefaultIconResId = R.drawable.pip_icon;
mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title);
if (mNotified) {
// update notification
// Update the notification.
update();
}
}
@@ -113,9 +143,7 @@ public class PipNotification {
private void update() {
mNotified = true;
mNotificationBuilder
.setShowWhen(true)
.setWhen(System.currentTimeMillis())
.setSmallIcon(mDefaultIconResId)
.setContentTitle(getNotificationTitle());
if (mArt != null) {
mNotificationBuilder.setStyle(new Notification.BigPictureStyle()
@@ -178,4 +206,45 @@ public class PipNotification {
return PendingIntent.getBroadcast(context, 0, new Intent(action),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
private class ActionBroadcastReceiver extends BroadcastReceiver {
final IntentFilter mIntentFilter;
{
mIntentFilter = new IntentFilter();
mIntentFilter.addAction(ACTION_CLOSE_PIP);
mIntentFilter.addAction(ACTION_SHOW_PIP_MENU);
}
boolean mRegistered = false;
void register() {
if (mRegistered) return;
mContext.registerReceiver(this, mIntentFilter, UserHandle.USER_ALL);
mRegistered = true;
}
void unregister() {
if (!mRegistered) return;
mContext.unregisterReceiver(this);
mRegistered = false;
}
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (DEBUG) Log.d(TAG, "on(Broadcast)Receive(), action=" + action);
if (ACTION_SHOW_PIP_MENU.equals(action)) {
mDelegate.showPictureInPictureMenu();
} else if (ACTION_CLOSE_PIP.equals(action)) {
mDelegate.closePip();
}
}
}
interface Delegate {
void showPictureInPictureMenu();
void closePip();
}
}

View File

@@ -171,4 +171,4 @@ private val StatusBarNotification.deleteIntent: PendingIntent?
get() = tvExtensions?.getParcelable("delete_intent")
private fun StatusBarNotification.isPipNotificationWithTitle(expectedTitle: String): Boolean =
tag == "PipNotification" && title == expectedTitle
tag == "TvPip" && title == expectedTitle

View File

@@ -25,7 +25,7 @@ import android.provider.Settings;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
import com.android.systemui.SystemUI;
import com.android.wm.shell.pip.tv.PipNotification;
import com.android.wm.shell.pip.tv.TvPipNotificationController;
import java.util.Arrays;
@@ -36,7 +36,7 @@ public class NotificationChannels extends SystemUI {
public static String GENERAL = "GEN";
public static String STORAGE = "DSK";
public static String BATTERY = "BAT";
public static String TVPIP = PipNotification.NOTIFICATION_CHANNEL_TVPIP;
public static String TVPIP = TvPipNotificationController.NOTIFICATION_CHANNEL; // "TVPIP"
public static String HINTS = "HNT";
public NotificationChannels(Context context) {

View File

@@ -32,9 +32,9 @@ 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.PipNotification;
import com.android.wm.shell.pip.tv.TvPipController;
import com.android.wm.shell.pip.tv.TvPipMenuController;
import com.android.wm.shell.pip.tv.TvPipNotificationController;
import java.util.Optional;
@@ -55,29 +55,22 @@ public abstract class TvPipModule {
PipTaskOrganizer pipTaskOrganizer,
TvPipMenuController tvPipMenuController,
PipMediaController pipMediaController,
PipNotification pipNotification,
TvPipNotificationController tvPipNotificationController,
TaskStackListenerImpl taskStackListener,
WindowManagerShellWrapper windowManagerShellWrapper) {
return Optional.of(
new PipController(
new TvPipController(
context,
pipBoundsState,
pipBoundsAlgorithm,
pipTaskOrganizer,
tvPipMenuController,
pipMediaController,
pipNotification,
tvPipNotificationController,
taskStackListener,
windowManagerShellWrapper));
}
@WMSingleton
@Provides
static PipNotification providePipNotification(Context context,
PipMediaController pipMediaController) {
return new PipNotification(context, pipMediaController);
}
@WMSingleton
@Provides
static PipBoundsAlgorithm providePipBoundsHandler(Context context,
@@ -93,7 +86,7 @@ public abstract class TvPipModule {
@WMSingleton
@Provides
static TvPipMenuController providesPipTvMenuController(
static TvPipMenuController providesTvPipMenuController(
Context context,
PipBoundsState pipBoundsState,
SystemWindows systemWindows,
@@ -101,17 +94,24 @@ public abstract class TvPipModule {
return new TvPipMenuController(context, pipBoundsState, systemWindows, pipMediaController);
}
@WMSingleton
@Provides
static TvPipNotificationController provideTvPipNotificationController(Context context,
PipMediaController pipMediaController) {
return new TvPipNotificationController(context, pipMediaController);
}
@WMSingleton
@Provides
static PipTaskOrganizer providePipTaskOrganizer(Context context,
TvPipMenuController tvMenuController,
TvPipMenuController tvPipMenuController,
PipBoundsState pipBoundsState,
PipBoundsAlgorithm pipBoundsAlgorithm,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
Optional<LegacySplitScreen> splitScreenOptional, DisplayController displayController,
PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer) {
return new PipTaskOrganizer(context, pipBoundsState, pipBoundsAlgorithm,
tvMenuController, pipSurfaceTransactionHelper, splitScreenOptional,
tvPipMenuController, pipSurfaceTransactionHelper, splitScreenOptional,
displayController, pipUiEventLogger, shellTaskOrganizer);
}
}