blob: 61ed40d5d78283e867f4143c608d0cba61d63ffb [file] [log] [blame]
/*
* 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.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.Looper;
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_DISMISS_PIP = 103;
public static final int MESSAGE_UPDATE_ACTIVITY_CALLBACK = 104;
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 dismissed.
*/
void onPipDismiss();
/**
* Called when the PIP requested to show the menu.
*/
void onPipShowMenu();
}
private Context mContext;
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(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_MENU_STATE_CHANGED: {
int menuState = msg.arg1;
boolean resize = msg.arg2 != 0;
onMenuStateChanged(menuState, resize);
break;
}
case MESSAGE_EXPAND_PIP: {
mListeners.forEach(Listener::onPipExpand);
break;
}
case MESSAGE_DISMISS_PIP: {
mListeners.forEach(Listener::onPipDismiss);
break;
}
case MESSAGE_SHOW_MENU: {
mListeners.forEach(Listener::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) {
final boolean resize = msg.arg1 != 0;
onMenuStateChanged(MENU_STATE_NONE, 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,
PipMediaController mediaController, InputConsumerController inputConsumerController) {
mContext = context;
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)
+ " allowMenuTimeout=" + allowMenuTimeout
+ " willResizeMenu=" + willResizeMenu
+ " callers=\n" + Debug.getCallers(5, " "));
}
if (mToActivityMessenger != null) {
Bundle data = new Bundle();
data.putInt(EXTRA_MENU_STATE, menuState);
if (stackBounds != null) {
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);
}
}