diff --git a/res/drawable/ic_volume_mute_black.xml b/res/drawable/ic_volume_mute_black.xml
new file mode 100644
--- /dev/null
+++ b/res/drawable/ic_volume_mute_black.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/list_item_systemvolume.xml b/res/layout/list_item_systemvolume.xml
new file mode 100644
--- /dev/null
+++ b/res/layout/list_item_systemvolume.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/mpris_control.xml b/res/layout/mpris_control.xml
--- a/res/layout/mpris_control.xml
+++ b/res/layout/mpris_control.xml
@@ -159,4 +159,8 @@
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -236,4 +236,8 @@
There are no commands registered
You can add new commands in the KDE Connect System Settings
+ System volume
+ Control the system volume of the remote device
+ Mute
+
diff --git a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
--- a/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/MprisPlugin/MprisActivity.java
@@ -28,6 +28,7 @@
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
+import android.support.v4.app.FragmentManager;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
@@ -45,6 +46,7 @@
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
+import org.kde.kdeconnect.Plugins.SystemvolumePlugin.SystemvolumeFragment;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect_tp.R;
@@ -87,8 +89,11 @@
Log.e("MprisActivity", "device has no mpris plugin!");
return;
}
+
targetPlayer = mpris.getPlayerStatus(targetPlayerName);
+ addSytemvolumeFragment();
+
mpris.setPlayerStatusUpdatedHandler("activity", new Handler() {
@Override
public void handleMessage(Message msg) {
@@ -180,6 +185,13 @@
}
+ private void addSytemvolumeFragment() {
+
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ ((SystemvolumeFragment) fragmentManager.findFragmentById(R.id.systemvolume_fragment)).connectToPlugin(deviceId);
+
+ }
+
private final BaseLinkProvider.ConnectionReceiver connectionReceiver = new BaseLinkProvider.ConnectionReceiver() {
@Override
public void onConnectionReceived(NetworkPacket identityPacket, BaseLink link) {
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
@@ -37,6 +37,7 @@
import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin;
import org.kde.kdeconnect.Plugins.SftpPlugin.SftpPlugin;
import org.kde.kdeconnect.Plugins.SharePlugin.SharePlugin;
+import org.kde.kdeconnect.Plugins.SystemvolumePlugin.SystemvolumePlugin;
import org.kde.kdeconnect.Plugins.TelepathyPlugin.TelepathyPlugin;
import org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin;
@@ -128,6 +129,7 @@
PluginFactory.registerPlugin(FindMyPhonePlugin.class);
PluginFactory.registerPlugin(RunCommandPlugin.class);
PluginFactory.registerPlugin(RemoteKeyboardPlugin.class);
+ PluginFactory.registerPlugin(SystemvolumePlugin.class);
}
public static PluginInfo getPluginInfo(Context context, String pluginKey) {
diff --git a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/Sink.java b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/Sink.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/Sink.java
@@ -0,0 +1,98 @@
+/*
+ * 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.SystemvolumePlugin;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class Sink {
+
+ interface UpdateListener {
+ void updateSink(Sink sink);
+ }
+
+ private int volume;
+ private String description;
+ private String name;
+ private boolean mute;
+ private int maxVolume;
+
+ private List listeners;
+
+ Sink(JSONObject obj) throws JSONException {
+ listeners = new ArrayList<>();
+ name = obj.getString("name");
+ volume = obj.getInt("volume");
+ mute = obj.getBoolean("muted");
+ description = obj.getString("description");
+ maxVolume = obj.getInt("maxVolume");
+ }
+
+ int getVolume() {
+ return volume;
+ }
+
+
+ void setVolume(int volume) {
+ this.volume = volume;
+ for (UpdateListener l : listeners) {
+ l.updateSink(this);
+ }
+ }
+
+ String getDescription() {
+ return description;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ boolean isMute() {
+ return mute;
+ }
+
+ void setMute(boolean mute) {
+ this.mute = mute;
+ for (UpdateListener l : listeners) {
+ l.updateSink(this);
+ }
+ }
+
+ void addListener(UpdateListener l) {
+
+ if (!listeners.contains(l)) {
+ listeners.add(l);
+ }
+ }
+
+ int getMaxVolume() {
+ return maxVolume;
+ }
+
+ void removeListener(UpdateListener l) {
+ listeners.remove(l);
+ }
+
+}
diff --git a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java
@@ -0,0 +1,191 @@
+/*
+ * 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.SystemvolumePlugin;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ListFragment;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import org.kde.kdeconnect.BackgroundService;
+import org.kde.kdeconnect.Device;
+import org.kde.kdeconnect_tp.R;
+
+public class SystemvolumeFragment extends ListFragment implements Sink.UpdateListener, SystemvolumePlugin.SinkListener {
+
+ private SystemvolumePlugin plugin;
+ private Activity activity;
+ private SinkAdapter adapter;
+ private Context context;
+ private boolean tracking;
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setDivider(null);
+ setListAdapter(new SinkAdapter(getContext(), new Sink[0]));
+ }
+
+ @Override
+ public void updateSink(final Sink sink) {
+
+ // Don't set progress while the slider is moved
+ if (!tracking) {
+
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ adapter.notifyDataSetChanged();
+ }
+ });
+ }
+ }
+
+ public void connectToPlugin(final String deviceId) {
+
+ BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() {
+ @Override
+ public void onServiceStart(BackgroundService service) {
+ Device device = service.getDevice(deviceId);
+
+ if (device == null)
+ return;
+
+ plugin = device.getPlugin(SystemvolumePlugin.class);
+
+ if (plugin == null) {
+ Log.e("SystemvolumeFragment", "device has no systemvolume plugin!");
+ return;
+ }
+
+ plugin.addSinkListener(SystemvolumeFragment.this);
+ plugin.requestSinkList();
+ Log.d("Systemvolume", "requestSinklist");
+ }
+ });
+
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ activity = getActivity();
+ this.context = context;
+ }
+
+ @Override
+ public void sinksChanged() {
+
+ for (Sink sink : plugin.getSinks()) {
+ sink.addListener(SystemvolumeFragment.this);
+ }
+
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ adapter = new SinkAdapter(context, plugin.getSinks().toArray(new Sink[0]));
+ setListAdapter(adapter);
+ }
+ });
+ }
+
+ private class SinkAdapter extends ArrayAdapter {
+
+ private SinkAdapter(@NonNull Context context, @NonNull Sink[] objects) {
+ super(context, R.layout.list_item_systemvolume, objects);
+ }
+
+ @NonNull
+ @Override
+ public View getView(final int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+
+ View view = getLayoutInflater().inflate(R.layout.list_item_systemvolume, parent, false);
+
+ UIListener listener = new UIListener(getItem(position));
+
+ ((TextView) view.findViewById(R.id.systemvolume_label)).setText(getItem(position).getDescription());
+
+ final SeekBar seekBar = (SeekBar) view.findViewById(R.id.systemvolume_seek);
+ seekBar.setMax(getItem(position).getMaxVolume());
+ seekBar.setProgress(getItem(position).getVolume());
+ seekBar.setOnSeekBarChangeListener(listener);
+
+ ImageButton button = (ImageButton) view.findViewById(R.id.systemvolume_mute);
+ int iconRes = getItem(position).isMute() ? R.drawable.ic_volume_mute_black : R.drawable.ic_volume_black;
+ button.setImageResource(iconRes);
+ button.setOnClickListener(listener);
+
+ return view;
+ }
+
+ }
+
+ private class UIListener implements SeekBar.OnSeekBarChangeListener, ImageButton.OnClickListener {
+
+ private Sink sink;
+
+ private UIListener(Sink sink) {
+ this.sink = sink;
+ }
+
+ @Override
+ public void onProgressChanged(final SeekBar seekBar, int i, boolean b) {
+ BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() {
+ @Override
+ public void onServiceStart(BackgroundService service) {
+ plugin.sendVolume(sink.getName(), seekBar.getProgress());
+ }
+ });
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ tracking = true;
+ }
+
+ @Override
+ public void onStopTrackingTouch(final SeekBar seekBar) {
+ tracking = false;
+ BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() {
+ @Override
+ public void onServiceStart(BackgroundService service) {
+ plugin.sendVolume(sink.getName(), seekBar.getProgress());
+ }
+ });
+ }
+
+ @Override
+ public void onClick(View view) {
+ plugin.sendMute(sink.getName(), !sink.isMute());
+ }
+ }
+}
diff --git a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java
@@ -0,0 +1,148 @@
+/*
+ * 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.SystemvolumePlugin;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.kde.kdeconnect.NetworkPacket;
+import org.kde.kdeconnect.Plugins.Plugin;
+import org.kde.kdeconnect_tp.R;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+
+
+public class SystemvolumePlugin extends Plugin {
+
+ private final static String PACKET_TYPE_SYSTEMVOLUME = "kdeconnect.systemvolume";
+
+ public interface SinkListener {
+ void sinksChanged();
+ }
+
+ private HashMap sinks;
+ private ArrayList listeners;
+
+ public SystemvolumePlugin() {
+ sinks = new HashMap<>();
+ listeners = new ArrayList<>();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return context.getResources().getString(R.string.pref_plugin_systemvolume);
+ }
+
+ @Override
+ public String getDescription() {
+ return context.getResources().getString(R.string.pref_plugin_systemvolume_desc);
+ }
+
+ @Override
+ public boolean onPacketReceived(NetworkPacket np) {
+
+ if (np.has("sinkList")) {
+ sinks.clear();
+
+ try {
+ JSONArray sinkArray = np.getJSONArray("sinkList");
+
+ for (int i = 0; i < sinkArray.length(); i++) {
+ JSONObject sinkObj = sinkArray.getJSONObject(i);
+ Sink sink = new Sink(sinkObj);
+ sinks.put(sink.getName(), sink);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ for (SinkListener l : listeners) {
+ l.sinksChanged();
+ }
+
+ } else {
+ String name = np.getString("name");
+ if (sinks.containsKey(name)) {
+ if (np.has("volume")) {
+ sinks.get(name).setVolume(np.getInt("volume"));
+ } else if (np.has("muted")) {
+ sinks.get(name).setMute(np.getBoolean("muted"));
+ }
+ }
+ }
+ return true;
+ }
+
+ void sendVolume(String name, int volume) {
+ NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME);
+ np.set("volume", volume);
+ np.set("name", name);
+ device.sendPacket(np);
+ }
+
+ void sendMute(String name, boolean mute) {
+ NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME);
+ np.set("muted", mute);
+ np.set("name", name);
+ device.sendPacket(np);
+ }
+
+ void requestSinkList() {
+ NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME);
+ np.set("requestSinks", true);
+ device.sendPacket(np);
+ }
+
+ @Override
+ public boolean hasMainActivity() {
+ return false;
+ }
+
+ @Override
+ public boolean displayInContextMenu() {
+ return false;
+ }
+
+ @Override
+ public String[] getSupportedPacketTypes() {
+ return new String[]{PACKET_TYPE_SYSTEMVOLUME};
+ }
+
+ @Override
+ public String[] getOutgoingPacketTypes() {
+ return new String[]{PACKET_TYPE_SYSTEMVOLUME};
+ }
+
+ Collection getSinks() {
+ return sinks.values();
+ }
+
+ void addSinkListener(SinkListener listener) {
+ listeners.add(listener);
+ }
+
+ void removeSinkListener(SinkListener listener) {
+ listeners.remove(listener);
+ }
+
+}