// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.media.ui;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.os.Build;
import android.os.IBinder;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.NotificationCompat;
import android.support.v7.media.MediaRouter;
import android.text.TextUtils;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.KeyEvent;
import org.chromium.base.ContextUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.content_public.common.MediaMetadata;
import javax.annotation.Nullable;
/**
* A class for notifications that provide information and optional media controls for a given media.
* Internally implements a Service for transforming notification Intents into
* {@link MediaNotificationListener} calls for all registered listeners.
* There's one service started for a distinct notification id.
*/
public class MediaNotificationManager {
private static final String TAG = "MediaNotification";
// MediaStyle large icon size for pre-N.
private static final int PRE_N_LARGE_ICON_SIZE_DP = 128;
// MediaStyle large icon size for N.
// TODO(zqzhang): use android.R.dimen.media_notification_expanded_image_max_size when Android
// SDK is rolled to level 24. See https://crbug.com/645059
private static final int N_LARGE_ICON_SIZE_DP = 94;
// We're always used on the UI thread but the LOCK is required by lint when creating the
// singleton.
private static final Object LOCK = new Object();
// Maps the notification ids to their corresponding notification managers.
private static SparseArray<MediaNotificationManager> sManagers;
/**
* Service used to transform intent requests triggered from the notification into
* {@code MediaNotificationListener} callbacks. We have to create a separate derived class for
* each type of notification since one class corresponds to one instance of the service only.
*/
private abstract static class ListenerService extends Service {
private static final String ACTION_PLAY =
"MediaNotificationManager.ListenerService.PLAY";
private static final String ACTION_PAUSE =
"MediaNotificationManager.ListenerService.PAUSE";
private static final String ACTION_STOP =
"MediaNotificationManager.ListenerService.STOP";
private static final String ACTION_SWIPE =
"MediaNotificationManager.ListenerService.SWIPE";
private static final String ACTION_CANCEL =
"MediaNotificationManager.ListenerService.CANCEL";
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
MediaNotificationManager manager = getManager();
if (manager == null) return;
manager.onServiceDestroyed();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (!processIntent(intent)) stopSelf();
return START_NOT_STICKY;
}
@Nullable
protected abstract MediaNotificationManager getManager();
private boolean processIntent(Intent intent) {
if (intent == null) return false;
MediaNotificationManager manager = getManager();
if (manager == null || manager.mMediaNotificationInfo == null) return false;
manager.onServiceStarted(this);
processAction(intent, manager);
return true;
}
private void processAction(Intent intent, MediaNotificationManager manager) {
String action = intent.getAction();
// Before Android L, instead of using the MediaSession callback, the system will fire
// ACTION_MEDIA_BUTTON intents which stores the information about the key event.
if (Intent.ACTION_MEDIA_BUTTON.equals(action)) {
assert Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (event == null) return;
if (event.getAction() != KeyEvent.ACTION_DOWN) return;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_MEDIA_PLAY:
manager.onPlay(
MediaNotificationListener.ACTION_SOURCE_MEDIA_SESSION);
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
manager.onPause(
MediaNotificationListener.ACTION_SOURCE_MEDIA_SESSION);
break;
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
if (manager.mMediaNotificationInfo.isPaused) {
manager.onPlay(MediaNotificationListener.ACTION_SOURCE_MEDIA_SESSION);
} else {
manager.onPause(
MediaNotificationListener.ACTION_SOURCE_MEDIA_SESSION);
}
break;
default:
break;
}
} else if (ACTION_STOP.equals(action)
|| ACTION_SWIPE.equals(action)
|| ACTION_CANCEL.equals(action)) {
manager.onStop(
MediaNotificationListener.ACTION_SOURCE_MEDIA_NOTIFICATION);
stopSelf();
} else if (ACTION_PLAY.equals(action)) {
manager.onPlay(MediaNotificationListener.ACTION_SOURCE_MEDIA_NOTIFICATION);
} else if (ACTION_PAUSE.equals(action)) {
manager.onPause(MediaNotificationListener.ACTION_SOURCE_MEDIA_NOTIFICATION);
} else if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(action)) {
manager.onPause(MediaNotificationListener.ACTION_SOURCE_HEADSET_UNPLUG);
}
}
}
/**
* This class is used internally but have to be public to be able to launch the service.
*/
public static final class PlaybackListenerService extends ListenerService {
private static final int NOTIFICATION_ID = R.id.media_playback_notification;
@Override
public void onCreate() {
super.onCreate();
IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
registerReceiver(mAudioBecomingNoisyReceiver, filter);
}
@Override
public void onDestroy() {
unregisterReceiver(mAudioBecomingNoisyReceiver);
super.onDestroy();
}
@Override
@Nullable
protected MediaNotificationManager getManager() {
return MediaNotificationManager.getManager(NOTIFICATION_ID);
}
private BroadcastReceiver mAudioBecomingNoisyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
return;
}
Intent i = new Intent(context, PlaybackListenerService.class);
i.setAction(intent.getAction());
context.startService(i);
}
};
}
/**
* This class is used internally but have to be public to be able to launch the service.
*/
public static final class PresentationListenerService extends ListenerService {
private static final int NOTIFICATION_ID = R.id.presentation_notification;
@Override
@Nullable
protected MediaNotificationManager getManager() {
return MediaNotificationManager.getManager(NOTIFICATION_ID);
}
}
/**
* This class is used internally but have to be public to be able to launch the service.
*/
public static final class CastListenerService extends ListenerService {
private static final int NOTIFICATION_ID = R.id.remote_notification;
@Override
@Nullable
protected MediaNotificationManager getManager() {
return MediaNotificationManager.getManager(NOTIFICATION_ID);
}
}
// Three classes to specify the right notification id in the intent.
/**
* This class is used internally but have to be public to be able to launch the service.
*/
public static final class PlaybackMediaButtonReceiver extends MediaButtonReceiver {
@Override
public String getServiceClassName() {
return PlaybackListenerService.class.getName();
}
}
/**
* This class is used internally but have to be public to be able to launch the service.
*/
public static final class PresentationMediaButtonReceiver extends MediaButtonReceiver {
@Override
public String getServiceClassName() {
return PresentationListenerService.class.getName();
}
}
/**
* This class is used internally but have to be public to be able to launch the service.
*/
public static final class CastMediaButtonReceiver extends MediaButtonReceiver {
@Override
public String getServiceClassName() {
return CastListenerService.class.getName();
}
}
private Intent createIntent(Context context) {
Intent intent = null;
if (mMediaNotificationInfo.id == PlaybackListenerService.NOTIFICATION_ID) {
intent = new Intent(context, PlaybackListenerService.class);
} else if (mMediaNotificationInfo.id == PresentationListenerService.NOTIFICATION_ID) {
intent = new Intent(context, PresentationListenerService.class);
} else if (mMediaNotificationInfo.id == CastListenerService.NOTIFICATION_ID) {
intent = new Intent(context, CastListenerService.class);
}
return intent;
}
private PendingIntent createPendingIntent(String action) {
assert mService != null;
Intent intent = createIntent(mService).setAction(action);
return PendingIntent.getService(mService, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
}
private String getButtonReceiverClassName() {
if (mMediaNotificationInfo.id == PlaybackListenerService.NOTIFICATION_ID) {
return PlaybackMediaButtonReceiver.class.getName();
}
if (mMediaNotificationInfo.id == PresentationListenerService.NOTIFICATION_ID) {
return PresentationMediaButtonReceiver.class.getName();
}
if (mMediaNotificationInfo.id == CastListenerService.NOTIFICATION_ID) {
return CastMediaButtonReceiver.class.getName();
}
assert false;
return null;
}
/**
* Shows the notification with media controls with the specified media info. Replaces/updates
* the current notification if already showing. Does nothing if |mediaNotificationInfo| hasn't
* changed from the last one.
*
* @param applicationContext context to create the notification with
* @param notificationInfo information to show in the notification
*/
public static void show(Context applicationContext,
MediaNotificationInfo notificationInfo) {
synchronized (LOCK) {
if (sManagers == null) {
sManagers = new SparseArray<MediaNotificationManager>();
}
}
MediaNotificationManager manager = sManagers.get(notificationInfo.id);
if (manager == null) {
manager = new MediaNotificationManager(applicationContext, notificationInfo.id);
sManagers.put(notificationInfo.id, manager);
}
manager.showNotification(notificationInfo);
}
/**
* Hides the notification for the specified tabId and notificationId
*
* @param tabId the id of the tab that showed the notification or invalid tab id.
* @param notificationId the id of the notification to hide for this tab.
*/
public static void hide(int tabId, int notificationId) {
MediaNotificationManager manager = getManager(notificationId);
if (manager == null) return;
manager.hideNotification(tabId);
}
/**
* Hides notifications with the specified id for all tabs if shown.
*
* @param notificationId the id of the notification to hide for all tabs.
*/
public static void clear(int notificationId) {
MediaNotificationManager manager = getManager(notificationId);
if (manager == null) return;
manager.clearNotification();
sManagers.remove(notificationId);
}
/**
* Hides notifications with all known ids for all tabs if shown.
*/
public static void clearAll() {
if (sManagers == null) return;
for (int i = 0; i < sManagers.size(); ++i) {
MediaNotificationManager manager = sManagers.valueAt(i);
manager.clearNotification();
}
sManagers.clear();
}
/**
* Scale a given bitmap to a proper size for display.
* @param icon The bitmap to be resized.
* @return A scaled icon to be used in media notification. Returns null if |icon| is null.
*/
public static Bitmap scaleIconForDisplay(Bitmap icon) {
if (icon == null) return null;
int largeIconSizePx;
if (isRunningN()) {
largeIconSizePx = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, N_LARGE_ICON_SIZE_DP,
ContextUtils.getApplicationContext().getResources().getDisplayMetrics());
} else {
largeIconSizePx = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, PRE_N_LARGE_ICON_SIZE_DP,
ContextUtils.getApplicationContext().getResources().getDisplayMetrics());
}
if (icon.getWidth() > largeIconSizePx || icon.getHeight() > largeIconSizePx) {
return icon.createScaledBitmap(
icon, largeIconSizePx, largeIconSizePx, true /* filter */);
}
return icon;
}
private static MediaNotificationManager getManager(int notificationId) {
if (sManagers == null) return null;
return sManagers.get(notificationId);
}
@VisibleForTesting
static boolean hasManagerForTesting(int notificationId) {
return getManager(notificationId) != null;
}
@VisibleForTesting
@Nullable
static NotificationCompat.Builder getNotificationBuilderForTesting(
int notificationId) {
MediaNotificationManager manager = getManager(notificationId);
if (manager == null) return null;
return manager.mNotificationBuilder;
}
private static boolean isRunningN() {
// TODO(zqzhang): Use Build.VERSION_CODES.N when Android SDK is rolled to level 24.
// See https://crbug.com/645059
return Build.VERSION.CODENAME.equals("N") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M;
}
private final Context mContext;
// ListenerService running for the notification. Only non-null when showing.
private ListenerService mService;
private final String mPlayDescription;
private final String mPauseDescription;
private final String mStopDescription;
private NotificationCompat.Builder mNotificationBuilder;
private Bitmap mNotificationIcon;
private Bitmap mDefaultLargeIcon;
// |mMediaNotificationInfo| should be not null if and only if the notification is showing.
private MediaNotificationInfo mMediaNotificationInfo;
private MediaSessionCompat mMediaSession;
private final MediaSessionCompat.Callback mMediaSessionCallback =
new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
MediaNotificationManager.this.onPlay(
MediaNotificationListener.ACTION_SOURCE_MEDIA_SESSION);
}
@Override
public void onPause() {
MediaNotificationManager.this.onPause(
MediaNotificationListener.ACTION_SOURCE_MEDIA_SESSION);
}
};
private MediaNotificationManager(Context context, int notificationId) {
mContext = context;
mPlayDescription = context.getResources().getString(R.string.accessibility_play);
mPauseDescription = context.getResources().getString(R.string.accessibility_pause);
mStopDescription = context.getResources().getString(R.string.accessibility_stop);
}
/**
* Registers the started {@link Service} with the manager and creates the notification.
*
* @param service the service that was started
*/
private void onServiceStarted(ListenerService service) {
mService = service;
updateNotification();
}
/**
* Handles the service destruction destruction.
*/
private void onServiceDestroyed() {
// Service already detached
if (mService == null) return;
// Notification is not showing
if (mMediaNotificationInfo == null) return;
clear(mMediaNotificationInfo.id);
mNotificationBuilder = null;
mService = null;
}
private void onPlay(int actionSource) {
if (!mMediaNotificationInfo.isPaused) return;
mMediaNotificationInfo.listener.onPlay(actionSource);
}
private void onPause(int actionSource) {
if (mMediaNotificationInfo.isPaused) return;
mMediaNotificationInfo.listener.onPause(actionSource);
}
private void onStop(int actionSource) {
mMediaNotificationInfo.listener.onStop(actionSource);
}
private void showNotification(MediaNotificationInfo mediaNotificationInfo) {
if (mediaNotificationInfo.equals(mMediaNotificationInfo)) return;
mMediaNotificationInfo = mediaNotificationInfo;
mContext.startService(createIntent(mContext));
updateNotification();
}
private void clearNotification() {
if (mMediaNotificationInfo == null) return;
NotificationManagerCompat manager = NotificationManagerCompat.from(mContext);
manager.cancel(mMediaNotificationInfo.id);
if (mMediaSession != null) {
mMediaSession.setCallback(null);
mMediaSession.setActive(false);
mMediaSession.release();
mMediaSession = null;
}
mContext.stopService(createIntent(mContext));
mMediaNotificationInfo = null;
}
private void hideNotification(int tabId) {
if (mMediaNotificationInfo == null || tabId != mMediaNotificationInfo.tabId) return;
clearNotification();
}
private MediaMetadataCompat createMetadata() {
MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
mMediaNotificationInfo.metadata.getTitle());
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
mMediaNotificationInfo.origin);
} else {
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE,
mMediaNotificationInfo.metadata.getTitle());
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST,
mMediaNotificationInfo.origin);
}
if (!TextUtils.isEmpty(mMediaNotificationInfo.metadata.getArtist())) {
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST,
mMediaNotificationInfo.metadata.getArtist());
}
if (!TextUtils.isEmpty(mMediaNotificationInfo.metadata.getAlbum())) {
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM,
mMediaNotificationInfo.metadata.getAlbum());
}
return metadataBuilder.build();
}
private void updateNotification() {
if (mService == null) return;
if (mMediaNotificationInfo == null) return;
updateMediaSession();
mNotificationBuilder = new NotificationCompat.Builder(mContext);
setMediaStyleLayoutForNotificationBuilder(mNotificationBuilder);
mNotificationBuilder.setSmallIcon(mMediaNotificationInfo.icon);
mNotificationBuilder.setAutoCancel(false);
mNotificationBuilder.setLocalOnly(true);
if (mMediaNotificationInfo.supportsSwipeAway()) {
mNotificationBuilder.setOngoing(!mMediaNotificationInfo.isPaused);
mNotificationBuilder.setDeleteIntent(createPendingIntent(ListenerService.ACTION_SWIPE));
}
// The intent will currently only be null when using a custom tab.
// TODO(avayvod) work out what we should do in this case. See https://crbug.com/585395.
if (mMediaNotificationInfo.contentIntent != null) {
mNotificationBuilder.setContentIntent(PendingIntent.getActivity(mContext,
mMediaNotificationInfo.tabId, mMediaNotificationInfo.contentIntent,
PendingIntent.FLAG_UPDATE_CURRENT));
// Set FLAG_UPDATE_CURRENT so that the intent extras is updated, otherwise the
// intent extras will stay the same for the same tab.
}
mNotificationBuilder.setVisibility(
mMediaNotificationInfo.isPrivate ? NotificationCompat.VISIBILITY_PRIVATE
: NotificationCompat.VISIBILITY_PUBLIC);
Notification notification = mNotificationBuilder.build();
// We keep the service as a foreground service while the media is playing. When it is not,
// the service isn't stopped but is no longer in foreground, thus at a lower priority.
// While the service is in foreground, the associated notification can't be swipped away.
// Moving it back to background allows the user to remove the notification.
if (mMediaNotificationInfo.supportsSwipeAway() && mMediaNotificationInfo.isPaused) {
mService.stopForeground(false /* removeNotification */);
NotificationManagerCompat manager = NotificationManagerCompat.from(mContext);
manager.notify(mMediaNotificationInfo.id, notification);
} else {
mService.startForeground(mMediaNotificationInfo.id, notification);
}
}
private void updateMediaSession() {
if (!mMediaNotificationInfo.supportsPlayPause()) return;
if (mMediaSession == null) mMediaSession = createMediaSession();
try {
// Tell the MediaRouter about the session, so that Chrome can control the volume
// on the remote cast device (if any).
// Pre-MR1 versions of JB do not have the complete MediaRouter APIs,
// so getting the MediaRouter instance will throw an exception.
MediaRouter.getInstance(mContext).setMediaSessionCompat(mMediaSession);
} catch (NoSuchMethodError e) {
// Do nothing. Chrome can't be casting without a MediaRouter, so there is nothing
// to do here.
}
mMediaSession.setMetadata(createMetadata());
PlaybackStateCompat.Builder playbackStateBuilder =
new PlaybackStateCompat.Builder().setActions(
PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE);
if (mMediaNotificationInfo.isPaused) {
playbackStateBuilder.setState(PlaybackStateCompat.STATE_PAUSED,
PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f);
} else {
// If notification only supports stop, still pretend
playbackStateBuilder.setState(PlaybackStateCompat.STATE_PLAYING,
PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f);
}
mMediaSession.setPlaybackState(playbackStateBuilder.build());
}
private MediaSessionCompat createMediaSession() {
MediaSessionCompat mediaSession = new MediaSessionCompat(
mContext,
mContext.getString(R.string.app_name),
new ComponentName(mContext.getPackageName(),
getButtonReceiverClassName()),
null);
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mediaSession.setCallback(mMediaSessionCallback);
// TODO(mlamouri): the following code is to work around a bug that hopefully
// MediaSessionCompat will handle directly. see b/24051980.
try {
mediaSession.setActive(true);
} catch (NullPointerException e) {
// Some versions of KitKat do not support AudioManager.registerMediaButtonIntent
// with a PendingIntent. They will throw a NullPointerException, in which case
// they should be able to activate a MediaSessionCompat with only transport
// controls.
mediaSession.setActive(false);
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mediaSession.setActive(true);
}
return mediaSession;
}
private void setMediaStyleLayoutForNotificationBuilder(NotificationCompat.Builder builder) {
setMediaStyleNotificationText(builder);
if (!mMediaNotificationInfo.supportsPlayPause()) {
builder.setLargeIcon(null);
} else if (mMediaNotificationInfo.largeIcon != null) {
builder.setLargeIcon(mMediaNotificationInfo.largeIcon);
} else if (!isRunningN()) {
if (mDefaultLargeIcon == null) {
int resourceId = (mMediaNotificationInfo.defaultLargeIcon != 0)
? mMediaNotificationInfo.defaultLargeIcon : R.drawable.audio_playing_square;
mDefaultLargeIcon = scaleIconForDisplay(
BitmapFactory.decodeResource(mContext.getResources(), resourceId));
}
builder.setLargeIcon(mDefaultLargeIcon);
}
// TODO(zqzhang): It's weird that setShowWhen() don't work on K. Calling setWhen() to force
// removing the time.
builder.setShowWhen(false).setWhen(0);
// Only apply MediaStyle when NotificationInfo supports play/pause.
if (mMediaNotificationInfo.supportsPlayPause()) {
NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle();
style.setMediaSession(mMediaSession.getSessionToken());
if (mMediaNotificationInfo.isPaused) {
builder.addAction(R.drawable.ic_vidcontrol_play, mPlayDescription,
createPendingIntent(ListenerService.ACTION_PLAY));
} else {
// If we're here, the notification supports play/pause button and is playing.
builder.addAction(R.drawable.ic_vidcontrol_pause, mPauseDescription,
createPendingIntent(ListenerService.ACTION_PAUSE));
}
style.setShowActionsInCompactView(0);
style.setCancelButtonIntent(createPendingIntent(ListenerService.ACTION_CANCEL));
style.setShowCancelButton(true);
builder.setStyle(style);
}
if (mMediaNotificationInfo.supportsStop()) {
builder.addAction(R.drawable.ic_vidcontrol_stop, mStopDescription,
createPendingIntent(ListenerService.ACTION_STOP));
}
}
private Bitmap drawableToBitmap(Drawable drawable) {
if (!(drawable instanceof BitmapDrawable)) return null;
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
return bitmapDrawable.getBitmap();
}
private void setMediaStyleNotificationText(NotificationCompat.Builder builder) {
builder.setContentTitle(mMediaNotificationInfo.metadata.getTitle());
String artistAndAlbumText = getArtistAndAlbumText(mMediaNotificationInfo.metadata);
if (isRunningN() || !artistAndAlbumText.isEmpty()) {
builder.setContentText(artistAndAlbumText);
builder.setSubText(mMediaNotificationInfo.origin);
} else {
// Leaving ContentText empty looks bad, so move origin up to the ContentText.
builder.setContentText(mMediaNotificationInfo.origin);
}
}
private String getArtistAndAlbumText(MediaMetadata metadata) {
String artist = (metadata.getArtist() == null) ? "" : metadata.getArtist();
String album = (metadata.getAlbum() == null) ? "" : metadata.getAlbum();
if (artist.isEmpty() || album.isEmpty()) {
return artist + album;
}
return artist + " - " + album;
}
}