diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/AlbumArtCache.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/AlbumArtCache.java index 7f18f3b3..86e661a5 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/AlbumArtCache.java +++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/AlbumArtCache.java @@ -1,431 +1,489 @@ /* * 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 list of urls currently being fetched + */ + private static final ArrayList isFetchingList = 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) { + public static Bitmap getAlbumArt(String albumUrl, MprisPlugin plugin, String player) { //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")) { + //We currently only support http(s) and file urls + if (!url.getProtocol().equals("http") && !url.getProtocol().equals("https") && !url.getProtocol().equals("file")) { 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) { 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); + if ("file".equals(url.getProtocol())) { + //Special-case file, since we need to fetch it from the remote + if (isFetchingList.contains(url)) return null; + + if (!plugin.askTransferAlbumArt(albumUrl, player)) { + //It doesn't support transferring the art, so mark it as failed in the memory cache + MemoryCacheItem cacheItem = new MemoryCacheItem(); + cacheItem.failedFetch = true; + cacheItem.albumArt = null; + memoryCache.put(url.toString(), cacheItem); + } + } else { + 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)) { + if (fetchUrlList.contains(url) || isFetchingList.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); + //Remove the url from the fetching list + isFetchingList.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); + //Remove the url from the to-fetch list + fetchUrlList.remove(url); + + if ("file".equals(url.getProtocol())) { + throw new AssertionError("Not file urls should be possible here!"); + } + + //Download the album art ourselves + ++numFetching; + //Add the url to the currently-fetching list + isFetchingList.add(url); 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!"); + try { + payload.close(); + } catch (IOException ignored) {} 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 + try { + payload.close(); + } catch (IOException ignored) {} return; } if (!"file".equals(url.getProtocol())) { //Shouldn't happen (otherwise we wouldn't have asked for the payload), but just to be sure + try { + payload.close(); + } catch (IOException ignored) {} return; } //Only fetch the URL if we're not fetching it already - if (fetchUrlList.contains(url)) { + if (isFetchingList.contains(url)) { + try { + payload.close(); + } catch (IOException ignored) {} return; } - fetchUrlList.add(url); + //Check if we already have this art + try { + if (memoryCache.get(albumUrl) != null || diskCache.get(urlToDiskCacheKey(albumUrl)) != null) { + try { + payload.close(); + } catch (IOException ignored) {} + return; + } + } catch (IOException e) { + Log.e("KDE/Mpris/AlbumArtCache", "Disk cache problem!", e); + try { + payload.close(); + } catch (IOException ignored) {} + return; + } + + //Add it to the currently-fetching list + isFetchingList.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; + try { + payload.close(); + } catch (IOException ignored) {} 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/MprisPlugin.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java index 32fb16e5..1f8f0dfe 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisPlugin.java @@ -1,465 +1,492 @@ /* * Copyright 2014 Albert Vaca Cintora * * 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.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Message; import android.support.v4.content.ContextCompat; import android.util.Log; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect_tp.R; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; public class MprisPlugin extends Plugin { public class MprisPlayer { private String player = ""; private boolean playing = false; private String currentSong = ""; private String title = ""; private String artist = ""; private String album = ""; private String albumArtUrl = ""; private int volume = 50; private long length = -1; private long lastPosition = 0; private long lastPositionTime; private boolean playAllowed = true; private boolean pauseAllowed = true; private boolean goNextAllowed = true; private boolean goPreviousAllowed = true; private boolean seekAllowed = true; public MprisPlayer() { lastPositionTime = System.currentTimeMillis(); } public String getCurrentSong() { return currentSong; } public String getTitle() { return title; } public String getArtist() { return artist; } public String getAlbum() { return album; } public String getPlayer() { return player; } private boolean isSpotify() { return getPlayer().toLowerCase().equals("spotify"); } public int getVolume() { return volume; } public long getLength() { return length; } public boolean isPlaying() { return playing; } public boolean isPlayAllowed() { return playAllowed; } public boolean isPauseAllowed() { return pauseAllowed; } public boolean isGoNextAllowed() { return goNextAllowed; } public boolean isGoPreviousAllowed() { return goPreviousAllowed; } public boolean isSeekAllowed() { return seekAllowed && getLength() >= 0 && getPosition() >= 0 && !isSpotify(); } public boolean hasAlbumArt() { return !albumArtUrl.isEmpty(); } /** * Returns the album art (if available). Note that this can return null even if hasAlbumArt() returns true. * * @return The album art, or null if not available */ public Bitmap getAlbumArt() { - return AlbumArtCache.getAlbumArt(albumArtUrl); + return AlbumArtCache.getAlbumArt(albumArtUrl, MprisPlugin.this, player); } public boolean isSetVolumeAllowed() { return !isSpotify(); } public long getPosition() { if (playing) { return lastPosition + (System.currentTimeMillis() - lastPositionTime); } else { return lastPosition; } } public void playPause() { if (isPauseAllowed() || isPlayAllowed()) { MprisPlugin.this.sendCommand(getPlayer(), "action", "PlayPause"); } } public void play() { if (isPlayAllowed()) { MprisPlugin.this.sendCommand(getPlayer(), "action", "Play"); } } public void pause() { if (isPauseAllowed()) { MprisPlugin.this.sendCommand(getPlayer(), "action", "Pause"); } } public void stop() { MprisPlugin.this.sendCommand(getPlayer(), "action", "Stop"); } public void previous() { if (isGoPreviousAllowed()) { MprisPlugin.this.sendCommand(getPlayer(), "action", "Previous"); } } public void next() { if (isGoNextAllowed()) { MprisPlugin.this.sendCommand(getPlayer(), "action", "Next"); } } public void setVolume(int volume) { if (isSetVolumeAllowed()) { MprisPlugin.this.sendCommand(getPlayer(), "setVolume", volume); } } public void setPosition(int position) { if (isSeekAllowed()) { MprisPlugin.this.sendCommand(getPlayer(), "SetPosition", position); lastPosition = position; lastPositionTime = System.currentTimeMillis(); } } public void seek(int offset) { if (isSeekAllowed()) { MprisPlugin.this.sendCommand(getPlayer(), "Seek", offset); } } } public final static String PACKET_TYPE_MPRIS = "kdeconnect.mpris"; public final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request"; private HashMap players = new HashMap<>(); + private boolean supportAlbumArtPayload = false; private HashMap playerStatusUpdated = new HashMap<>(); private HashMap playerListUpdated = new HashMap<>(); @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_mpris); } @Override public String getDescription() { return context.getResources().getString(R.string.pref_plugin_mpris_desc); } @Override public Drawable getIcon() { return ContextCompat.getDrawable(context, R.drawable.mpris_plugin_action); } @Override public boolean hasSettings() { return true; } @Override public boolean onCreate() { MprisMediaSession.getInstance().onCreate(context.getApplicationContext(), this, device.getDeviceId()); //Always request the player list so the data is up-to-date requestPlayerList(); AlbumArtCache.initializeDiskCache(context); AlbumArtCache.registerPlugin(this); return true; } @Override public void onDestroy() { players.clear(); AlbumArtCache.deregisterPlugin(this); MprisMediaSession.getInstance().onDestroy(this, device.getDeviceId()); } private void sendCommand(String player, String method, String value) { NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST); np.set("player", player); np.set(method, value); device.sendPacket(np); } private void sendCommand(String player, String method, int value) { NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST); np.set("player", player); np.set(method, value); device.sendPacket(np); } @Override public boolean onPacketReceived(NetworkPacket np) { + if (np.getBoolean("transferringAlbumArt", false)) { + AlbumArtCache.payloadToDiskCache(np.getString("albumArtUrl"), np.getPayload()); + return true; + } + if (np.has("player")) { MprisPlayer playerStatus = players.get(np.getString("player")); if (playerStatus != null) { playerStatus.currentSong = np.getString("nowPlaying", playerStatus.currentSong); //Note: title, artist and album will not be available for all desktop clients playerStatus.title = np.getString("title", playerStatus.title); playerStatus.artist = np.getString("artist", playerStatus.artist); playerStatus.album = np.getString("album", playerStatus.album); playerStatus.volume = np.getInt("volume", playerStatus.volume); playerStatus.length = np.getLong("length", playerStatus.length); if (np.has("pos")) { playerStatus.lastPosition = np.getLong("pos", playerStatus.lastPosition); playerStatus.lastPositionTime = System.currentTimeMillis(); } playerStatus.playing = np.getBoolean("isPlaying", playerStatus.playing); playerStatus.playAllowed = np.getBoolean("canPlay", playerStatus.playAllowed); playerStatus.pauseAllowed = np.getBoolean("canPause", playerStatus.pauseAllowed); playerStatus.goNextAllowed = np.getBoolean("canGoNext", playerStatus.goNextAllowed); playerStatus.goPreviousAllowed = np.getBoolean("canGoPrevious", playerStatus.goPreviousAllowed); playerStatus.seekAllowed = np.getBoolean("canSeek", playerStatus.seekAllowed); String newAlbumArtUrlstring = np.getString("albumArtUrl", playerStatus.albumArtUrl); try { //Turn the url into canonical form (and check its validity) URL newAlbumArtUrl = new URL(newAlbumArtUrlstring); playerStatus.albumArtUrl = newAlbumArtUrl.toString(); } catch (MalformedURLException ignored) { } for (String key : playerStatusUpdated.keySet()) { try { playerStatusUpdated.get(key).dispatchMessage(new Message()); } catch (Exception e) { e.printStackTrace(); Log.e("MprisControl", "Exception"); playerStatusUpdated.remove(key); } } } } + //Remember if the connected device support album art payloads + supportAlbumArtPayload = np.getBoolean("supportAlbumArtPayload", supportAlbumArtPayload); + List newPlayerList = np.getStringList("playerList"); if (newPlayerList != null) { boolean equals = true; for (String newPlayer : newPlayerList) { if (!players.containsKey(newPlayer)) { equals = false; MprisPlayer player = new MprisPlayer(); player.player = newPlayer; players.put(newPlayer, player); //Immediately ask for the data of this player requestPlayerStatus(newPlayer); } } Iterator> iter = players.entrySet().iterator(); while (iter.hasNext()) { String oldPlayer = iter.next().getKey(); boolean found = false; for (String newPlayer : newPlayerList) { if (newPlayer.equals(oldPlayer)) { found = true; break; } } if (!found) { iter.remove(); equals = false; } } if (!equals) { for (String key : playerListUpdated.keySet()) { try { playerListUpdated.get(key).dispatchMessage(new Message()); } catch (Exception e) { e.printStackTrace(); Log.e("MprisControl", "Exception"); playerListUpdated.remove(key); } } } } return true; } @Override public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_MPRIS}; } @Override public String[] getOutgoingPacketTypes() { return new String[]{PACKET_TYPE_MPRIS_REQUEST}; } public void setPlayerStatusUpdatedHandler(String id, Handler h) { playerStatusUpdated.put(id, h); h.dispatchMessage(new Message()); } public void removePlayerStatusUpdatedHandler(String id) { playerStatusUpdated.remove(id); } public void setPlayerListUpdatedHandler(String id, Handler h) { playerListUpdated.put(id, h); h.dispatchMessage(new Message()); } public void removePlayerListUpdatedHandler(String id) { playerListUpdated.remove(id); } public List getPlayerList() { List playerlist = new ArrayList<>(players.keySet()); Collections.sort(playerlist); return playerlist; } public MprisPlayer getPlayerStatus(String player) { return players.get(player); } public MprisPlayer getEmptyPlayer() { return new MprisPlayer(); } /** * Returns a playing mpris player, if any exist * * @return null if no players are playing, a playing player otherwise */ public MprisPlayer getPlayingPlayer() { for (MprisPlayer player : players.values()) { if (player.isPlaying()) { return player; } } return null; } private void requestPlayerList() { NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST); np.set("requestPlayerList", true); device.sendPacket(np); } private void requestPlayerStatus(String player) { NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST); np.set("player", player); np.set("requestNowPlaying", true); np.set("requestVolume", true); device.sendPacket(np); } @Override public boolean hasMainActivity() { return true; } @Override public void startMainActivity(Activity parentActivity) { Intent intent = new Intent(parentActivity, MprisActivity.class); intent.putExtra("deviceId", device.getDeviceId()); parentActivity.startActivity(intent); } @Override public String getActionName() { return context.getString(R.string.open_mpris_controls); } public void fetchedAlbumArt(String url) { boolean doEmitUpdate = false; for (MprisPlayer player : players.values()) { if (url.equals(player.albumArtUrl)) { doEmitUpdate = true; } } if (doEmitUpdate) { for (String key : playerStatusUpdated.keySet()) { try { playerStatusUpdated.get(key).dispatchMessage(new Message()); } catch (Exception e) { e.printStackTrace(); Log.e("MprisControl", "Exception"); playerStatusUpdated.remove(key); } } } } + + public boolean askTransferAlbumArt(String url, String playerName) { + //First check if the remote supports transferring album art + if (!supportAlbumArtPayload) return false; + if (url.isEmpty()) return false; + + MprisPlayer player = getPlayerStatus(playerName); + if (player == null) return false; + + if (player.albumArtUrl.equals(url)) { + NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST); + np.set("player", player.getPlayer()); + np.set("albumArtUrl", url); + device.sendPacket(np); + return true; + } + return false; + } }