From 26c63563972568c1d7c4ed302ec51628a8b1806d Mon Sep 17 00:00:00 2001 From: Jaewan Kim Date: Wed, 26 Apr 2017 15:41:43 +0900 Subject: [PATCH] TV PIP: Fix broken TV PIP Bug: 37249867 Test: Manual test (checked that the notification UI is shown when the PIP starts, and dismissed when the PIP is closed. The 'DETAILS' and the 'DISMISS' button in the notification also worked.) Change-Id: I12e385b51f834991a0115ce5ba7dd98180577adb --- packages/SystemUI/res/values/arrays_tv.xml | 1 + packages/SystemUI/res/values/config_tv.xml | 10 +- packages/SystemUI/res/values/strings_tv.xml | 8 + .../android/systemui/pip/tv/PipManager.java | 13 +- .../systemui/pip/tv/PipNotification.java | 225 ++++++++++++++++++ .../systemui/util/NotificationChannels.java | 10 + proto/src/system_messages.proto | 4 + 7 files changed, 258 insertions(+), 13 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java diff --git a/packages/SystemUI/res/values/arrays_tv.xml b/packages/SystemUI/res/values/arrays_tv.xml index e52c5db9d227b..7541b0e8c0841 100644 --- a/packages/SystemUI/res/values/arrays_tv.xml +++ b/packages/SystemUI/res/values/arrays_tv.xml @@ -31,5 +31,6 @@ com.google.android.katniss.setting/.SpeechSettingsActivity com.google.android.katniss.setting/.SearchSettingsActivity com.google.android.gsf.notouch/.UsageDiagnosticsSettingActivity + com.google.android.tvlauncher/.notifications.NotificationsSidePanelActivity diff --git a/packages/SystemUI/res/values/config_tv.xml b/packages/SystemUI/res/values/config_tv.xml index 40e3b128a55e5..ffd58dcfd50db 100644 --- a/packages/SystemUI/res/values/config_tv.xml +++ b/packages/SystemUI/res/values/config_tv.xml @@ -17,17 +17,9 @@ - "662 54 1142 324" + "662 756 1142 1026" "596 280 1324 690" - - - "800 54 1120 234" - - - "775 54 1145 262" diff --git a/packages/SystemUI/res/values/strings_tv.xml b/packages/SystemUI/res/values/strings_tv.xml index e578068163d75..a9bdb71de039e 100644 --- a/packages/SystemUI/res/values/strings_tv.xml +++ b/packages/SystemUI/res/values/strings_tv.xml @@ -17,6 +17,14 @@ */ --> + + + + Picture-in-Picture + + (No title program) + 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 657f08be8b529..d37f64629f6a9 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java @@ -61,7 +61,8 @@ import static android.view.Display.DEFAULT_DISPLAY; */ public class PipManager implements BasePipManager { private static final String TAG = "PipManager"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final String SETTINGS_PACKAGE_AND_CLASS_DELIMITER = "/"; private static PipManager sPipManager; @@ -122,6 +123,7 @@ public class PipManager implements BasePipManager { private ComponentName mPipComponentName; private MediaController mPipMediaController; private String[] mLastPackagesResourceGranted; + private PipNotification mPipNotification; private final PinnedStackListener mPinnedStackListener = new PinnedStackListener(); @@ -246,6 +248,8 @@ public class PipManager implements BasePipManager { } catch (RemoteException e) { Log.e(TAG, "Failed to register pinned stack listener", e); } + + mPipNotification = new PipNotification(context); } private void loadConfigurationsAndApply() { @@ -267,6 +271,7 @@ public class PipManager implements BasePipManager { */ public void onConfigurationChanged() { loadConfigurationsAndApply(); + mPipNotification.onConfigurationChanged(mContext); } /** @@ -345,7 +350,7 @@ public class PipManager implements BasePipManager { * @param state In Pip state also used to determine the new size for the Pip. */ void resizePinnedStack(int state) { - if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + state); + if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + state, new Exception()); boolean wasStateNoPip = (mState == STATE_NO_PIP); mResumeResizePinnedStackRunnable = state; for (int i = mListeners.size() - 1; i >= 0; --i) { @@ -511,8 +516,8 @@ public class PipManager implements BasePipManager { /** * Returns the PIPed activity's playback state. - * This returns one of {@link PLAYBACK_STATE_PLAYING}, {@link PLAYBACK_STATE_PAUSED}, - * or {@link PLAYBACK_STATE_UNAVAILABLE}. + * This returns one of {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, + * or {@link #PLAYBACK_STATE_UNAVAILABLE}. */ int getPlaybackState() { if (mPipMediaController == null || mPipMediaController.getPlaybackState() == null) { diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java new file mode 100644 index 0000000000000..727eb5a3dba44 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java @@ -0,0 +1,225 @@ +/* + * 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.tv; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.Icon; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.PlaybackState; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; + +import com.android.systemui.util.NotificationChannels; +import com.android.systemui.R; +import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; + +/** + * A notification that informs users that PIP is running and also provides PIP controls. + *

Once it's created, it will manage the PIP notification UI by itself except for handling + * configuration changes. + */ +public class PipNotification { + private static final String TAG = "PipNotification"; + private static final boolean DEBUG = PipManager.DEBUG; + + private static final String ACTION_MENU = "PipNotification.menu"; + private static final String ACTION_CLOSE = "PipNotification.close"; + + private final PipManager mPipManager = PipManager.getInstance(); + + private final NotificationManager mNotificationManager; + private final Notification.Builder mNotificationBuilder; + + private MediaController mMediaController; + private String mDefaultTitle; + private Icon mDefaultIcon; + + private boolean mNotified; + private String mTitle; + private Bitmap mArt; + + private PipManager.Listener mPipListener = new PipManager.Listener() { + @Override + public void onPipEntered() { + updateMediaControllerMetadata(); + notifyPipNotification(); + } + + @Override + public void onPipActivityClosed() { + dismissPipNotification(); + } + + @Override + public void onShowPipMenu() { + // no-op. + } + + @Override + public void onMoveToFullscreen() { + dismissPipNotification(); + } + + @Override + public void onPipResizeAboutToStart() { + // no-op. + } + }; + + private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (updateMediaControllerMetadata() && mNotified) { + // update notification + notifyPipNotification(); + } + } + }; + + private final PipManager.MediaListener mPipMediaListener = new PipManager.MediaListener() { + @Override + public void onMediaControllerChanged() { + MediaController newController = mPipManager.getMediaController(); + if (mMediaController == newController) { + return; + } + if (mMediaController != null) { + mMediaController.unregisterCallback(mMediaControllerCallback); + } + mMediaController = newController; + if (mMediaController != null) { + mMediaController.registerCallback(mMediaControllerCallback); + } + if (updateMediaControllerMetadata() && mNotified) { + // update notification + notifyPipNotification(); + } + } + }; + + private final BroadcastReceiver mEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) { + Log.d(TAG, "Received " + intent.getAction() + " from the notification UI"); + } + switch (intent.getAction()) { + case ACTION_MENU: + mPipManager.showPictureInPictureMenu(); + break; + case ACTION_CLOSE: + mPipManager.closePip(); + break; + } + } + }; + + public PipNotification(Context context) { + mNotificationManager = (NotificationManager) context.getSystemService( + Context.NOTIFICATION_SERVICE); + + mNotificationBuilder = new Notification.Builder(context, NotificationChannels.TVPIP) + .setLocalOnly(true) + .setOngoing(false) + .setCategory(Notification.CATEGORY_SYSTEM) + .extend(new Notification.TvExtender() + .setContentIntent(createPendingIntent(context, ACTION_MENU)) + .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE))); + + mPipManager.addListener(mPipListener); + mPipManager.addMediaListener(mPipMediaListener); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_MENU); + intentFilter.addAction(ACTION_CLOSE); + context.registerReceiver(mEventReceiver, intentFilter); + + onConfigurationChanged(context); + } + + /** + * Called by {@link PipManager} when the configuration is changed. + */ + void onConfigurationChanged(Context context) { + Resources res = context.getResources(); + mDefaultTitle = res.getString(R.string.pip_notification_unknown_title); + mDefaultIcon = Icon.createWithResource(context, + res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR + ? R.drawable.pip_expand_ll : R.drawable.pip_expand_lr); + if (mNotified) { + // update notification + notifyPipNotification(); + } + } + + private void notifyPipNotification() { + mNotified = true; + mNotificationBuilder + .setShowWhen(true) + .setWhen(System.currentTimeMillis()) + // TODO: Sending bitmap doesn't work in launcher side. Once launcher supports it, + // we can set icon. + //.setSmallIcon(mArt != null ? Icon.createWithBitmap(mArt) : mDefaultIcon) + .setSmallIcon(mDefaultIcon.getResId()) + .setContentTitle(!TextUtils.isEmpty(mTitle) ? mTitle : mDefaultTitle); + mNotificationManager.notify(SystemMessage.NOTE_TV_PIP, mNotificationBuilder.build()); + } + + private void dismissPipNotification() { + mNotified = false; + mNotificationManager.cancel(SystemMessage.NOTE_TV_PIP); + } + + private boolean updateMediaControllerMetadata() { + String title = null; + Bitmap art = null; + if (mPipManager.getMediaController() != null) { + MediaMetadata metadata = mPipManager.getMediaController().getMetadata(); + if (metadata != null) { + title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE); + if (TextUtils.isEmpty(title)) { + title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE); + } + art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); + if (art == null) { + art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART); + } + } + } + if (!TextUtils.equals(title, mTitle) || art != mArt) { + mTitle = title; + mArt = art; + return true; + } + return false; + } + + private static PendingIntent createPendingIntent(Context context, String action) { + return PendingIntent.getBroadcast(context, 0, + new Intent(action), PendingIntent.FLAG_CANCEL_CURRENT); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java index cd85a760159bb..ae8afe4dcad7b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java +++ b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java @@ -30,6 +30,7 @@ public class NotificationChannels extends SystemUI { public static String SCREENSHOTS = "SCN"; public static String GENERAL = "GEN"; public static String STORAGE = "DSK"; + public static String TVPIP = "TPP"; @VisibleForTesting static void createAll(Context context) { @@ -55,6 +56,15 @@ public class NotificationChannels extends SystemUI { ? NotificationManager.IMPORTANCE_DEFAULT : NotificationManager.IMPORTANCE_LOW) )); + if (isTv(context)) { + // TV specific notification channel for TV PIP controls. + // Importance should be {@link NotificationManager#IMPORTANCE_MAX} to have the highest + // priority, so it can be shown in all times. + nm.createNotificationChannel(new NotificationChannel( + TVPIP, + context.getString(R.string.notification_channel_tv_pip), + NotificationManager.IMPORTANCE_MAX)); + } } @Override diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto index 53b3fe9c1a0d6..2f6b7e619e682 100644 --- a/proto/src/system_messages.proto +++ b/proto/src/system_messages.proto @@ -212,6 +212,10 @@ message SystemMessage { // Package: com.android.systemui NOTE_LOGOUT_USER = 1011; + // Notify the user that a TV PIP is running. + // Package: com.android.systemui + NOTE_TV_PIP = 1100; + // Communicate to the user about remote bugreports. // Package: android NOTE_REMOTE_BUGREPORT = 678432343;