summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Beth Thibodeau <ethibodeau@google.com> 2022-02-08 12:56:42 -0500
committer Beth Thibodeau <ethibodeau@google.com> 2022-02-10 11:00:00 -0500
commitded4a734d163ed022e84940c3786cf99a80b0f06 (patch)
tree096b13fb1f0ad07b938956ee97947ffb3c1d1962
parent4353b0b03fbe8f18cd1c994b95791c08a88e9848 (diff)
Add SystemAPI to override default media output switcher
To support RCN use case, allow system apps with the MEDIA_CONTENT_CONTROL permission to declare that their MediaStyle notification is for remote playback, and pass in their own device name, icon, and intent to use in place of the default output switcher that appears on the media controls Bug: 204910409 Test: atest Test: manual with test app Change-Id: Icbfa288c48da9c2ab2fdfdd6b0bfe2b545afaac0
-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;