diff --git a/res/values/strings.xml b/res/values/strings.xml
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -31,6 +31,7 @@
Cancel
Open settings
You need to grant permission to access notifications
+ To be able to control your media players you need to grant access to the notifications
Send ping
Multimedia control
remotekeyboard_editing_only
@@ -237,5 +238,7 @@
You will need to confirm the command on the desktop
There are no commands registered
You can add new commands in the KDE Connect System Settings
+ Media Player Control
+ Control your phones media players from another device
diff --git a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverCallback.java b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverCallback.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverCallback.java
@@ -0,0 +1,78 @@
+/*
+ * 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 android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+
+
+@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) {
+ Log.d(TAG, "State changed");
+ switch (state.getState()) {
+ case PlaybackState.STATE_PLAYING:
+ Log.d(TAG, "Playing");
+ player.setPlaying(true);
+ plugin.sendPlaying(player);
+ break;
+ case PlaybackState.STATE_PAUSED:
+ player.setPaused(true);
+ plugin.sendPlaying(player);
+ Log.d(TAG, "Paused");
+ break;
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(@Nullable MediaMetadata metadata) {
+ if (metadata == null)
+ return;
+ Log.d(TAG, "TITLE " + metadata.getString(MediaMetadata.METADATA_KEY_TITLE));
+ Log.d(TAG, "ALBUM " + metadata.getString(MediaMetadata.METADATA_KEY_ALBUM));
+ Log.d(TAG, "ALBUM ARTIST " + metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST));
+ Log.d(TAG, "ART URI " + metadata.getString(MediaMetadata.METADATA_KEY_ART_URI));
+ Log.d(TAG, "DISPLAY TITLE " + metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE));
+ Log.d(TAG, "MEDIA ID " + metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID));
+ Log.d(TAG, "WRITER " + metadata.getString(MediaMetadata.METADATA_KEY_WRITER));
+ plugin.sendMetadata(player);
+ }
+
+}
diff --git a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlayer.java b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlayer.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlayer.java
@@ -0,0 +1,117 @@
+/*
+ * 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 android.support.annotation.RequiresApi;
+
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class MprisReceiverPlayer {
+
+ private MediaController controller;
+
+ private 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;
+ }
+
+ void setPlaying(boolean playing) {
+ isPlaying = playing;
+ }
+
+ boolean isPaused() {
+ return !isPlaying;
+ }
+
+ void setPaused(boolean paused) {
+ isPlaying = !paused;
+ }
+
+ void playPause() {
+ 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);
+ return album != null ? album : "";
+ }
+
+ String getArtist() {
+ if (controller.getMetadata() == null)
+ return "";
+
+ 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);
+ return title != null ? title : "";
+ }
+
+ void previous() {
+ controller.getTransportControls().skipToPrevious();
+ }
+
+ void next() {
+ controller.getTransportControls().skipToNext();
+ }
+
+ 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();
+ }
+}
diff --git a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java
@@ -0,0 +1,245 @@
+/*
+ * 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.app.Activity;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+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.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+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.UserInterface.MainActivity;
+import org.kde.kdeconnect_tp.R;
+
+import java.util.HashMap;
+import java.util.List;
+
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+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) {
+ Log.d(TAG, "creating players");
+ for (MediaController controller : sessions) {
+ Log.d(TAG, controller.getPackageName());
+ 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;
+ }
+ Log.d(TAG, np.getString("player"));
+ MprisReceiverPlayer player = players.get(np.getString("player"));
+
+ if (null == player) {
+ return false;
+ }
+
+ if (np.getBoolean("requestNowPlaying", false)) {
+ Log.d(TAG, "Sending status for player " + player.getName());
+ sendMetadata(player);
+ return true;
+ }
+
+ if (np.has("action")) {
+ String action = np.getString("action");
+
+ switch (action) {
+ case "PlayPause":
+ player.playPause();
+ break;
+ case "Next":
+ player.next();
+ break;
+ case "Previous":
+ player.previous();
+ }
+ }
+
+ 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) {
+ Log.d(TAG, "Empty");
+ return;
+ }
+
+ players.clear();
+
+ createPlayers(controllers);
+ sendPlayerList();
+
+ }
+
+ private void createPlayer(MediaController controller) {
+ Log.d(TAG, "Found new Mediacontroller " + controller.getPackageName());
+ 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() {
+ Log.d(TAG, "Sending player list");
+ 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) {
+ Log.d(TAG, "Sending metadata");
+ 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("volume", player.getVolume());
+ device.sendPacket(np);
+ }
+
+ @Override
+ public AlertDialog getErrorDialog(final Activity deviceActivity) {
+
+ return new AlertDialog.Builder(deviceActivity)
+ .setTitle(R.string.pref_plugin_mpris)
+ .setMessage(R.string.no_permission_mprisreceiver)
+ .setPositiveButton(R.string.open_settings, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
+ deviceActivity.startActivityForResult(intent, MainActivity.RESULT_NEEDS_RELOAD);
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ //Do nothing
+ }
+ })
+ .create();
+ }
+
+ private boolean hasPermission() {
+ String notificationListenerList = Settings.Secure.getString(context.getContentResolver(), "enabled_notification_listeners");
+ return (notificationListenerList != null && notificationListenerList.contains(context.getPackageName()));
+ }
+
+}
diff --git a/src/org/kde/kdeconnect/Plugins/PluginFactory.java b/src/org/kde/kdeconnect/Plugins/PluginFactory.java
--- a/src/org/kde/kdeconnect/Plugins/PluginFactory.java
+++ b/src/org/kde/kdeconnect/Plugins/PluginFactory.java
@@ -30,6 +30,7 @@
import org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhonePlugin;
import org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadPlugin;
import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin;
+import org.kde.kdeconnect.Plugins.MprisReceiverPlugin.MprisReceiverPlugin;
import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationsPlugin;
import org.kde.kdeconnect.Plugins.PingPlugin.PingPlugin;
import org.kde.kdeconnect.Plugins.ReceiveNotificationsPlugin.ReceiveNotificationsPlugin;
@@ -128,6 +129,7 @@
PluginFactory.registerPlugin(FindMyPhonePlugin.class);
PluginFactory.registerPlugin(RunCommandPlugin.class);
PluginFactory.registerPlugin(RemoteKeyboardPlugin.class);
+ PluginFactory.registerPlugin(MprisReceiverPlugin.class);
}
public static PluginInfo getPluginInfo(Context context, String pluginKey) {