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 }