diff --git a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverCallback.java b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverCallback.java index cf701cc3..97a4f615 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverCallback.java +++ b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverCallback.java @@ -1,68 +1,57 @@ /* * Copyright 2018 Nicolas Fella * * 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.MprisReceiverPlugin; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.PlaybackState; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) class MprisReceiverCallback extends MediaController.Callback { private static final String TAG = "MprisReceiver"; private final MprisReceiverPlayer player; private final MprisReceiverPlugin plugin; MprisReceiverCallback(MprisReceiverPlugin plugin, MprisReceiverPlayer player) { this.player = player; this.plugin = plugin; } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override public void onPlaybackStateChanged(@NonNull PlaybackState state) { - switch (state.getState()) { - case PlaybackState.STATE_PLAYING: - player.setPlaying(true); - plugin.sendPlaying(player); - break; - case PlaybackState.STATE_PAUSED: - player.setPaused(true); - plugin.sendPlaying(player); - break; - } + plugin.sendMetadata(player); } @Override public void onMetadataChanged(@Nullable MediaMetadata metadata) { - if (metadata == null) - return; plugin.sendMetadata(player); } } diff --git a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlayer.java b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlayer.java index e0431601..a138e860 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlayer.java +++ b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlayer.java @@ -1,118 +1,171 @@ /* * Copyright 2018 Nicolas Fella * * 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.MprisReceiverPlugin; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.PlaybackState; import android.os.Build; import androidx.annotation.RequiresApi; @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) class MprisReceiverPlayer { private final MediaController controller; private final String name; - private boolean isPlaying; - MprisReceiverPlayer(MediaController controller, String name) { - this.controller = controller; this.name = name; - - if (controller.getPlaybackState() != null) { - isPlaying = controller.getPlaybackState().getState() == PlaybackState.STATE_PLAYING; - } } boolean isPlaying() { - return isPlaying; + PlaybackState state = controller.getPlaybackState(); + if (state == null) return false; + + return state.getState() == PlaybackState.STATE_PLAYING; } - void setPlaying(boolean playing) { - isPlaying = playing; + boolean canPlay() { + PlaybackState state = controller.getPlaybackState(); + if (state == null) return false; + + if (state.getState() == PlaybackState.STATE_PLAYING) return true; + + return (state.getActions() & (PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_PAUSE)) != 0; } - boolean isPaused() { - return !isPlaying; + boolean canPause() { + PlaybackState state = controller.getPlaybackState(); + if (state == null) return false; + + if (state.getState() == PlaybackState.STATE_PAUSED) return true; + + return (state.getActions() & (PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY_PAUSE)) != 0; + } + + boolean canGoPrevious() { + PlaybackState state = controller.getPlaybackState(); + if (state == null) return false; + + return (state.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0; + } + + boolean canGoNext() { + PlaybackState state = controller.getPlaybackState(); + if (state == null) return false; + + return (state.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0; } - void setPaused(boolean paused) { - isPlaying = !paused; + boolean canSeek() { + PlaybackState state = controller.getPlaybackState(); + if (state == null) return false; + + return (state.getActions() & PlaybackState.ACTION_SEEK_TO) != 0; } void playPause() { - if (isPlaying) { + if (isPlaying()) { controller.getTransportControls().pause(); } else { controller.getTransportControls().play(); } } String getName() { return name; } String getAlbum() { - if (controller.getMetadata() == null) - return ""; - String album = controller.getMetadata().getString(MediaMetadata.METADATA_KEY_ALBUM); + MediaMetadata metadata = controller.getMetadata(); + if (metadata == null) return ""; + + String album = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM); return album != null ? album : ""; } String getArtist() { - if (controller.getMetadata() == null) - return ""; + MediaMetadata metadata = controller.getMetadata(); + if (metadata == null) return ""; + + String artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST); + if (artist == null || artist.isEmpty()) artist = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST); + if (artist == null || artist.isEmpty()) artist = metadata.getString(MediaMetadata.METADATA_KEY_AUTHOR); + if (artist == null || artist.isEmpty()) artist = metadata.getString(MediaMetadata.METADATA_KEY_WRITER); - String artist = controller.getMetadata().getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST); return artist != null ? artist : ""; } String getTitle() { - if (controller.getMetadata() == null) - return ""; - String title = controller.getMetadata().getString(MediaMetadata.METADATA_KEY_TITLE); + MediaMetadata metadata = controller.getMetadata(); + if (metadata == null) return ""; + + String title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE); + if (title == null || title.isEmpty()) title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE); return title != null ? title : ""; } void previous() { controller.getTransportControls().skipToPrevious(); } void next() { controller.getTransportControls().skipToNext(); } + void play() { + controller.getTransportControls().play(); + } + + void pause() { + controller.getTransportControls().pause(); + } + + void stop() { + controller.getTransportControls().stop(); + } + int getVolume() { if (controller.getPlaybackInfo() == null) return 0; return 100 * controller.getPlaybackInfo().getCurrentVolume() / controller.getPlaybackInfo().getMaxVolume(); } long getPosition() { if (controller.getPlaybackState() == null) return 0; return controller.getPlaybackState().getPosition(); } + + void setPosition(long position) { + controller.getTransportControls().seekTo(position); + } + + long getLength() { + MediaMetadata metadata = controller.getMetadata(); + if (metadata == null) return 0; + + return metadata.getLong(MediaMetadata.METADATA_KEY_DURATION); + } } diff --git a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java index bddd150b..5f3ddff5 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java @@ -1,237 +1,244 @@ /* * Copyright 2018 Nicolas Fella * * 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.MprisReceiverPlugin; import android.content.ComponentName; import android.content.Context; import android.media.session.MediaController; import android.media.session.MediaSessionManager; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.util.Log; import org.kde.kdeconnect.Helpers.AppsHelper; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationReceiver; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.UserInterface.AlertDialogFragment; import org.kde.kdeconnect.UserInterface.StartActivityAlertDialogFragment; import org.kde.kdeconnect_tp.R; import java.util.HashMap; import java.util.List; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @PluginFactory.LoadablePlugin public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.OnActiveSessionsChangedListener { private final static String PACKET_TYPE_MPRIS = "kdeconnect.mpris"; private final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request"; private static final String TAG = "MprisReceiver"; private HashMap players; @Override public boolean onCreate() { if (!hasPermission()) return false; players = new HashMap<>(); try { MediaSessionManager manager = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); if (null == manager) return false; manager.addOnActiveSessionsChangedListener(MprisReceiverPlugin.this, new ComponentName(context, NotificationReceiver.class), new Handler(Looper.getMainLooper())); createPlayers(manager.getActiveSessions(new ComponentName(context, NotificationReceiver.class))); sendPlayerList(); } catch (Exception e) { Log.e(TAG, "Exception", e); } return true; } private void createPlayers(List sessions) { for (MediaController controller : sessions) { createPlayer(controller); } } @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_mprisreceiver); } @Override public String getDescription() { return context.getResources().getString(R.string.pref_plugin_mprisreceiver_desc); } @Override public boolean onPacketReceived(NetworkPacket np) { if (np.getBoolean("requestPlayerList")) { sendPlayerList(); return true; } if (!np.has("player")) { return false; } MprisReceiverPlayer player = players.get(np.getString("player")); if (null == player) { return false; } if (np.getBoolean("requestNowPlaying", false)) { sendMetadata(player); return true; } + if (np.has("SetPosition")) { + long position = np.getLong("SetPosition", 0); + player.setPosition(position); + } + if (np.has("action")) { String action = np.getString("action"); switch (action) { + case "Play": + player.play(); + break; + case "Pause": + player.pause(); + break; case "PlayPause": player.playPause(); break; case "Next": player.next(); break; case "Previous": player.previous(); + break; + case "Stop": + player.stop(); + break; } } return true; } @Override public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_MPRIS_REQUEST}; } @Override public String[] getOutgoingPacketTypes() { return new String[]{PACKET_TYPE_MPRIS}; } @Override public void onActiveSessionsChanged(@Nullable List controllers) { if (null == controllers) { return; } players.clear(); createPlayers(controllers); sendPlayerList(); } private void createPlayer(MediaController controller) { // Skip the media session we created ourselves as KDE Connect if (controller.getPackageName().equals(context.getPackageName())) return; MprisReceiverPlayer player = new MprisReceiverPlayer(controller, AppsHelper.appNameLookup(context, controller.getPackageName())); controller.registerCallback(new MprisReceiverCallback(this, player), new Handler(Looper.getMainLooper())); players.put(player.getName(), player); } private void sendPlayerList() { NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS); np.set("playerList", players.keySet()); device.sendPacket(np); } - void sendPlaying(MprisReceiverPlayer player) { - - NetworkPacket np = new NetworkPacket(MprisReceiverPlugin.PACKET_TYPE_MPRIS); - np.set("player", player.getName()); - np.set("isPlaying", player.isPlaying()); - device.sendPacket(np); - } - @Override public int getMinSdk() { return Build.VERSION_CODES.LOLLIPOP_MR1; } - public void sendMetadata(MprisReceiverPlayer player) { + void sendMetadata(MprisReceiverPlayer player) { NetworkPacket np = new NetworkPacket(MprisReceiverPlugin.PACKET_TYPE_MPRIS); np.set("player", player.getName()); if (player.getArtist().isEmpty()) { np.set("nowPlaying", player.getTitle()); } else { np.set("nowPlaying", player.getArtist() + " - " + player.getTitle()); } np.set("title", player.getTitle()); np.set("artist", player.getArtist()); np.set("album", player.getAlbum()); np.set("isPlaying", player.isPlaying()); np.set("pos", player.getPosition()); - device.sendPacket(np); - } - - public void sendVolume(MprisReceiverPlayer player) { - NetworkPacket np = new NetworkPacket(MprisReceiverPlugin.PACKET_TYPE_MPRIS); - np.set("player", player.getName()); + np.set("length", player.getLength()); + np.set("canPlay", player.canPlay()); + np.set("canPause", player.canPause()); + np.set("canGoPrevious", player.canGoPrevious()); + np.set("canGoNext", player.canGoNext()); + np.set("canSeek", player.canSeek()); np.set("volume", player.getVolume()); device.sendPacket(np); } @Override public boolean checkRequiredPermissions() { //Notifications use a different kind of permission, because it was added before the current runtime permissions model return hasPermission(); } @Override public AlertDialogFragment getPermissionExplanationDialog(int requestCode) { return new StartActivityAlertDialogFragment.Builder() .setTitle(R.string.pref_plugin_mpris) .setMessage(R.string.no_permission_mprisreceiver) .setPositiveButton(R.string.open_settings) .setNegativeButton(R.string.cancel) .setIntentAction("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS") .setStartForResult(true) .setRequestCode(requestCode) .create(); } private boolean hasPermission() { String notificationListenerList = Settings.Secure.getString(context.getContentResolver(), "enabled_notification_listeners"); return (notificationListenerList != null && notificationListenerList.contains(context.getPackageName())); } }