diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java deleted file mode 100644 index ffa6c9921d97e..0000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipController.java +++ /dev/null @@ -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 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 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 + ")"; - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java new file mode 100644 index 0000000000000..8bc60f9dbcd90 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -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 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); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 9192cf14cd9b5..470ab0c9c0e3a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -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(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index f7b76c1ec7452..e08ca52fd2ca7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -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; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java similarity index 62% rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java rename to libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java index 5716c7fc71624..ce4b608933677 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/PipNotification.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -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; *

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(); + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt index 75388bf2a189d..5258e90300750 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt @@ -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 \ No newline at end of file + tag == "TvPip" && title == expectedTitle \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java index 0d63324966fd2..0ba072e9e72f5 100644 --- a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java +++ b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java @@ -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) { diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java index 4d3af9c011539..8a79acef756c0 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java @@ -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 splitScreenOptional, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer) { return new PipTaskOrganizer(context, pipBoundsState, pipBoundsAlgorithm, - tvMenuController, pipSurfaceTransactionHelper, splitScreenOptional, + tvPipMenuController, pipSurfaceTransactionHelper, splitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer); } }