diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/AlbumArtCache.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/AlbumArtCache.java index 733e0c18..7f18f3b3 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/AlbumArtCache.java +++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/AlbumArtCache.java @@ -1,435 +1,431 @@ /* * Copyright 2017 Matthijs Tijink * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.MprisPlugin; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.support.v4.util.LruCache; import android.util.Log; import com.jakewharton.disklrucache.DiskLruCache; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; /** * Handles the cache for album art */ public final class AlbumArtCache { private static final class MemoryCacheItem { boolean failedFetch; Bitmap albumArt; } /** * An in-memory cache for album art bitmaps. Holds at most 10 entries (to prevent too much memory usage) * Also remembers failure to fetch urls. */ private static final LruCache memoryCache = new LruCache<>(10); /** * An on-disk cache for album art bitmaps. */ private static DiskLruCache diskCache; /** * A list of urls yet to be fetched. */ private static final ArrayList fetchUrlList = new ArrayList<>(); /** * A integer indicating how many fetches are in progress. */ private static int numFetching = 0; /** * A list of plugins to notify on fetched album art */ private static ArrayList registeredPlugins = new ArrayList<>(); /** * Initializes the disk cache. Needs to be called at least once before trying to use the cache * * @param context The context */ public static void initializeDiskCache(Context context) { if (diskCache != null) return; File cacheDir = new File(context.getCacheDir(), "album_art"); int versionCode; try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); versionCode = info.versionCode; //Initialize the disk cache with a limit of 5 MB storage (fits ~830 images, taking Spotify as reference) diskCache = DiskLruCache.open(cacheDir, versionCode, 1, 1000 * 1000 * 5); } catch (PackageManager.NameNotFoundException e) { throw new AssertionError(e); } catch (IOException e) { Log.e("KDE/Mpris/AlbumArtCache", "Could not open the album art disk cache!", e); } } /** * Registers a mpris plugin, such that it gets notified of fetched album art * * @param mpris The mpris plugin */ public static void registerPlugin(MprisPlugin mpris) { registeredPlugins.add(mpris); } /** * Deregister a mpris plugin * * @param mpris The mpris plugin */ public static void deregisterPlugin(MprisPlugin mpris) { registeredPlugins.remove(mpris); } /** * Get the album art for the given url. Currently only handles http(s) urls. * If it's not in the cache, will initiate a request to fetch it. * * @param albumUrl The album art url * @return A bitmap for the album art. Can be null if not (yet) found */ public static Bitmap getAlbumArt(String albumUrl) { //If the url is invalid, return "no album art" if (albumUrl == null || albumUrl.isEmpty()) { return null; } URL url; try { url = new URL(albumUrl); } catch (MalformedURLException e) { //Invalid url, so just return "no album art" //Shouldn't happen (checked on receival of the url), but just to be sure return null; } //We currently only support http(s) urls if (!url.getProtocol().equals("http") && !url.getProtocol().equals("https")) { return null; } //First, check the in-memory cache if (memoryCache.get(albumUrl) != null) { MemoryCacheItem item = memoryCache.get(albumUrl); //Do not retry failed fetches if (item.failedFetch) { return null; } else { return item.albumArt; } } //If not found, check the disk cache if (diskCache == null) { Log.e("KDE/Mpris/AlbumArtCache", "The disk cache is not intialized!"); return null; } try { DiskLruCache.Snapshot item = diskCache.get(urlToDiskCacheKey(albumUrl)); if (item != null) { - BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); - decodeOptions.inScaled = false; - decodeOptions.inDensity = 1; - decodeOptions.inTargetDensity = 1; - Bitmap result = BitmapFactory.decodeStream(item.getInputStream(0), null, decodeOptions); + Bitmap result = BitmapFactory.decodeStream(item.getInputStream(0)); item.close(); MemoryCacheItem memItem = new MemoryCacheItem(); if (result != null) { memItem.failedFetch = false; memItem.albumArt = result; } else { //Invalid bitmap, so remember it as a "failed fetch" and remove it from the disk cache memItem.failedFetch = true; memItem.albumArt = null; diskCache.remove(urlToDiskCacheKey(albumUrl)); Log.d("KDE/Mpris/AlbumArtCache", "Invalid image: " + albumUrl); } memoryCache.put(albumUrl, memItem); return result; } } catch (IOException e) { return null; } /* If not found, we have not tried fetching it (recently), or a fetch is in-progress. Either way, just add it to the fetch queue and starting fetching it if no fetch is running. */ fetchUrl(url); return null; } /** * Fetches an album art url and puts it in the cache * * @param url The url */ private static void fetchUrl(URL url) { //We need the disk cache for this if (diskCache == null) { Log.e("KDE/Mpris/AlbumArtCache", "The disk cache is not intialized!"); return; } //Only fetch an URL if we're not fetching it already if (fetchUrlList.contains(url)) { return; } fetchUrlList.add(url); initiateFetch(); } private static final class FetchURLTask extends AsyncTask { private URL url; private InputStream input; private DiskLruCache.Editor cacheItem; private OutputStream output; /** * Initialize an url fetch * * @param url The url being fetched * @param payloadInput A payload input stream (if from the connected device). null if fetched from http(s) * @param cacheItem The disk cache item to edit * @throws IOException */ FetchURLTask(URL url, InputStream payloadInput, DiskLruCache.Editor cacheItem) throws IOException { this.url = url; this.input = payloadInput; this.cacheItem = cacheItem; output = cacheItem.newOutputStream(0); } /** * Opens the http(s) connection * * @return True if succeeded * @throws IOException */ private boolean openHttp() throws IOException { //Default android behaviour does not follow https -> http urls, so do this manually URL currentUrl = url; HttpURLConnection connection; for (int i = 0; i < 5; ++i) { connection = (HttpURLConnection) currentUrl.openConnection(); connection.setConnectTimeout(10000); connection.setReadTimeout(10000); connection.setInstanceFollowRedirects(false); switch (connection.getResponseCode()) { case HttpURLConnection.HTTP_MOVED_PERM: case HttpURLConnection.HTTP_MOVED_TEMP: String location = connection.getHeaderField("Location"); location = URLDecoder.decode(location, "UTF-8"); currentUrl = new URL(currentUrl, location); // Deal with relative URLs //Again, only support http(s) if (!currentUrl.getProtocol().equals("http") && !currentUrl.getProtocol().equals("https")) { return false; } connection.disconnect(); continue; } //Found a non-redirecting connection, so do something with it input = connection.getInputStream(); return true; } return false; } @Override protected Boolean doInBackground(Void... params) { try { //See if we need to open a http(s) connection here, or if we use a payload input stream if (input == null) { if (!openHttp()) { return false; } } byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = input.read(buffer)) != -1) { output.write(buffer, 0, bytesRead); } output.flush(); output.close(); return true; } catch (IOException e) { return false; } } @Override protected void onPostExecute(Boolean success) { try { if (success) { cacheItem.commit(); } else { cacheItem.abort(); } } catch (IOException e) { success = false; Log.e("KDE/Mpris/AlbumArtCache", "Problem with the disk cache", e); } if (success) { //Now it's in the disk cache, the getAlbumArt() function should be able to read it //So notify the mpris plugins of the fetched art for (MprisPlugin mpris : registeredPlugins) { mpris.fetchedAlbumArt(url.toString()); } } else { //Mark the fetch as failed in the memory cache MemoryCacheItem cacheItem = new MemoryCacheItem(); cacheItem.failedFetch = true; cacheItem.albumArt = null; memoryCache.put(url.toString(), cacheItem); } //Remove the url from the to-fetch list fetchUrlList.remove(url); //Fetch the next url (if any) --numFetching; initiateFetch(); } } /** * Does the actual fetching and makes sure only not too many fetches are running at the same time */ private static void initiateFetch() { if (numFetching >= 2) return; if (fetchUrlList.isEmpty()) return; ++numFetching; //Fetch the last-requested url first, it will probably be needed first URL url = fetchUrlList.get(fetchUrlList.size() - 1); try { DiskLruCache.Editor cacheItem = diskCache.edit(urlToDiskCacheKey(url.toString())); if (cacheItem == null) { Log.e("KDE/Mpris/AlbumArtCache", "Two disk cache edits happened at the same time, should be impossible!"); --numFetching; return; } //Do the actual fetch in the background new FetchURLTask(url, null, cacheItem).execute(); } catch (IOException e) { Log.e("KDE/Mpris/AlbumArtCache", "Problems with the disk cache", e); --numFetching; } } /** * The disk cache requires mostly alphanumeric characters, and at most 64 characters. * So hash the url to get a valid key * * @param url The url * @return A valid disk cache key */ private static String urlToDiskCacheKey(String url) { MessageDigest hasher; try { hasher = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { //Should always be available throw new AssertionError(e); } StringBuilder builder = new StringBuilder(); for (byte singleByte : hasher.digest(url.getBytes())) { builder.append(String.format("%02x", singleByte)); } return builder.toString(); } /** * Transfer an asked-for album art payload to the disk cache. * * @param albumUrl The url of the album art (should be a file:// url) * @param payload The payload input stream */ public static void payloadToDiskCache(String albumUrl, InputStream payload) { //We need the disk cache for this if (diskCache == null) { Log.e("KDE/Mpris/AlbumArtCache", "The disk cache is not intialized!"); return; } URL url; try { url = new URL(albumUrl); } catch (MalformedURLException e) { //Shouldn't happen (checked on receival of the url), but just to be sure return; } if (!"file".equals(url.getProtocol())) { //Shouldn't happen (otherwise we wouldn't have asked for the payload), but just to be sure return; } //Only fetch the URL if we're not fetching it already if (fetchUrlList.contains(url)) { return; } fetchUrlList.add(url); ++numFetching; try { DiskLruCache.Editor cacheItem = diskCache.edit(urlToDiskCacheKey(url.toString())); if (cacheItem == null) { Log.e("KDE/Mpris/AlbumArtCache", "Two disk cache edits happened at the same time, should be impossible!"); --numFetching; return; } //Do the actual fetch in the background new FetchURLTask(url, payload, cacheItem).execute(); } catch (IOException e) { Log.e("KDE/Mpris/AlbumArtCache", "Problems with the disk cache", e); --numFetching; } } } diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisMediaSession.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisMediaSession.java index e5470361..207f4ea5 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisMediaSession.java +++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisMediaSession.java @@ -1,413 +1,423 @@ /* * Copyright 2017 Matthijs Tijink * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.MprisPlugin; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Bitmap; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.preference.PreferenceManager; 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 org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect_tp.R; import java.util.HashSet; /** * Controls the mpris media control notification *

* There are two parts to this: * - The notification (with buttons etc.) * - The media session (via MediaSessionCompat; for lock screen control on * older Android version. And in the future for lock screen album covers) */ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceChangeListener { public final static int MPRIS_MEDIA_NOTIFICATION_ID = 0x91b70463; // echo MprisNotification | md5sum | head -c 8 public final static String MPRIS_MEDIA_SESSION_TAG = "org.kde.kdeconnect_tp.media_session"; private static MprisMediaSession instance = new MprisMediaSession(); public static MprisMediaSession getInstance() { return instance; } public static MediaSessionCompat getMediaSession() { return instance.mediaSession; } //Holds the device and player displayed in the notification private String notificationDevice = null; private MprisPlugin.MprisPlayer notificationPlayer = null; //Holds the device ids for which we can display a notification private HashSet mprisDevices = new HashSet<>(); private Context context; private MediaSessionCompat mediaSession; //Callback for mpris plugin updates private Handler mediaNotificationHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { updateMediaNotification(); } }; //Callback for control via the media session API private MediaSessionCompat.Callback mediaSessionCallback = new MediaSessionCompat.Callback() { @Override public void onPlay() { notificationPlayer.play(); } @Override public void onPause() { notificationPlayer.pause(); } @Override public void onSkipToNext() { notificationPlayer.next(); } @Override public void onSkipToPrevious() { notificationPlayer.previous(); } @Override public void onStop() { notificationPlayer.stop(); } }; /** * Called by the mpris plugin when it wants media control notifications for its device *

* Can be called multiple times, once for each device * * @param _context The context * @param mpris The mpris plugin * @param device The device id */ public void onCreate(Context _context, MprisPlugin mpris, String device) { if (mprisDevices.isEmpty()) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(_context); prefs.registerOnSharedPreferenceChangeListener(this); } context = _context; mprisDevices.add(device); mpris.setPlayerListUpdatedHandler("media_notification", mediaNotificationHandler); mpris.setPlayerStatusUpdatedHandler("media_notification", mediaNotificationHandler); updateMediaNotification(); } /** * Called when a device disconnects/does not want notifications anymore *

* Can be called multiple times, once for each device * * @param mpris The mpris plugin * @param device The device id */ public void onDestroy(MprisPlugin mpris, String device) { mprisDevices.remove(device); mpris.removePlayerStatusUpdatedHandler("media_notification"); mpris.removePlayerListUpdatedHandler("media_notification"); updateMediaNotification(); if (mprisDevices.isEmpty()) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs.unregisterOnSharedPreferenceChangeListener(this); } } /** * Updates which device+player we're going to use in the notification *

* Prefers playing devices/mpris players, but tries to keep displaying the same * player and device, while possible. * * @param service The background service */ private void updateCurrentPlayer(BackgroundService service) { Device device = null; MprisPlugin.MprisPlayer playing = null; //First try the previously displayed player if (notificationDevice != null && mprisDevices.contains(notificationDevice) && notificationPlayer != null) { device = service.getDevice(notificationDevice); } MprisPlugin mpris = null; if (device != null) { mpris = device.getPlugin(MprisPlugin.class); } if (mpris != null) { playing = mpris.getPlayerStatus(notificationPlayer.getPlayer()); } //If nonexistant or not playing, try a different player for the same device if ((playing == null || !playing.isPlaying()) && mpris != null) { MprisPlugin.MprisPlayer playingPlayer = mpris.getPlayingPlayer(); //Only replace the previously found player if we really found one if (playingPlayer != null) { playing = playingPlayer; } } //If nonexistant or not playing, try a different player for another device if (playing == null || !playing.isPlaying()) { for (Device otherDevice : service.getDevices().values()) { //First, check if we actually display notification for this device if (!mprisDevices.contains(otherDevice.getDeviceId())) continue; mpris = otherDevice.getPlugin(MprisPlugin.class); if (mpris == null) continue; MprisPlugin.MprisPlayer playingPlayer = mpris.getPlayingPlayer(); //Only replace the previously found player if we really found one if (playingPlayer != null) { playing = playingPlayer; device = otherDevice; break; } } } //Update the last-displayed device and player notificationDevice = device == null ? null : device.getDeviceId(); notificationPlayer = playing; } /** * Update the media control notification */ private void updateMediaNotification() { BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() { @Override public void onServiceStart(BackgroundService service) { //If the user disabled the media notification, do not show it SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (!prefs.getBoolean(context.getString(R.string.mpris_notification_key), true)) { closeMediaNotification(); return; } //Make sure our information is up-to-date updateCurrentPlayer(service); //If the player disappeared (and no other playing one found), just remove the notification if (notificationPlayer == null) { closeMediaNotification(); return; } //Update the metadata and playback status if (mediaSession == null) { mediaSession = new MediaSessionCompat(context, MPRIS_MEDIA_SESSION_TAG); mediaSession.setCallback(mediaSessionCallback); mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); } MediaMetadataCompat.Builder metadata = new MediaMetadataCompat.Builder(); //Fallback because older KDE connect versions do not support getTitle() if (!notificationPlayer.getTitle().isEmpty()) { metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getTitle()); } else { metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getCurrentSong()); } if (!notificationPlayer.getArtist().isEmpty()) { metadata.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, notificationPlayer.getArtist()); } if (!notificationPlayer.getAlbum().isEmpty()) { metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, notificationPlayer.getAlbum()); } if (notificationPlayer.getLength() > 0) { metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, notificationPlayer.getLength()); } + Bitmap albumArt = notificationPlayer.getAlbumArt(); + if (albumArt != null) { + metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt); + } + mediaSession.setMetadata(metadata.build()); PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder(); if (notificationPlayer.isPlaying()) { playbackState.setState(PlaybackStateCompat.STATE_PLAYING, notificationPlayer.getPosition(), 1.0f); } else { playbackState.setState(PlaybackStateCompat.STATE_PAUSED, notificationPlayer.getPosition(), 0.0f); } //Create all actions (previous/play/pause/next) Intent iPlay = new Intent(service, MprisMediaNotificationReceiver.class); iPlay.setAction(MprisMediaNotificationReceiver.ACTION_PLAY); iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice); iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer()); PendingIntent piPlay = PendingIntent.getBroadcast(service, 0, iPlay, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Action.Builder aPlay = new NotificationCompat.Action.Builder( R.drawable.ic_play_white, service.getString(R.string.mpris_play), piPlay); Intent iPause = new Intent(service, MprisMediaNotificationReceiver.class); iPause.setAction(MprisMediaNotificationReceiver.ACTION_PAUSE); iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice); iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer()); PendingIntent piPause = PendingIntent.getBroadcast(service, 0, iPause, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Action.Builder aPause = new NotificationCompat.Action.Builder( R.drawable.ic_pause_white, service.getString(R.string.mpris_pause), piPause); Intent iPrevious = new Intent(service, MprisMediaNotificationReceiver.class); iPrevious.setAction(MprisMediaNotificationReceiver.ACTION_PREVIOUS); iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice); iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer()); PendingIntent piPrevious = PendingIntent.getBroadcast(service, 0, iPrevious, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Action.Builder aPrevious = new NotificationCompat.Action.Builder( R.drawable.ic_previous_white, service.getString(R.string.mpris_previous), piPrevious); Intent iNext = new Intent(service, MprisMediaNotificationReceiver.class); iNext.setAction(MprisMediaNotificationReceiver.ACTION_NEXT); iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice); iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer()); PendingIntent piNext = PendingIntent.getBroadcast(service, 0, iNext, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Action.Builder aNext = new NotificationCompat.Action.Builder( R.drawable.ic_next_white, service.getString(R.string.mpris_next), piNext); Intent iOpenActivity = new Intent(service, MprisActivity.class); iOpenActivity.putExtra("deviceId", notificationDevice); iOpenActivity.putExtra("player", notificationPlayer.getPlayer()); PendingIntent piOpenActivity = PendingIntent.getActivity(service, 0, iOpenActivity, PendingIntent.FLAG_UPDATE_CURRENT); //Create the notification final NotificationCompat.Builder notification = new NotificationCompat.Builder(service); notification .setAutoCancel(false) .setContentIntent(piOpenActivity) .setSmallIcon(R.drawable.ic_play_white) .setShowWhen(false) .setColor(service.getResources().getColor(R.color.primary)) .setVisibility(android.support.v4.app.NotificationCompat.VISIBILITY_PUBLIC); if (!notificationPlayer.getTitle().isEmpty()) { notification.setContentTitle(notificationPlayer.getTitle()); } else { notification.setContentTitle(notificationPlayer.getCurrentSong()); } //Only set the notification body text if we have an author and/or album if (!notificationPlayer.getArtist().isEmpty() && !notificationPlayer.getAlbum().isEmpty()) { notification.setContentText(notificationPlayer.getArtist() + " - " + notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayer() + ")"); } else if (!notificationPlayer.getArtist().isEmpty()) { notification.setContentText(notificationPlayer.getArtist() + " (" + notificationPlayer.getPlayer() + ")"); } else if (!notificationPlayer.getAlbum().isEmpty()) { notification.setContentText(notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayer() + ")"); } else { notification.setContentText(notificationPlayer.getPlayer()); } + if (albumArt != null) { + notification.setLargeIcon(albumArt); + } + if (!notificationPlayer.isPlaying()) { Intent iCloseNotification = new Intent(service, MprisMediaNotificationReceiver.class); iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION); iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice); iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer()); PendingIntent piCloseNotification = PendingIntent.getActivity(service, 0, iCloseNotification, PendingIntent.FLAG_UPDATE_CURRENT); notification.setDeleteIntent(piCloseNotification); } //Add media control actions int numActions = 0; long playbackActions = 0; if (notificationPlayer.isGoPreviousAllowed()) { notification.addAction(aPrevious.build()); playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; ++numActions; } if (notificationPlayer.isPlaying() && notificationPlayer.isPauseAllowed()) { notification.addAction(aPause.build()); playbackActions |= PlaybackStateCompat.ACTION_PAUSE; ++numActions; } if (!notificationPlayer.isPlaying() && notificationPlayer.isPlayAllowed()) { notification.addAction(aPlay.build()); playbackActions |= PlaybackStateCompat.ACTION_PLAY; ++numActions; } if (notificationPlayer.isGoNextAllowed()) { notification.addAction(aNext.build()); playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT; ++numActions; } playbackState.setActions(playbackActions); mediaSession.setPlaybackState(playbackState.build()); //Only allow deletion if no music is notificationPlayer if (notificationPlayer.isPlaying()) { notification.setOngoing(true); } else { notification.setOngoing(false); } //Use the MediaStyle notification, so it feels like other media players. That also allows adding actions NotificationCompat.MediaStyle mediaStyle = new NotificationCompat.MediaStyle(); if (numActions == 1) { mediaStyle.setShowActionsInCompactView(0); } else if (numActions == 2) { mediaStyle.setShowActionsInCompactView(0, 1); } else if (numActions >= 3) { mediaStyle.setShowActionsInCompactView(0, 1, 2); } mediaStyle.setMediaSession(mediaSession.getSessionToken()); notification.setStyle(mediaStyle); //Display the notification mediaSession.setActive(true); final NotificationManager nm = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(MPRIS_MEDIA_NOTIFICATION_ID, notification.build()); } }); } public void closeMediaNotification() { //Remove the notification NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel(MPRIS_MEDIA_NOTIFICATION_ID); //Clear the current player and media session notificationPlayer = null; if (mediaSession != null) { mediaSession.release(); mediaSession = null; } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { updateMediaNotification(); } public void playerSelected(MprisPlugin.MprisPlayer player) { notificationPlayer = player; updateMediaNotification(); } }