Merge changes Iabb33606,I3c364ebe

* changes:
  Falling back to media session controls.
  Move activity to fullscreen stack when it is relaunched in PIP.
This commit is contained in:
Winson Chung
2017-01-19 20:50:43 +00:00
committed by Android (Google) Code Review
11 changed files with 410 additions and 53 deletions

View File

@@ -31,8 +31,10 @@ oneway interface ITaskStackListener {
* Called whenever IActivityManager.startActivity is called on an activity that is already
* running in the pinned stack and the activity is not actually started, but the task is either
* brought to the front or a new Intent is delivered to it.
*
* @param sourceComponent the component name of the activity that initiated the restart attempt
*/
void onPinnedActivityRestartAttempt();
void onPinnedActivityRestartAttempt(in ComponentName sourceComponent);
/**
* Called whenever the pinned stack is done animating a resize.

View File

@@ -35,7 +35,7 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub {
}
@Override
public void onPinnedActivityRestartAttempt() throws RemoteException {
public void onPinnedActivityRestartAttempt(ComponentName sourceComponent) throws RemoteException {
}
@Override

View File

@@ -16,14 +16,20 @@
package com.android.systemui.pip.phone;
import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
import static android.view.Display.DEFAULT_DISPLAY;
import android.app.ActivityManager;
import android.app.ActivityManager.StackInfo;
import android.app.ActivityOptions;
import android.app.IActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.view.IPinnedStackController;
import android.view.IPinnedStackListener;
@@ -49,6 +55,7 @@ public class PipManager {
private final PinnedStackListener mPinnedStackListener = new PinnedStackListener();
private PipMenuActivityController mMenuController;
private PipMediaController mMediaController;
private PipTouchHandler mTouchHandler;
/**
@@ -57,7 +64,11 @@ public class PipManager {
TaskStackListener mTaskStackListener = new TaskStackListener() {
@Override
public void onActivityPinned() {
if (!checkCurrentUserId(false /* debug */)) {
return;
}
mTouchHandler.onActivityPinned();
mMediaController.onActivityPinned();
}
@Override
@@ -66,8 +77,24 @@ public class PipManager {
}
@Override
public void onPinnedActivityRestartAttempt() {
// TODO(winsonc): Hide the menu and expand the PiP
public void onPinnedActivityRestartAttempt(ComponentName sourceComponent) {
if (!checkCurrentUserId(false /* debug */)) {
return;
}
// Expand the activity back to fullscreen only if it was attempted to be restarted from
// another package than the top activity in the stack
boolean expandPipToFullscreen = true;
if (sourceComponent != null) {
ComponentName topActivity = PipUtils.getTopPinnedActivity(mActivityManager);
expandPipToFullscreen = topActivity != null && topActivity.getPackageName().equals(
sourceComponent.getPackageName());
}
if (expandPipToFullscreen) {
mTouchHandler.expandPinnedStackToFullscreen();
} else {
Log.w(TAG, "Can not expand PiP to fullscreen via intent from the same package.");
}
}
};
@@ -91,7 +118,7 @@ public class PipManager {
@Override
public void onActionsChanged(ParceledListSlice actions) {
mHandler.post(() -> {
mMenuController.setActions(actions);
mMenuController.setAppActions(actions);
});
}
@@ -127,7 +154,9 @@ public class PipManager {
}
SystemServicesProxy.getInstance(mContext).registerTaskStackListener(mTaskStackListener);
mMenuController = new PipMenuActivityController(context, mActivityManager, mWindowManager);
mMediaController = new PipMediaController(context, mActivityManager);
mMenuController = new PipMenuActivityController(context, mActivityManager, mWindowManager,
mMediaController);
mTouchHandler = new PipTouchHandler(context, mMenuController, mActivityManager,
mWindowManager);
}

View File

