| /* |
| * 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.ActivityManager.StackId.PINNED_STACK_ID; |
| |
| import android.app.ActivityManager.StackInfo; |
| import android.app.ActivityOptions; |
| 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.Handler; |
| import android.os.Message; |
| import android.os.Messenger; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.util.Log; |
| import android.view.IWindowManager; |
| |
| import com.android.systemui.pip.phone.PipMediaController.ActionListener; |
| |
| 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_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; |
| |
| /** |
| * 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 boolean mStartActivityRequested; |
| private Messenger mToActivityMessenger; |
| private Messenger mMessenger = new Messenger(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_REGISTER_INPUT_CONSUMER: { |
| mInputConsumerController.registerInputConsumer(); |
| break; |
| } |
| case MESSAGE_UNREGISTER_INPUT_CONSUMER: { |
| mInputConsumerController.unregisterInputConsumer(); |
| break; |
| } |
| case MESSAGE_UPDATE_ACTIVITY_CALLBACK: { |
| mToActivityMessenger = msg.replyTo; |
| mStartActivityRequested = false; |
| // Mark the menu as invisible once the activity finishes as well |
| if (mToActivityMessenger == null) { |
| onMenuStateChanged(MENU_STATE_NONE, true /* resize */); |
| } |
| break; |
| } |
| } |
| } |
| }); |
| |
| 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 void onActivityPinned() { |
| if (mMenuState == MENU_STATE_NONE) { |
| // If the menu is not visible, then re-register the input consumer if it is not already |
| // registered |
| mInputConsumerController.registerInputConsumer(); |
| } |
| } |
| |
| 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) { |
| startMenuActivity(MENU_STATE_NONE, null /* stackBounds */, |
| null /* movementBounds */, false /* allowMenuTimeout */); |
| } |
| } |
| |
| /** |
| * Shows the menu activity. |
| */ |
| public void showMenu(int menuState, Rect stackBounds, Rect movementBounds, |
| boolean allowMenuTimeout) { |
| if (DEBUG) { |
| Log.d(TAG, "showMenu() hasActivity=" + (mToActivityMessenger != null)); |
| } |
| 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); |
| 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) { |
| startMenuActivity(menuState, stackBounds, movementBounds, allowMenuTimeout); |
| } |
| } |
| |
| /** |
| * 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() hasActivity=" + (mToActivityMessenger != null)); |
| } |
| 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); |
| } |
| } |
| } |
| |
| /** |
| * 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 */); |
| } |
| |
| /** |
| * @return the current menu state. |
| */ |
| public int getMenuState() { |
| return mMenuState; |
| } |
| |
| /** |
| * 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) { |
| try { |
| StackInfo pinnedStackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID); |
| if (pinnedStackInfo != null && pinnedStackInfo.taskIds != null && |
| pinnedStackInfo.taskIds.length > 0) { |
| Intent intent = new Intent(mContext, PipMenuActivity.class); |
| 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); |
| 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); |
| mStartActivityRequested = true; |
| } else { |
| Log.e(TAG, "No PIP tasks found"); |
| } |
| } catch (RemoteException e) { |
| mStartActivityRequested = 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 = mActivityManager.getStackInfo(PINNED_STACK_ID); |
| 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; |
| } |
| |
| /** |
| * 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 == MENU_STATE_NONE) { |
| mInputConsumerController.registerInputConsumer(); |
| } else { |
| mInputConsumerController.unregisterInputConsumer(); |
| } |
| 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; |
| } |
| |
| 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()); |
| } |
| } |