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

Commit 500df7f0 authored by Jacqueline Bronger's avatar Jacqueline Bronger Committed by Automerger Merge Worker
Browse files

Merge "Add more information to the TV PiP notification" into tm-dev am:...

Merge "Add more information to the TV PiP notification" into tm-dev am: b6de8327 am: 762b8b69 am: 81dbc596

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17197863



Change-Id: Iba4a910613740d16195a991ade268124d4819d54
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents a99a8bf7 81dbc596
Loading
Loading
Loading
Loading
+15 −1
Original line number Diff line number Diff line
@@ -137,6 +137,18 @@ public class ImageUtils {
     */
    public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth,
            int maxHeight) {
        return buildScaledBitmap(drawable, maxWidth, maxHeight, false);
    }

    /**
     * Convert a drawable to a bitmap, scaled to fit within maxWidth and maxHeight.
     *
     * @param allowUpscaling if true, the drawable will not only be scaled down, but also scaled up
     *                       to fit within the maximum size given. This is useful for converting
     *                       vectorized icons which usually have a very small intrinsic size.
     */
    public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth,
            int maxHeight, boolean allowUpscaling) {
        if (drawable == null) {
            return null;
        }
@@ -155,7 +167,9 @@ public class ImageUtils {
        // a large notification icon if necessary
        float ratio = Math.min((float) maxWidth / (float) originalWidth,
                (float) maxHeight / (float) originalHeight);
        if (!allowUpscaling) {
            ratio = Math.min(1.0f, ratio);
        }
        int scaledWidth = (int) (ratio * originalWidth);
        int scaledHeight = (int) (ratio * originalHeight);
        Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888);
+4 −1
Original line number Diff line number Diff line
@@ -143,8 +143,11 @@ public abstract class TvPipModule {
    @Provides
    static TvPipNotificationController provideTvPipNotificationController(Context context,
            PipMediaController pipMediaController,
            PipParamsChangedForwarder pipParamsChangedForwarder,
            TvPipBoundsState tvPipBoundsState,
            @ShellMainThread Handler mainHandler) {
        return new TvPipNotificationController(context, pipMediaController, mainHandler);
        return new TvPipNotificationController(context, pipMediaController,
                pipParamsChangedForwarder, tvPipBoundsState, mainHandler);
    }

    @WMSingleton
+46 −2
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import android.content.IntentFilter;
import android.graphics.drawable.Icon;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.Handler;
@@ -64,7 +65,7 @@ public class PipMediaController {
     */
    public interface ActionListener {
        /**
         * Called when the media actions changes.
         * Called when the media actions changed.
         */
        void onMediaActionsChanged(List<RemoteAction> actions);
    }
@@ -74,11 +75,21 @@ public class PipMediaController {
     */
    public interface MetadataListener {
        /**
         * Called when the media metadata changes.
         * Called when the media metadata changed.
         */
        void onMediaMetadataChanged(MediaMetadata metadata);
    }

    /**
     * A listener interface to receive notification on changes to the media session token.
     */
    public interface TokenListener {
        /**
         * Called when the media session token changed.
         */
        void onMediaSessionTokenChanged(MediaSession.Token token);
    }

    private final Context mContext;
    private final Handler mMainHandler;
    private final HandlerExecutor mHandlerExecutor;
@@ -133,6 +144,7 @@ public class PipMediaController {

    private final ArrayList<ActionListener> mActionListeners = new ArrayList<>();
    private final ArrayList<MetadataListener> mMetadataListeners = new ArrayList<>();
    private final ArrayList<TokenListener> mTokenListeners = new ArrayList<>();

    public PipMediaController(Context context, Handler mainHandler) {
        mContext = context;
@@ -204,6 +216,31 @@ public class PipMediaController {
        mMetadataListeners.remove(listener);
    }

    /**
     * Adds a new token listener.
     */
    public void addTokenListener(TokenListener listener) {
        if (!mTokenListeners.contains(listener)) {
            mTokenListeners.add(listener);
            listener.onMediaSessionTokenChanged(getToken());
        }
    }

    /**
     * Removes a token listener.
     */
    public void removeTokenListener(TokenListener listener) {
        listener.onMediaSessionTokenChanged(null);
        mTokenListeners.remove(listener);
    }

    private MediaSession.Token getToken() {
        if (mMediaController == null) {
            return null;
        }
        return mMediaController.getSessionToken();
    }

    private MediaMetadata getMediaMetadata() {
        return mMediaController != null ? mMediaController.getMetadata() : null;
    }
@@ -294,6 +331,7 @@ public class PipMediaController {
            }
            notifyActionsChanged();
            notifyMetadataChanged(getMediaMetadata());
            notifyTokenChanged(getToken());

            // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
        }
@@ -317,4 +355,10 @@ public class PipMediaController {
            mMetadataListeners.forEach(l -> l.onMediaMetadataChanged(metadata));
        }
    }

    private void notifyTokenChanged(MediaSession.Token token) {
        if (!mTokenListeners.isEmpty()) {
            mTokenListeners.forEach(l -> l.onMediaSessionTokenChanged(token));
        }
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -293,6 +293,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
        }
        mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
        mTvPipBoundsState.setTvPipExpanded(expanding);
        mPipNotificationController.updateExpansionState();

        updatePinnedStackBounds();
    }

+248 −67
Original line number Diff line number Diff line
@@ -16,36 +16,47 @@

package com.android.wm.shell.pip.tv;

import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE;
import static android.app.Notification.Action.SEMANTIC_ACTION_NONE;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.media.MediaMetadata;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.session.MediaSession;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;

import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.ImageUtils;
import com.android.wm.shell.R;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;

import java.util.Objects;
import java.util.ArrayList;
import java.util.List;

/**
 * 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.
 * 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 and user initiated expanded PiP toggling.
 */
public class TvPipNotificationController {
    private static final String TAG = "TvPipNotification";
    private static final boolean DEBUG = TvPipController.DEBUG;

    // Referenced in com.android.systemui.util.NotificationChannels.
    public static final String NOTIFICATION_CHANNEL = "TVPIP";
@@ -60,6 +71,8 @@ public class TvPipNotificationController {
            "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP";
    private static final String ACTION_TOGGLE_EXPANDED_PIP =
            "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP";
    private static final String ACTION_FULLSCREEN =
            "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN";

    private final Context mContext;
    private final PackageManager mPackageManager;
@@ -68,44 +81,88 @@ public class TvPipNotificationController {
    private final ActionBroadcastReceiver mActionBroadcastReceiver;
    private final Handler mMainHandler;
    private Delegate mDelegate;
    private final TvPipBoundsState mTvPipBoundsState;

    private String mDefaultTitle;

    private final List<RemoteAction> mCustomActions = new ArrayList<>();
    private final List<RemoteAction> mMediaActions = new ArrayList<>();
    private RemoteAction mCustomCloseAction;

    private MediaSession.Token mMediaSessionToken;

    /** Package name for the application that owns PiP window. */
    private String mPackageName;
    private boolean mNotified;
    private String mMediaTitle;
    private Bitmap mArt;

    private boolean mIsNotificationShown;
    private String mPipTitle;
    private String mPipSubtitle;

    private Bitmap mActivityIcon;

    public TvPipNotificationController(Context context, PipMediaController pipMediaController,
            PipParamsChangedForwarder pipParamsChangedForwarder, TvPipBoundsState tvPipBoundsState,
            Handler mainHandler) {
        mContext = context;
        mPackageManager = context.getPackageManager();
        mNotificationManager = context.getSystemService(NotificationManager.class);
        mMainHandler = mainHandler;
        mTvPipBoundsState = tvPipBoundsState;

        mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL)
                .setLocalOnly(true)
                .setOngoing(false)
                .setOngoing(true)
                .setCategory(Notification.CATEGORY_SYSTEM)
                .setShowWhen(true)
                .setSmallIcon(R.drawable.pip_icon)
                .setAllowSystemGeneratedContextualActions(false)
                .setContentIntent(createPendingIntent(context, ACTION_FULLSCREEN))
                .setDeleteIntent(getCloseAction().actionIntent)
                .extend(new Notification.TvExtender()
                        .setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU))
                        .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP)));

        mActionBroadcastReceiver = new ActionBroadcastReceiver();

        pipMediaController.addMetadataListener(this::onMediaMetadataChanged);
        pipMediaController.addActionListener(this::onMediaActionsChanged);
        pipMediaController.addTokenListener(this::onMediaSessionTokenChanged);

        pipParamsChangedForwarder.addListener(
                new PipParamsChangedForwarder.PipParamsChangedCallback() {
                    @Override
                    public void onExpandedAspectRatioChanged(float ratio) {
                        updateExpansionState();
                    }

                    @Override
                    public void onActionsChanged(List<RemoteAction> actions,
                            RemoteAction closeAction) {
                        mCustomActions.clear();
                        mCustomActions.addAll(actions);
                        mCustomCloseAction = closeAction;
                        updateNotificationContent();
                    }

                    @Override
                    public void onTitleChanged(String title) {
                        mPipTitle = title;
                        updateNotificationContent();
                    }

                    @Override
                    public void onSubtitleChanged(String subtitle) {
                        mPipSubtitle = subtitle;
                        updateNotificationContent();
                    }
                });

        onConfigurationChanged(context);
    }

    void setDelegate(Delegate delegate) {
        if (DEBUG) {
            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                    "%s: setDelegate(), delegate=%s", TAG, delegate);
        }
        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setDelegate(), delegate=%s",
                TAG, delegate);

        if (mDelegate != null) {
            throw new IllegalStateException(
                    "The delegate has already been set and should not change.");
@@ -118,90 +175,181 @@ public class TvPipNotificationController {
    }

    void show(String packageName) {
        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: show %s", TAG, packageName);
        if (mDelegate == null) {
            throw new IllegalStateException("Delegate is not set.");
        }

        mIsNotificationShown = true;
        mPackageName = packageName;
        update();
        mActivityIcon = getActivityIcon();
        mActionBroadcastReceiver.register();

        updateNotificationContent();
    }

    void dismiss() {
        mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP);
        mNotified = false;
        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: dismiss()", TAG);

        mIsNotificationShown = false;
        mPackageName = null;
        mActionBroadcastReceiver.unregister();

        mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP);
    }

    private void onMediaMetadataChanged(MediaMetadata metadata) {
        if (updateMediaControllerMetadata(metadata) && mNotified) {
            // update notification
            update();
    private Notification.Action getToggleAction(boolean expanded) {
        if (expanded) {
            return createSystemAction(R.drawable.pip_ic_collapse,
                    R.string.pip_collapse, ACTION_TOGGLE_EXPANDED_PIP);
        } else {
            return createSystemAction(R.drawable.pip_ic_expand, R.string.pip_expand,
                    ACTION_TOGGLE_EXPANDED_PIP);
        }
    }

    /**
     * Called by {@link PipController} when the configuration is changed.
     */
    void onConfigurationChanged(Context context) {
        mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title);
        if (mNotified) {
            // Update the notification.
            update();
    private Notification.Action createSystemAction(int iconRes, int titleRes, String action) {
        Notification.Action.Builder builder = new Notification.Action.Builder(
                Icon.createWithResource(mContext, iconRes),
                mContext.getString(titleRes),
                createPendingIntent(mContext, action));
        builder.setContextual(true);
        return builder.build();
    }

    private void onMediaActionsChanged(List<RemoteAction> actions) {
        mMediaActions.clear();
        mMediaActions.addAll(actions);
        if (mCustomActions.isEmpty()) {
            updateNotificationContent();
        }
    }

    private void update() {
        mNotified = true;
        mNotificationBuilder
                .setWhen(System.currentTimeMillis())
                .setContentTitle(getNotificationTitle());
        if (mArt != null) {
            mNotificationBuilder.setStyle(new Notification.BigPictureStyle()
                    .bigPicture(mArt));
    private void onMediaSessionTokenChanged(MediaSession.Token token) {
        mMediaSessionToken = token;
        updateNotificationContent();
    }

    private Notification.Action remoteToNotificationAction(RemoteAction action) {
        return remoteToNotificationAction(action, SEMANTIC_ACTION_NONE);
    }

    private Notification.Action remoteToNotificationAction(RemoteAction action,
            int semanticAction) {
        Notification.Action.Builder builder = new Notification.Action.Builder(action.getIcon(),
                action.getTitle(),
                action.getActionIntent());
        if (action.getContentDescription() != null) {
            Bundle extras = new Bundle();
            extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION,
                    action.getContentDescription());
            builder.addExtras(extras);
        }
        builder.setSemanticAction(semanticAction);
        builder.setContextual(true);
        return builder.build();
    }

    private Notification.Action[] getNotificationActions() {
        final List<Notification.Action> actions = new ArrayList<>();

        // 1. Fullscreen
        actions.add(getFullscreenAction());
        // 2. Close
        actions.add(getCloseAction());
        // 3. App actions
        final List<RemoteAction> appActions =
                mCustomActions.isEmpty() ? mMediaActions : mCustomActions;
        for (RemoteAction appAction : appActions) {
            if (PipUtils.remoteActionsMatch(mCustomCloseAction, appAction)
                    || !appAction.isEnabled()) {
                continue;
            }
            actions.add(remoteToNotificationAction(appAction));
        }
        // 4. Move
        actions.add(getMoveAction());
        // 5. Toggle expansion (if expanded PiP enabled)
        if (mTvPipBoundsState.getDesiredTvExpandedAspectRatio() > 0
                && mTvPipBoundsState.isTvExpandedPipSupported()) {
            actions.add(getToggleAction(mTvPipBoundsState.isTvPipExpanded()));
        }
        return actions.toArray(new Notification.Action[0]);
    }

    private Notification.Action getCloseAction() {
        if (mCustomCloseAction == null) {
            return createSystemAction(R.drawable.pip_ic_close_white, R.string.pip_close,
                    ACTION_CLOSE_PIP);
        } else {
            mNotificationBuilder.setStyle(null);
            return remoteToNotificationAction(mCustomCloseAction, SEMANTIC_ACTION_DELETE);
        }
        mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP,
                mNotificationBuilder.build());
    }

    private boolean updateMediaControllerMetadata(MediaMetadata metadata) {
        String title = null;
        Bitmap art = null;
        if (metadata != null) {
            title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE);
            if (TextUtils.isEmpty(title)) {
                title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE);
    private Notification.Action getFullscreenAction() {
        return createSystemAction(R.drawable.pip_ic_fullscreen_white,
                R.string.pip_fullscreen, ACTION_FULLSCREEN);
    }
            art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
            if (art == null) {
                art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART);

    private Notification.Action getMoveAction() {
        return createSystemAction(R.drawable.pip_ic_move_white, R.string.pip_move,
                ACTION_MOVE_PIP);
    }

    /**
     * Called by {@link TvPipController} when the configuration is changed.
     */
    void onConfigurationChanged(Context context) {
        mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title);
        updateNotificationContent();
    }

        if (TextUtils.equals(title, mMediaTitle) && Objects.equals(art, mArt)) {
            return false;
    void updateExpansionState() {
        updateNotificationContent();
    }

        mMediaTitle = title;
        mArt = art;
    private void updateNotificationContent() {
        if (mPackageManager == null || !mIsNotificationShown) {
            return;
        }

        return true;
        Notification.Action[] actions = getNotificationActions();
        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                "%s: update(), title: %s, subtitle: %s, mediaSessionToken: %s, #actions: %s", TAG,
                getNotificationTitle(), mPipSubtitle, mMediaSessionToken, actions.length);
        for (Notification.Action action : actions) {
            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: action: %s", TAG,
                    action.toString());
        }

        mNotificationBuilder
                .setWhen(System.currentTimeMillis())
                .setContentTitle(getNotificationTitle())
                .setContentText(mPipSubtitle)
                .setSubText(getApplicationLabel(mPackageName))
                .setActions(actions);
        setPipIcon();

        Bundle extras = new Bundle();
        extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken);
        mNotificationBuilder.setExtras(extras);

        // TvExtender not recognized if not set last.
        mNotificationBuilder.extend(new Notification.TvExtender()
                .setContentIntent(createPendingIntent(mContext, ACTION_SHOW_PIP_MENU))
                .setDeleteIntent(createPendingIntent(mContext, ACTION_CLOSE_PIP)));
        mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP,
                mNotificationBuilder.build());
    }

    private String getNotificationTitle() {
        if (!TextUtils.isEmpty(mMediaTitle)) {
            return mMediaTitle;
        if (!TextUtils.isEmpty(mPipTitle)) {
            return mPipTitle;
        }

        final String applicationTitle = getApplicationLabel(mPackageName);
        if (!TextUtils.isEmpty(applicationTitle)) {
            return applicationTitle;
        }

        return mDefaultTitle;
    }

@@ -214,10 +362,37 @@ public class TvPipNotificationController {
        }
    }

    private void setPipIcon() {
        if (mActivityIcon != null) {
            mNotificationBuilder.setLargeIcon(mActivityIcon);
            return;
        }
        // Fallback: Picture-in-Picture icon
        mNotificationBuilder.setLargeIcon(Icon.createWithResource(mContext, R.drawable.pip_icon));
    }

    private Bitmap getActivityIcon() {
        if (mContext == null) return null;
        ComponentName componentName = PipUtils.getTopPipActivity(mContext).first;
        if (componentName == null) return null;

        Drawable drawable;
        try {
            drawable = mPackageManager.getActivityIcon(componentName);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
        int width = mContext.getResources().getDimensionPixelSize(
                android.R.dimen.notification_large_icon_width);
        int height = mContext.getResources().getDimensionPixelSize(
                android.R.dimen.notification_large_icon_height);
        return ImageUtils.buildScaledBitmap(drawable, width, height, /* allowUpscaling */ true);
    }

    private static PendingIntent createPendingIntent(Context context, String action) {
        return PendingIntent.getBroadcast(context, 0,
                new Intent(action).setPackage(context.getPackageName()),
                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
    }

    private class ActionBroadcastReceiver extends BroadcastReceiver {
@@ -228,6 +403,7 @@ public class TvPipNotificationController {
            mIntentFilter.addAction(ACTION_SHOW_PIP_MENU);
            mIntentFilter.addAction(ACTION_MOVE_PIP);
            mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP);
            mIntentFilter.addAction(ACTION_FULLSCREEN);
        }
        boolean mRegistered = false;

@@ -249,10 +425,8 @@ public class TvPipNotificationController {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            if (DEBUG) {
            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                    "%s: on(Broadcast)Receive(), action=%s", TAG, action);
            }

            if (ACTION_SHOW_PIP_MENU.equals(action)) {
                mDelegate.showPictureInPictureMenu();
@@ -262,14 +436,21 @@ public class TvPipNotificationController {
                mDelegate.enterPipMovementMenu();
            } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) {
                mDelegate.togglePipExpansion();
            } else if (ACTION_FULLSCREEN.equals(action)) {
                mDelegate.movePipToFullscreen();
            }
        }
    }

    interface Delegate {
        void showPictureInPictureMenu();

        void closePip();

        void enterPipMovementMenu();

        void togglePipExpansion();

        void movePipToFullscreen();
    }
}