diff options
11 files changed, 248 insertions, 17 deletions
diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl index 5768d1a0393f..47817a72e962 100644 --- a/core/java/android/app/ITaskStackListener.aidl +++ b/core/java/android/app/ITaskStackListener.aidl @@ -25,7 +25,10 @@ oneway interface ITaskStackListener { void onTaskStackChanged(); /** Called whenever an Activity is moved to the pinned stack from another stack. */ - void onActivityPinned(); + void onActivityPinned(String packageName); + + /** Called whenever an Activity is moved from the pinned stack to another stack. */ + void onActivityUnpinned(); /** * Called whenever IActivityManager.startActivity is called on an activity that is already diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java index a07e11e2b8af..57fc874517b7 100644 --- a/core/java/android/app/TaskStackListener.java +++ b/core/java/android/app/TaskStackListener.java @@ -31,7 +31,11 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override - public void onActivityPinned() throws RemoteException { + public void onActivityPinned(String packageName) throws RemoteException { + } + + @Override + public void onActivityUnpinned() throws RemoteException { } @Override diff --git a/packages/SystemUI/res/drawable/pip_notification_icon.xml b/packages/SystemUI/res/drawable/pip_notification_icon.xml new file mode 100644 index 000000000000..592bc60d553e --- /dev/null +++ b/packages/SystemUI/res/drawable/pip_notification_icon.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c9e7e57ecc5a..368c5484f902 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1750,6 +1750,18 @@ <!-- Label for PIP the drag to close zone [CHAR LIMIT=NONE]--> <string name="pip_phone_close">Close</string> + <!-- Title of menu shown over picture-in-picture. Used for accessibility. --> + <string name="pip_menu_title">Picture in picture menu</string> + + <!-- User visible notification channel name for the PiP BTW notification. [CHAR LIMIT=NONE] --> + <string name="pip_notification_channel_name">Picture-in-picture</string> + + <!-- PiP BTW notification title. [CHAR LIMIT=50] --> + <string name="pip_notification_title"><xliff:g id="name" example="Google Maps">%s</xliff:g> is in picture-in-picture</string> + + <!-- PiP BTW notification description. [CHAR LIMIT=NONE] --> + <string name="pip_notification_message">If you don’t want <xliff:g id="name" example="Google Maps">%s</xliff:g> to use this feature, tap to open settings and turn it off.</string> + <!-- Tuner string --> <string name="change_theme_reboot" translatable="false">Changing the theme requires a restart.</string> <!-- Tuner string --> @@ -1818,9 +1830,6 @@ <!-- App label of the instant apps notification [CHAR LIMIT=60] --> <string name="instant_apps">Instant Apps</string> - <!-- Title of menu shown over picture-in-picture. Used for accessibility. --> - <string name="pip_menu_title">Picture in picture menu</string> - <!-- Message of the instant apps notification indicating they don't need install [CHAR LIMIT=NONE] --> <string name="instant_apps_message">Instant apps don\'t require installation.</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 ecc2fadf5ae2..6cda0766663d 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java @@ -56,6 +56,7 @@ public class PipManager implements BasePipManager { private InputConsumerController mInputConsumerController; private PipMenuActivityController mMenuController; private PipMediaController mMediaController; + private PipNotificationController mNotificationController; private PipTouchHandler mTouchHandler; /** @@ -63,13 +64,24 @@ public class PipManager implements BasePipManager { */ TaskStackListener mTaskStackListener = new TaskStackListener() { @Override - public void onActivityPinned() { + public void onActivityPinned(String packageName) { if (!checkCurrentUserId(false /* debug */)) { return; } + mTouchHandler.onActivityPinned(); mMediaController.onActivityPinned(); mMenuController.onActivityPinned(); + mNotificationController.onActivityPinned(packageName); + } + + @Override + public void onActivityUnpinned() { + if (!checkCurrentUserId(false /* debug */)) { + return; + } + + mNotificationController.onActivityUnpinned(); } @Override @@ -160,6 +172,7 @@ public class PipManager implements BasePipManager { mInputConsumerController); mTouchHandler = new PipTouchHandler(context, mActivityManager, mMenuController, mInputConsumerController); + mNotificationController = new PipNotificationController(context, mActivityManager); } /** diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipNotificationController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipNotificationController.java new file mode 100644 index 000000000000..bdd6b65026f0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipNotificationController.java @@ -0,0 +1,142 @@ +/* + * 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.NotificationManager.IMPORTANCE_MIN; +import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS; + +import android.app.IActivityManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.util.Log; + +import com.android.systemui.R; +import com.android.systemui.SystemUI; + +/** + * Manages the BTW notification that shows whenever an activity enters or leaves picture-in-picture. + */ +public class PipNotificationController { + private static final String TAG = PipNotificationController.class.getSimpleName(); + + private static final String CHANNEL_ID = PipNotificationController.class.getName(); + private static final int BTW_NOTIFICATION_ID = 0; + + private Context mContext; + private IActivityManager mActivityManager; + private NotificationManager mNotificationManager; + + public PipNotificationController(Context context, IActivityManager activityManager) { + mContext = context; + mActivityManager = activityManager; + mNotificationManager = NotificationManager.from(context); + createNotificationChannel(); + } + + public void onActivityPinned(String packageName) { + // Clear any existing notification + mNotificationManager.cancel(CHANNEL_ID, BTW_NOTIFICATION_ID); + + // Build a new notification + final Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID) + .setLocalOnly(true) + .setOngoing(true) + .setSmallIcon(R.drawable.pip_notification_icon) + .setColor(mContext.getColor( + com.android.internal.R.color.system_notification_accent_color)); + if (updateNotificationForApp(builder, packageName)) { + SystemUI.overrideNotificationAppName(mContext, builder); + + // Show the new notification + mNotificationManager.notify(CHANNEL_ID, BTW_NOTIFICATION_ID, builder.build()); + } + } + + public void onActivityUnpinned() { + ComponentName topPipActivity = PipUtils.getTopPinnedActivity(mContext, mActivityManager); + if (topPipActivity != null) { + onActivityPinned(topPipActivity.getPackageName()); + } else { + mNotificationManager.cancel(CHANNEL_ID, BTW_NOTIFICATION_ID); + } + } + + /** + * Create the notification channel for the PiP BTW notifications if necessary. + */ + private NotificationChannel createNotificationChannel() { + NotificationChannel channel = mNotificationManager.getNotificationChannel(CHANNEL_ID); + if (channel == null) { + channel = new NotificationChannel(CHANNEL_ID, + mContext.getString(R.string.pip_notification_channel_name), IMPORTANCE_MIN); + channel.enableLights(false); + channel.enableVibration(false); + mNotificationManager.createNotificationChannel(channel); + } + return channel; + } + + /** + * Updates the notification builder with app-specific information, returning whether it was + * successful. + */ + private boolean updateNotificationForApp(Notification.Builder builder, String packageName) { + final PackageManager pm = mContext.getPackageManager(); + final ApplicationInfo appInfo; + try { + appInfo = pm.getApplicationInfo(packageName, 0); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not update notification for application", e); + return false; + } + + if (appInfo != null) { + final String appName = pm.getApplicationLabel(appInfo).toString(); + final String message = mContext.getString(R.string.pip_notification_message, appName); + final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS, + Uri.fromParts("package", packageName, null)); + settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); + final Icon appIcon = appInfo.icon != 0 + ? Icon.createWithResource(packageName, appInfo.icon) + : Icon.createWithResource(Resources.getSystem(), + com.android.internal.R.drawable.sym_def_app_icon); + + builder.setContentTitle(mContext.getString(R.string.pip_notification_title, appName)) + .setContentText(message) + .setContentIntent(PendingIntent.getActivity(mContext, packageName.hashCode(), + settingsIntent, FLAG_CANCEL_CURRENT)) + .setStyle(new Notification.BigTextStyle().bigText(message)) + .setLargeIcon(appIcon); + return true; + } + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java index b71c87d4cc23..b96b0ae9ddd9 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java @@ -627,7 +627,7 @@ public class PipManager implements BasePipManager { } @Override - public void onActivityPinned() { + public void onActivityPinned(String packageName) { if (DEBUG) Log.d(TAG, "onActivityPinned()"); if (!checkCurrentUserId(DEBUG)) { return; diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java index 47468aeaeb97..ac8c0f2fe1f4 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java +++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java @@ -153,7 +153,8 @@ public class SystemServicesProxy { public abstract static class TaskStackListener { public void onTaskStackChanged() { } public void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) { } - public void onActivityPinned() { } + public void onActivityPinned(String packageName) { } + public void onActivityUnpinned() { } public void onPinnedActivityRestartAttempt() { } public void onPinnedStackAnimationStarted() { } public void onPinnedStackAnimationEnded() { } @@ -194,17 +195,22 @@ public class SystemServicesProxy { } @Override - public void onActivityPinned() throws RemoteException { + public void onActivityPinned(String packageName) throws RemoteException { mHandler.removeMessages(H.ON_ACTIVITY_PINNED); - mHandler.sendEmptyMessage(H.ON_ACTIVITY_PINNED); + mHandler.obtainMessage(H.ON_ACTIVITY_PINNED, packageName).sendToTarget(); + } + + @Override + public void onActivityUnpinned() throws RemoteException { + mHandler.removeMessages(H.ON_ACTIVITY_UNPINNED); + mHandler.sendEmptyMessage(H.ON_ACTIVITY_UNPINNED); } @Override public void onPinnedActivityRestartAttempt() throws RemoteException{ mHandler.removeMessages(H.ON_PINNED_ACTIVITY_RESTART_ATTEMPT); - mHandler.obtainMessage(H.ON_PINNED_ACTIVITY_RESTART_ATTEMPT) - .sendToTarget(); + mHandler.sendEmptyMessage(H.ON_PINNED_ACTIVITY_RESTART_ATTEMPT); } @Override @@ -1231,6 +1237,7 @@ public class SystemServicesProxy { private static final int ON_ACTIVITY_DISMISSING_DOCKED_STACK = 7; private static final int ON_TASK_PROFILE_LOCKED = 8; private static final int ON_PINNED_STACK_ANIMATION_STARTED = 9; + private static final int ON_ACTIVITY_UNPINNED = 10; @Override public void handleMessage(Message msg) { @@ -1250,7 +1257,13 @@ public class SystemServicesProxy { } case ON_ACTIVITY_PINNED: { for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { - mTaskStackListeners.get(i).onActivityPinned(); + mTaskStackListeners.get(i).onActivityPinned((String) msg.obj); + } + break; + } + case ON_ACTIVITY_UNPINNED: { + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onActivityUnpinned(); } break; } diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java index 2885e663af3f..e64b4b325642 100644 --- a/services/core/java/com/android/server/am/ActivityStack.java +++ b/services/core/java/com/android/server/am/ActivityStack.java @@ -4983,6 +4983,11 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai } task.setStack(null); + + // Notify if a task from the pinned stack is being removed (or moved depending on the mode) + if (mStackId == PINNED_STACK_ID) { + mService.mTaskChangeNotificationController.notifyActivityUnpinned(); + } } TaskRecord createTaskRecord(int taskId, ActivityInfo info, Intent intent, diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java index 42efe0b5d8e0..bce8c404a9e8 100644 --- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java @@ -2852,7 +2852,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D resumeFocusedStackTopActivityLocked(); stack.animateResizePinnedStack(bounds, -1 /* animationDuration */); - mService.mTaskChangeNotificationController.notifyActivityPinned(); + mService.mTaskChangeNotificationController.notifyActivityPinned(r.packageName); } /** Move activity with its stack to front and make the stack focused. */ diff --git a/services/core/java/com/android/server/am/TaskChangeNotificationController.java b/services/core/java/com/android/server/am/TaskChangeNotificationController.java index 3cec7e478046..94cf092baed3 100644 --- a/services/core/java/com/android/server/am/TaskChangeNotificationController.java +++ b/services/core/java/com/android/server/am/TaskChangeNotificationController.java @@ -47,6 +47,7 @@ class TaskChangeNotificationController { static final int NOTIFY_TASK_PROFILE_LOCKED_LISTENERS_MSG = 14; static final int NOTIFY_TASK_SNAPSHOT_CHANGED_LISTENERS_MSG = 15; static final int NOTIFY_PINNED_STACK_ANIMATION_STARTED_LISTENERS_MSG = 16; + static final int NOTIFY_ACTIVITY_UNPINNED_LISTENERS_MSG = 17; // Delay in notifying task stack change listeners (in millis) static final int NOTIFY_TASK_STACK_CHANGE_LISTENERS_DELAY = 100; @@ -94,7 +95,11 @@ class TaskChangeNotificationController { }; private final TaskStackConsumer mNotifyActivityPinned = (l, m) -> { - l.onActivityPinned(); + l.onActivityPinned((String) m.obj); + }; + + private final TaskStackConsumer mNotifyActivityUnpinned = (l, m) -> { + l.onActivityUnpinned(); }; private final TaskStackConsumer mNotifyPinnedActivityRestartAttempt = (l, m) -> { @@ -168,6 +173,9 @@ class TaskChangeNotificationController { case NOTIFY_ACTIVITY_PINNED_LISTENERS_MSG: forAllRemoteListeners(mNotifyActivityPinned, msg); break; + case NOTIFY_ACTIVITY_UNPINNED_LISTENERS_MSG: + forAllRemoteListeners(mNotifyActivityUnpinned, msg); + break; case NOTIFY_PINNED_ACTIVITY_RESTART_ATTEMPT_LISTENERS_MSG: forAllRemoteListeners(mNotifyPinnedActivityRestartAttempt, msg); break; @@ -263,13 +271,22 @@ class TaskChangeNotificationController { } /** Notifies all listeners when an Activity is pinned. */ - void notifyActivityPinned() { + void notifyActivityPinned(String packageName) { mHandler.removeMessages(NOTIFY_ACTIVITY_PINNED_LISTENERS_MSG); - final Message msg = mHandler.obtainMessage(NOTIFY_ACTIVITY_PINNED_LISTENERS_MSG); + final Message msg = mHandler.obtainMessage(NOTIFY_ACTIVITY_PINNED_LISTENERS_MSG, + packageName); forAllLocalListeners(mNotifyActivityPinned, msg); msg.sendToTarget(); } + /** Notifies all listeners when an Activity is unpinned. */ + void notifyActivityUnpinned() { + mHandler.removeMessages(NOTIFY_ACTIVITY_UNPINNED_LISTENERS_MSG); + final Message msg = mHandler.obtainMessage(NOTIFY_ACTIVITY_UNPINNED_LISTENERS_MSG); + forAllLocalListeners(mNotifyActivityUnpinned, msg); + msg.sendToTarget(); + } + /** * Notifies all listeners when an attempt was made to start an an activity that is already * running in the pinned stack and the activity was not actually started, but the task is |