mprisDevices = new HashSet<>();
private Context context;
private MediaSessionCompat mediaSession;
//Callback for mpris plugin updates
private final Handler mediaNotificationHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
updateMediaNotification();
}
};
//Callback for control via the media session API
private final MediaSessionCompat.Callback mediaSessionCallback = new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
notificationPlayer.play();
}
@Override
public void onPause() {
notificationPlayer.pause();
}
@Override
public void onSkipToNext() {
notificationPlayer.next();
}
@Override
public void onSkipToPrevious() {
notificationPlayer.previous();
}
@Override
public void onStop() {
notificationPlayer.stop();
}
};
/**
* Called by the mpris plugin when it wants media control notifications for its device
*
* Can be called multiple times, once for each device
*
* @param _context The context
* @param mpris The mpris plugin
* @param device The device id
*/
public void onCreate(Context _context, MprisPlugin mpris, String device) {
if (mprisDevices.isEmpty()) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(_context);
prefs.registerOnSharedPreferenceChangeListener(this);
}
context = _context;
mprisDevices.add(device);
mpris.setPlayerListUpdatedHandler("media_notification", mediaNotificationHandler);
mpris.setPlayerStatusUpdatedHandler("media_notification", mediaNotificationHandler);
updateMediaNotification();
}
/**
* Called when a device disconnects/does not want notifications anymore
*
* Can be called multiple times, once for each device
*
* @param mpris The mpris plugin
* @param device The device id
*/
public void onDestroy(MprisPlugin mpris, String device) {
mprisDevices.remove(device);
mpris.removePlayerStatusUpdatedHandler("media_notification");
mpris.removePlayerListUpdatedHandler("media_notification");
updateMediaNotification();
if (mprisDevices.isEmpty()) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.unregisterOnSharedPreferenceChangeListener(this);
}
}
/**
* Updates which device+player we're going to use in the notification
*
* Prefers playing devices/mpris players, but tries to keep displaying the same
* player and device, while possible.
*
* @param service The background service
*/
private void updateCurrentPlayer(BackgroundService service) {
Device device = null;
MprisPlugin.MprisPlayer playing = null;
//First try the previously displayed player
if (notificationDevice != null && mprisDevices.contains(notificationDevice) && notificationPlayer != null) {
device = service.getDevice(notificationDevice);
}
MprisPlugin mpris = null;
if (device != null) {
mpris = device.getPlugin(MprisPlugin.class);
}
if (mpris != null) {
playing = mpris.getPlayerStatus(notificationPlayer.getPlayer());
}
//If nonexistant or not playing, try a different player for the same device
if ((playing == null || !playing.isPlaying()) && mpris != null) {
MprisPlugin.MprisPlayer playingPlayer = mpris.getPlayingPlayer();
//Only replace the previously found player if we really found one
if (playingPlayer != null) {
playing = playingPlayer;
}
}
//If nonexistant or not playing, try a different player for another device
if (playing == null || !playing.isPlaying()) {
for (Device otherDevice : service.getDevices().values()) {
//First, check if we actually display notification for this device
if (!mprisDevices.contains(otherDevice.getDeviceId())) continue;
mpris = otherDevice.getPlugin(MprisPlugin.class);
if (mpris == null) continue;
MprisPlugin.MprisPlayer playingPlayer = mpris.getPlayingPlayer();
//Only replace the previously found player if we really found one
if (playingPlayer != null) {
playing = playingPlayer;
device = otherDevice;
break;
}
}
}
//Update the last-displayed device and player
notificationDevice = device == null ? null : device.getDeviceId();
notificationPlayer = playing;
}
/**
* Update the media control notification
*/
private void updateMediaNotification() {
BackgroundService.RunCommand(context, service -> {
//If the user disabled the media notification, do not show it
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (!prefs.getBoolean(context.getString(R.string.mpris_notification_key), true)) {
closeMediaNotification();
return;
}
//Make sure our information is up-to-date
updateCurrentPlayer(service);
//If the player disappeared (and no other playing one found), just remove the notification
if (notificationPlayer == null) {
closeMediaNotification();
return;
}
//Update the metadata and playback status
if (mediaSession == null) {
mediaSession = new MediaSessionCompat(context, MPRIS_MEDIA_SESSION_TAG);
mediaSession.setCallback(mediaSessionCallback);
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
}
MediaMetadataCompat.Builder metadata = new MediaMetadataCompat.Builder();
//Fallback because older KDE connect versions do not support getTitle()
if (!notificationPlayer.getTitle().isEmpty()) {
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getTitle());
} else {
metadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, notificationPlayer.getCurrentSong());
}
if (!notificationPlayer.getArtist().isEmpty()) {
metadata.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, notificationPlayer.getArtist());
metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, notificationPlayer.getArtist());
}
if (!notificationPlayer.getAlbum().isEmpty()) {
metadata.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, notificationPlayer.getAlbum());
}
if (notificationPlayer.getLength() > 0) {
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, notificationPlayer.getLength());
}
Bitmap albumArt = notificationPlayer.getAlbumArt();
if (albumArt != null) {
metadata.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt);
}
mediaSession.setMetadata(metadata.build());
PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder();
if (notificationPlayer.isPlaying()) {
playbackState.setState(PlaybackStateCompat.STATE_PLAYING, notificationPlayer.getPosition(), 1.0f);
} else {
playbackState.setState(PlaybackStateCompat.STATE_PAUSED, notificationPlayer.getPosition(), 0.0f);
}
//Create all actions (previous/play/pause/next)
Intent iPlay = new Intent(service, MprisMediaNotificationReceiver.class);
iPlay.setAction(MprisMediaNotificationReceiver.ACTION_PLAY);
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
PendingIntent piPlay = PendingIntent.getBroadcast(service, 0, iPlay, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action.Builder aPlay = new NotificationCompat.Action.Builder(
R.drawable.ic_play_white, service.getString(R.string.mpris_play), piPlay);
Intent iPause = new Intent(service, MprisMediaNotificationReceiver.class);
iPause.setAction(MprisMediaNotificationReceiver.ACTION_PAUSE);
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
PendingIntent piPause = PendingIntent.getBroadcast(service, 0, iPause, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action.Builder aPause = new NotificationCompat.Action.Builder(
R.drawable.ic_pause_white, service.getString(R.string.mpris_pause), piPause);
Intent iPrevious = new Intent(service, MprisMediaNotificationReceiver.class);
iPrevious.setAction(MprisMediaNotificationReceiver.ACTION_PREVIOUS);
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
PendingIntent piPrevious = PendingIntent.getBroadcast(service, 0, iPrevious, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action.Builder aPrevious = new NotificationCompat.Action.Builder(
R.drawable.ic_previous_white, service.getString(R.string.mpris_previous), piPrevious);
Intent iNext = new Intent(service, MprisMediaNotificationReceiver.class);
iNext.setAction(MprisMediaNotificationReceiver.ACTION_NEXT);
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
PendingIntent piNext = PendingIntent.getBroadcast(service, 0, iNext, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action.Builder aNext = new NotificationCompat.Action.Builder(
R.drawable.ic_next_white, service.getString(R.string.mpris_next), piNext);
Intent iOpenActivity = new Intent(service, MprisActivity.class);
iOpenActivity.putExtra("deviceId", notificationDevice);
iOpenActivity.putExtra("player", notificationPlayer.getPlayer());
/*
TODO: Remove when Min SDK >= 16
The only way the intent extra's are delivered on API 14 and 15 is by either using a different requestCode every time
or using PendingIntent.FLAG_CANCEL_CURRENT instead of PendingIntent.FLAG_UPDATE_CURRENT
*/
PendingIntent piOpenActivity = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(iOpenActivity)
.getPendingIntent(Build.VERSION.SDK_INT > 15 ? 0 : (int)System.currentTimeMillis(), PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NotificationHelper.Channels.MEDIA_CONTROL);
notification
.setAutoCancel(false)
.setContentIntent(piOpenActivity)
.setSmallIcon(R.drawable.ic_play_white)
.setShowWhen(false)
.setColor(service.getResources().getColor(R.color.primary))
- .setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC);
+ .setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC)
+ .setSubText(service.getDevice(notificationDevice).getName());
if (!notificationPlayer.getTitle().isEmpty()) {
notification.setContentTitle(notificationPlayer.getTitle());
} else {
notification.setContentTitle(notificationPlayer.getCurrentSong());
}
//Only set the notification body text if we have an author and/or album
if (!notificationPlayer.getArtist().isEmpty() && !notificationPlayer.getAlbum().isEmpty()) {
notification.setContentText(notificationPlayer.getArtist() + " - " + notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayer() + ")");
} else if (!notificationPlayer.getArtist().isEmpty()) {
notification.setContentText(notificationPlayer.getArtist() + " (" + notificationPlayer.getPlayer() + ")");
} else if (!notificationPlayer.getAlbum().isEmpty()) {
notification.setContentText(notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayer() + ")");
} else {
notification.setContentText(notificationPlayer.getPlayer());
}
if (albumArt != null) {
notification.setLargeIcon(albumArt);
}
if (!notificationPlayer.isPlaying()) {
Intent iCloseNotification = new Intent(service, MprisMediaNotificationReceiver.class);
iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION);
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayer());
PendingIntent piCloseNotification = PendingIntent.getActivity(service, 0, iCloseNotification, PendingIntent.FLAG_UPDATE_CURRENT);
notification.setDeleteIntent(piCloseNotification);
}
//Add media control actions
int numActions = 0;
long playbackActions = 0;
if (notificationPlayer.isGoPreviousAllowed()) {
notification.addAction(aPrevious.build());
playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
++numActions;
}
if (notificationPlayer.isPlaying() && notificationPlayer.isPauseAllowed()) {
notification.addAction(aPause.build());
playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
++numActions;
}
if (!notificationPlayer.isPlaying() && notificationPlayer.isPlayAllowed()) {
notification.addAction(aPlay.build());
playbackActions |= PlaybackStateCompat.ACTION_PLAY;
++numActions;
}
if (notificationPlayer.isGoNextAllowed()) {
notification.addAction(aNext.build());
playbackActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
++numActions;
}
playbackState.setActions(playbackActions);
mediaSession.setPlaybackState(playbackState.build());
//Only allow deletion if no music is notificationPlayer
if (notificationPlayer.isPlaying()) {
notification.setOngoing(true);
} else {
notification.setOngoing(false);
}
//Use the MediaStyle notification, so it feels like other media players. That also allows adding actions
MediaStyle mediaStyle = new MediaStyle();
if (numActions == 1) {
mediaStyle.setShowActionsInCompactView(0);
} else if (numActions == 2) {
mediaStyle.setShowActionsInCompactView(0, 1);
} else if (numActions >= 3) {
mediaStyle.setShowActionsInCompactView(0, 1, 2);
}
mediaStyle.setMediaSession(mediaSession.getSessionToken());
notification.setStyle(mediaStyle);
notification.setGroup("MprisMediaSession");
//Display the notification
mediaSession.setActive(true);
final NotificationManager nm = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(MPRIS_MEDIA_NOTIFICATION_ID, notification.build());
});
}
public void closeMediaNotification() {
//Remove the notification
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(MPRIS_MEDIA_NOTIFICATION_ID);
//Clear the current player and media session
notificationPlayer = null;
if (mediaSession != null) {
mediaSession.release();
mediaSession = null;
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
updateMediaNotification();
}
public void playerSelected(MprisPlugin.MprisPlayer player) {
notificationPlayer = player;
updateMediaNotification();
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java b/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java
index 5cb252a1..b689b7ea 100644
--- a/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/PhotoPlugin/PhotoActivity.java
@@ -1,72 +1,72 @@
package org.kde.kdeconnect.Plugins.PhotoPlugin;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import org.kde.kdeconnect.BackgroundService;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
public class PhotoActivity extends AppCompatActivity {
private Uri photoURI;
private PhotoPlugin plugin;
@Override
protected void onStart() {
super.onStart();
- BackgroundService.runWithPlugin(this, getIntent().getStringExtra("deviceId"), PhotoPlugin.class, plugin -> {
+ BackgroundService.RunWithPlugin(this, getIntent().getStringExtra("deviceId"), PhotoPlugin.class, plugin -> {
this.plugin = plugin;
});
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = createImageFile();
} catch (IOException ignored) {
}
// Continue only if the File was successfully created
if (photoFile != null) {
photoURI = FileProvider.getUriForFile(this,
"org.kde.kdeconnect_tp.fileprovider",
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(takePictureIntent, 1);
}
}
}
private File createImageFile() throws IOException {
// Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
return File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == -1) {
plugin.sendPhoto(photoURI);
}
finish();
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/PluginFactory.java b/src/org/kde/kdeconnect/Plugins/PluginFactory.java
index 95f2d0c9..1e29eea9 100644
--- a/src/org/kde/kdeconnect/Plugins/PluginFactory.java
+++ b/src/org/kde/kdeconnect/Plugins/PluginFactory.java
@@ -1,222 +1,222 @@
/*
* Copyright 2014 Albert Vaca Cintora
*
* 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;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Log;
import org.atteo.classindex.IndexAnnotated;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin;
import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardPlugin;
import org.kde.kdeconnect.Plugins.ContactsPlugin.ContactsPlugin;
import org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhonePlugin;
import org.kde.kdeconnect.Plugins.FindRemoteDevicePlugin.FindRemoteDevicePlugin;
import org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadPlugin;
import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin;
import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationsPlugin;
import org.kde.kdeconnect.Plugins.PhotoPlugin.PhotoPlugin;
import org.kde.kdeconnect.Plugins.PingPlugin.PingPlugin;
import org.kde.kdeconnect.Plugins.PresenterPlugin.PresenterPlugin;
import org.kde.kdeconnect.Plugins.ReceiveNotificationsPlugin.ReceiveNotificationsPlugin;
import org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin.RemoteKeyboardPlugin;
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.SystemvolumePlugin.SystemVolumePlugin;
import org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class PluginFactory {
@IndexAnnotated
public @interface LoadablePlugin { } //Annotate plugins with this so PluginFactory finds them
public static class PluginInfo {
PluginInfo(String displayName, String description, Drawable icon,
boolean enabledByDefault, boolean hasSettings, boolean listenToUnpaired,
String[] supportedPacketTypes, String[] outgoingPacketTypes,
Class extends Plugin> instantiableClass) {
this.displayName = displayName;
this.description = description;
this.icon = icon;
this.enabledByDefault = enabledByDefault;
this.hasSettings = hasSettings;
this.listenToUnpaired = listenToUnpaired;
HashSet incoming = new HashSet<>();
if (supportedPacketTypes != null) Collections.addAll(incoming, supportedPacketTypes);
this.supportedPacketTypes = Collections.unmodifiableSet(incoming);
HashSet outgoing = new HashSet<>();
if (outgoingPacketTypes != null) Collections.addAll(outgoing, outgoingPacketTypes);
this.outgoingPacketTypes = Collections.unmodifiableSet(outgoing);
this.instantiableClass = instantiableClass;
}
public String getDisplayName() {
return displayName;
}
public String getDescription() {
return description;
}
public Drawable getIcon() {
return icon;
}
public boolean hasSettings() {
return hasSettings;
}
public boolean isEnabledByDefault() {
return enabledByDefault;
}
public boolean listenToUnpaired() {
return listenToUnpaired;
}
Set getOutgoingPacketTypes() {
return outgoingPacketTypes;
}
public Set getSupportedPacketTypes() {
return supportedPacketTypes;
}
Class extends Plugin> getInstantiableClass() {
return instantiableClass;
}
private final String displayName;
private final String description;
private final Drawable icon;
private final boolean enabledByDefault;
private final boolean hasSettings;
private final boolean listenToUnpaired;
private final Set supportedPacketTypes;
private final Set outgoingPacketTypes;
private final Class extends Plugin> instantiableClass;
}
private static final Map pluginInfo = new ConcurrentHashMap<>();
static Class pluginClasses[] = {
BatteryPlugin.class,
ClipboardPlugin.class,
ContactsPlugin.class,
FindMyPhonePlugin.class,
FindRemoteDevicePlugin.class,
MousePadPlugin.class,
MprisPlugin.class,
//MprisReceiverPlugin.class, //Breaks in Android 4
NotificationsPlugin.class,
PhotoPlugin.class,
PingPlugin.class,
PresenterPlugin.class,
ReceiveNotificationsPlugin.class,
RemoteKeyboardPlugin.class,
RunCommandPlugin.class,
SftpPlugin.class,
SharePlugin.class,
//SMSPlugin.class,
- SystemvolumePlugin.class,
+ SystemVolumePlugin.class,
TelephonyPlugin.class,
};
public static PluginInfo getPluginInfo(String pluginKey) {
return pluginInfo.get(pluginKey);
}
public static void initPluginInfo(Context context) {
try {
for (Class> pluginClass : pluginClasses) {
Plugin p = ((Plugin) pluginClass.newInstance());
p.setContext(context, null);
PluginInfo info = new PluginInfo(p.getDisplayName(), p.getDescription(), p.getIcon(),
p.isEnabledByDefault(), p.hasSettings(), p.listensToUnpairedDevices(),
p.getSupportedPacketTypes(), p.getOutgoingPacketTypes(), p.getClass());
pluginInfo.put(p.getPluginKey(), info);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
Log.i("PluginFactory","Loaded "+pluginInfo.size()+" plugins");
}
public static Set getAvailablePlugins() {
return pluginInfo.keySet();
}
public static Plugin instantiatePluginForDevice(Context context, String pluginKey, Device device) {
PluginInfo info = pluginInfo.get(pluginKey);
try {
Plugin plugin = info.getInstantiableClass().newInstance();
plugin.setContext(context, device);
return plugin;
} catch (Exception e) {
Log.e("PluginFactory", "Could not instantiate plugin: " + pluginKey);
e.printStackTrace();
return null;
}
}
public static Set getIncomingCapabilities() {
HashSet capabilities = new HashSet<>();
for (PluginInfo plugin : pluginInfo.values()) {
capabilities.addAll(plugin.getSupportedPacketTypes());
}
return capabilities;
}
public static Set getOutgoingCapabilities() {
HashSet capabilities = new HashSet<>();
for (PluginInfo plugin : pluginInfo.values()) {
capabilities.addAll(plugin.getOutgoingPacketTypes());
}
return capabilities;
}
public static Set pluginsForCapabilities(Set incoming, Set outgoing) {
HashSet plugins = new HashSet<>();
for (Map.Entry entry : pluginInfo.entrySet()) {
String pluginId = entry.getKey();
PluginInfo info = entry.getValue();
//Check incoming against outgoing
if (Collections.disjoint(outgoing, info.getSupportedPacketTypes())
&& Collections.disjoint(incoming, info.getOutgoingPacketTypes())) {
Log.i("PluginFactory", "Won't load " + pluginId + " because of unmatched capabilities");
continue; //No capabilities in common, do not load this plugin
}
plugins.add(pluginId);
}
return plugins;
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java b/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java
index 26ca6352..fedea60c 100644
--- a/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java
@@ -1,141 +1,141 @@
/*
* Copyright 2014 Ahmed I. Khalil
*
* 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.PresenterPlugin;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media.VolumeProviderCompat;
public class PresenterActivity extends AppCompatActivity {
private MediaSessionCompat mMediaSession;
private PresenterPlugin plugin;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeUtil.setUserPreferredTheme(this);
setContentView(R.layout.activity_presenter);
final String deviceId = getIntent().getStringExtra("deviceId");
- BackgroundService.runWithPlugin(this, deviceId, PresenterPlugin.class, plugin -> runOnUiThread(() -> {
+ BackgroundService.RunWithPlugin(this, deviceId, PresenterPlugin.class, plugin -> runOnUiThread(() -> {
this.plugin = plugin;
findViewById(R.id.next_button).setOnClickListener(v -> plugin.sendNext());
findViewById(R.id.previous_button).setOnClickListener(v -> plugin.sendPrevious());
}));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_presenter, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.fullscreen:
plugin.sendFullscreen();
return true;
case R.id.exit_presentation:
plugin.sendEsc();
return true;
default:
return super.onContextItemSelected(item);
}
}
@Override
protected void onStart() {
super.onStart();
BackgroundService.addGuiInUseCounter(this);
if (mMediaSession != null) {
mMediaSession.setActive(true);
return;
}
createMediaSession(); //Mediasession will keep
}
@Override
protected void onStop() {
super.onStop();
BackgroundService.removeGuiInUseCounter(this);
if (mMediaSession != null) {
PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
boolean screenOn;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
screenOn = pm.isInteractive();
} else {
screenOn = pm.isScreenOn();
}
if (screenOn) {
mMediaSession.release();
} // else we are in the lockscreen, keep the mediasession
}
}
private void createMediaSession() {
mMediaSession = new MediaSessionCompat(this, "kdeconnect");
mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mMediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 0)
.build());
mMediaSession.setPlaybackToRemote(getVolumeProvider());
mMediaSession.setActive(true);
}
private VolumeProviderCompat getVolumeProvider() {
final int VOLUME_UP = 1;
final int VOLUME_DOWN = -1;
return new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, 1, 0) {
@Override
public void onAdjustVolume(int direction) {
if (direction == VOLUME_UP) {
plugin.sendNext();
}
else if (direction == VOLUME_DOWN) {
plugin.sendPrevious();
}
}
};
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java
index 7958e8db..d3e96f26 100644
--- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java
@@ -1,201 +1,176 @@
/*
* Copyright 2015 Aleix Pol Gonzalez
* Copyright 2015 Albert Vaca Cintora
*
* 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.RunCommandPlugin;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
-import android.util.Log;
import android.view.ContextMenu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.json.JSONException;
import org.json.JSONObject;
import org.kde.kdeconnect.BackgroundService;
-import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collections;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
public class RunCommandActivity extends AppCompatActivity {
private String deviceId;
private final RunCommandPlugin.CommandsChangedCallback commandsChangedCallback = this::updateView;
private ArrayList commandItems;
private void updateView() {
- BackgroundService.RunCommand(this, service -> {
-
- final Device device = service.getDevice(deviceId);
- final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
- if (plugin == null) {
- Log.e("RunCommandActivity", "device has no runcommand plugin!");
- return;
- }
+ BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> {
runOnUiThread(() -> {
ListView view = findViewById(R.id.runcommandslist);
registerForContextMenu(view);
commandItems = new ArrayList<>();
for (JSONObject obj : plugin.getCommandList()) {
try {
commandItems.add(new CommandEntry(obj.getString("name"),
obj.getString("command"), obj.getString("key")));
} catch (JSONException e) {
e.printStackTrace();
}
}
Collections.sort(commandItems, (lhs, rhs) -> {
String lName = ((CommandEntry) lhs).getName();
String rName = ((CommandEntry) rhs).getName();
return lName.compareTo(rName);
});
ListAdapter adapter = new ListAdapter(RunCommandActivity.this, commandItems);
view.setAdapter(adapter);
view.setOnItemClickListener((adapterView, view1, i, l) -> {
CommandEntry entry = (CommandEntry) commandItems.get(i);
plugin.runCommand(entry.getKey());
});
TextView explanation = findViewById(R.id.addcomand_explanation);
String text = getString(R.string.addcommand_explanation);
if (!plugin.canAddCommand()) {
text += "\n" + getString(R.string.addcommand_explanation2);
}
explanation.setText(text);
explanation.setVisibility(commandItems.isEmpty() ? View.VISIBLE : View.GONE);
});
});
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeUtil.setUserPreferredTheme(this);
setContentView(R.layout.activity_runcommand);
deviceId = getIntent().getStringExtra("deviceId");
boolean canAddCommands = BackgroundService.getInstance().getDevice(deviceId).getPlugin(RunCommandPlugin.class).canAddCommand();
FloatingActionButton addCommandButton = findViewById(R.id.add_command_button);
- addCommandButton.setVisibility(canAddCommands ? View.VISIBLE : View.GONE);
-
- addCommandButton.setOnClickListener(view -> BackgroundService.RunCommand(RunCommandActivity.this, service -> {
-
- final Device device = service.getDevice(deviceId);
- final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
- if (plugin == null) {
- Log.e("RunCommandActivity", "device has no runcommand plugin!");
- return;
- }
+ if (canAddCommands) {
+ addCommandButton.show();
+ } else {
+ addCommandButton.hide();
+ }
- plugin.sendSetupPacket();
+ addCommandButton.setOnClickListener(v -> {
- AlertDialog dialog = new AlertDialog.Builder(RunCommandActivity.this)
- .setTitle(R.string.add_command)
- .setMessage(R.string.add_command_description)
- .setPositiveButton(R.string.ok, null)
- .create();
- dialog.show();
+ BackgroundService.RunWithPlugin(RunCommandActivity.this, deviceId, RunCommandPlugin.class, plugin -> {
+ plugin.sendSetupPacket();
+ AlertDialog dialog = new AlertDialog.Builder(RunCommandActivity.this)
+ .setTitle(R.string.add_command)
+ .setMessage(R.string.add_command_description)
+ .setPositiveButton(R.string.ok, null)
+ .create();
+ dialog.show();
+ });
- }));
+ });
updateView();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.runcommand_context, menu);
}
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
if (item.getItemId() == R.id.copy_url_to_clipboard) {
CommandEntry entry = (CommandEntry) commandItems.get(info.position);
String url = "kdeconnect://runcommand/" + deviceId + "/" + entry.getKey();
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setText(url);
Toast toast = Toast.makeText(this, R.string.clipboard_toast, Toast.LENGTH_SHORT);
toast.show();
}
return false;
}
@Override
protected void onResume() {
super.onResume();
- BackgroundService.RunCommand(this, service -> {
-
- final Device device = service.getDevice(deviceId);
- final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
- if (plugin == null) {
- Log.e("RunCommandActivity", "device has no runcommand plugin!");
- return;
- }
+ BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> {
plugin.addCommandsUpdatedCallback(commandsChangedCallback);
});
}
@Override
protected void onPause() {
super.onPause();
- BackgroundService.RunCommand(this, service -> {
-
- final Device device = service.getDevice(deviceId);
- final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
- if (plugin == null) {
- Log.e("RunCommandActivity", "device has no runcommand plugin!");
- return;
- }
+ BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> {
plugin.removeCommandsUpdatedCallback(commandsChangedCallback);
});
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java
index 37847a5e..9dfbe308 100644
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SendFileActivity.java
@@ -1,102 +1,102 @@
/*
* Copyright 2014 Albert Vaca Cintora
*
* 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.SharePlugin;
import android.app.Activity;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import androidx.appcompat.app.AppCompatActivity;
public class SendFileActivity extends AppCompatActivity {
private String mDeviceId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeUtil.setUserPreferredTheme(this);
mDeviceId = getIntent().getStringExtra("deviceId");
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
intent.addCategory(Intent.CATEGORY_OPENABLE);
try {
startActivityForResult(
Intent.createChooser(intent, getString(R.string.send_files)), Activity.RESULT_FIRST_USER);
} catch (android.content.ActivityNotFoundException ex) {
Toast.makeText(this, R.string.no_file_browser, Toast.LENGTH_SHORT).show();
finish();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case Activity.RESULT_FIRST_USER:
if (resultCode == RESULT_OK) {
final ArrayList uris = new ArrayList<>();
Uri uri = data.getData();
if (uri != null) {
uris.add(uri);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
ClipData clipdata = data.getClipData();
if (clipdata != null) {
for (int i = 0; i < clipdata.getItemCount(); i++) {
uris.add(clipdata.getItemAt(i).getUri());
}
}
}
if (uris.isEmpty()) {
Log.w("SendFileActivity", "No files to send?");
} else {
- BackgroundService.runWithPlugin(this, mDeviceId, SharePlugin.class, plugin -> plugin.queuedSendUriList(uris));
+ BackgroundService.RunWithPlugin(this, mDeviceId, SharePlugin.class, plugin -> plugin.queuedSendUriList(uris));
}
}
finish();
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
index f93e6bfa..affa4e45 100644
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
@@ -1,173 +1,173 @@
/*
* Copyright 2014 Albert Vaca Cintora
*
* 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.SharePlugin;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ListView;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.UserInterface.List.EntryItem;
import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.List.SectionItem;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collection;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
public class ShareActivity extends AppCompatActivity {
private SwipeRefreshLayout mSwipeRefreshLayout;
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.refresh, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_refresh:
updateComputerListAction();
break;
default:
break;
}
return true;
}
private void updateComputerListAction() {
updateComputerList();
BackgroundService.RunCommand(ShareActivity.this, BackgroundService::onNetworkChange);
mSwipeRefreshLayout.setRefreshing(true);
new Thread(() -> {
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
}
runOnUiThread(() -> mSwipeRefreshLayout.setRefreshing(false));
}).start();
}
private void updateComputerList() {
final Intent intent = getIntent();
String action = intent.getAction();
if (!Intent.ACTION_SEND.equals(action) && !Intent.ACTION_SEND_MULTIPLE.equals(action)) {
finish();
return;
}
BackgroundService.RunCommand(this, service -> {
Collection devices = service.getDevices().values();
final ArrayList devicesList = new ArrayList<>();
final ArrayList items = new ArrayList<>();
SectionItem section = new SectionItem(getString(R.string.share_to));
items.add(section);
for (Device d : devices) {
if (d.isReachable() && d.isPaired()) {
devicesList.add(d);
items.add(new EntryItem(d.getName()));
section.isEmpty = false;
}
}
runOnUiThread(() -> {
ListView list = findViewById(R.id.devices_list);
list.setAdapter(new ListAdapter(ShareActivity.this, items));
list.setOnItemClickListener((adapterView, view, i, l) -> {
Device device = devicesList.get(i - 1); //NOTE: -1 because of the title!
- BackgroundService.runWithPlugin(this, device.getDeviceId(), SharePlugin.class, plugin -> plugin.share(intent));
+ BackgroundService.RunWithPlugin(this, device.getDeviceId(), SharePlugin.class, plugin -> plugin.share(intent));
finish();
});
});
});
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeUtil.setUserPreferredTheme(this);
setContentView(R.layout.devices_list);
ActionBar actionBar = getSupportActionBar();
mSwipeRefreshLayout = findViewById(R.id.refresh_list_layout);
mSwipeRefreshLayout.setOnRefreshListener(
this::updateComputerListAction
);
if (actionBar != null) {
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM);
}
}
@Override
protected void onStart() {
super.onStart();
final Intent intent = getIntent();
final String deviceId = intent.getStringExtra("deviceId");
if (deviceId != null) {
- BackgroundService.runWithPlugin(this, deviceId, SharePlugin.class, plugin -> {
+ BackgroundService.RunWithPlugin(this, deviceId, SharePlugin.class, plugin -> {
plugin.share(intent);
finish();
});
} else {
BackgroundService.addGuiInUseCounter(this);
BackgroundService.RunCommand(this, service -> {
service.onNetworkChange();
service.addDeviceListChangedCallback("ShareActivity", this::updateComputerList);
});
updateComputerList();
}
}
@Override
protected void onStop() {
BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("ShareActivity"));
BackgroundService.removeGuiInUseCounter(this);
super.onStop();
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemVolumePlugin.java
similarity index 94%
rename from src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java
rename to src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemVolumePlugin.java
index 01b81b22..3b5b306f 100644
--- a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumePlugin.java
+++ b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemVolumePlugin.java
@@ -1,151 +1,151 @@
/*
* 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.Plugins.PluginFactory;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
+import java.util.concurrent.ConcurrentHashMap;
@PluginFactory.LoadablePlugin
-public class SystemvolumePlugin extends Plugin {
+public class SystemVolumePlugin extends Plugin {
private final static String PACKET_TYPE_SYSTEMVOLUME = "kdeconnect.systemvolume";
private final static String PACKET_TYPE_SYSTEMVOLUME_REQUEST = "kdeconnect.systemvolume.request";
public interface SinkListener {
void sinksChanged();
}
- private final HashMap sinks;
+ private final ConcurrentHashMap sinks;
private final ArrayList listeners;
- public SystemvolumePlugin() {
- sinks = new HashMap<>();
+ public SystemVolumePlugin() {
+ sinks = new ConcurrentHashMap<>();
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"));
}
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_REQUEST);
np.set("volume", volume);
np.set("name", name);
device.sendPacket(np);
}
void sendMute(String name, boolean mute) {
NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST);
np.set("muted", mute);
np.set("name", name);
device.sendPacket(np);
}
void requestSinkList() {
NetworkPacket np = new NetworkPacket(PACKET_TYPE_SYSTEMVOLUME_REQUEST);
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_REQUEST};
}
Collection getSinks() {
return sinks.values();
}
void addSinkListener(SinkListener listener) {
listeners.add(listener);
}
void removeSinkListener(SinkListener listener) {
listeners.remove(listener);
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java
index aa09417e..c102528f 100644
--- a/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java
+++ b/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/SystemvolumeFragment.java
@@ -1,153 +1,153 @@
/*
* 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.os.Bundle;
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_tp.R;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.ListFragment;
-public class SystemvolumeFragment extends ListFragment implements Sink.UpdateListener, SystemvolumePlugin.SinkListener {
+public class SystemvolumeFragment extends ListFragment implements Sink.UpdateListener, SystemVolumePlugin.SinkListener {
- private SystemvolumePlugin plugin;
+ 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(() -> adapter.notifyDataSetChanged());
}
}
public void connectToPlugin(final String deviceId) {
- BackgroundService.runWithPlugin(activity, deviceId, SystemvolumePlugin.class, plugin -> {
+ BackgroundService.RunWithPlugin(activity, deviceId, SystemVolumePlugin.class, plugin -> {
this.plugin = plugin;
plugin.addSinkListener(SystemvolumeFragment.this);
plugin.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(() -> {
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 = view.findViewById(R.id.systemvolume_seek);
seekBar.setMax(getItem(position).getMaxVolume());
seekBar.setProgress(getItem(position).getVolume());
seekBar.setOnSeekBarChangeListener(listener);
ImageButton button = 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 final Sink sink;
private UIListener(Sink sink) {
this.sink = sink;
}
@Override
public void onProgressChanged(final SeekBar seekBar, int i, boolean b) {
BackgroundService.RunCommand(activity, 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, service -> plugin.sendVolume(sink.getName(), seekBar.getProgress()));
}
@Override
public void onClick(View view) {
plugin.sendMute(sink.getName(), !sink.isMute());
}
}
}