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-land/activity_mpris.xml b/res/layout-land/activity_mpris.xml --- a/res/layout-land/activity_mpris.xml +++ b/res/layout-land/activity_mpris.xml @@ -23,4 +23,9 @@ android:layout_height="match_parent" android:layout_weight="1" /> + + diff --git a/res/layout/activity_mpris.xml b/res/layout/activity_mpris.xml --- a/res/layout/activity_mpris.xml +++ b/res/layout/activity_mpris.xml @@ -22,4 +22,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + + 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 @@ -158,5 +158,4 @@ android:max="100" /> - 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); + } + +}