summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/system-current.txt4
-rw-r--r--core/api/test-current.txt3
-rw-r--r--core/java/android/app/Notification.java73
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaData.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt18
-rwxr-xr-xservices/core/java/com/android/server/notification/NotificationManagerService.java17
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java59
12 files changed, 244 insertions, 41 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index e1d723aa480f..f478fc50787f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -848,6 +848,10 @@ package android.app {
field public static final int SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY = 11; // 0xb
}
+ public static class Notification.MediaStyle extends android.app.Notification.Style {
+ method @NonNull @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public android.app.Notification.MediaStyle setRemotePlaybackInfo(@NonNull CharSequence, @DrawableRes int, @Nullable android.app.PendingIntent);
+ }
+
public static final class Notification.TvExtender implements android.app.Notification.Extender {
ctor public Notification.TvExtender();
ctor public Notification.TvExtender(android.app.Notification);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 4132c64a44c9..5fd5d317b8d6 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -299,6 +299,9 @@ package android.app {
public class Notification implements android.os.Parcelable {
method public boolean shouldShowForegroundImmediately();
+ field public static final String EXTRA_MEDIA_REMOTE_DEVICE = "android.mediaRemoteDevice";
+ field public static final String EXTRA_MEDIA_REMOTE_ICON = "android.mediaRemoteIcon";
+ field public static final String EXTRA_MEDIA_REMOTE_INTENT = "android.mediaRemoteIntent";
}
public final class NotificationChannel implements android.os.Parcelable {
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 6a147720f8e9..78db59b5c13b 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1330,6 +1330,32 @@ public class Notification implements Parcelable
public static final String EXTRA_MEDIA_SESSION = "android.mediaSession";
/**
+ * {@link #extras} key: A {@code CharSequence} name of a remote device used for a media session
+ * associated with a {@link Notification.MediaStyle} notification. This will show in the media
+ * controls output switcher instead of the local device name.
+ * @hide
+ */
+ @TestApi
+ public static final String EXTRA_MEDIA_REMOTE_DEVICE = "android.mediaRemoteDevice";
+
+ /**
+ * {@link #extras} key: A {@code int} resource ID for an icon that should show in the output
+ * switcher of the media controls for a {@link Notification.MediaStyle} notification.
+ * @hide
+ */
+ @TestApi
+ public static final String EXTRA_MEDIA_REMOTE_ICON = "android.mediaRemoteIcon";
+
+ /**
+ * {@link #extras} key: A {@code PendingIntent} that will replace the default action for the
+ * media controls output switcher chip, associated with a {@link Notification.MediaStyle}
+ * notification. This should launch an activity.
+ * @hide
+ */
+ @TestApi
+ public static final String EXTRA_MEDIA_REMOTE_INTENT = "android.mediaRemoteIntent";
+
+ /**
* {@link #extras} key: the indices of actions to be shown in the compact view,
* as supplied to (e.g.) {@link MediaStyle#setShowActionsInCompactView(int...)}.
*/
@@ -8943,6 +8969,9 @@ public class Notification implements Parcelable
private int[] mActionsToShowInCompact = null;
private MediaSession.Token mToken;
+ private CharSequence mDeviceName;
+ private int mDeviceIcon;
+ private PendingIntent mDeviceIntent;
public MediaStyle() {
}
@@ -8976,6 +9005,32 @@ public class Notification implements Parcelable
}
/**
+ * For media notifications associated with playback on a remote device, provide device
+ * information that will replace the default values for the output switcher chip on the
+ * media control, as well as an intent to use when the output switcher chip is tapped,
+ * on devices where this is supported.
+ *
+ * @param deviceName The name of the remote device to display
+ * @param iconResource Icon resource representing the device
+ * @param chipIntent PendingIntent to send when the output switcher is tapped. May be
+ * {@code null}, in which case the output switcher will be disabled.
+ * This intent should open an Activity or it will be ignored.
+ * @return MediaStyle
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL)
+ @NonNull
+ public MediaStyle setRemotePlaybackInfo(@NonNull CharSequence deviceName,
+ @DrawableRes int iconResource, @Nullable PendingIntent chipIntent) {
+ mDeviceName = deviceName;
+ mDeviceIcon = iconResource;
+ mDeviceIntent = chipIntent;
+ return this;
+ }
+
+ /**
* @hide
*/
@Override
@@ -9023,6 +9078,15 @@ public class Notification implements Parcelable
if (mActionsToShowInCompact != null) {
extras.putIntArray(EXTRA_COMPACT_ACTIONS, mActionsToShowInCompact);
}
+ if (mDeviceName != null) {
+ extras.putCharSequence(EXTRA_MEDIA_REMOTE_DEVICE, mDeviceName);
+ }
+ if (mDeviceIcon > 0) {
+ extras.putInt(EXTRA_MEDIA_REMOTE_ICON, mDeviceIcon);
+ }
+ if (mDeviceIntent != null) {
+ extras.putParcelable(EXTRA_MEDIA_REMOTE_INTENT, mDeviceIntent);
+ }
}
/**
@@ -9038,6 +9102,15 @@ public class Notification implements Parcelable
if (extras.containsKey(EXTRA_COMPACT_ACTIONS)) {
mActionsToShowInCompact = extras.getIntArray(EXTRA_COMPACT_ACTIONS);
}
+ if (extras.containsKey(EXTRA_MEDIA_REMOTE_DEVICE)) {
+ mDeviceName = extras.getCharSequence(EXTRA_MEDIA_REMOTE_DEVICE);
+ }
+ if (extras.containsKey(EXTRA_MEDIA_REMOTE_ICON)) {
+ mDeviceIcon = extras.getInt(EXTRA_MEDIA_REMOTE_ICON);
+ }
+ if (extras.containsKey(EXTRA_MEDIA_REMOTE_INTENT)) {
+ mDeviceIntent = extras.getParcelable(EXTRA_MEDIA_REMOTE_INTENT);
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index b3e66829a72b..ce7a6977df92 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -371,13 +371,6 @@ public class MediaControlPanel {
// Output switcher chip
ViewGroup seamlessView = mMediaViewHolder.getSeamless();
seamlessView.setVisibility(View.VISIBLE);
- seamlessView.setOnClickListener(
- v -> {
- if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
- mMediaOutputDialogFactory.create(data.getPackageName(), true,
- mMediaViewHolder.getSeamlessButton());
- }
- });
ImageView iconView = mMediaViewHolder.getSeamlessIcon();
TextView deviceName = mMediaViewHolder.getSeamlessText();
final MediaDeviceData device = data.getDevice();
@@ -387,8 +380,8 @@ public class MediaControlPanel {
final float seamlessAlpha = seamlessDisabled ? DISABLED_ALPHA : 1.0f;
mMediaViewHolder.getSeamlessButton().setAlpha(seamlessAlpha);
seamlessView.setEnabled(!seamlessDisabled);
- String deviceString = null;
- if (device != null && device.getEnabled()) {
+ CharSequence deviceString = mContext.getString(R.string.media_seamless_other_device);
+ if (device != null) {
Drawable icon = device.getIcon();
if (icon instanceof AdaptiveIcon) {
AdaptiveIcon aIcon = (AdaptiveIcon) icon;
@@ -399,13 +392,32 @@ public class MediaControlPanel {
}
deviceString = device.getName();
} else {
- // Reset to default
- Log.w(TAG, "Device is null or not enabled: " + device + ", not binding output chip.");
+ // Set to default icon
iconView.setImageResource(R.drawable.ic_media_home_devices);
- deviceString = mContext.getString(R.string.media_seamless_other_device);
}
deviceName.setText(deviceString);
seamlessView.setContentDescription(deviceString);
+ seamlessView.setOnClickListener(
+ v -> {
+ if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+ return;
+ }
+ if (device.getIntent() != null) {
+ if (device.getIntent().isActivity()) {
+ mActivityStarter.startActivity(
+ device.getIntent().getIntent(), true);
+ } else {
+ try {
+ device.getIntent().send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "Device pending intent was canceled");
+ }
+ }
+ } else {
+ mMediaOutputDialogFactory.create(data.getPackageName(), true,
+ mMediaViewHolder.getSeamlessButton());
+ }
+ });
// Dismiss
mMediaViewHolder.getDismissText().setAlpha(isDismissible ? 1 : DISABLED_ALPHA);
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
index 4b8dfdee040e..500e82efdb0a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
@@ -164,8 +164,17 @@ data class MediaAction(
)
/** State of the media device. */
-data class MediaDeviceData(
+data class MediaDeviceData
+@JvmOverloads constructor(
+ /** Whether or not to enable the chip */
val enabled: Boolean,
+
+ /** Device icon to show in the chip */
val icon: Drawable?,
- val name: String?
+
+ /** Device display name */
+ val name: CharSequence?,
+
+ /** Optional intent to override the default output switcher for this control */
+ val intent: PendingIntent? = null
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 240ca3678765..e1ff110f8d73 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -27,8 +27,6 @@ import android.content.ContentResolver
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.graphics.ImageDecoder
import android.graphics.drawable.Icon
@@ -129,7 +127,7 @@ class MediaDataManager(
private val useQsMediaPlayer: Boolean,
private val systemClock: SystemClock,
private val tunerService: TunerService,
- private val mediaFlags: MediaFlags,
+ private val mediaFlags: MediaFlags
) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
companion object {
@@ -170,20 +168,9 @@ class MediaDataManager(
/**
* Check whether this notification is an RCN
- * TODO(b/204910409) implement new API for explicitly declaring this
*/
private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
- val pm = context.packageManager
- try {
- val info = pm.getApplicationInfo(sbn.packageName, PackageManager.MATCH_SYSTEM_ONLY)
- if (info.privateFlags and ApplicationInfo.PRIVATE_FLAG_PRIVILEGED != 0) {
- val extras = sbn.notification.extras
- if (extras.containsKey(Notification.EXTRA_SUBSTITUTE_APP_NAME)) {
- return true
- }
- }
- } catch (e: PackageManager.NameNotFoundException) { }
- return false
+ return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
}
@Inject
@@ -597,6 +584,25 @@ class MediaDataManager(
artist = HybridGroupManager.resolveText(notif)
}
+ // Device name (used for remote cast notifications)
+ var device: MediaDeviceData? = null
+ if (isRemoteCastNotification(sbn)) {
+ val extras = sbn.notification.extras
+ val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
+ val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
+ val deviceIntent = extras.getParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT)
+ as PendingIntent?
+ Log.d(TAG, "$key is RCN for $deviceName")
+
+ if (deviceName != null && deviceIcon > -1) {
+ // Name and icon must be present, but intent may be null
+ val enabled = deviceIntent != null && deviceIntent.isActivity
+ val deviceDrawable = Icon.createWithResource(sbn.packageName, deviceIcon)
+ .loadDrawable(sbn.getPackageContext(context))
+ device = MediaDeviceData(enabled, deviceDrawable, deviceName, deviceIntent)
+ }
+ }
+
// Control buttons
// If flag is enabled and controller has a PlaybackState, create actions from session info
// Otherwise, use the notification actions
@@ -624,7 +630,7 @@ class MediaDataManager(
val active = mediaEntries[key]?.active ?: true
onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app,
smallIcon, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed,
- semanticActions, sbn.packageName, token, notif.contentIntent, null,
+ semanticActions, sbn.packageName, token, notif.contentIntent, device,
active, resumeAction = resumeAction, playbackLocation = playbackLocation,
notificationKey = key, hasCheckedForResume = hasCheckedForResume,
isPlaying = isPlaying, isClearable = sbn.isClearable(),
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index bed254fe8249..934b01c7e888 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -77,6 +77,12 @@ class MediaDeviceManager @Inject constructor(
var entry = entries[key]
if (entry == null || entry?.token != data.token) {
entry?.stop()
+ if (data.device != null) {
+ // If we were already provided device info (e.g. from RCN), keep that and don't
+ // listen for updates, but process once to push updates to listeners
+ processDevice(key, oldKey, data.device)
+ return
+ }
val controller = data.token?.let {
controllerFactory.create(it)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 609291a983ce..708fc915410c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -74,6 +74,7 @@ private const val SESSION_KEY = "SESSION_KEY"
private const val SESSION_ARTIST = "SESSION_ARTIST"
private const val SESSION_TITLE = "SESSION_TITLE"
private const val USER_ID = 0
+private const val DISABLED_DEVICE_NAME = "DISABLED_DEVICE_NAME"
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -131,7 +132,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var session: MediaSession
private val device = MediaDeviceData(true, null, DEVICE_NAME)
- private val disabledDevice = MediaDeviceData(false, null, "Disabled Device")
+ private val disabledDevice = MediaDeviceData(false, null, DISABLED_DEVICE_NAME)
private lateinit var mediaData: MediaData
private val clock = FakeSystemClock()
@@ -396,13 +397,12 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindDisabledDevice() {
seamless.id = 1
- val fallbackString = context.getString(R.string.media_seamless_other_device)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
val state = mediaData.copy(device = disabledDevice)
player.bindPlayer(state, PACKAGE)
assertThat(seamless.isEnabled()).isFalse()
- assertThat(seamlessText.getText()).isEqualTo(fallbackString)
- assertThat(seamless.contentDescription).isEqualTo(fallbackString)
+ assertThat(seamlessText.getText()).isEqualTo(DISABLED_DEVICE_NAME)
+ assertThat(seamless.contentDescription).isEqualTo(DISABLED_DEVICE_NAME)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
index 649ee872c99e..f4fa921703aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -1,6 +1,5 @@
package com.android.systemui.media
-import android.app.Notification
import android.app.Notification.MediaStyle
import android.app.PendingIntent
import android.app.smartspace.SmartspaceAction
@@ -240,15 +239,14 @@ class MediaDataManagerTest : SysuiTestCase() {
@Test
fun testOnNotificationAdded_isRcn_markedRemote() {
- val bundle = Bundle().apply {
- putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, "Remote Cast Notification")
- }
val rcn = SbnBuilder().run {
setPkg("com.android.systemui") // System package
modifyNotification(context).also {
it.setSmallIcon(android.R.drawable.ic_media_pause)
- it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
- it.addExtras(bundle)
+ it.setStyle(MediaStyle().apply {
+ setMediaSession(session.sessionToken)
+ setRemotePlaybackInfo("Remote device", 0, null)
+ })
}
build()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
index 3d59497fd978..ccf5ab753ec9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
@@ -376,6 +376,24 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
verify(mr2, never()).getRoutingSessionForMediaController(eq(controller))
}
+ @Test
+ fun testRemotePlaybackDeviceOverride() {
+ whenever(route.name).thenReturn(DEVICE_NAME)
+ val deviceData = MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null)
+ val mediaDataWithDevice = mediaData.copy(device = deviceData)
+
+ // GIVEN media data that already has a device set
+ manager.onMediaDataLoaded(KEY, null, mediaDataWithDevice)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
+
+ // THEN we keep the device info, and don't register a listener
+ val data = captureDeviceData(KEY)
+ assertThat(data.enabled).isFalse()
+ assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME)
+ verify(lmm, never()).registerCallback(any())
+ }
+
fun captureCallback(): LocalMediaManager.DeviceCallback {
val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java)
verify(lmm).registerCallback(captor.capture())
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index bc38087ebc73..9b7070baa6b6 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -6503,7 +6503,7 @@ public class NotificationManagerService extends SystemService {
@VisibleForTesting
protected void fixNotification(Notification notification, String pkg, String tag, int id,
- int userId) throws NameNotFoundException {
+ int userId) throws NameNotFoundException, RemoteException {
final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(
pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
(userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId);
@@ -6537,6 +6537,21 @@ public class NotificationManagerService extends SystemService {
actions.toArray(notification.actions);
}
+ // Ensure MediaStyle has correct permissions for remote device extras
+ if (notification.isStyle(Notification.MediaStyle.class)) {
+ int hasMediaContentControlPermission = mPackageManager.checkPermission(
+ android.Manifest.permission.MEDIA_CONTENT_CONTROL, pkg, userId);
+ if (hasMediaContentControlPermission != PERMISSION_GRANTED) {
+ notification.extras.remove(Notification.EXTRA_MEDIA_REMOTE_DEVICE);
+ notification.extras.remove(Notification.EXTRA_MEDIA_REMOTE_ICON);
+ notification.extras.remove(Notification.EXTRA_MEDIA_REMOTE_INTENT);
+ if (DBG) {
+ Slog.w(TAG, "Package " + pkg + ": Use of setRemotePlayback requires the "
+ + "MEDIA_CONTENT_CONTROL permission");
+ }
+ }
+ }
+
// Remote views? Are they too big?
checkRemoteViews(pkg, tag, id, notification);
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 9f92294135c0..976b5e178be1 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -102,6 +102,7 @@ import static org.mockito.Mockito.when;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
+import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AlarmManager;
@@ -241,6 +242,7 @@ import java.util.function.Consumer;
@SmallTest
@RunWith(AndroidTestingRunner.class)
+@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
@RunWithLooper
public class NotificationManagerServiceTest extends UiServiceTestCase {
private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId";
@@ -4003,6 +4005,63 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ public void testMediaStyleRemote_hasPermission() throws RemoteException {
+ String deviceName = "device";
+ when(mPackageManager.checkPermission(
+ eq(android.Manifest.permission.MEDIA_CONTENT_CONTROL), any(), anyInt()))
+ .thenReturn(PERMISSION_GRANTED);
+ Notification.MediaStyle style = new Notification.MediaStyle();
+ style.setRemotePlaybackInfo(deviceName, 0, null);
+ Notification.Builder nb = new Notification.Builder(mContext,
+ mTestNotificationChannel.getId())
+ .setStyle(style);
+
+ StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+ "testMediaStyleRemoteHasPermission", mUid, 0,
+ nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+ NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, sbn.getTag(),
+ nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+ waitForIdle();
+
+ NotificationRecord posted = mService.findNotificationLocked(
+ PKG, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getUserId());
+ Bundle extras = posted.getNotification().extras;
+
+ assertTrue(extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE));
+ assertEquals(deviceName, extras.getString(Notification.EXTRA_MEDIA_REMOTE_DEVICE));
+ }
+
+ @Test
+ public void testMediaStyleRemote_noPermission() throws RemoteException {
+ String deviceName = "device";
+ when(mPackageManager.checkPermission(
+ eq(android.Manifest.permission.MEDIA_CONTENT_CONTROL), any(), anyInt()))
+ .thenReturn(PERMISSION_DENIED);
+ Notification.MediaStyle style = new Notification.MediaStyle();
+ style.setRemotePlaybackInfo(deviceName, 0, null);
+ Notification.Builder nb = new Notification.Builder(mContext,
+ mTestNotificationChannel.getId())
+ .setStyle(style);
+
+ StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+ "testMediaStyleRemoteNoPermission", mUid, 0,
+ nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+ NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
+
+ mBinderService.enqueueNotificationWithTag(PKG, PKG, sbn.getTag(),
+ nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+ waitForIdle();
+
+ NotificationRecord posted = mService.findNotificationLocked(
+ PKG, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getUserId());
+
+ assertFalse(posted.getNotification().extras
+ .containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE));
+ }
+
+ @Test
public void testGetNotificationCountLocked() {
String sampleTagToExclude = null;
int sampleIdToExclude = 0;