| /* |
| * Copyright (C) 2016 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.systemui.pip.phone; |
| |
| import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; |
| |
| import android.app.ActivityManager.StackInfo; |
| import android.app.ActivityOptions; |
| import android.app.ActivityTaskManager; |
| import android.app.IActivityManager; |
| import android.app.RemoteAction; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ParceledListSlice; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.os.Debug; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.os.Messenger; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| |
| import com.android.systemui.pip.phone.PipMediaController.ActionListener; |
| import com.android.systemui.shared.system.InputConsumerController; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Manages the PiP menu activity which can show menu options or a scrim. |
| * |
| * The current media session provides actions whenever there are no valid actions provided by the |
| * current PiP activity. Otherwise, those actions always take precedence. |
| */ |
| public class PipMenuActivityController { |
| |
| private static final String TAG = "PipMenuActController"; |
| private static final boolean DEBUG = false; |
| |
| public static final String EXTRA_CONTROLLER_MESSENGER = "messenger"; |
| public static final String EXTRA_ACTIONS = "actions"; |
| public static final String EXTRA_STACK_BOUNDS = "stack_bounds"; |
| public static final String EXTRA_MOVEMENT_BOUNDS = "movement_bounds"; |
| public static final String EXTRA_ALLOW_TIMEOUT = "allow_timeout"; |
| public static final String EXTRA_WILL_RESIZE_MENU = "resize_menu_on_show"; |
| public static final String EXTRA_DISMISS_FRACTION = "dismiss_fraction"; |
| public static final String EXTRA_MENU_STATE = "menu_state"; |
| |
| public static final int MESSAGE_MENU_STATE_CHANGED = 100; |
| public static final int MESSAGE_EXPAND_PIP = 101; |
| public static final int MESSAGE_MINIMIZE_PIP = 102; |
| public static final int MESSAGE_DISMISS_PIP = 103; |
| public static final int MESSAGE_UPDATE_ACTIVITY_CALLBACK = 104; |
| public static final int MESSAGE_REGISTER_INPUT_CONSUMER = 105; |
| public static final int MESSAGE_UNREGISTER_INPUT_CONSUMER = 106; |
| public static final int MESSAGE_SHOW_MENU = 107; |
| |
| public static final int MENU_STATE_NONE = 0; |
| public static final int MENU_STATE_CLOSE = 1; |
| public static final int MENU_STATE_FULL = 2; |
| |
| // The duration to wait before we consider the start activity as having timed out |
| private static final long START_ACTIVITY_REQUEST_TIMEOUT_MS = 300; |
| |
| /** |
| * A listener interface to receive notification on changes in PIP. |
| */ |
| public interface Listener { |
| /** |
| * Called when the PIP menu visibility changes. |
| * |
| * @param menuState the current state of the menu |
| * @param resize whether or not to resize the PiP with the state change |
| */ |
| void onPipMenuStateChanged(int menuState, boolean resize); |
| |
| /** |
| * Called when the PIP requested to be expanded. |
| */ |
| void onPipExpand(); |
| |
| /** |
| * Called when the PIP requested to be minimized. |
| */ |
| void onPipMinimize(); |
| |
| /** |
| * Called when the PIP requested to be dismissed. |
| */ |
| void onPipDismiss(); |
| |
| /** |
| * Called when the PIP requested to show the menu. |
| */ |
| void onPipShowMenu(); |
| } |
| |
| private Context mContext; |
| private IActivityManager mActivityManager; |
| private PipMediaController mMediaController; |
| private InputConsumerController mInputConsumerController; |
| |
| private ArrayList<Listener> mListeners = new ArrayList<>(); |
| private ParceledListSlice mAppActions; |
| private ParceledListSlice mMediaActions; |
| private int mMenuState; |
| |
| // The dismiss fraction update is sent frequently, so use a temporary bundle for the message |
| private Bundle mTmpDismissFractionData = new Bundle(); |
| |
| private Runnable mOnAnimationEndRunnable; |
| private boolean mStartActivityRequested; |
| private long mStartActivityRequestedTime; |
| private Messenger mToActivityMessenger; |
| private Handler mHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MESSAGE_MENU_STATE_CHANGED: { |
| int menuState = msg.arg1; |
| onMenuStateChanged(menuState, true /* resize */); |
| break; |
| } |
| case MESSAGE_EXPAND_PIP: { |
| mListeners.forEach(l -> l.onPipExpand()); |
| break; |
| } |
| case MESSAGE_MINIMIZE_PIP: { |
| mListeners.forEach(l -> l.onPipMinimize()); |
| break; |
| } |
| case MESSAGE_DISMISS_PIP: { |
| mListeners.forEach(l -> l.onPipDismiss()); |
| break; |
| } |
| case MESSAGE_SHOW_MENU: { |
| mListeners.forEach(l -> l.onPipShowMenu()); |
| break; |
| } |
| case MESSAGE_UPDATE_ACTIVITY_CALLBACK: { |
| mToActivityMessenger = msg.replyTo; |
| setStartActivityRequested(false); |
| if (mOnAnimationEndRunnable != null) { |
| mOnAnimationEndRunnable.run(); |
| mOnAnimationEndRunnable = null; |
| } |
| // Mark the menu as invisible once the activity finishes as well |
| if (mToActivityMessenger == null) { |
| onMenuStateChanged(MENU_STATE_NONE, true /* resize */); |
| } |
| break; |
| } |
| } |
| } |
| }; |
| private Messenger mMessenger = new Messenger(mHandler); |
| |
| private Runnable mStartActivityRequestedTimeoutRunnable = () -> { |
| setStartActivityRequested(false); |
| if (mOnAnimationEndRunnable != null) { |
| mOnAnimationEndRunnable.run(); |
| mOnAnimationEndRunnable = null; |
| } |
| Log.e(TAG, "Expected start menu activity request timed out"); |
| }; |
| |
| private ActionListener mMediaActionListener = new ActionListener() { |
| @Override |
| public void onMediaActionsChanged(List<RemoteAction> mediaActions) { |
| mMediaActions = new ParceledListSlice<>(mediaActions); |
| updateMenuActions(); |
| } |
| }; |
| |
| public PipMenuActivityController(Context context, IActivityManager activityManager, |
| PipMediaController mediaController, InputConsumerController inputConsumerController) { |
| mContext = context; |
| mActivityManager = activityManager; |
| mMediaController = mediaController; |
| mInputConsumerController = inputConsumerController; |
| } |
| |
| public boolean isMenuActivityVisible() { |
| return mToActivityMessenger != null; |
| } |
| |
| public void onActivityPinned() { |
| mInputConsumerController.registerInputConsumer(); |
| } |
| |
| public void onActivityUnpinned() { |
| hideMenu(); |
| mInputConsumerController.unregisterInputConsumer(); |
| setStartActivityRequested(false); |
| } |
| |
| public void onPinnedStackAnimationEnded() { |
| // Note: Only active menu activities care about this event |
| if (mToActivityMessenger != null) { |
| Message m = Message.obtain(); |
| m.what = PipMenuActivity.MESSAGE_ANIMATION_ENDED; |
| try { |
| mToActivityMessenger.send(m); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not notify menu pinned animation ended", e); |
| } |
| } |
| } |
| |
| /** |
| * Adds a new menu activity listener. |
| */ |
| public void addListener(Listener listener) { |
| if (!mListeners.contains(listener)) { |
| mListeners.add(listener); |
| } |
| } |
| |
| /** |
| * Updates the appearance of the menu and scrim on top of the PiP while dismissing. |
| */ |
| public void setDismissFraction(float fraction) { |
| if (DEBUG) { |
| Log.d(TAG, "setDismissFraction() hasActivity=" + (mToActivityMessenger != null) |
| + " fraction=" + fraction); |
| } |
| if (mToActivityMessenger != null) { |
| mTmpDismissFractionData.clear(); |
| mTmpDismissFractionData.putFloat(EXTRA_DISMISS_FRACTION, fraction); |
| Message m = Message.obtain(); |
| m.what = PipMenuActivity.MESSAGE_UPDATE_DISMISS_FRACTION; |
| m.obj = mTmpDismissFractionData; |
| try { |
| mToActivityMessenger.send(m); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not notify menu to update dismiss fraction", e); |
| } |
| } else if (!mStartActivityRequested || isStartActivityRequestedElapsed()) { |
| // If we haven't requested the start activity, or if it previously took too long to |
| // start, then start it |
| startMenuActivity(MENU_STATE_NONE, null /* stackBounds */, |
| null /* movementBounds */, false /* allowMenuTimeout */, |
| false /* resizeMenuOnShow */); |
| } |
| } |
| |
| /** |
| * Shows the menu activity. |
| */ |
| public void showMenu(int menuState, Rect stackBounds, Rect movementBounds, |
| boolean allowMenuTimeout, boolean willResizeMenu) { |
| if (DEBUG) { |
| Log.d(TAG, "showMenu() state=" + menuState |
| + " hasActivity=" + (mToActivityMessenger != null) |
| + " callers=\n" + Debug.getCallers(5, " ")); |
| } |
| |
| if (mToActivityMessenger != null) { |
| Bundle data = new Bundle(); |
| data.putInt(EXTRA_MENU_STATE, menuState); |
| data.putParcelable(EXTRA_STACK_BOUNDS, stackBounds); |
| data.putParcelable(EXTRA_MOVEMENT_BOUNDS, movementBounds); |
| data.putBoolean(EXTRA_ALLOW_TIMEOUT, allowMenuTimeout); |
| data.putBoolean(EXTRA_WILL_RESIZE_MENU, willResizeMenu); |
| Message m = Message.obtain(); |
| m.what = PipMenuActivity.MESSAGE_SHOW_MENU; |
| m.obj = data; |
| try { |
| mToActivityMessenger.send(m); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not notify menu to show", e); |
| } |
| } else if (!mStartActivityRequested || isStartActivityRequestedElapsed()) { |
| // If we haven't requested the start activity, or if it previously took too long to |
| // start, then start it |
| startMenuActivity(menuState, stackBounds, movementBounds, allowMenuTimeout, |
| willResizeMenu); |
| } |
| } |
| |
| /** |
| * Pokes the menu, indicating that the user is interacting with it. |
| */ |
| public void pokeMenu() { |
| if (DEBUG) { |
| Log.d(TAG, "pokeMenu() hasActivity=" + (mToActivityMessenger != null)); |
| } |
| if (mToActivityMessenger != null) { |
| Message m = Message.obtain(); |
| m.what = PipMenuActivity.MESSAGE_POKE_MENU; |
| try { |
| mToActivityMessenger.send(m); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not notify poke menu", e); |
| } |
| } |
| } |
| |
| /** |
| * Hides the menu activity. |
| */ |
| public void hideMenu() { |
| if (DEBUG) { |
| Log.d(TAG, "hideMenu() state=" + mMenuState |
| + " hasActivity=" + (mToActivityMessenger != null) |
| + " callers=\n" + Debug.getCallers(5, " ")); |
| } |
| if (mToActivityMessenger != null) { |
| Message m = Message.obtain(); |
| m.what = PipMenuActivity.MESSAGE_HIDE_MENU; |
| try { |
| mToActivityMessenger.send(m); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not notify menu to hide", e); |
| } |
| } |
| } |
| |
| /** |
| * Hides the menu activity. |
| */ |
| public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) { |
| if (mStartActivityRequested) { |
| // If the menu has been start-requested, but not actually started, then we defer the |
| // trigger callback until the menu has started and called back to the controller. |
| mOnAnimationEndRunnable = onEndCallback; |
| onStartCallback.run(); |
| |
| // Fallback for b/63752800, we have started the PipMenuActivity but it has not made any |
| // callbacks. Don't continue to wait for the menu to show past some timeout. |
| mHandler.removeCallbacks(mStartActivityRequestedTimeoutRunnable); |
| mHandler.postDelayed(mStartActivityRequestedTimeoutRunnable, |
| START_ACTIVITY_REQUEST_TIMEOUT_MS); |
| } else if (mMenuState != MENU_STATE_NONE && mToActivityMessenger != null) { |
| // If the menu is visible in either the closed or full state, then hide the menu and |
| // trigger the animation trigger afterwards |
| onStartCallback.run(); |
| Message m = Message.obtain(); |
| m.what = PipMenuActivity.MESSAGE_HIDE_MENU; |
| m.obj = onEndCallback; |
| try { |
| mToActivityMessenger.send(m); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not notify hide menu", e); |
| } |
| } |
| } |
| |
| /** |
| * Preemptively mark the menu as invisible, used when we are directly manipulating the pinned |
| * stack and don't want to trigger a resize which can animate the stack in a conflicting way |
| * (ie. when manually expanding or dismissing). |
| */ |
| public void hideMenuWithoutResize() { |
| onMenuStateChanged(MENU_STATE_NONE, false /* resize */); |
| } |
| |
| /** |
| * Sets the menu actions to the actions provided by the current PiP activity. |
| */ |
| public void setAppActions(ParceledListSlice appActions) { |
| mAppActions = appActions; |
| updateMenuActions(); |
| } |
| |
| /** |
| * @return the best set of actions to show in the PiP menu. |
| */ |
| private ParceledListSlice resolveMenuActions() { |
| if (isValidActions(mAppActions)) { |
| return mAppActions; |
| } |
| return mMediaActions; |
| } |
| |
| /** |
| * Starts the menu activity on the top task of the pinned stack. |
| */ |
| private void startMenuActivity(int menuState, Rect stackBounds, Rect movementBounds, |
| boolean allowMenuTimeout, boolean willResizeMenu) { |
| try { |
| StackInfo pinnedStackInfo = ActivityTaskManager.getService().getStackInfo( |
| WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); |
| if (pinnedStackInfo != null && pinnedStackInfo.taskIds != null && |
| pinnedStackInfo.taskIds.length > 0) { |
| Intent intent = new Intent(mContext, PipMenuActivity.class); |
| intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| intent.putExtra(EXTRA_CONTROLLER_MESSENGER, mMessenger); |
| intent.putExtra(EXTRA_ACTIONS, resolveMenuActions()); |
| if (stackBounds != null) { |
| intent.putExtra(EXTRA_STACK_BOUNDS, stackBounds); |
| } |
| if (movementBounds != null) { |
| intent.putExtra(EXTRA_MOVEMENT_BOUNDS, movementBounds); |
| } |
| intent.putExtra(EXTRA_MENU_STATE, menuState); |
| intent.putExtra(EXTRA_ALLOW_TIMEOUT, allowMenuTimeout); |
| intent.putExtra(EXTRA_WILL_RESIZE_MENU, willResizeMenu); |
| ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0); |
| options.setLaunchTaskId( |
| pinnedStackInfo.taskIds[pinnedStackInfo.taskIds.length - 1]); |
| options.setTaskOverlay(true, true /* canResume */); |
| mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT); |
| setStartActivityRequested(true); |
| } else { |
| Log.e(TAG, "No PIP tasks found"); |
| } |
| } catch (RemoteException e) { |
| setStartActivityRequested(false); |
| Log.e(TAG, "Error showing PIP menu activity", e); |
| } |
| } |
| |
| /** |
| * Updates the PiP menu activity with the best set of actions provided. |
| */ |
| private void updateMenuActions() { |
| if (mToActivityMessenger != null) { |
| // Fetch the pinned stack bounds |
| Rect stackBounds = null; |
| try { |
| StackInfo pinnedStackInfo = ActivityTaskManager.getService().getStackInfo( |
| WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); |
| if (pinnedStackInfo != null) { |
| stackBounds = pinnedStackInfo.bounds; |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error showing PIP menu activity", e); |
| } |
| |
| Bundle data = new Bundle(); |
| data.putParcelable(EXTRA_STACK_BOUNDS, stackBounds); |
| data.putParcelable(EXTRA_ACTIONS, resolveMenuActions()); |
| Message m = Message.obtain(); |
| m.what = PipMenuActivity.MESSAGE_UPDATE_ACTIONS; |
| m.obj = data; |
| try { |
| mToActivityMessenger.send(m); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not notify menu activity to update actions", e); |
| } |
| } |
| } |
| |
| /** |
| * Returns whether the set of actions are valid. |
| */ |
| private boolean isValidActions(ParceledListSlice actions) { |
| return actions != null && actions.getList().size() > 0; |
| } |
| |
| /** |
| * @return whether the time of the activity request has exceeded the timeout. |
| */ |
| private boolean isStartActivityRequestedElapsed() { |
| return (SystemClock.uptimeMillis() - mStartActivityRequestedTime) |
| >= START_ACTIVITY_REQUEST_TIMEOUT_MS; |
| } |
| |
| /** |
| * Handles changes in menu visibility. |
| */ |
| private void onMenuStateChanged(int menuState, boolean resize) { |
| if (DEBUG) { |
| Log.d(TAG, "onMenuStateChanged() mMenuState=" + mMenuState |
| + " menuState=" + menuState + " resize=" + resize); |
| } |
| |
| if (menuState != mMenuState) { |
| mListeners.forEach(l -> l.onPipMenuStateChanged(menuState, resize)); |
| if (menuState == MENU_STATE_FULL) { |
| // Once visible, start listening for media action changes. This call will trigger |
| // the menu actions to be updated again. |
| mMediaController.addListener(mMediaActionListener); |
| } else { |
| // Once hidden, stop listening for media action changes. This call will trigger |
| // the menu actions to be updated again. |
| mMediaController.removeListener(mMediaActionListener); |
| } |
| } |
| mMenuState = menuState; |
| } |
| |
| private void setStartActivityRequested(boolean requested) { |
| mHandler.removeCallbacks(mStartActivityRequestedTimeoutRunnable); |
| mStartActivityRequested = requested; |
| mStartActivityRequestedTime = requested ? SystemClock.uptimeMillis() : 0; |
| } |
| |
| /** |
| * Handles a pointer event sent from pip input consumer. |
| */ |
| void handlePointerEvent(MotionEvent ev) { |
| if (mToActivityMessenger != null) { |
| Message m = Message.obtain(); |
| m.what = PipMenuActivity.MESSAGE_POINTER_EVENT; |
| m.obj = ev; |
| try { |
| mToActivityMessenger.send(m); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not dispatch touch event", e); |
| } |
| } |
| } |
| |
| public void dump(PrintWriter pw, String prefix) { |
| final String innerPrefix = prefix + " "; |
| pw.println(prefix + TAG); |
| pw.println(innerPrefix + "mMenuState=" + mMenuState); |
| pw.println(innerPrefix + "mToActivityMessenger=" + mToActivityMessenger); |
| pw.println(innerPrefix + "mListeners=" + mListeners.size()); |
| pw.println(innerPrefix + "mStartActivityRequested=" + mStartActivityRequested); |
| pw.println(innerPrefix + "mStartActivityRequestedTime=" + mStartActivityRequestedTime); |
| } |
| } |