Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 26c63563 authored by Jaewan Kim's avatar Jaewan Kim Committed by Youngsang Cho
Browse files

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
parent b0744b82
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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>
+1 −9
Original line number Diff line number Diff line
@@ -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>
+8 −0
Original line number Diff line number Diff line
@@ -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] -->
+9 −4
Original line number Diff line number Diff line
@@ -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) {
+225 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
Loading