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;