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

0001 /*
0002  * SPDX-FileCopyrightText: 2018 Nicolas Fella <nicolas.fella@gmx.de>
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.MprisReceiverPlugin;
0008 
0009 import android.content.ComponentName;
0010 import android.media.session.MediaController;
0011 import android.media.session.MediaSessionManager;
0012 import android.os.Build;
0013 import android.os.Handler;
0014 import android.os.Looper;
0015 import android.provider.Settings;
0016 import android.util.Log;
0017 
0018 import androidx.annotation.NonNull;
0019 import androidx.annotation.Nullable;
0020 import androidx.annotation.RequiresApi;
0021 import androidx.core.content.ContextCompat;
0022 import androidx.fragment.app.DialogFragment;
0023 
0024 import org.apache.commons.lang3.StringUtils;
0025 import org.kde.kdeconnect.Helpers.AppsHelper;
0026 import org.kde.kdeconnect.NetworkPacket;
0027 import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationReceiver;
0028 import org.kde.kdeconnect.Plugins.Plugin;
0029 import org.kde.kdeconnect.Plugins.PluginFactory;
0030 import org.kde.kdeconnect.UserInterface.MainActivity;
0031 import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment;
0032 import org.kde.kdeconnect_tp.R;
0033 
0034 import java.util.HashMap;
0035 import java.util.List;
0036 import java.util.stream.Collectors;
0037 import java.util.stream.Stream;
0038 
0039 @PluginFactory.LoadablePlugin
0040 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
0041 public class MprisReceiverPlugin extends Plugin {
0042     private final static String PACKET_TYPE_MPRIS = "kdeconnect.mpris";
0043     private final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request";
0044 
0045     private static final String TAG = "MprisReceiver";
0046 
0047     private HashMap<String, MprisReceiverPlayer> players;
0048     private HashMap<String, MprisReceiverCallback> playerCbs;
0049     private MediaSessionChangeListener mediaSessionChangeListener;
0050 
0051     @Override
0052     public boolean onCreate() {
0053 
0054         if (!hasPermission())
0055             return false;
0056 
0057         players = new HashMap<>();
0058         playerCbs = new HashMap<>();
0059         try {
0060             MediaSessionManager manager = ContextCompat.getSystemService(context, MediaSessionManager.class);
0061             if (null == manager)
0062                 return false;
0063 
0064             assert(mediaSessionChangeListener == null);
0065             mediaSessionChangeListener = new MediaSessionChangeListener();
0066             manager.addOnActiveSessionsChangedListener(mediaSessionChangeListener, new ComponentName(context, NotificationReceiver.class), new Handler(Looper.getMainLooper()));
0067 
0068             createPlayers(manager.getActiveSessions(new ComponentName(context, NotificationReceiver.class)));
0069             sendPlayerList();
0070         } catch (Exception e) {
0071             Log.e(TAG, "Exception", e);
0072         }
0073 
0074         return true;
0075     }
0076 
0077     @Override
0078     public void onDestroy() {
0079         super.onDestroy();
0080         MediaSessionManager manager = ContextCompat.getSystemService(context, MediaSessionManager.class);
0081         if (manager != null && mediaSessionChangeListener != null) {
0082             manager.removeOnActiveSessionsChangedListener(mediaSessionChangeListener);
0083             mediaSessionChangeListener = null;
0084         }
0085     }
0086 
0087     private void createPlayers(List<MediaController> sessions) {
0088         for (MediaController controller : sessions) {
0089             createPlayer(controller);
0090         }
0091     }
0092 
0093     @Override
0094     public @NonNull String getDisplayName() {
0095         return context.getResources().getString(R.string.pref_plugin_mprisreceiver);
0096     }
0097 
0098     @Override
0099     public @NonNull String getDescription() {
0100         return context.getResources().getString(R.string.pref_plugin_mprisreceiver_desc);
0101     }
0102 
0103     @Override
0104     public boolean onPacketReceived(@NonNull NetworkPacket np) {
0105 
0106         if (np.getBoolean("requestPlayerList")) {
0107             sendPlayerList();
0108             return true;
0109         }
0110 
0111         if (!np.has("player")) {
0112             return false;
0113         }
0114         MprisReceiverPlayer player = players.get(np.getString("player"));
0115 
0116         if (null == player) {
0117             return false;
0118         }
0119 
0120         if (np.getBoolean("requestNowPlaying", false)) {
0121             sendMetadata(player);
0122             return true;
0123         }
0124 
0125         if (np.has("SetPosition")) {
0126             long position = np.getLong("SetPosition", 0);
0127             player.setPosition(position);
0128         }
0129 
0130         if (np.has("setVolume")) {
0131             int volume = np.getInt("setVolume", 100);
0132             player.setVolume(volume);
0133             //Setting volume doesn't seem to always trigger the callback
0134             sendMetadata(player);
0135         }
0136 
0137         if (np.has("action")) {
0138             String action = np.getString("action");
0139 
0140             switch (action) {
0141                 case "Play":
0142                     player.play();
0143                     break;
0144                 case "Pause":
0145                     player.pause();
0146                     break;
0147                 case "PlayPause":
0148                     player.playPause();
0149                     break;
0150                 case "Next":
0151                     player.next();
0152                     break;
0153                 case "Previous":
0154                     player.previous();
0155                     break;
0156                 case "Stop":
0157                     player.stop();
0158                     break;
0159             }
0160         }
0161 
0162         return true;
0163     }
0164 
0165     @Override
0166     public @NonNull String[] getSupportedPacketTypes() {
0167         return new String[]{PACKET_TYPE_MPRIS_REQUEST};
0168     }
0169 
0170     @Override
0171     public @NonNull String[] getOutgoingPacketTypes() {
0172         return new String[]{PACKET_TYPE_MPRIS};
0173     }
0174 
0175     private final class MediaSessionChangeListener implements MediaSessionManager.OnActiveSessionsChangedListener {
0176         @Override
0177         public void onActiveSessionsChanged(@Nullable List<MediaController> controllers) {
0178 
0179             if (null == controllers) {
0180                 return;
0181             }
0182             for (MprisReceiverPlayer p : players.values()) {
0183                 p.getController().unregisterCallback(playerCbs.get(p.getName()));
0184             }
0185             playerCbs.clear();
0186             players.clear();
0187 
0188             createPlayers(controllers);
0189             sendPlayerList();
0190 
0191         }
0192     }
0193 
0194     private void createPlayer(MediaController controller) {
0195         // Skip the media session we created ourselves as KDE Connect
0196         if (controller.getPackageName().equals(context.getPackageName())) return;
0197 
0198         MprisReceiverPlayer player = new MprisReceiverPlayer(controller, AppsHelper.appNameLookup(context, controller.getPackageName()));
0199         MprisReceiverCallback cb = new MprisReceiverCallback(this, player);
0200         controller.registerCallback(cb, new Handler(Looper.getMainLooper()));
0201         playerCbs.put(player.getName(), cb);
0202         players.put(player.getName(), player);
0203     }
0204 
0205     private void sendPlayerList() {
0206         NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS);
0207         np.set("playerList", players.keySet());
0208         device.sendPacket(np);
0209     }
0210 
0211     @Override
0212     public int getMinSdk() {
0213         return Build.VERSION_CODES.LOLLIPOP_MR1;
0214     }
0215 
0216     void sendMetadata(MprisReceiverPlayer player) {
0217         NetworkPacket np = new NetworkPacket(MprisReceiverPlugin.PACKET_TYPE_MPRIS);
0218         np.set("player", player.getName());
0219         np.set("title", player.getTitle());
0220         np.set("artist", player.getArtist());
0221         String nowPlaying = Stream.of(player.getArtist(), player.getTitle())
0222             .filter(StringUtils::isNotEmpty).collect(Collectors.joining(" - "));
0223         np.set("nowPlaying", nowPlaying); // GSConnect 50 (so, Ubuntu 22.04) needs this
0224         np.set("album", player.getAlbum());
0225         np.set("isPlaying", player.isPlaying());
0226         np.set("pos", player.getPosition());
0227         np.set("length", player.getLength());
0228         np.set("canPlay", player.canPlay());
0229         np.set("canPause", player.canPause());
0230         np.set("canGoPrevious", player.canGoPrevious());
0231         np.set("canGoNext", player.canGoNext());
0232         np.set("canSeek", player.canSeek());
0233         np.set("volume", player.getVolume());
0234         device.sendPacket(np);
0235     }
0236 
0237     @Override
0238     public boolean checkRequiredPermissions() {
0239         //Notifications use a different kind of permission, because it was added before the current runtime permissions model
0240         return hasPermission();
0241     }
0242 
0243     @Override
0244     public @NonNull DialogFragment getPermissionExplanationDialog() {
0245         return new StartActivityAlertDialogFragment.Builder()
0246                 .setTitle(R.string.pref_plugin_mpris)
0247                 .setMessage(R.string.no_permission_mprisreceiver)
0248                 .setPositiveButton(R.string.open_settings)
0249                 .setNegativeButton(R.string.cancel)
0250                 .setIntentAction("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")
0251                 .setStartForResult(true)
0252                 .setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
0253                 .create();
0254     }
0255 
0256     private boolean hasPermission() {
0257         String notificationListenerList = Settings.Secure.getString(context.getContentResolver(), "enabled_notification_listeners");
0258         return StringUtils.contains(notificationListenerList, context.getPackageName());
0259     }
0260 
0261 }