Adding support for PIP actions.

- Introduced generic RemoteAction to represents an action
  that can be made across processes with an icon and text
  description based on a Notification action.
- Modified PinnedStackController to ensure that it notifies
  the listeners from the source of truth, this ensures that
  SysUI is in the right state if killed and re-registers
  itself.

Test: Enable menu & minimize in SystemUI tuner.
Test: android.server.cts.ActivityManagerPinnedStackTests
Test: #testNumPipActions

Change-Id: I5b5d0cf9de3f06b5687337d59cfb91e17355bdb1
Signed-off-by: Winson Chung <winsonc@google.com>
This commit is contained in:
Winson Chung
2016-12-14 12:01:27 -08:00
parent 7198ca863e
commit a29eb98d9f
24 changed files with 852 additions and 148 deletions

View File

@@ -45,6 +45,7 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ParceledListSlice;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
@@ -2019,7 +2020,29 @@ public class Activity extends ContextThemeWrapper
}
/**
* Updates the aspect ratio of the current picture-in-picture activity.
* Requests to the system that the activity can be automatically put into picture-in-picture
* mode when the user leaves the activity causing it normally to be hidden. Generally, this
* happens when another task is brought to the forground or the task containing this activity
* is moved to the background. This is a *not* a guarantee that the activity will actually be
* put in picture-in-picture mode, and depends on a number of factors, including whether there
* is already something in picture-in-picture.
*
* @param enterPictureInPictureOnMoveToBg whether or not this activity can automatically enter
* picture-in-picture
*/
public void enterPictureInPictureModeOnMoveToBackground(
boolean enterPictureInPictureOnMoveToBg) {
try {
ActivityManagerNative.getDefault().enterPictureInPictureModeOnMoveToBackground(mToken,
enterPictureInPictureOnMoveToBg);
} catch (RemoteException e) {
}
}
/**
* Updates the aspect ratio of the current picture-in-picture activity if this activity is
* already in picture-in-picture mode, or sets it to be used later if
* {@link #enterPictureInPictureModeOnMoveToBackground(boolean)} is requested.
*
* @param aspectRatio the new aspect ratio of the picture-in-picture.
*/
@@ -2031,23 +2054,19 @@ public class Activity extends ContextThemeWrapper
}
/**
* Requests to the system that the activity can be automatically put into picture-in-picture
* mode when the user leaves the activity causing it normally to be hidden. This is a *not*
* a guarantee that the activity will actually be put in picture-in-picture mode, and depends
* on a number of factors, including whether there is already something in picture-in-picture.
* Updates the set of user actions associated with the picture-in-picture activity.
*
* If {@param enterPictureInPictureOnMoveToBg} is true, then you may also call
* {@link #setPictureInPictureAspectRatio(float)} to specify the aspect ratio to automatically
* enter picture-in-picture with.
*
* @param enterPictureInPictureOnMoveToBg whether or not this activity can automatically enter
* picture-in-picture
* @param actions the new actions for picture-in-picture (can be null to reset the set of
* actions). The maximum number of actions that will be displayed on this device
* is defined by {@link ActivityManager#getMaxNumPictureInPictureActions()}.
*/
public void enterPictureInPictureModeOnMoveToBackground(
boolean enterPictureInPictureOnMoveToBg) {
public void setPictureInPictureActions(List<RemoteAction> actions) {
try {
ActivityManagerNative.getDefault().enterPictureInPictureModeOnMoveToBackground(mToken,
enterPictureInPictureOnMoveToBg);
if (actions == null) {
actions = new ArrayList<>();
}
ActivityManagerNative.getDefault().setPictureInPictureActions(mToken,
new ParceledListSlice<RemoteAction>(actions));
} catch (RemoteException e) {
}
}

View File

@@ -87,8 +87,9 @@ public class ActivityManager {
private static int gMaxRecentTasks = -1;
private static final int NUM_ALLOWED_PIP_ACTIONS = 3;
private final Context mContext;
private final Handler mHandler;
private static volatile boolean sSystemReady = false;
@@ -491,7 +492,6 @@ public class ActivityManager {
/*package*/ ActivityManager(Context context, Handler handler) {
mContext = context;
mHandler = handler;
}
/**
@@ -1011,6 +1011,14 @@ public class ActivityManager {
com.android.internal.R.bool.config_supportsSplitScreenMultiWindow);
}
/**
* Return the maximum number of actions that will be displayed in the picture-in-picture UI when
* the user interacts with the activity currently in picture-in-picture mode.
*/
public static int getMaxNumPictureInPictureActions() {
return NUM_ALLOWED_PIP_ACTIONS;
}
/**
* Information you can set and retrieve about the current activity within the recent task list.
*/

View File

@@ -28,6 +28,8 @@ import android.service.voice.IVoiceInteractionSession;
import com.android.internal.app.IVoiceInteractor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
/**
@@ -61,6 +63,36 @@ public abstract class ActivityManagerInternal {
*/
public static final int APP_TRANSITION_TIMEOUT = 3;
/**
* Class to hold deferred properties to apply for picture-in-picture for a given activity.
*/
public static class PictureInPictureArguments {
/**
* The expected aspect ratio of the picture-in-picture.
*/
public float aspectRatio;
/**
* The set of actions that are associated with this activity when in picture in picture.
*/
public List<RemoteAction> userActions = new ArrayList<>();
public void dump(PrintWriter pw, String prefix) {
pw.println(prefix + "aspectRatio=" + aspectRatio);
if (userActions.isEmpty()) {
pw.println(prefix + " userActions=[]");
} else {
pw.println(prefix + " userActions=[");
for (int i = 0; i < userActions.size(); i++) {
RemoteAction action = userActions.get(i);
pw.print(prefix + " Action[" + i + "]: ");
action.dump("", pw);
}
pw.println(prefix + " ]");
}
}
}
/**
* Grant Uri permissions from one app to another. This method only extends
* permission grants if {@code callingUid} has permission to them.

View File

@@ -474,6 +474,7 @@ interface IActivityManager {
void enterPictureInPictureModeOnMoveToBackground(in IBinder token,
boolean enterPictureInPictureOnMoveToBg);
void setPictureInPictureAspectRatio(in IBinder token, float aspectRatio);
void setPictureInPictureActions(in IBinder token, in ParceledListSlice actions);
void activityRelaunched(in IBinder token);
IBinder getUriPermissionOwnerForActivity(in IBinder activityToken);
/**

View File

@@ -0,0 +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 android.app;
parcelable RemoteAction;

View File

@@ -0,0 +1,159 @@
/*
* 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 android.app;
import android.annotation.NonNull;
import android.graphics.drawable.Icon;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import java.io.PrintWriter;
/**
* Represents a remote action that can be called from another process. The action can have an
* associated visualization including metadata like an icon or title.
*/
public final class RemoteAction implements Parcelable {
/**
* Interface definition for a callback to be invoked when an action is invoked.
*/
public interface OnActionListener {
/**
* Called when the associated action is invoked.
*
* @param action The action that was invoked.
*/
void onAction(RemoteAction action);
}
private static final String TAG = "RemoteAction";
private static final int MESSAGE_ACTION_INVOKED = 1;
private final Icon mIcon;
private final CharSequence mTitle;
private final CharSequence mContentDescription;
private OnActionListener mActionCallback;
private final Messenger mMessenger;
RemoteAction(Parcel in) {
mIcon = Icon.CREATOR.createFromParcel(in);
mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
mMessenger = in.readParcelable(Messenger.class.getClassLoader());
}
public RemoteAction(@NonNull Icon icon, @NonNull CharSequence title,
@NonNull CharSequence contentDescription, @NonNull OnActionListener callback) {
if (icon == null || title == null || contentDescription == null || callback == null) {
throw new IllegalArgumentException("Expected icon, title, content description and " +
"action callback");
}
mIcon = icon;
mTitle = title;
mContentDescription = contentDescription;
mActionCallback = callback;
mMessenger = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_ACTION_INVOKED:
mActionCallback.onAction(RemoteAction.this);
break;
}
}
});
}
/**
* Return an icon representing the action.
*/
public @NonNull Icon getIcon() {
return mIcon;
}
/**
* Return an title representing the action.
*/
public @NonNull CharSequence getTitle() {
return mTitle;
}
/**
* Return a content description representing the action.
*/
public @NonNull CharSequence getContentDescription() {
return mContentDescription;
}
/**
* Sends a message that the action was invoked.
* @hide
*/
public void sendActionInvoked() {
Message m = Message.obtain();
m.what = MESSAGE_ACTION_INVOKED;
try {
mMessenger.send(m);
} catch (RemoteException e) {
Log.e(TAG, "Could not send action-invoked", e);
}
}
@Override
public RemoteAction clone() {
return new RemoteAction(mIcon, mTitle, mContentDescription, mActionCallback);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
mIcon.writeToParcel(out, 0);
TextUtils.writeToParcel(mTitle, out, flags);
TextUtils.writeToParcel(mContentDescription, out, flags);
out.writeParcelable(mMessenger, flags);
}
public void dump(String prefix, PrintWriter pw) {
pw.print(prefix);
pw.print("title=" + mTitle);
pw.print(" contentDescription=" + mContentDescription);
pw.print(" icon=" + mIcon);
pw.println();
}
public static final Parcelable.Creator<RemoteAction> CREATOR =
new Parcelable.Creator<RemoteAction>() {
public RemoteAction createFromParcel(Parcel in) {
return new RemoteAction(in);
}
public RemoteAction[] newArray(int size) {
return new RemoteAction[size];
}
};
}

View File

@@ -16,13 +16,14 @@
package android.view;
import android.content.pm.ParceledListSlice;
import android.view.IPinnedStackController;
/**
* Listener for changes to the pinned stack made by the WindowManager.
*
* @hide
*/
* Listener for changes to the pinned stack made by the WindowManager.
*
* @hide
*/
oneway interface IPinnedStackListener {
/**
@@ -36,4 +37,24 @@ oneway interface IPinnedStackListener {
* is first registered to allow the listener to synchronized its state with the controller.
*/
void onBoundsChanged(boolean adjustedForIme);
/**
* Called when window manager decides to adjust the minimized state, or when the listener
* is first registered to allow the listener to synchronized its state with the controller.
*/
void onMinimizedStateChanged(boolean isMinimized);
/**
* Called when window manager decides to adjust the snap-to-edge state, which determines whether
* to snap only to the corners of the screen or to the closest edge. It is called when the
* listener is first registered to allow the listener to synchronized its state with the
* controller.
*/
void onSnapToEdgeStateChanged(boolean isSnapToEdge);
/**
* Called when the set of actions for the current PiP activity changes, or when the listener
* is first registered to allow the listener to synchronized its state with the controller.
*/
void onActionsChanged(in ParceledListSlice actions);
}