diff options
| author | 2016-12-14 12:01:27 -0800 | |
|---|---|---|
| committer | 2016-12-14 12:15:07 -0800 | |
| commit | a29eb98d9fba99528f0809c448daf2ddae37de7e (patch) | |
| tree | 07a42f846e9a8b86beadd68c017598b776cfdc80 | |
| parent | 7198ca863e57a04319efd376770a912b508d3e4a (diff) | |
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>
24 files changed, 842 insertions, 138 deletions
diff --git a/api/current.txt b/api/current.txt index 67bc1168c7c8..dfe034050176 100644 --- a/api/current.txt +++ b/api/current.txt @@ -3657,6 +3657,7 @@ package android.app { method public void setIntent(android.content.Intent); method public final void setMediaController(android.media.session.MediaController); method public void setOverlayWithDecorCaptionEnabled(boolean); + method public void setPictureInPictureActions(java.util.List<android.app.RemoteAction>); method public void setPictureInPictureAspectRatio(float); method public final deprecated void setProgress(int); method public final deprecated void setProgressBarIndeterminate(boolean); @@ -3737,6 +3738,7 @@ package android.app { method public int getLauncherLargeIconDensity(); method public int getLauncherLargeIconSize(); method public int getLockTaskModeState(); + method public static int getMaxNumPictureInPictureActions(); method public int getMemoryClass(); method public void getMemoryInfo(android.app.ActivityManager.MemoryInfo); method public static void getMyMemoryState(android.app.ActivityManager.RunningAppProcessInfo); @@ -5547,6 +5549,22 @@ package android.app { field public static final int STYLE_SPINNER = 0; // 0x0 } + public final class RemoteAction implements android.os.Parcelable { + ctor public RemoteAction(android.graphics.drawable.Icon, java.lang.CharSequence, java.lang.CharSequence, android.app.RemoteAction.OnActionListener); + method public android.app.RemoteAction clone(); + method public int describeContents(); + method public void dump(java.lang.String, java.io.PrintWriter); + method public java.lang.CharSequence getContentDescription(); + method public android.graphics.drawable.Icon getIcon(); + method public java.lang.CharSequence getTitle(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.RemoteAction> CREATOR; + } + + public static abstract interface RemoteAction.OnActionListener { + method public abstract void onAction(android.app.RemoteAction); + } + public final class RemoteInput implements android.os.Parcelable { method public static void addResultsToIntent(android.app.RemoteInput[], android.content.Intent, android.os.Bundle); method public int describeContents(); diff --git a/api/system-current.txt b/api/system-current.txt index 4f6e921bf06a..076b0ba25682 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -3776,6 +3776,7 @@ package android.app { method public void setIntent(android.content.Intent); method public final void setMediaController(android.media.session.MediaController); method public void setOverlayWithDecorCaptionEnabled(boolean); + method public void setPictureInPictureActions(java.util.List<android.app.RemoteAction>); method public void setPictureInPictureAspectRatio(float); method public final deprecated void setProgress(int); method public final deprecated void setProgressBarIndeterminate(boolean); @@ -3862,6 +3863,7 @@ package android.app { method public int getLauncherLargeIconDensity(); method public int getLauncherLargeIconSize(); method public int getLockTaskModeState(); + method public static int getMaxNumPictureInPictureActions(); method public int getMemoryClass(); method public void getMemoryInfo(android.app.ActivityManager.MemoryInfo); method public static void getMyMemoryState(android.app.ActivityManager.RunningAppProcessInfo); @@ -5714,6 +5716,22 @@ package android.app { field public static final int STYLE_SPINNER = 0; // 0x0 } + public final class RemoteAction implements android.os.Parcelable { + ctor public RemoteAction(android.graphics.drawable.Icon, java.lang.CharSequence, java.lang.CharSequence, android.app.RemoteAction.OnActionListener); + method public android.app.RemoteAction clone(); + method public int describeContents(); + method public void dump(java.lang.String, java.io.PrintWriter); + method public java.lang.CharSequence getContentDescription(); + method public android.graphics.drawable.Icon getIcon(); + method public java.lang.CharSequence getTitle(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.RemoteAction> CREATOR; + } + + public static abstract interface RemoteAction.OnActionListener { + method public abstract void onAction(android.app.RemoteAction); + } + public final class RemoteInput implements android.os.Parcelable { method public static void addResultsToIntent(android.app.RemoteInput[], android.content.Intent, android.os.Bundle); method public int describeContents(); diff --git a/api/test-current.txt b/api/test-current.txt index c8c07902c18b..52903069aea8 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -3659,6 +3659,7 @@ package android.app { method public void setIntent(android.content.Intent); method public final void setMediaController(android.media.session.MediaController); method public void setOverlayWithDecorCaptionEnabled(boolean); + method public void setPictureInPictureActions(java.util.List<android.app.RemoteAction>); method public void setPictureInPictureAspectRatio(float); method public final deprecated void setProgress(int); method public final deprecated void setProgressBarIndeterminate(boolean); @@ -3740,6 +3741,7 @@ package android.app { method public int getLauncherLargeIconDensity(); method public int getLauncherLargeIconSize(); method public int getLockTaskModeState(); + method public static int getMaxNumPictureInPictureActions(); method public int getMemoryClass(); method public void getMemoryInfo(android.app.ActivityManager.MemoryInfo); method public static void getMyMemoryState(android.app.ActivityManager.RunningAppProcessInfo); @@ -5558,6 +5560,22 @@ package android.app { field public static final int STYLE_SPINNER = 0; // 0x0 } + public final class RemoteAction implements android.os.Parcelable { + ctor public RemoteAction(android.graphics.drawable.Icon, java.lang.CharSequence, java.lang.CharSequence, android.app.RemoteAction.OnActionListener); + method public android.app.RemoteAction clone(); + method public int describeContents(); + method public void dump(java.lang.String, java.io.PrintWriter); + method public java.lang.CharSequence getContentDescription(); + method public android.graphics.drawable.Icon getIcon(); + method public java.lang.CharSequence getTitle(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.RemoteAction> CREATOR; + } + + public static abstract interface RemoteAction.OnActionListener { + method public abstract void onAction(android.app.RemoteAction); + } + public final class RemoteInput implements android.os.Parcelable { method public static void addResultsToIntent(android.app.RemoteInput[], android.content.Intent, android.os.Bundle); method public int describeContents(); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 2cdda3d06c0e..2ccfe0e3b72d 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -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. - * - * 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. + * Updates the set of user actions associated with the picture-in-picture activity. * - * @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) { } } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index f04455b7306d..761da35f3607 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -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; } /** @@ -1012,6 +1012,14 @@ public class ActivityManager { } /** + * 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. */ public static class TaskDescription implements Parcelable { diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 913edfd81081..87700dc7aa35 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -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; /** @@ -62,6 +64,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. */ diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index a10fffe063cb..fcc6e3d1e913 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -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); /** diff --git a/core/java/android/app/RemoteAction.aidl b/core/java/android/app/RemoteAction.aidl new file mode 100644 index 000000000000..2f9f1cce7389 --- /dev/null +++ b/core/java/android/app/RemoteAction.aidl @@ -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; diff --git a/core/java/android/app/RemoteAction.java b/core/java/android/app/RemoteAction.java new file mode 100644 index 000000000000..a37680f7a8f8 --- /dev/null +++ b/core/java/android/app/RemoteAction.java @@ -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]; + } + }; +}
\ No newline at end of file diff --git a/core/java/android/view/IPinnedStackListener.aidl b/core/java/android/view/IPinnedStackListener.aidl index 3050dbba062f..3c348c50369f 100644 --- a/core/java/android/view/IPinnedStackListener.aidl +++ b/core/java/android/view/IPinnedStackListener.aidl @@ -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); } diff --git a/packages/SystemUI/res/layout/pip_menu_action.xml b/packages/SystemUI/res/layout/pip_menu_action.xml new file mode 100644 index 000000000000..db6ae19a9c34 --- /dev/null +++ b/packages/SystemUI/res/layout/pip_menu_action.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="?android:selectableItemBackground"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:orientation="vertical"> + <ImageView + android:id="@+id/icon" + android:layout_width="@dimen/pip_menu_action_icon_size" + android:layout_height="@dimen/pip_menu_action_icon_size" + android:layout_gravity="center_horizontal" + android:layout_marginBottom="4dp" /> + <TextView + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:includeFontPadding="false" + android:gravity="center" + android:textSize="12sp" + android:textColor="#ffffffff" + android:textAllCaps="true" + android:fontFamily="sans-serif" /> + </LinearLayout> +</FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/pip_menu_activity.xml b/packages/SystemUI/res/layout/pip_menu_activity.xml index 88e6e725c976..054bfabb2f84 100644 --- a/packages/SystemUI/res/layout/pip_menu_activity.xml +++ b/packages/SystemUI/res/layout/pip_menu_activity.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2014 The Android Open Source Project +<!-- 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. @@ -16,21 +16,44 @@ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#33000000"> + <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center"> - <Button - android:id="@+id/expand_pip" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:textSize="14sp" + android:id="@+id/actions" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="48dp"> + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="48dp" + android:layout_gravity="bottom" + android:orientation="horizontal"> + <TextView + android:id="@+id/dismiss" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center" + android:textSize="12sp" + android:textColor="#ffffffff" + android:fontFamily="sans-serif" + android:text="@string/pip_phone_dismiss" + android:background="?android:selectableItemBackground" /> + <TextView + android:id="@+id/minimize" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center" + android:textSize="12sp" android:textColor="#ffffffff" - android:text="@string/pip_phone_expand" - android:fontFamily="sans-serif" /> + android:fontFamily="sans-serif" + android:text="@string/pip_phone_minimize" + android:background="?android:selectableItemBackground" /> </LinearLayout> </FrameLayout> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index a7d4aa094cdd..fe5606e408f2 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -717,6 +717,9 @@ <!-- The size of the PIP dismiss target. --> <dimen name="pip_dismiss_target_size">48dp</dimen> + <!-- The size of a PIP menu action icon. --> + <dimen name="pip_menu_action_icon_size">32dp</dimen> + <!-- Values specific to grid-based recents. --> <!-- Margins around recent tasks. --> <dimen name="recents_grid_margin_left">15dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c8b3b69dce37..6af2fc624aac 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1688,9 +1688,15 @@ not appear on production builds ever. --> <string name="tuner_doze_always_on" translatable="false">Always on</string> - <!-- Making the PIP fullscreen --> + <!-- Making the PIP fullscreen [CHAR LIMIT=25] --> <string name="pip_phone_expand">Expand</string> + <!-- Label for PIP action to Minimize the PIP [CHAR LIMIT=25] --> + <string name="pip_phone_minimize">Minimize</string> + + <!-- Label for PIP action to Dismiss the PIP --> + <string name="pip_phone_dismiss">Dismiss</string> + <!-- PIP section of the tuner. Non-translatable since it should not appear on production builds ever. --> <string name="picture_in_picture" translatable="false">Picture-in-Picture</string> diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java index 43cfa3249a90..7e275d81031d 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java @@ -16,9 +16,17 @@ package com.android.systemui.pip.phone; +import static android.view.Display.DEFAULT_DISPLAY; + import android.app.ActivityManager; import android.app.IActivityManager; import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Log; +import android.view.IPinnedStackController; +import android.view.IPinnedStackListener; import android.view.IWindowManager; import android.view.WindowManagerGlobal; @@ -33,10 +41,52 @@ public class PipManager { private Context mContext; private IActivityManager mActivityManager; private IWindowManager mWindowManager; + private Handler mHandler = new Handler(); + + private final PinnedStackListener mPinnedStackListener = new PinnedStackListener(); private PipMenuActivityController mMenuController; private PipTouchHandler mTouchHandler; + /** + * Handler for messages from the PIP controller. + */ + private class PinnedStackListener extends IPinnedStackListener.Stub { + + @Override + public void onListenerRegistered(IPinnedStackController controller) { + mHandler.post(() -> { + mTouchHandler.setPinnedStackController(controller); + }); + } + + @Override + public void onBoundsChanged(boolean adjustedForIme) { + // Do nothing + } + + @Override + public void onActionsChanged(ParceledListSlice actions) { + mHandler.post(() -> { + mMenuController.setActions(actions); + }); + } + + @Override + public void onMinimizedStateChanged(boolean isMinimized) { + mHandler.post(() -> { + mTouchHandler.onMinimizedStateChanged(isMinimized); + }); + } + + @Override + public void onSnapToEdgeStateChanged(boolean isSnapToEdge) { + mHandler.post(() -> { + mTouchHandler.onSnapToEdgeStateChanged(isSnapToEdge); + }); + } + } + private PipManager() {} /** @@ -47,6 +97,12 @@ public class PipManager { mActivityManager = ActivityManager.getService(); mWindowManager = WindowManagerGlobal.getWindowManagerService(); + try { + mWindowManager.registerPinnedStackListener(DEFAULT_DISPLAY, mPinnedStackListener); + } catch (RemoteException e) { + Log.e(TAG, "Failed to register pinned stack listener", e); + } + mMenuController = new PipMenuActivityController(context, mActivityManager, mWindowManager); mTouchHandler = new PipTouchHandler(context, mMenuController, mActivityManager, mWindowManager); diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java index bfe5cff90a24..fe8ee6f6b27f 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java @@ -19,16 +19,27 @@ package com.android.systemui.pip.phone; import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityManager; +import android.app.PendingIntent.CanceledException; +import android.app.RemoteAction; import android.content.Intent; +import android.content.pm.ParceledListSlice; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + import com.android.systemui.R; +import java.util.ArrayList; +import java.util.List; + /** * Translucent activity that gets started on top of a task in PIP to allow the user to control it. */ @@ -36,16 +47,25 @@ public class PipMenuActivity extends Activity { private static final String TAG = "PipMenuActivity"; - public static final int MESSAGE_FINISH_SELF = 2; + public static final int MESSAGE_FINISH_SELF = 1; + public static final int MESSAGE_UPDATE_ACTIONS = 2; private static final long INITIAL_DISMISS_DELAY = 2000; private static final long POST_INTERACTION_DISMISS_DELAY = 1500; + private List<RemoteAction> mActions = new ArrayList<>(); + private View mDismissButton; + private View mMinimizeButton; + + private Handler mHandler = new Handler(); private Messenger mToControllerMessenger; private Messenger mMessenger = new Messenger(new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { + case MESSAGE_UPDATE_ACTIONS: + setActions(((ParceledListSlice) msg.obj).getList()); + break; case MESSAGE_FINISH_SELF: finish(); break; @@ -63,15 +83,27 @@ public class PipMenuActivity extends Activity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setContentView(R.layout.pip_menu_activity); - Intent startingIntet = getIntent(); - mToControllerMessenger = startingIntet.getParcelableExtra( + Intent startingIntent = getIntent(); + mToControllerMessenger = startingIntent.getParcelableExtra( PipMenuActivityController.EXTRA_CONTROLLER_MESSENGER); + ParceledListSlice actions = startingIntent.getParcelableExtra( + PipMenuActivityController.EXTRA_ACTIONS); + if (actions != null) { + setActions(actions.getList()); + } - setContentView(R.layout.pip_menu_activity); - findViewById(R.id.expand_pip).setOnClickListener((view) -> { - finish(); - notifyExpandPip(); + findViewById(R.id.menu).setOnClickListener((v) -> { + expandPip(); + }); + mDismissButton = findViewById(R.id.dismiss); + mDismissButton.setOnClickListener((v) -> { + dismissPip(); + }); + mMinimizeButton = findViewById(R.id.minimize); + mMinimizeButton.setOnClickListener((v) -> { + minimizePip(); }); } @@ -107,25 +139,74 @@ public class PipMenuActivity extends Activity { // Do nothing } + private void setActions(List<RemoteAction> actions) { + mActions.clear(); + mActions.addAll(actions); + updateActionViews(); + } + + private void updateActionViews() { + ViewGroup actionsContainer = (ViewGroup) findViewById(R.id.actions); + if (actionsContainer != null) { + actionsContainer.removeAllViews(); + + // Recreate the layout + final LayoutInflater inflater = LayoutInflater.from(this); + for (int i = 0; i < mActions.size(); i++) { + final RemoteAction action = mActions.get(i); + final ViewGroup actionContainer = (ViewGroup) inflater.inflate( + R.layout.pip_menu_action, actionsContainer, false); + actionContainer.setOnClickListener((v) -> { + action.sendActionInvoked(); + }); + + final TextView title = (TextView) actionContainer.findViewById(R.id.title); + title.setText(action.getTitle()); + title.setContentDescription(action.getContentDescription()); + + final ImageView icon = (ImageView) actionContainer.findViewById(R.id.icon); + action.getIcon().loadDrawableAsync(this, (d) -> { + icon.setImageDrawable(d); + }, mHandler); + actionsContainer.addView(actionContainer); + } + } + } + private void notifyActivityVisibility(boolean visible) { Message m = Message.obtain(); m.what = PipMenuActivityController.MESSAGE_ACTIVITY_VISIBILITY_CHANGED; m.arg1 = visible ? 1 : 0; m.replyTo = visible ? mMessenger : null; - try { - mToControllerMessenger.send(m); - } catch (RemoteException e) { - Log.e(TAG, "Could not notify controller of PIP menu visibility", e); - } + sendMessage(m, "Could not notify controller of PIP menu visibility"); + } + + private void expandPip() { + sendEmptyMessage(PipMenuActivityController.MESSAGE_EXPAND_PIP, + "Could not notify controller to expand PIP"); + } + + private void minimizePip() { + sendEmptyMessage(PipMenuActivityController.MESSAGE_MINIMIZE_PIP, + "Could not notify controller to minimize PIP"); } - private void notifyExpandPip() { + private void dismissPip() { + sendEmptyMessage(PipMenuActivityController.MESSAGE_DISMISS_PIP, + "Could not notify controller to dismiss PIP"); + } + + private void sendEmptyMessage(int what, String errorMsg) { Message m = Message.obtain(); - m.what = PipMenuActivityController.MESSAGE_EXPAND_PIP; + m.what = what; + sendMessage(m, errorMsg); + } + + private void sendMessage(Message m, String errorMsg) { try { mToControllerMessenger.send(m); } catch (RemoteException e) { - Log.e(TAG, "Could not notify controller to expand PIP", e); + Log.e(TAG, errorMsg, e); } } diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java index d1bce0c74ccd..64e2d1a7ff07 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java @@ -1,14 +1,13 @@ package com.android.systemui.pip.phone; import static android.app.ActivityManager.StackId.PINNED_STACK_ID; -import static android.view.WindowManager.INPUT_CONSUMER_PIP; import android.app.ActivityManager.StackInfo; import android.app.ActivityOptions; import android.app.IActivityManager; import android.content.Context; import android.content.Intent; -import android.graphics.Rect; +import android.content.pm.ParceledListSlice; import android.os.Handler; import android.os.Message; import android.os.Messenger; @@ -24,8 +23,12 @@ public class PipMenuActivityController { private static final String TAG = "PipMenuActivityController"; public static final String EXTRA_CONTROLLER_MESSENGER = "messenger"; - public static final int MESSAGE_ACTIVITY_VISIBILITY_CHANGED = 1; - public static final int MESSAGE_EXPAND_PIP = 3; + public static final String EXTRA_ACTIONS = "actions"; + + public static final int MESSAGE_ACTIVITY_VISIBILITY_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; /** * A listener interface to receive notification on changes in PIP. @@ -35,12 +38,29 @@ public class PipMenuActivityController { * Called when the PIP menu visibility changes. */ void onPipMenuVisibilityChanged(boolean visible); + + /** + * 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 expanded. + */ + void onPipDismiss(); } private Context mContext; private IActivityManager mActivityManager; private IWindowManager mWindowManager; + private ArrayList<Listener> mListeners = new ArrayList<>(); + private ParceledListSlice mActions; private Messenger mToActivityMessenger; private Messenger mMessenger = new Messenger(new Handler() { @@ -57,10 +77,23 @@ public class PipMenuActivityController { break; } case MESSAGE_EXPAND_PIP: { - try { - mActivityManager.resizeStack(PINNED_STACK_ID, null, true, true, true, 225); - } catch (RemoteException e) { - Log.e(TAG, "Error showing PIP menu activity", e); + int listenerCount = mListeners.size(); + for (int i = 0; i < listenerCount; i++) { + mListeners.get(i).onPipExpand(); + } + break; + } + case MESSAGE_MINIMIZE_PIP: { + int listenerCount = mListeners.size(); + for (int i = 0; i < listenerCount; i++) { + mListeners.get(i).onPipMinimize(); + } + break; + } + case MESSAGE_DISMISS_PIP: { + int listenerCount = mListeners.size(); + for (int i = 0; i < listenerCount; i++) { + mListeners.get(i).onPipDismiss(); } break; } @@ -95,6 +128,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); ActivityOptions options = ActivityOptions.makeBasic(); options.setLaunchTaskId( pinnedStackInfo.taskIds[pinnedStackInfo.taskIds.length - 1]); @@ -123,4 +157,22 @@ public class PipMenuActivityController { mToActivityMessenger = null; } } + + /** + * Sets the {@param actions} associated with the PiP. + */ + public void setActions(ParceledListSlice actions) { + mActions = actions; + + if (mToActivityMessenger != null) { + Message m = Message.obtain(); + m.what = PipMenuActivity.MESSAGE_UPDATE_ACTIONS; + m.obj = actions; + try { + mToActivityMessenger.send(m); + } catch (RemoteException e) { + Log.e(TAG, "Could not notify menu activity to update actions", e); + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java index 09671e7ac0e6..ff3cc7981bd9 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -17,7 +17,6 @@ package com.android.systemui.pip.phone; import static android.app.ActivityManager.StackId.PINNED_STACK_ID; -import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.INPUT_CONSUMER_PIP; import static com.android.systemui.Interpolators.FAST_OUT_LINEAR_IN; @@ -38,7 +37,6 @@ import android.os.Looper; import android.os.RemoteException; import android.util.Log; import android.view.IPinnedStackController; -import android.view.IPinnedStackListener; import android.view.IWindowManager; import android.view.InputChannel; import android.view.InputEvent; @@ -80,7 +78,6 @@ public class PipTouchHandler implements TunerService.Tunable { private final IActivityManager mActivityManager; private final IWindowManager mWindowManager; private final ViewConfiguration mViewConfig; - private final PinnedStackListener mPinnedStackListener = new PinnedStackListener(); private final PipMenuListener mMenuListener = new PipMenuListener(); private IPinnedStackController mPinnedStackController; @@ -149,26 +146,6 @@ public class PipTouchHandler implements TunerService.Tunable { } /** - * Handler for messages from the PIP controller. - */ - private class PinnedStackListener extends IPinnedStackListener.Stub { - - @Override - public void onListenerRegistered(IPinnedStackController controller) { - mPinnedStackController = controller; - - // Update the controller with the current tuner state - setMinimizedState(mIsMinimized); - setSnapToEdge(mEnableSnapToEdge); - } - - @Override - public void onBoundsChanged(boolean adjustedForIme) { - // Do nothing - } - } - - /** * A listener for the PIP menu activity. */ private class PipMenuListener implements PipMenuActivityController.Listener { @@ -181,17 +158,30 @@ public class PipTouchHandler implements TunerService.Tunable { unregisterInputConsumer(); } } + + @Override + public void onPipExpand() { + if (!mIsMinimized) { + expandPinnedStackToFullscreen(); + } + } + + @Override + public void onPipMinimize() { + setMinimizedState(true); + animateToClosestMinimizedTarget(); + } + + @Override + public void onPipDismiss() { + animateDismissPinnedStack(mPinnedStackBounds); + } } public PipTouchHandler(Context context, PipMenuActivityController menuController, IActivityManager activityManager, IWindowManager windowManager) { // Initialize the Pip input consumer - try { - windowManager.registerPinnedStackListener(DEFAULT_DISPLAY, mPinnedStackListener); - } catch (RemoteException e) { - Log.e(TAG, "Failed to create PIP input consumer", e); - } mContext = context; mActivityManager = activityManager; mWindowManager = windowManager; @@ -255,6 +245,14 @@ public class PipTouchHandler implements TunerService.Tunable { updateBoundedPinnedStackBounds(false /* updatePinnedStackBounds */); } + public void onMinimizedStateChanged(boolean isMinimized) { + mIsMinimized = isMinimized; + } + + public void onSnapToEdgeStateChanged(boolean isSnapToEdge) { + mSnapAlgorithm.setSnapToEdge(isSnapToEdge); + } + private boolean handleTouchEvent(MotionEvent ev) { // Skip touch handling until we are bound to the controller if (mPinnedStackController == null) { @@ -353,10 +351,17 @@ public class PipTouchHandler implements TunerService.Tunable { } /** - * Sets the snap-to-edge state. + * Sets the controller to update the system of changes from user interaction. + */ + void setPinnedStackController(IPinnedStackController controller) { + mPinnedStackController = controller; + } + + /** + * Sets the snap-to-edge state and notifies the controller. */ private void setSnapToEdge(boolean snapToEdge) { - mSnapAlgorithm.setSnapToEdge(snapToEdge); + onSnapToEdgeStateChanged(snapToEdge); if (mPinnedStackController != null) { try { @@ -371,7 +376,7 @@ public class PipTouchHandler implements TunerService.Tunable { * Sets the minimized state and notifies the controller. */ private void setMinimizedState(boolean isMinimized) { - mIsMinimized = isMinimized; + onMinimizedStateChanged(isMinimized); if (mPinnedStackController != null) { try { @@ -432,6 +437,12 @@ public class PipTouchHandler implements TunerService.Tunable { mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds, toBounds, MINIMIZE_STACK_MAX_DURATION, LINEAR_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener); + mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mMenuController.hideMenu(); + } + }); mPinnedStackBoundsAnimator.start(); } @@ -759,7 +770,8 @@ public class PipTouchHandler implements TunerService.Tunable { @Override public boolean onUp(PipTouchState touchState) { - if (mEnableTapThrough && !touchState.isDragging() && !mIsTappingThrough) { + if (mEnableTapThrough && !touchState.isDragging() && !mIsMinimized && + !mIsTappingThrough) { mMenuController.showMenu(); mIsTappingThrough = true; return true; diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java index 17d9864e4d25..2e84ceda8ae0 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java @@ -16,11 +16,7 @@ package com.android.systemui.pip.phone; -import android.app.IActivityManager; import android.graphics.PointF; -import android.view.IPinnedStackController; -import android.view.IPinnedStackListener; -import android.view.IWindowManager; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 4ea73e7399a3..1b68b24354b6 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -17,9 +17,11 @@ package com.android.server.am; import android.annotation.Nullable; +import android.app.ActivityManagerInternal.PictureInPictureArguments; import android.app.ApplicationThreadConstants; import android.app.ContentProviderHolder; import android.app.IActivityManager; +import android.app.RemoteAction; import android.app.WaitResult; import android.os.IDeviceIdentifiersPolicyService; @@ -7514,6 +7516,23 @@ public class ActivityManagerService extends IActivityManager.Stub enterPictureInPictureMode(token, DEFAULT_DISPLAY, aspectRatio, true /* checkAspectRatio */); } + @Override + public void enterPictureInPictureModeOnMoveToBackground(IBinder token, + boolean enterPictureInPictureOnMoveToBg) { + final long origId = Binder.clearCallingIdentity(); + try { + synchronized(this) { + final ActivityRecord r = ensureValidPictureInPictureActivityLocked( + "enterPictureInPictureModeOnMoveToBackground", token, -1f /* aspectRatio */, + false /* checkAspectRatio */, false /* checkActivityVisibility */); + + r.supportsPipOnMoveToBackground = enterPictureInPictureOnMoveToBg; + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + private void enterPictureInPictureMode(IBinder token, int displayId, float aspectRatio, boolean checkAspectRatio) { final long origId = Binder.clearCallingIdentity(); @@ -7523,7 +7542,8 @@ public class ActivityManagerService extends IActivityManager.Stub "enterPictureInPictureMode", token, aspectRatio, checkAspectRatio, true /* checkActivityVisibility */); - enterPictureInPictureModeLocked(r, displayId, aspectRatio, + r.pictureInPictureArgs.aspectRatio = aspectRatio; + enterPictureInPictureModeLocked(r, displayId, r.pictureInPictureArgs, true /* moveHomeStackToFront */, "enterPictureInPictureMode"); } } finally { @@ -7531,25 +7551,29 @@ public class ActivityManagerService extends IActivityManager.Stub } } - void enterPictureInPictureModeLocked(ActivityRecord r, int displayId, float aspectRatio, - boolean moveHomeStackToFront, String reason) { - final Rect bounds = isValidPictureInPictureAspectRatio(aspectRatio) - ? mWindowManager.getPictureInPictureBounds(displayId, aspectRatio) + void enterPictureInPictureModeLocked(ActivityRecord r, int displayId, + PictureInPictureArguments pipArgs, boolean moveHomeStackToFront, String reason) { + final Rect bounds = isValidPictureInPictureAspectRatio(pipArgs.aspectRatio) + ? mWindowManager.getPictureInPictureBounds(displayId, pipArgs.aspectRatio) : mWindowManager.getPictureInPictureDefaultBounds(displayId); mStackSupervisor.moveActivityToPinnedStackLocked(r, reason, bounds, moveHomeStackToFront); + mWindowManager.setPictureInPictureActions(pipArgs.userActions); } @Override - public void enterPictureInPictureModeOnMoveToBackground(IBinder token, - boolean enterPictureInPictureOnMoveToBg) { + public void setPictureInPictureAspectRatio(IBinder token, float aspectRatio) { final long origId = Binder.clearCallingIdentity(); try { synchronized(this) { final ActivityRecord r = ensureValidPictureInPictureActivityLocked( - "requestAutoEnterPictureInPicture", token, -1f /* aspectRatio */, - false /* checkAspectRatio */, false /* checkActivityVisibility */); + "setPictureInPictureAspectRatio", token, aspectRatio, + true /* checkAspectRatio */, false /* checkActivityVisibility */); - r.supportsPipOnMoveToBackground = enterPictureInPictureOnMoveToBg; + r.pictureInPictureArgs.aspectRatio = aspectRatio; + if (r.getStack().getStackId() == PINNED_STACK_ID) { + // If the activity is already in picture-in-picture, update the pinned stack now + mWindowManager.setPictureInPictureAspectRatio(aspectRatio); + } } } finally { Binder.restoreCallingIdentity(origId); @@ -7557,19 +7581,27 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void setPictureInPictureAspectRatio(IBinder token, float aspectRatio) { + public void setPictureInPictureActions(IBinder token, ParceledListSlice actionsList) { final long origId = Binder.clearCallingIdentity(); try { synchronized(this) { final ActivityRecord r = ensureValidPictureInPictureActivityLocked( - "setPictureInPictureAspectRatio", token, aspectRatio, - true /* checkAspectRatio */, false /* checkActivityVisibility */); + "setPictureInPictureActions", token, -1 /* aspectRatio */, + false /* checkAspectRatio */, false /* checkActivityVisibility */); + + final List<RemoteAction> actions = actionsList.getList(); + if (actions.size() > ActivityManager.getMaxNumPictureInPictureActions()) { + throw new IllegalArgumentException("setPictureInPictureActions: Invalid number" + + " of picture-in-picture actions. Only a maximum of " + + ActivityManager.getMaxNumPictureInPictureActions() + + " actions allowed"); + } + r.pictureInPictureArgs.userActions = actions; if (r.getStack().getStackId() == PINNED_STACK_ID) { // If the activity is already in picture-in-picture, update the pinned stack now - mWindowManager.setPictureInPictureAspectRatio(aspectRatio); + mWindowManager.setPictureInPictureActions(actions); } - r.pictureInPictureAspectRatio = aspectRatio; } } finally { Binder.restoreCallingIdentity(origId); @@ -7605,7 +7637,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (!r.canEnterPictureInPicture(checkActivityVisibility)) { throw new IllegalArgumentException(caller - + "Current activity does not support picture-in-picture or is not " + + ": Current activity does not support picture-in-picture or is not " + "visible r=" + r); } diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java index 7dd2e84c90d6..ef197002f0a7 100644 --- a/services/core/java/com/android/server/am/ActivityRecord.java +++ b/services/core/java/com/android/server/am/ActivityRecord.java @@ -54,6 +54,7 @@ import static com.android.server.am.TaskRecord.INVALID_TASK_ID; import android.annotation.NonNull; import android.app.ActivityManager.TaskDescription; +import android.app.ActivityManagerInternal.PictureInPictureArguments; import android.app.ActivityOptions; import android.app.PendingIntent; import android.app.ResultInfo; @@ -217,13 +218,13 @@ final class ActivityRecord { boolean immersive; // immersive mode (don't interrupt if possible) boolean forceNewConfig; // force re-create with new config next time boolean supportsPipOnMoveToBackground; // Supports automatically entering picture-in-picture - // when this activity is hidden. This flag is requested by the activity. + // when this activity is hidden. This flag is requested by the activity. private boolean enterPipOnMoveToBackground; // Flag to enter picture in picture when this - // activity is made invisible. This flag is set specifically when another task is being - // launched or moved to the front which may cause this activity to try and enter PiP - // when it is next made invisible. - float pictureInPictureAspectRatio; // The aspect ratio to use when auto-entering - // picture-in-picture + // activity is made invisible. This flag is set specifically when another task is being + // launched or moved to the front which may cause this activity to try and enter PiP + // when it is next made invisible. + PictureInPictureArguments pictureInPictureArgs = new PictureInPictureArguments(); // The PiP + // arguments used when deferring the entering of picture-in-picture. int launchCount; // count of launches since last state long lastLaunchTime; // time of last launch of this activity ComponentName requestedVrComponent; // the requested component for handling VR mode. @@ -441,9 +442,10 @@ final class ActivityRecord { pw.println(prefix + "resizeMode=" + ActivityInfo.resizeModeToString(info.resizeMode)); } if (supportsPipOnMoveToBackground) { - pw.println(prefix + "supportsPipOnMoveToBackground=1 " - + "enterPipOnMoveToBackground=" - + (enterPipOnMoveToBackground ? 1 : 0)); + pw.println(prefix + "supportsPipOnMoveToBackground=1"); + pw.println(prefix + "enterPipOnMoveToBackground=" + + (enterPipOnMoveToBackground ? 1 : 0)); + pictureInPictureArgs.dump(pw, prefix); } } diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java index 671c84e57968..5bdae574421b 100644 --- a/services/core/java/com/android/server/am/ActivityStack.java +++ b/services/core/java/com/android/server/am/ActivityStack.java @@ -1895,7 +1895,7 @@ final class ActivityStack extends ConfigurationContainer { // since it will affect the focused stack's visibility and occlude // starting activities mService.enterPictureInPictureModeLocked(r, r.getDisplayId(), - r.pictureInPictureAspectRatio, false /* moveHomeStackToFront */, + r.pictureInPictureArgs, false /* moveHomeStackToFront */, "ensureActivitiesVisibleLocked"); return true; } diff --git a/services/core/java/com/android/server/wm/PinnedStackController.java b/services/core/java/com/android/server/wm/PinnedStackController.java index 0ad4e0a64791..34633c243d65 100644 --- a/services/core/java/com/android/server/wm/PinnedStackController.java +++ b/services/core/java/com/android/server/wm/PinnedStackController.java @@ -18,12 +18,13 @@ package com.android.server.wm; import static android.app.ActivityManager.StackId.PINNED_STACK_ID; import static android.util.TypedValue.COMPLEX_UNIT_DIP; -import static android.view.Display.DEFAULT_DISPLAY; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.animation.ValueAnimator; +import android.app.RemoteAction; +import android.content.pm.ParceledListSlice; import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; @@ -41,12 +42,13 @@ import android.view.Gravity; import android.view.IPinnedStackController; import android.view.IPinnedStackListener; -import com.android.internal.os.BackgroundThread; import com.android.internal.policy.PipMotionHelper; import com.android.internal.policy.PipSnapAlgorithm; import com.android.server.UiThread; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; /** * Holds the common state of the pinned stack between the system and SystemUI. @@ -70,10 +72,14 @@ class PinnedStackController { // States that affect how the PIP can be manipulated private boolean mInInteractiveMode; private boolean mIsMinimized; + private boolean mIsSnappingToEdge; private boolean mIsImeShowing; private int mImeHeight; private ValueAnimator mBoundsAnimator = null; + // The set of actions that are currently allowed on the PiP activity + private ArrayList<RemoteAction> mActions = new ArrayList<>(); + // Used to calculate stack bounds across rotations private final DisplayInfo mDisplayInfo = new DisplayInfo(); @@ -113,6 +119,7 @@ class PinnedStackController { @Override public void setSnapToEdge(final boolean snapToEdge) { mHandler.post(() -> { + mIsSnappingToEdge = snapToEdge; mSnapAlgorithm.setSnapToEdge(snapToEdge); }); } @@ -171,6 +178,9 @@ class PinnedStackController { listener.onListenerRegistered(mCallbacks); mPinnedStackListener = listener; notifyBoundsChanged(mIsImeShowing); + notifyMinimizeChanged(mIsMinimized); + notifySnapToEdgeChanged(mIsSnappingToEdge); + notifyActionsChanged(mActions); } catch (RemoteException e) { Log.e(TAG, "Failed to register pinned stack listener", e); } @@ -315,7 +325,16 @@ class PinnedStackController { } /** - * Sends a broadcast that the PIP movement bounds have changed. + * Sets the current set of actions. + */ + void setActions(List<RemoteAction> actions) { + mActions.clear(); + mActions.addAll(actions); + notifyActionsChanged(mActions); + } + + /** + * Notifies listeners that the PIP movement bounds have changed. */ private void notifyBoundsChanged(boolean adjustedForIme) { if (mPinnedStackListener != null) { @@ -328,6 +347,45 @@ class PinnedStackController { } /** + * Notifies listeners that the PIP minimized state has changed. + */ + private void notifyMinimizeChanged(boolean isMinimized) { + if (mPinnedStackListener != null) { + try { + mPinnedStackListener.onMinimizedStateChanged(isMinimized); + } catch (RemoteException e) { + Slog.e(TAG_WM, "Error delivering minimize changed event.", e); + } + } + } + + /** + * Notifies listeners that the PIP snap-to-edge state has changed. + */ + private void notifySnapToEdgeChanged(boolean isSnappingToEdge) { + if (mPinnedStackListener != null) { + try { + mPinnedStackListener.onSnapToEdgeStateChanged(isSnappingToEdge); + } catch (RemoteException e) { + Slog.e(TAG_WM, "Error delivering snap-to-edge changed event.", e); + } + } + } + + /** + * Notifies listeners that the PIP actions have changed. + */ + private void notifyActionsChanged(List<RemoteAction> actions) { + if (mPinnedStackListener != null) { + try { + mPinnedStackListener.onActionsChanged(new ParceledListSlice(actions)); + } catch (RemoteException e) { + Slog.e(TAG_WM, "Error delivering actions changed event.", e); + } + } + } + + /** * @return the bounds on the screen that the PIP can be visible in. */ private void getInsetBounds(Rect outRect) { @@ -355,5 +413,16 @@ class PinnedStackController { pw.println(prefix + " mIsImeShowing=" + mIsImeShowing); pw.println(prefix + " mInInteractiveMode=" + mInInteractiveMode); pw.println(prefix + " mIsMinimized=" + mIsMinimized); + if (mActions.isEmpty()) { + pw.println(prefix + " mActions=[]"); + } else { + pw.println(prefix + " mActions=["); + for (int i = 0; i < mActions.size(); i++) { + RemoteAction action = mActions.get(i); + pw.print(prefix + " Action[" + i + "]: "); + action.dump("", pw); + } + pw.println(prefix + " ]"); + } } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 158fd2bfa9e5..577e5a0c2e20 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -26,6 +26,7 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AppOpsManager; import android.app.IActivityManager; +import android.app.RemoteAction; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; @@ -3405,22 +3406,6 @@ public class WindowManagerService extends IWindowManager.Stub } } - public void setPictureInPictureAspectRatio(float aspectRatio) { - synchronized (mWindowMap) { - if (!mSupportsPictureInPicture) { - return; - } - - final TaskStack stack = mStackIdToStack.get(PINNED_STACK_ID); - if (stack == null) { - return; - } - - animateResizePinnedStack(getPictureInPictureBounds( - stack.getDisplayContent().getDisplayId(), aspectRatio), -1); - } - } - public Rect getPictureInPictureBounds(int displayId, float aspectRatio) { synchronized (mWindowMap) { if (!mSupportsPictureInPicture) { @@ -3447,6 +3432,36 @@ public class WindowManagerService extends IWindowManager.Stub } /** + * Sets the current picture-in-picture aspect ratio. + */ + public void setPictureInPictureAspectRatio(float aspectRatio) { + synchronized (mWindowMap) { + final TaskStack stack = mStackIdToStack.get(PINNED_STACK_ID); + if (!mSupportsPictureInPicture || stack == null) { + return; + } + + final int displayId = stack.getDisplayContent().getDisplayId(); + final Rect toBounds = getPictureInPictureBounds(displayId, aspectRatio); + animateResizePinnedStack(toBounds, -1 /* duration */); + } + } + + /** + * Sets the current picture-in-picture actions. + */ + public void setPictureInPictureActions(List<RemoteAction> actions) { + synchronized (mWindowMap) { + final TaskStack stack = mStackIdToStack.get(PINNED_STACK_ID); + if (!mSupportsPictureInPicture || stack == null) { + return; + } + + stack.getDisplayContent().getPinnedStackController().setActions(actions); + } + } + + /** * Place a TaskStack on a DisplayContent. Will create a new TaskStack if none is found with * specified stackId. * @param stackId The unique identifier of the new stack. |