Loading core/java/com/android/internal/util/ImageUtils.java +15 −1 Original line number Diff line number Diff line Loading @@ -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; } Loading @@ -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); Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java +4 −1 Original line number Diff line number Diff line Loading @@ -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 Loading libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java +46 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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; } Loading Loading @@ -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) } Loading @@ -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)); } } } libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +2 −0 Original line number Diff line number Diff line Loading @@ -293,6 +293,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding); mTvPipBoundsState.setTvPipExpanded(expanding); mPipNotificationController.updateExpansionState(); updatePinnedStackBounds(); } Loading libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +248 −67 Original line number Diff line number Diff line Loading @@ -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"; Loading @@ -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; Loading @@ -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."); Loading @@ -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; } Loading @@ -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 { Loading @@ -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; Loading @@ -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(); Loading @@ -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(); } } Loading
core/java/com/android/internal/util/ImageUtils.java +15 −1 Original line number Diff line number Diff line Loading @@ -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; } Loading @@ -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); Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java +4 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java +46 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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; } Loading Loading @@ -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) } Loading @@ -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)); } } }
libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +2 −0 Original line number Diff line number Diff line Loading @@ -293,6 +293,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding); mTvPipBoundsState.setTvPipExpanded(expanding); mPipNotificationController.updateExpansionState(); updatePinnedStackBounds(); } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +248 −67 Original line number Diff line number Diff line Loading @@ -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"; Loading @@ -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; Loading @@ -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."); Loading @@ -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; } Loading @@ -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 { Loading @@ -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; Loading @@ -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(); Loading @@ -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(); } }