@@ -0,0 +1,193 @@
/*
* Copyright (C) 2017 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 android.app.IActivityManager;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.drawable.Icon;
import android.media.session.MediaController;
import android.media.session.MediaController.TransportControls;
import android.media.session.MediaSession;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import com.android.systemui.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Interfaces with the {@link MediaSessionManager} to compose the right set of actions to show (only
* if there are no actions from the PiP activity itself). The active media controller is only set
* when there is a media session from the top PiP activity.
*/
public class PipMediaController {
/**
* A listener interface to receive notification on changes to the media actions.
*/
public interface ActionListener {
/**
* Called when the media actions changes.
*/
void onMediaActionsChanged(List<RemoteAction> actions);
}
private final Context mContext;
private final IActivityManager mActivityManager;
private final MediaSessionManager mMediaSessionManager;
private MediaController mMediaController;
private RemoteAction mPauseAction;
private RemoteAction mPlayAction;
private MediaController.Callback mPlaybackChangedListener = new MediaController.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackState state) {
if (!mListeners.isEmpty()) {
notifyActionsChanged(getMediaActions());
}
}
};
private ArrayList<ActionListener> mListeners = new ArrayList<>();
public PipMediaController(Context context, IActivityManager activityManager) {
mContext = context;
mActivityManager = activityManager;
createMediaActions();
mMediaSessionManager =
(MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
mMediaSessionManager.addOnActiveSessionsChangedListener(controllers -> {
resolveActiveMediaController(controllers);
}, null);
}
/**
* Handles when an activity is pinned.
*/
public void onActivityPinned() {
// Once we enter PiP, try to find the active media controller for the top most activity
resolveActiveMediaController(mMediaSessionManager.getActiveSessions(null));
}
/**
* Adds a new media action listener.
*/
public void addListener(ActionListener listener) {
if (!mListeners.contains(listener)) {
mListeners.add(listener);
listener.onMediaActionsChanged(getMediaActions());
}
}
/**
* Removes a media action listener.
*/
public void removeListener(ActionListener listener) {
listener.onMediaActionsChanged(Collections.EMPTY_LIST);
mListeners.remove(listener);
}
/**
* Gets the set of media actions currently available.
*/
private List<RemoteAction> getMediaActions() {
if (mMediaController == null || mMediaController.getPlaybackState() == null) {
return Collections.EMPTY_LIST;
}
ArrayList<RemoteAction> mediaActions = new ArrayList<>();
int state = mMediaController.getPlaybackState().getState();
boolean isPlaying = MediaSession.isActiveState(state);
long actions = mMediaController.getPlaybackState().getActions();
if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
mediaActions.add(mPauseAction);
} else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
mediaActions.add(mPlayAction);
}
return mediaActions;
}
/**
* Creates the standard media buttons that we may show.
*/
private void createMediaActions() {
String pauseDescription = mContext.getString(R.string.pip_pause);
mPauseAction = new RemoteAction(Icon.createWithResource(mContext,
R.drawable.ic_pause_white_24dp), pauseDescription, pauseDescription,
action -> mMediaController.getTransportControls().pause());
String playDescription = mContext.getString(R.string.pip_play);
mPlayAction = new RemoteAction(Icon.createWithResource(mContext,
R.drawable.ic_play_arrow_white_24dp), playDescription, playDescription,
action -> mMediaController.getTransportControls().play());
}
/**
* Tries to find and set the active media controller for the top PiP activity.
*/
private void resolveActiveMediaController(List<MediaController> controllers) {
if (controllers != null) {
final ComponentName topActivity = PipUtils.getTopPinnedActivity(mActivityManager);
if (topActivity != null) {
for (int i = 0; i < controllers.size(); i++) {
final MediaController controller = controllers.get(i);
if (controller.getPackageName().equals(topActivity.getPackageName())) {
setActiveMediaController(controller);
return;
}
}
}
}
setActiveMediaController(null);
}
/**
* Sets the active media controller for the top PiP activity.
*/
private void setActiveMediaController(MediaController controller) {
if (controller != mMediaController) {
if (mMediaController != null) {
mMediaController.unregisterCallback(mPlaybackChangedListener);
}
mMediaController = controller;
if (controller != null) {
controller.registerCallback(mPlaybackChangedListener);
}
if (!mListeners.isEmpty()) {
notifyActionsChanged(getMediaActions());
}
// TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
}
}
/**
* Notifies all listeners that the actions have changed.
*/
private void notifyActionsChanged(List<RemoteAction> actions) {
if (!mListeners.isEmpty()) {
mListeners.forEach(l -> l.onMediaActionsChanged(actions));
}
}
}

