Merge "Add more information to the TV PiP notification" into tm-dev am: b6de832709
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17197863 Change-Id: I04f8e24a88d9179a64e8ee7a0df23d788865dd83 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
committed by
Automerger Merge Worker
commit
5bfd8f5d2b
@@ -137,6 +137,18 @@ public class ImageUtils {
|
|||||||
*/
|
*/
|
||||||
public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth,
|
public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth,
|
||||||
int maxHeight) {
|
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) {
|
if (drawable == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -155,7 +167,9 @@ public class ImageUtils {
|
|||||||
// a large notification icon if necessary
|
// a large notification icon if necessary
|
||||||
float ratio = Math.min((float) maxWidth / (float) originalWidth,
|
float ratio = Math.min((float) maxWidth / (float) originalWidth,
|
||||||
(float) maxHeight / (float) originalHeight);
|
(float) maxHeight / (float) originalHeight);
|
||||||
ratio = Math.min(1.0f, ratio);
|
if (!allowUpscaling) {
|
||||||
|
ratio = Math.min(1.0f, ratio);
|
||||||
|
}
|
||||||
int scaledWidth = (int) (ratio * originalWidth);
|
int scaledWidth = (int) (ratio * originalWidth);
|
||||||
int scaledHeight = (int) (ratio * originalHeight);
|
int scaledHeight = (int) (ratio * originalHeight);
|
||||||
Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888);
|
Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888);
|
||||||
|
|||||||
@@ -143,8 +143,11 @@ public abstract class TvPipModule {
|
|||||||
@Provides
|
@Provides
|
||||||
static TvPipNotificationController provideTvPipNotificationController(Context context,
|
static TvPipNotificationController provideTvPipNotificationController(Context context,
|
||||||
PipMediaController pipMediaController,
|
PipMediaController pipMediaController,
|
||||||
|
PipParamsChangedForwarder pipParamsChangedForwarder,
|
||||||
|
TvPipBoundsState tvPipBoundsState,
|
||||||
@ShellMainThread Handler mainHandler) {
|
@ShellMainThread Handler mainHandler) {
|
||||||
return new TvPipNotificationController(context, pipMediaController, mainHandler);
|
return new TvPipNotificationController(context, pipMediaController,
|
||||||
|
pipParamsChangedForwarder, tvPipBoundsState, mainHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@WMSingleton
|
@WMSingleton
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import android.content.IntentFilter;
|
|||||||
import android.graphics.drawable.Icon;
|
import android.graphics.drawable.Icon;
|
||||||
import android.media.MediaMetadata;
|
import android.media.MediaMetadata;
|
||||||
import android.media.session.MediaController;
|
import android.media.session.MediaController;
|
||||||
|
import android.media.session.MediaSession;
|
||||||
import android.media.session.MediaSessionManager;
|
import android.media.session.MediaSessionManager;
|
||||||
import android.media.session.PlaybackState;
|
import android.media.session.PlaybackState;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@@ -64,7 +65,7 @@ public class PipMediaController {
|
|||||||
*/
|
*/
|
||||||
public interface ActionListener {
|
public interface ActionListener {
|
||||||
/**
|
/**
|
||||||
* Called when the media actions changes.
|
* Called when the media actions changed.
|
||||||
*/
|
*/
|
||||||
void onMediaActionsChanged(List<RemoteAction> actions);
|
void onMediaActionsChanged(List<RemoteAction> actions);
|
||||||
}
|
}
|
||||||
@@ -74,11 +75,21 @@ public class PipMediaController {
|
|||||||
*/
|
*/
|
||||||
public interface MetadataListener {
|
public interface MetadataListener {
|
||||||
/**
|
/**
|
||||||
* Called when the media metadata changes.
|
* Called when the media metadata changed.
|
||||||
*/
|
*/
|
||||||
void onMediaMetadataChanged(MediaMetadata metadata);
|
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 Context mContext;
|
||||||
private final Handler mMainHandler;
|
private final Handler mMainHandler;
|
||||||
private final HandlerExecutor mHandlerExecutor;
|
private final HandlerExecutor mHandlerExecutor;
|
||||||
@@ -133,6 +144,7 @@ public class PipMediaController {
|
|||||||
|
|
||||||
private final ArrayList<ActionListener> mActionListeners = new ArrayList<>();
|
private final ArrayList<ActionListener> mActionListeners = new ArrayList<>();
|
||||||
private final ArrayList<MetadataListener> mMetadataListeners = new ArrayList<>();
|
private final ArrayList<MetadataListener> mMetadataListeners = new ArrayList<>();
|
||||||
|
private final ArrayList<TokenListener> mTokenListeners = new ArrayList<>();
|
||||||
|
|
||||||
public PipMediaController(Context context, Handler mainHandler) {
|
public PipMediaController(Context context, Handler mainHandler) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
@@ -204,6 +216,31 @@ public class PipMediaController {
|
|||||||
mMetadataListeners.remove(listener);
|
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() {
|
private MediaMetadata getMediaMetadata() {
|
||||||
return mMediaController != null ? mMediaController.getMetadata() : null;
|
return mMediaController != null ? mMediaController.getMetadata() : null;
|
||||||
}
|
}
|
||||||
@@ -294,6 +331,7 @@ public class PipMediaController {
|
|||||||
}
|
}
|
||||||
notifyActionsChanged();
|
notifyActionsChanged();
|
||||||
notifyMetadataChanged(getMediaMetadata());
|
notifyMetadataChanged(getMediaMetadata());
|
||||||
|
notifyTokenChanged(getToken());
|
||||||
|
|
||||||
// TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
|
// 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));
|
mMetadataListeners.forEach(l -> l.onMediaMetadataChanged(metadata));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void notifyTokenChanged(MediaSession.Token token) {
|
||||||
|
if (!mTokenListeners.isEmpty()) {
|
||||||
|
mTokenListeners.forEach(l -> l.onMediaSessionTokenChanged(token));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,6 +293,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
|
|||||||
}
|
}
|
||||||
mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
|
mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding);
|
||||||
mTvPipBoundsState.setTvPipExpanded(expanding);
|
mTvPipBoundsState.setTvPipExpanded(expanding);
|
||||||
|
mPipNotificationController.updateExpansionState();
|
||||||
|
|
||||||
updatePinnedStackBounds();
|
updatePinnedStackBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,36 +16,47 @@
|
|||||||
|
|
||||||
package com.android.wm.shell.pip.tv;
|
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.Notification;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
|
import android.app.RemoteAction;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.Bitmap;
|
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.os.Handler;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
|
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
|
||||||
import com.android.internal.protolog.common.ProtoLog;
|
import com.android.internal.protolog.common.ProtoLog;
|
||||||
|
import com.android.internal.util.ImageUtils;
|
||||||
import com.android.wm.shell.R;
|
import com.android.wm.shell.R;
|
||||||
import com.android.wm.shell.pip.PipMediaController;
|
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 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.
|
* 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
|
* <p>Once it's created, it will manage the PiP notification UI by itself except for handling
|
||||||
* configuration changes.
|
* configuration changes and user initiated expanded PiP toggling.
|
||||||
*/
|
*/
|
||||||
public class TvPipNotificationController {
|
public class TvPipNotificationController {
|
||||||
private static final String TAG = "TvPipNotification";
|
private static final String TAG = "TvPipNotification";
|
||||||
private static final boolean DEBUG = TvPipController.DEBUG;
|
|
||||||
|
|
||||||
// Referenced in com.android.systemui.util.NotificationChannels.
|
// Referenced in com.android.systemui.util.NotificationChannels.
|
||||||
public static final String NOTIFICATION_CHANNEL = "TVPIP";
|
public static final String NOTIFICATION_CHANNEL = "TVPIP";
|
||||||
@@ -60,6 +71,8 @@ public class TvPipNotificationController {
|
|||||||
"com.android.wm.shell.pip.tv.notification.action.MOVE_PIP";
|
"com.android.wm.shell.pip.tv.notification.action.MOVE_PIP";
|
||||||
private static final String ACTION_TOGGLE_EXPANDED_PIP =
|
private static final String ACTION_TOGGLE_EXPANDED_PIP =
|
||||||
"com.android.wm.shell.pip.tv.notification.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 Context mContext;
|
||||||
private final PackageManager mPackageManager;
|
private final PackageManager mPackageManager;
|
||||||
@@ -68,44 +81,88 @@ public class TvPipNotificationController {
|
|||||||
private final ActionBroadcastReceiver mActionBroadcastReceiver;
|
private final ActionBroadcastReceiver mActionBroadcastReceiver;
|
||||||
private final Handler mMainHandler;
|
private final Handler mMainHandler;
|
||||||
private Delegate mDelegate;
|
private Delegate mDelegate;
|
||||||
|
private final TvPipBoundsState mTvPipBoundsState;
|
||||||
|
|
||||||
private String mDefaultTitle;
|
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. */
|
/** Package name for the application that owns PiP window. */
|
||||||
private String mPackageName;
|
private String mPackageName;
|
||||||
private boolean mNotified;
|
|
||||||
private String mMediaTitle;
|
private boolean mIsNotificationShown;
|
||||||
private Bitmap mArt;
|
private String mPipTitle;
|
||||||
|
private String mPipSubtitle;
|
||||||
|
|
||||||
|
private Bitmap mActivityIcon;
|
||||||
|
|
||||||
public TvPipNotificationController(Context context, PipMediaController pipMediaController,
|
public TvPipNotificationController(Context context, PipMediaController pipMediaController,
|
||||||
|
PipParamsChangedForwarder pipParamsChangedForwarder, TvPipBoundsState tvPipBoundsState,
|
||||||
Handler mainHandler) {
|
Handler mainHandler) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mPackageManager = context.getPackageManager();
|
mPackageManager = context.getPackageManager();
|
||||||
mNotificationManager = context.getSystemService(NotificationManager.class);
|
mNotificationManager = context.getSystemService(NotificationManager.class);
|
||||||
mMainHandler = mainHandler;
|
mMainHandler = mainHandler;
|
||||||
|
mTvPipBoundsState = tvPipBoundsState;
|
||||||
|
|
||||||
mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL)
|
mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL)
|
||||||
.setLocalOnly(true)
|
.setLocalOnly(true)
|
||||||
.setOngoing(false)
|
.setOngoing(true)
|
||||||
.setCategory(Notification.CATEGORY_SYSTEM)
|
.setCategory(Notification.CATEGORY_SYSTEM)
|
||||||
.setShowWhen(true)
|
.setShowWhen(true)
|
||||||
.setSmallIcon(R.drawable.pip_icon)
|
.setSmallIcon(R.drawable.pip_icon)
|
||||||
|
.setAllowSystemGeneratedContextualActions(false)
|
||||||
|
.setContentIntent(createPendingIntent(context, ACTION_FULLSCREEN))
|
||||||
|
.setDeleteIntent(getCloseAction().actionIntent)
|
||||||
.extend(new Notification.TvExtender()
|
.extend(new Notification.TvExtender()
|
||||||
.setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU))
|
.setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU))
|
||||||
.setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP)));
|
.setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP)));
|
||||||
|
|
||||||
mActionBroadcastReceiver = new ActionBroadcastReceiver();
|
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);
|
onConfigurationChanged(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setDelegate(Delegate delegate) {
|
void setDelegate(Delegate delegate) {
|
||||||
if (DEBUG) {
|
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setDelegate(), delegate=%s",
|
||||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
TAG, delegate);
|
||||||
"%s: setDelegate(), delegate=%s", TAG, delegate);
|
|
||||||
}
|
|
||||||
if (mDelegate != null) {
|
if (mDelegate != null) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"The delegate has already been set and should not change.");
|
"The delegate has already been set and should not change.");
|
||||||
@@ -118,90 +175,181 @@ public class TvPipNotificationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void show(String packageName) {
|
void show(String packageName) {
|
||||||
|
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: show %s", TAG, packageName);
|
||||||
if (mDelegate == null) {
|
if (mDelegate == null) {
|
||||||
throw new IllegalStateException("Delegate is not set.");
|
throw new IllegalStateException("Delegate is not set.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mIsNotificationShown = true;
|
||||||
mPackageName = packageName;
|
mPackageName = packageName;
|
||||||
update();
|
mActivityIcon = getActivityIcon();
|
||||||
mActionBroadcastReceiver.register();
|
mActionBroadcastReceiver.register();
|
||||||
|
|
||||||
|
updateNotificationContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
void dismiss() {
|
void dismiss() {
|
||||||
mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP);
|
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: dismiss()", TAG);
|
||||||
mNotified = false;
|
|
||||||
|
mIsNotificationShown = false;
|
||||||
mPackageName = null;
|
mPackageName = null;
|
||||||
mActionBroadcastReceiver.unregister();
|
mActionBroadcastReceiver.unregister();
|
||||||
|
|
||||||
|
mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMediaMetadataChanged(MediaMetadata metadata) {
|
private Notification.Action getToggleAction(boolean expanded) {
|
||||||
if (updateMediaControllerMetadata(metadata) && mNotified) {
|
if (expanded) {
|
||||||
// update notification
|
return createSystemAction(R.drawable.pip_ic_collapse,
|
||||||
update();
|
R.string.pip_collapse, ACTION_TOGGLE_EXPANDED_PIP);
|
||||||
|
} else {
|
||||||
|
return createSystemAction(R.drawable.pip_ic_expand, R.string.pip_expand,
|
||||||
|
ACTION_TOGGLE_EXPANDED_PIP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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 {
|
||||||
|
return remoteToNotificationAction(mCustomCloseAction, SEMANTIC_ACTION_DELETE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Notification.Action getFullscreenAction() {
|
||||||
|
return createSystemAction(R.drawable.pip_ic_fullscreen_white,
|
||||||
|
R.string.pip_fullscreen, ACTION_FULLSCREEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Notification.Action getMoveAction() {
|
||||||
|
return createSystemAction(R.drawable.pip_ic_move_white, R.string.pip_move,
|
||||||
|
ACTION_MOVE_PIP);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called by {@link PipController} when the configuration is changed.
|
* Called by {@link TvPipController} when the configuration is changed.
|
||||||
*/
|
*/
|
||||||
void onConfigurationChanged(Context context) {
|
void onConfigurationChanged(Context context) {
|
||||||
mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title);
|
mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title);
|
||||||
if (mNotified) {
|
updateNotificationContent();
|
||||||
// Update the notification.
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update() {
|
void updateExpansionState() {
|
||||||
mNotified = true;
|
updateNotificationContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNotificationContent() {
|
||||||
|
if (mPackageManager == null || !mIsNotificationShown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
mNotificationBuilder
|
||||||
.setWhen(System.currentTimeMillis())
|
.setWhen(System.currentTimeMillis())
|
||||||
.setContentTitle(getNotificationTitle());
|
.setContentTitle(getNotificationTitle())
|
||||||
if (mArt != null) {
|
.setContentText(mPipSubtitle)
|
||||||
mNotificationBuilder.setStyle(new Notification.BigPictureStyle()
|
.setSubText(getApplicationLabel(mPackageName))
|
||||||
.bigPicture(mArt));
|
.setActions(actions);
|
||||||
} else {
|
setPipIcon();
|
||||||
mNotificationBuilder.setStyle(null);
|
|
||||||
}
|
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,
|
mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP,
|
||||||
mNotificationBuilder.build());
|
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);
|
|
||||||
}
|
|
||||||
art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
|
|
||||||
if (art == null) {
|
|
||||||
art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.equals(title, mMediaTitle) && Objects.equals(art, mArt)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mMediaTitle = title;
|
|
||||||
mArt = art;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private String getNotificationTitle() {
|
private String getNotificationTitle() {
|
||||||
if (!TextUtils.isEmpty(mMediaTitle)) {
|
if (!TextUtils.isEmpty(mPipTitle)) {
|
||||||
return mMediaTitle;
|
return mPipTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String applicationTitle = getApplicationLabel(mPackageName);
|
final String applicationTitle = getApplicationLabel(mPackageName);
|
||||||
if (!TextUtils.isEmpty(applicationTitle)) {
|
if (!TextUtils.isEmpty(applicationTitle)) {
|
||||||
return applicationTitle;
|
return applicationTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mDefaultTitle;
|
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) {
|
private static PendingIntent createPendingIntent(Context context, String action) {
|
||||||
return PendingIntent.getBroadcast(context, 0,
|
return PendingIntent.getBroadcast(context, 0,
|
||||||
new Intent(action).setPackage(context.getPackageName()),
|
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 {
|
private class ActionBroadcastReceiver extends BroadcastReceiver {
|
||||||
@@ -228,6 +403,7 @@ public class TvPipNotificationController {
|
|||||||
mIntentFilter.addAction(ACTION_SHOW_PIP_MENU);
|
mIntentFilter.addAction(ACTION_SHOW_PIP_MENU);
|
||||||
mIntentFilter.addAction(ACTION_MOVE_PIP);
|
mIntentFilter.addAction(ACTION_MOVE_PIP);
|
||||||
mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP);
|
mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP);
|
||||||
|
mIntentFilter.addAction(ACTION_FULLSCREEN);
|
||||||
}
|
}
|
||||||
boolean mRegistered = false;
|
boolean mRegistered = false;
|
||||||
|
|
||||||
@@ -249,10 +425,8 @@ public class TvPipNotificationController {
|
|||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
final String action = intent.getAction();
|
final String action = intent.getAction();
|
||||||
if (DEBUG) {
|
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
||||||
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
|
"%s: on(Broadcast)Receive(), action=%s", TAG, action);
|
||||||
"%s: on(Broadcast)Receive(), action=%s", TAG, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ACTION_SHOW_PIP_MENU.equals(action)) {
|
if (ACTION_SHOW_PIP_MENU.equals(action)) {
|
||||||
mDelegate.showPictureInPictureMenu();
|
mDelegate.showPictureInPictureMenu();
|
||||||
@@ -262,14 +436,21 @@ public class TvPipNotificationController {
|
|||||||
mDelegate.enterPipMovementMenu();
|
mDelegate.enterPipMovementMenu();
|
||||||
} else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) {
|
} else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) {
|
||||||
mDelegate.togglePipExpansion();
|
mDelegate.togglePipExpansion();
|
||||||
|
} else if (ACTION_FULLSCREEN.equals(action)) {
|
||||||
|
mDelegate.movePipToFullscreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Delegate {
|
interface Delegate {
|
||||||
void showPictureInPictureMenu();
|
void showPictureInPictureMenu();
|
||||||
|
|
||||||
void closePip();
|
void closePip();
|
||||||
|
|
||||||
void enterPipMovementMenu();
|
void enterPipMovementMenu();
|
||||||
|
|
||||||
void togglePipExpansion();
|
void togglePipExpansion();
|
||||||
|
|
||||||
|
void movePipToFullscreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user