File indexing completed on 2024-12-22 04:41:39

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 package org.kde.kdeconnect.Plugins.MprisPlugin;
0008 
0009 import android.Manifest;
0010 import android.app.Activity;
0011 import android.content.Context;
0012 import android.content.Intent;
0013 import android.graphics.Bitmap;
0014 import android.os.Build;
0015 import android.util.Log;
0016 
0017 import androidx.annotation.DrawableRes;
0018 import androidx.annotation.NonNull;
0019 
0020 import org.apache.commons.lang3.ArrayUtils;
0021 import org.kde.kdeconnect.NetworkPacket;
0022 import org.kde.kdeconnect.Plugins.Plugin;
0023 import org.kde.kdeconnect.Plugins.PluginFactory;
0024 import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
0025 import org.kde.kdeconnect_tp.R;
0026 
0027 import java.net.MalformedURLException;
0028 import java.net.URL;
0029 import java.util.ArrayList;
0030 import java.util.Collections;
0031 import java.util.HashMap;
0032 import java.util.Iterator;
0033 import java.util.List;
0034 import java.util.concurrent.ConcurrentHashMap;
0035 
0036 @PluginFactory.LoadablePlugin
0037 public class MprisPlugin extends Plugin {
0038     public class MprisPlayer {
0039         private String player = "";
0040         private boolean playing = false;
0041         private String title = "";
0042         private String artist = "";
0043         private String album = "";
0044         private String albumArtUrl = "";
0045         private String url = "";
0046         private String loopStatus = "";
0047         private boolean loopStatusAllowed = false;
0048         private boolean shuffle = false;
0049         private boolean shuffleAllowed = false;
0050         private int volume = 50;
0051         private long length = -1;
0052         private long lastPosition = 0;
0053         private long lastPositionTime;
0054         private boolean playAllowed = true;
0055         private boolean pauseAllowed = true;
0056         private boolean goNextAllowed = true;
0057         private boolean goPreviousAllowed = true;
0058         private boolean seekAllowed = true;
0059 
0060         MprisPlayer() {
0061             lastPositionTime = System.currentTimeMillis();
0062         }
0063 
0064         public String getTitle() {
0065             return title;
0066         }
0067 
0068         public String getArtist() {
0069             return artist;
0070         }
0071 
0072         public String getAlbum() {
0073             return album;
0074         }
0075 
0076         public String getPlayerName() {
0077             return player;
0078         }
0079 
0080         boolean isSpotify() {
0081             return getPlayerName().equalsIgnoreCase("spotify");
0082         }
0083 
0084         public String getLoopStatus() {
0085             return loopStatus;
0086         }
0087 
0088         public boolean getShuffle() {
0089             return shuffle;
0090         }
0091 
0092         public int getVolume() {
0093             return volume;
0094         }
0095 
0096         public long getLength() {
0097             return length;
0098         }
0099 
0100         public boolean isPlaying() {
0101             return playing;
0102         }
0103 
0104         public boolean isPlayAllowed() {
0105             return playAllowed;
0106         }
0107 
0108         public boolean isPauseAllowed() {
0109             return pauseAllowed;
0110         }
0111 
0112         public boolean isGoNextAllowed() {
0113             return goNextAllowed;
0114         }
0115 
0116         public boolean isGoPreviousAllowed() {
0117             return goPreviousAllowed;
0118         }
0119 
0120         public boolean isSeekAllowed() {
0121             return seekAllowed && getLength() >= 0 && getPosition() >= 0;
0122         }
0123 
0124         public boolean hasAlbumArt() {
0125             return !albumArtUrl.isEmpty();
0126         }
0127 
0128         /**
0129          * Returns the album art (if available). Note that this can return null even if hasAlbumArt() returns true.
0130          *
0131          * @return The album art, or null if not available
0132          */
0133         public Bitmap getAlbumArt() {
0134             return AlbumArtCache.getAlbumArt(albumArtUrl, MprisPlugin.this, player);
0135         }
0136 
0137         //@NonNull
0138         public String getUrl() {
0139             return url;
0140         }
0141 
0142         public boolean isLoopStatusAllowed() {
0143             return loopStatusAllowed;
0144         }
0145 
0146         public boolean isShuffleAllowed() {
0147             return shuffleAllowed;
0148         }
0149 
0150         public boolean isSetVolumeAllowed() {
0151             return getVolume() > -1;
0152         }
0153 
0154         public long getPosition() {
0155             if (playing) {
0156                 return lastPosition + (System.currentTimeMillis() - lastPositionTime);
0157             } else {
0158                 return lastPosition;
0159             }
0160         }
0161 
0162         public void playPause() {
0163             if (isPauseAllowed() || isPlayAllowed()) {
0164                 sendCommand(getPlayerName(), "action", "PlayPause");
0165             }
0166         }
0167 
0168         public void play() {
0169             if (isPlayAllowed()) {
0170                 sendCommand(getPlayerName(), "action", "Play");
0171             }
0172         }
0173 
0174         public void pause() {
0175             if (isPauseAllowed()) {
0176                 sendCommand(getPlayerName(), "action", "Pause");
0177             }
0178         }
0179 
0180         public void stop() {
0181             sendCommand(getPlayerName(), "action", "Stop");
0182         }
0183 
0184         public void previous() {
0185             if (isGoPreviousAllowed()) {
0186                 sendCommand(getPlayerName(), "action", "Previous");
0187             }
0188         }
0189 
0190         public void next() {
0191             if (isGoNextAllowed()) {
0192                 sendCommand(getPlayerName(), "action", "Next");
0193             }
0194         }
0195 
0196         public void setLoopStatus(String loopStatus) {
0197             sendCommand(getPlayerName(), "setLoopStatus", loopStatus);
0198         }
0199 
0200         public void setShuffle(boolean shuffle) {
0201             sendCommand(getPlayerName(), "setShuffle", shuffle);
0202         }
0203 
0204         public void setVolume(int volume) {
0205             if (isSetVolumeAllowed()) {
0206                 sendCommand(getPlayerName(), "setVolume", volume);
0207             }
0208         }
0209 
0210         public void setPosition(int position) {
0211             if (isSeekAllowed()) {
0212                 sendCommand(getPlayerName(), "SetPosition", position);
0213 
0214                 lastPosition = position;
0215                 lastPositionTime = System.currentTimeMillis();
0216             }
0217         }
0218 
0219         public void seek(int offset) {
0220             if (isSeekAllowed()) {
0221                 sendCommand(getPlayerName(), "Seek", offset);
0222             }
0223         }
0224     }
0225 
0226     public interface Callback {
0227         void callback();
0228     }
0229 
0230     public final static String DEVICE_ID_KEY = "deviceId";
0231     private final static String PACKET_TYPE_MPRIS = "kdeconnect.mpris";
0232     private final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request";
0233 
0234     private final ConcurrentHashMap<String, MprisPlayer> players = new ConcurrentHashMap<>();
0235     private boolean supportAlbumArtPayload = false;
0236     private final ConcurrentHashMap<String, Callback> playerStatusUpdated = new ConcurrentHashMap<>();
0237     private final ConcurrentHashMap<String, Callback> playerListUpdated = new ConcurrentHashMap<>();
0238 
0239     @Override
0240     public @NonNull String getDisplayName() {
0241         return context.getResources().getString(R.string.pref_plugin_mpris);
0242     }
0243 
0244     @Override
0245     public @NonNull String getDescription() {
0246         return context.getResources().getString(R.string.pref_plugin_mpris_desc);
0247     }
0248 
0249     @Override
0250     public @DrawableRes int getIcon() {
0251         return R.drawable.mpris_plugin_action_24dp;
0252     }
0253 
0254     @Override
0255     public boolean hasSettings() {
0256         return true;
0257     }
0258 
0259     @Override
0260     public PluginSettingsFragment getSettingsFragment(Activity activity) {
0261         return PluginSettingsFragment.newInstance(getPluginKey(), R.xml.mprisplugin_preferences);
0262     }
0263 
0264     @Override
0265     public boolean onCreate() {
0266         MprisMediaSession.getInstance().onCreate(context.getApplicationContext(), this, device.getDeviceId());
0267 
0268         //Always request the player list so the data is up-to-date
0269         requestPlayerList();
0270 
0271         AlbumArtCache.initializeDiskCache(context);
0272         AlbumArtCache.registerPlugin(this);
0273 
0274         return true;
0275     }
0276 
0277     @Override
0278     public void onDestroy() {
0279         players.clear();
0280         AlbumArtCache.deregisterPlugin(this);
0281         MprisMediaSession.getInstance().onDestroy(this, device.getDeviceId());
0282     }
0283 
0284     private void sendCommand(String player, String method, String value) {
0285         NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
0286         np.set("player", player);
0287         np.set(method, value);
0288         device.sendPacket(np);
0289     }
0290 
0291     private void sendCommand(String player, String method, boolean value) {
0292         NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
0293         np.set("player", player);
0294         np.set(method, value);
0295         device.sendPacket(np);
0296     }
0297 
0298     private void sendCommand(String player, String method, int value) {
0299         NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
0300         np.set("player", player);
0301         np.set(method, value);
0302         device.sendPacket(np);
0303     }
0304 
0305     @Override
0306     public boolean onPacketReceived(@NonNull NetworkPacket np) {
0307         if (np.getBoolean("transferringAlbumArt", false)) {
0308             AlbumArtCache.payloadToDiskCache(np.getString("albumArtUrl"), np.getPayload());
0309             return true;
0310         }
0311 
0312         if (np.has("player")) {
0313             MprisPlayer playerStatus = players.get(np.getString("player"));
0314             if (playerStatus != null) {
0315                 //Note: title, artist and album will not be available for all desktop clients
0316                 playerStatus.title = np.getString("title", playerStatus.title);
0317                 playerStatus.artist = np.getString("artist", playerStatus.artist);
0318                 playerStatus.album = np.getString("album", playerStatus.album);
0319                 playerStatus.url = np.getString("url", playerStatus.url);
0320                 if (np.has("loopStatus")) {
0321                     playerStatus.loopStatus = np.getString("loopStatus", playerStatus.loopStatus);
0322                     playerStatus.loopStatusAllowed = true;
0323                 }
0324                 if (np.has("shuffle")) {
0325                     playerStatus.shuffle = np.getBoolean("shuffle", playerStatus.shuffle);
0326                     playerStatus.shuffleAllowed = true;
0327                 }
0328                 playerStatus.volume = np.getInt("volume", playerStatus.volume);
0329                 playerStatus.length = np.getLong("length", playerStatus.length);
0330                 if (np.has("pos")) {
0331                     playerStatus.lastPosition = np.getLong("pos", playerStatus.lastPosition);
0332                     playerStatus.lastPositionTime = System.currentTimeMillis();
0333                 }
0334                 playerStatus.playing = np.getBoolean("isPlaying", playerStatus.playing);
0335                 playerStatus.playAllowed = np.getBoolean("canPlay", playerStatus.playAllowed);
0336                 playerStatus.pauseAllowed = np.getBoolean("canPause", playerStatus.pauseAllowed);
0337                 playerStatus.goNextAllowed = np.getBoolean("canGoNext", playerStatus.goNextAllowed);
0338                 playerStatus.goPreviousAllowed = np.getBoolean("canGoPrevious", playerStatus.goPreviousAllowed);
0339                 playerStatus.seekAllowed = np.getBoolean("canSeek", playerStatus.seekAllowed);
0340                 String newAlbumArtUrlstring = np.getString("albumArtUrl", playerStatus.albumArtUrl);
0341                 try {
0342                     //Turn the url into canonical form (and check its validity)
0343                     URL newAlbumArtUrl = new URL(newAlbumArtUrlstring);
0344                     playerStatus.albumArtUrl = newAlbumArtUrl.toString();
0345                 } catch (MalformedURLException ignored) {
0346                     playerStatus.albumArtUrl = "";
0347                 }
0348 
0349                 for (String key : playerStatusUpdated.keySet()) {
0350                     try {
0351                         playerStatusUpdated.get(key).callback();
0352                     } catch (Exception e) {
0353                         Log.e("MprisControl", "Exception", e);
0354                         playerStatusUpdated.remove(key);
0355                     }
0356                 }
0357             }
0358         }
0359 
0360         //Remember if the connected device support album art payloads
0361         supportAlbumArtPayload = np.getBoolean("supportAlbumArtPayload", supportAlbumArtPayload);
0362 
0363         List<String> newPlayerList = np.getStringList("playerList");
0364         if (newPlayerList != null) {
0365             boolean equals = true;
0366             for (String newPlayer : newPlayerList) {
0367                 if (!players.containsKey(newPlayer)) {
0368                     equals = false;
0369 
0370                     MprisPlayer player = new MprisPlayer();
0371                     player.player = newPlayer;
0372                     players.put(newPlayer, player);
0373 
0374                     //Immediately ask for the data of this player
0375                     requestPlayerStatus(newPlayer);
0376                 }
0377             }
0378             Iterator<HashMap.Entry<String, MprisPlayer>> iter = players.entrySet().iterator();
0379             while (iter.hasNext()) {
0380                 String oldPlayer = iter.next().getKey();
0381                 final boolean found = newPlayerList.stream().anyMatch(newPlayer ->
0382                         newPlayer.equals(oldPlayer));
0383 
0384                 if (!found) {
0385                     iter.remove();
0386                     equals = false;
0387                 }
0388             }
0389             if (!equals) {
0390                 for (String key : playerListUpdated.keySet()) {
0391                     try {
0392                         playerListUpdated.get(key).callback();
0393                     } catch (Exception e) {
0394                         Log.e("MprisControl", "Exception", e);
0395                         playerListUpdated.remove(key);
0396                     }
0397                 }
0398             }
0399         }
0400 
0401         return true;
0402     }
0403 
0404     @Override
0405     public @NonNull String[] getSupportedPacketTypes() {
0406         return new String[]{PACKET_TYPE_MPRIS};
0407     }
0408 
0409     @Override
0410     public @NonNull String[] getOutgoingPacketTypes() {
0411         return new String[]{PACKET_TYPE_MPRIS_REQUEST};
0412     }
0413 
0414     public void setPlayerStatusUpdatedHandler(String id, Callback h) {
0415         playerStatusUpdated.put(id, h);
0416         h.callback();
0417     }
0418 
0419     public void removePlayerStatusUpdatedHandler(String id) {
0420         playerStatusUpdated.remove(id);
0421     }
0422 
0423     public void setPlayerListUpdatedHandler(String id, Callback h) {
0424         playerListUpdated.put(id, h);
0425 
0426         h.callback();
0427     }
0428 
0429     public void removePlayerListUpdatedHandler(String id) {
0430         playerListUpdated.remove(id);
0431     }
0432 
0433     public List<String> getPlayerList() {
0434         List<String> playerlist = new ArrayList<>(players.keySet());
0435         Collections.sort(playerlist);
0436         return playerlist;
0437     }
0438 
0439     public MprisPlayer getPlayerStatus(String player) {
0440         if (player == null) {
0441             return null;
0442         }
0443         return players.get(player);
0444     }
0445 
0446     public MprisPlayer getEmptyPlayer() {
0447         return new MprisPlayer();
0448     }
0449 
0450     /**
0451      * Returns a playing mpris player, if any exist
0452      *
0453      * @return null if no players are playing, a playing player otherwise
0454      */
0455     public MprisPlayer getPlayingPlayer() {
0456         return players.values().stream().filter(MprisPlayer::isPlaying).findFirst().orElse(null);
0457     }
0458 
0459     boolean hasPlayer(MprisPlayer player) {
0460         if (player == null) {
0461             return false;
0462         }
0463         return players.containsValue(player);
0464     }
0465 
0466     private void requestPlayerList() {
0467         NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
0468         np.set("requestPlayerList", true);
0469         device.sendPacket(np);
0470     }
0471 
0472     private void requestPlayerStatus(String player) {
0473         NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
0474         np.set("player", player);
0475         np.set("requestNowPlaying", true);
0476         np.set("requestVolume", true);
0477         device.sendPacket(np);
0478     }
0479 
0480     @Override
0481     public boolean displayAsButton(Context context) {
0482         return true;
0483     }
0484 
0485     @Override
0486     public void startMainActivity(Activity parentActivity) {
0487         Intent intent = new Intent(parentActivity, MprisActivity.class);
0488         intent.putExtra("deviceId", device.getDeviceId());
0489         parentActivity.startActivity(intent);
0490     }
0491 
0492     @Override
0493     public @NonNull String getActionName() {
0494         return context.getString(R.string.open_mpris_controls);
0495     }
0496 
0497     public void fetchedAlbumArt(String url) {
0498         if (players.values().stream().anyMatch(player -> url.equals(player.albumArtUrl))) {
0499             for (String key : playerStatusUpdated.keySet()) {
0500                 try {
0501                     playerStatusUpdated.get(key).callback();
0502                 } catch (Exception e) {
0503                     Log.e("MprisControl", "Exception", e);
0504                     playerStatusUpdated.remove(key);
0505                 }
0506             }
0507         }
0508     }
0509 
0510     public boolean askTransferAlbumArt(String url, String playerName) {
0511         //First check if the remote supports transferring album art
0512         if (!supportAlbumArtPayload) return false;
0513         if (url.isEmpty()) return false;
0514 
0515         MprisPlayer player = getPlayerStatus(playerName);
0516         if (player == null) return false;
0517 
0518         if (player.albumArtUrl.equals(url)) {
0519             NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
0520             np.set("player", player.getPlayerName());
0521             np.set("albumArtUrl", url);
0522             device.sendPacket(np);
0523             return true;
0524         }
0525         return false;
0526     }
0527 
0528     @NonNull
0529     @Override
0530     protected String[] getOptionalPermissions() {
0531         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
0532             return new String[]{Manifest.permission.POST_NOTIFICATIONS};
0533         } else {
0534             return ArrayUtils.EMPTY_STRING_ARRAY;
0535         }
0536     }
0537 
0538     @Override
0539     protected int getOptionalPermissionExplanation() {
0540         return R.string.mpris_notifications_explanation;
0541     }
0542 }