View File

@@ -1,3 +1,19 @@
/*
* 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;
@@ -5,6 +21,7 @@ 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;
@@ -16,11 +33,20 @@ import android.os.UserHandle;
import android.util.Log;
import android.view.IWindowManager;
import java.util.ArrayList;
import com.android.systemui.pip.phone.PipMediaController.ActionListener;
import java.util.ArrayList;
import java.util.List;
/**
* Manages the PiP menu activity.
*
* 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 = "PipMenuActivityController";
private static final String TAG = "PipMenuActController";
public static final String EXTRA_CONTROLLER_MESSENGER = "messenger";
public static final String EXTRA_ACTIONS = "actions";
@@ -59,9 +85,12 @@ public class PipMenuActivityController {
private Context mContext;
private IActivityManager mActivityManager;
private IWindowManager mWindowManager;
private PipMediaController mMediaController;
private ArrayList<Listener> mListeners = new ArrayList<>();
private ParceledListSlice mActions;
private ParceledListSlice mAppActions;
private ParceledListSlice mMediaActions;
private boolean mVisible;
private Messenger mToActivityMessenger;
private Messenger mMessenger = new Messenger(new Handler() {
@@ -70,13 +99,13 @@ public class PipMenuActivityController {
switch (msg.what) {
case MESSAGE_MENU_VISIBILITY_CHANGED: {
boolean visible = msg.arg1 > 0;
mListeners.forEach(l -> l.onPipMenuVisibilityChanged(visible));
onMenuVisibilityChanged(visible);
break;
}
case MESSAGE_EXPAND_PIP: {
mListeners.forEach(l -> l.onPipExpand());
// Preemptively mark the menu as invisible once we expand the PiP
mListeners.forEach(l -> l.onPipMenuVisibilityChanged(false));
onMenuVisibilityChanged(false);
break;
}
case MESSAGE_MINIMIZE_PIP: {
@@ -86,14 +115,14 @@ public class PipMenuActivityController {
case MESSAGE_DISMISS_PIP: {
mListeners.forEach(l -> l.onPipDismiss());
// Preemptively mark the menu as invisible once we dismiss the PiP
mListeners.forEach(l -> l.onPipMenuVisibilityChanged(false));
onMenuVisibilityChanged(false);
break;
}
case MESSAGE_UPDATE_ACTIVITY_CALLBACK: {
mToActivityMessenger = msg.replyTo;
// Mark the menu as invisible once the activity finishes as well
if (mToActivityMessenger == null) {
mListeners.forEach(l -> l.onPipMenuVisibilityChanged(false));
onMenuVisibilityChanged(false);
}
break;
}
@@ -101,11 +130,20 @@ public class PipMenuActivityController {
}
});
private ActionListener mMediaActionListener = new ActionListener() {
@Override
public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
mMediaActions = new ParceledListSlice<>(mediaActions);
updateMenuActions();
}
};
public PipMenuActivityController(Context context, IActivityManager activityManager,
IWindowManager windowManager) {
IWindowManager windowManager, PipMediaController mediaController) {
mContext = context;
mActivityManager = activityManager;
mWindowManager = windowManager;
mMediaController = mediaController;
}
/**
@@ -137,7 +175,7 @@ public class PipMenuActivityController {
pinnedStackInfo.taskIds.length > 0) {
Intent intent = new Intent(mContext, PipMenuActivity.class);
intent.putExtra(EXTRA_CONTROLLER_MESSENGER, mMessenger);
intent.putExtra(EXTRA_ACTIONS, mActions);
intent.putExtra(EXTRA_ACTIONS, resolveMenuActions());
ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
options.setLaunchTaskId(
pinnedStackInfo.taskIds[pinnedStackInfo.taskIds.length - 1]);
@@ -168,15 +206,31 @@ public class PipMenuActivityController {
}
/**
* Sets the {@param actions} associated with the PiP.
* Sets the menu actions to the actions provided by the current PiP activity.
*/
public void setActions(ParceledListSlice actions) {
mActions = actions;
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;
}
/**
* Updates the PiP menu activity with the best set of actions provided.
*/
private void updateMenuActions() {
if (mToActivityMessenger != null) {
Message m = Message.obtain();
m.what = PipMenuActivity.MESSAGE_UPDATE_ACTIONS;
m.obj = actions;
m.obj = resolveMenuActions();
try {
mToActivityMessenger.send(m);
} catch (RemoteException e) {
@@ -184,4 +238,30 @@ public class PipMenuActivityController {
}
}
}
/**
* 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 onMenuVisibilityChanged(boolean visible) {
mListeners.forEach(l -> l.onPipMenuVisibilityChanged(visible));
if (visible != mVisible) {
if (visible) {
// 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);
}
}
mVisible = visible;
}
}

View File

@@ -487,7 +487,7 @@ public class PipTouchHandler implements TunerService.Tunable {
/**
* Resizes the pinned stack back to fullscreen.
*/
private void expandPinnedStackToFullscreen() {
void expandPinnedStackToFullscreen() {
BackgroundThread.getHandler().post(() -> {
try {
mActivityManager.resizeStack(PINNED_STACK_ID, null /* bounds */,

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2017 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.IActivityManager;
import android.content.ComponentName;
import android.os.RemoteException;
import android.util.Log;
public class PipUtils {
private static final String TAG = "PipUtils";
/**
* @return the ComponentName of the top activity in the pinned stack, or null if none exists.
*/
public static ComponentName getTopPinnedActivity(IActivityManager activityManager) {
try {
StackInfo pinnedStackInfo = activityManager.getStackInfo(PINNED_STACK_ID);
if (pinnedStackInfo != null && pinnedStackInfo.taskIds != null &&
pinnedStackInfo.taskIds.length > 0) {
return pinnedStackInfo.topActivity;
}
} catch (RemoteException e) {
Log.w(TAG, "Unable to get pinned stack.");
}
return null;
}
}

View File

@@ -590,7 +590,7 @@ public class PipManager {
@Override
public void onTaskStackChanged() {
if (DEBUG) Log.d(TAG, "onTaskStackChanged()");
if (!checkCurrentUserId()) {
if (!checkCurrentUserId(DEBUG)) {
return;
}
if (mState != STATE_NO_PIP) {
@@ -627,7 +627,7 @@ public class PipManager {
@Override
public void onActivityPinned() {
if (DEBUG) Log.d(TAG, "onActivityPinned()");
if (!checkCurrentUserId()) {
if (!checkCurrentUserId(DEBUG)) {
return;
}
StackInfo stackInfo = getPinnedStackInfo();
@@ -658,9 +658,9 @@ public class PipManager {
}
@Override
public void onPinnedActivityRestartAttempt() {
public void onPinnedActivityRestartAttempt(ComponentName sourceComponent) {
if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()");
if (!checkCurrentUserId()) {
if (!checkCurrentUserId(DEBUG)) {
return;
}
// If PIPed activity is launched again by Launcher or intent, make it fullscreen.
@@ -670,7 +670,7 @@ public class PipManager {
@Override
public void onPinnedStackAnimationEnded() {
if (DEBUG) Log.d(TAG, "onPinnedStackAnimationEnded()");
if (!checkCurrentUserId()) {
if (!checkCurrentUserId(DEBUG)) {
return;
}
switch (mState) {
@@ -693,26 +693,6 @@ public class PipManager {
break;
}
}
// {@link android.app.ITaskStackListener} isn't multi-user aware.
// Check the current uid and current SystemUI's running uid
// so we can handle the PIP status change only once.
private boolean checkCurrentUserId() {
try {
int processUserId = UserHandle.myUserId();
int currentUserId = mActivityManager.getCurrentUser().id;
if (processUserId != currentUserId) {
if (DEBUG) {
Log.d(TAG, "UID mismatch. SystemUI is running uid=" + processUserId
+ " and the current user is uid=" + currentUserId);
}
return false;
}
} catch (RemoteException e) {
Log.w(TAG, "Unable to get current user.");
}
return true;
}
};
/**

View File

@@ -79,6 +79,7 @@ import android.view.accessibility.AccessibilityManager;
import com.android.internal.app.AssistUtils;
import com.android.internal.os.BackgroundThread;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.R;
import com.android.systemui.pip.tv.PipMenuActivity;
import com.android.systemui.pip.tv.PipOnboardingActivity;
@@ -152,11 +153,30 @@ public class SystemServicesProxy {
public void onTaskStackChanged() { }
public void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) { }
public void onActivityPinned() { }
public void onPinnedActivityRestartAttempt() { }
public void onPinnedActivityRestartAttempt(ComponentName sourceComponent) { }
public void onPinnedStackAnimationEnded() { }
public void onActivityForcedResizable(String packageName, int taskId) { }
public void onActivityDismissingDockedStack() { }
public void onTaskProfileLocked(int taskId, int userId) { }
/**
* Checks that the current user matches the user's SystemUI process. Since
* {@link android.app.ITaskStackListener} is not multi-user aware, handlers of
* TaskStackListener should make this call to verify that we don't act on events from other
* user's processes.
*/
protected final boolean checkCurrentUserId(boolean debug) {
int processUserId = UserHandle.myUserId();
int currentUserId = KeyguardUpdateMonitor.getCurrentUser();
if (processUserId != currentUserId) {
if (debug) {
Log.d(TAG, "UID mismatch. SystemUI is running uid=" + processUserId
+ " and the current user is uid=" + currentUserId);
}
return false;
}
return true;
}
}
/**
@@ -178,9 +198,11 @@ public class SystemServicesProxy {
}
@Override
public void onPinnedActivityRestartAttempt() throws RemoteException{
public void onPinnedActivityRestartAttempt(ComponentName sourceComponent)
throws RemoteException{
mHandler.removeMessages(H.ON_PINNED_ACTIVITY_RESTART_ATTEMPT);
mHandler.sendEmptyMessage(H.ON_PINNED_ACTIVITY_RESTART_ATTEMPT);
mHandler.obtainMessage(H.ON_PINNED_ACTIVITY_RESTART_ATTEMPT, sourceComponent)
.sendToTarget();
}
@Override
@@ -1214,7 +1236,8 @@ public class SystemServicesProxy {
}
case ON_PINNED_ACTIVITY_RESTART_ATTEMPT: {
for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
mTaskStackListeners.get(i).onPinnedActivityRestartAttempt();
mTaskStackListeners.get(i).onPinnedActivityRestartAttempt(
(ComponentName) msg.obj);
}
break;
}

View File

@@ -582,7 +582,10 @@ class ActivityStarter {
// The activity was already running in the pinned stack so it wasn't started, but either
// brought to the front or the new intent was delivered to it since it was already in
// front. Notify anyone interested in this piece of information.
mService.mTaskChangeNotificationController.notifyPinnedActivityRestartAttempt();
final ComponentName sourceComponent = sourceRecord == null ? null :
sourceRecord.realActivity;
mService.mTaskChangeNotificationController.notifyPinnedActivityRestartAttempt(
sourceComponent);
return;
}
}

View File

@@ -97,7 +97,7 @@ class TaskChangeNotificationController {
};
private final TaskStackConsumer mNotifyPinnedActivityRestartAttempt = (l, m) -> {
l.onPinnedActivityRestartAttempt();
l.onPinnedActivityRestartAttempt((ComponentName) m.obj);
};
private final TaskStackConsumer mNotifyPinnedStackAnimationEnded = (l, m) -> {
@@ -267,10 +267,11 @@ class TaskChangeNotificationController {
* running in the pinned stack and the activity was not actually started, but the task is
* either brought to the front or a new Intent is delivered to it.
*/
void notifyPinnedActivityRestartAttempt() {
void notifyPinnedActivityRestartAttempt(ComponentName sourceComponent) {
mHandler.removeMessages(NOTIFY_PINNED_ACTIVITY_RESTART_ATTEMPT_LISTENERS_MSG);
final Message msg =
mHandler.obtainMessage(NOTIFY_PINNED_ACTIVITY_RESTART_ATTEMPT_LISTENERS_MSG);
mHandler.obtainMessage(NOTIFY_PINNED_ACTIVITY_RESTART_ATTEMPT_LISTENERS_MSG,
sourceComponent);
forAllLocalListeners(mNotifyPinnedActivityRestartAttempt, msg);
msg.sendToTarget();
}