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
diff --git a/packages/SystemUI/res/values/arrays_tv.xml b/packages/SystemUI/res/values/arrays_tv.xml
index e52c5db..7541b0e 100644
--- a/packages/SystemUI/res/values/arrays_tv.xml
+++ b/packages/SystemUI/res/values/arrays_tv.xml
@@ -31,5 +31,6 @@
         <item>com.google.android.katniss.setting/.SpeechSettingsActivity</item>
         <item>com.google.android.katniss.setting/.SearchSettingsActivity</item>
         <item>com.google.android.gsf.notouch/.UsageDiagnosticsSettingActivity</item>
+        <item>com.google.android.tvlauncher/.notifications.NotificationsSidePanelActivity</item>
     </string-array>
 </resources>
diff --git a/packages/SystemUI/res/values/config_tv.xml b/packages/SystemUI/res/values/config_tv.xml
index 40e3b12..ffd58dc 100644
--- a/packages/SystemUI/res/values/config_tv.xml
+++ b/packages/SystemUI/res/values/config_tv.xml
@@ -17,17 +17,9 @@
 <resources>
     <!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows,
          when the PIP menu is shown with settings. -->
-    <string translatable="false" name="pip_settings_bounds">"662 54 1142 324"</string>
+    <string translatable="false" name="pip_settings_bounds">"662 756 1142 1026"</string>
 
     <!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows,
          when the PIP menu is shown in center. -->
     <string translatable="false" name="pip_menu_bounds">"596 280 1324 690"</string>
-
-    <!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows,
-         when the PIP is shown in Recents without focus. -->
-    <string translatable="false" name="pip_recents_bounds">"800 54 1120 234"</string>
-
-    <!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows,
-         when the PIP is shown in Recents with focus. -->
-    <string translatable="false" name="pip_recents_focused_bounds">"775 54 1145 262"</string>
 </resources>
diff --git a/packages/SystemUI/res/values/strings_tv.xml b/packages/SystemUI/res/values/strings_tv.xml
index e578068..a9bdb71 100644
--- a/packages/SystemUI/res/values/strings_tv.xml
+++ b/packages/SystemUI/res/values/strings_tv.xml
@@ -17,6 +17,14 @@
  */
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- Picture-in-Picture (PIP) notification -->
+    <!-- Title for the notification channel for TV PIP controls. [CHAR LIMIT=NONE] -->
+    <string name="notification_channel_tv_pip">Picture-in-Picture</string>
+    <!-- Title of the picture-in-picture (PIP) notification title
+         when the media doesn't have title [CHAR LIMIT=NONE] -->
+    <string name="pip_notification_unknown_title">(No title program)</string>
+
     <!-- Picture-in-Picture (PIP) menu -->
     <eat-comment />
     <!-- Button to close picture-in-picture (PIP) in PIP menu [CHAR LIMIT=30] -->
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 657f08b..d37f646 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 @@
  */
 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 @@
     private ComponentName mPipComponentName;
     private MediaController mPipMediaController;
     private String[] mLastPackagesResourceGranted;
+    private PipNotification mPipNotification;
 
     private final PinnedStackListener mPinnedStackListener = new PinnedStackListener();
 
@@ -246,6 +248,8 @@
         } 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 void onConfigurationChanged() {
         loadConfigurationsAndApply();
+        mPipNotification.onConfigurationChanged(mContext);
     }
 
     /**
@@ -345,7 +350,7 @@
      * @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 @@
 
     /**
      * 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 0000000..727eb5a
--- /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.
+ * <p>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 cd85a76..ae8afe4 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 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 @@
                                 ? 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 53b3fe9..2f6b7e6 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -212,6 +212,10 @@
     // 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;