2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 18:07:55 +00:00

Refactor BackgroundService

Added a new KdeConnect Application class that holds the Devices now, while
BackgroundService "only" takes care of the LinkProviders.

Since KdeConnect subclasses Application we have the guarantee that it will
exist as long as our process does, so we can use it as a singleton. This
removes the "BackgroundService.RunCommand" hack (which sent an Intent that
would awake BackgroundService in case it wasn't running already and then
call our code in a callback). This saves lots of round trips between the
system and us and makes things simpler (and stack traces useful) by making
the code sequential.

We already had an Application subclass that I moved to a new helper, which
now the KdeConnect class initializes together with all the other helpers.
This commit is contained in:
Albert Vaca Cintora 2023-05-24 19:26:54 +02:00
parent a6eea8e996
commit ae23413971
46 changed files with 1264 additions and 1385 deletions

View File

@ -52,7 +52,7 @@
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:theme="@style/KdeConnectTheme.NoActionBar" android:theme="@style/KdeConnectTheme.NoActionBar"
android:name="org.kde.kdeconnect.MyApplication" android:name="org.kde.kdeconnect.KdeConnect"
android:enableOnBackInvokedCallback="true"> android:enableOnBackInvokedCallback="true">
<receiver <receiver

View File

@ -53,11 +53,6 @@ public abstract class BaseLink {
return linkProvider; return linkProvider;
} }
//The daemon will periodically destroy unpaired links if this returns false
public boolean linkShouldBeKeptAlive() {
return false;
}
public void addPacketReceiver(PacketReceiver pr) { public void addPacketReceiver(PacketReceiver pr) {
receivers.add(pr); receivers.add(pr);
} }

View File

@ -186,11 +186,6 @@ public class BluetoothLink extends BaseLink {
} }
} }
@Override
public boolean linkShouldBeKeptAlive() {
return receivingThread.isAlive();
}
/* /*
public boolean isConnected() { public boolean isConnected() {
return socket.isConnected(); return socket.isConnected();

View File

@ -252,14 +252,4 @@ public class LanLink extends BaseLink {
packetReceived(np); packetReceived(np);
} }
@Override
public boolean linkShouldBeKeptAlive() {
return true; //FIXME: Current implementation is broken, so for now we will keep links always established
//We keep the remotely initiated connections, since the remotes require them if they want to request
//pairing to us, or connections that are already paired.
//return (connectionSource == ConnectionStarted.Remotely);
}
} }

View File

@ -14,12 +14,12 @@ import android.util.Log;
import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.ThreadHelper; import org.kde.kdeconnect.Helpers.ThreadHelper;
import org.kde.kdeconnect.Helpers.TrustedNetworkHelper; import org.kde.kdeconnect.Helpers.TrustedNetworkHelper;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.UserInterface.CustomDevicesActivity; import org.kde.kdeconnect.UserInterface.CustomDevicesActivity;
@ -196,13 +196,13 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) { if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) {
//Device paired with and old version, we can't use it as we lack the certificate //Device paired with and old version, we can't use it as we lack the certificate
BackgroundService.RunCommand(context, service -> { Device device = KdeConnect.getInstance().getDevice(deviceId);
Device device = service.getDevice(deviceId); if (device == null) {
if (device == null) return; return;
device.unpair(); }
//Retry as unpaired device.unpair();
identityPacketReceived(identityPacket, socket, connectionStarted); //Retry as unpaired
}); identityPacketReceived(identityPacket, socket, connectionStarted);
} }
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted); Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted);
@ -217,11 +217,11 @@ public class LanLinkProvider extends BaseLinkProvider implements LanLink.LinkDis
addLink(identityPacket, sslsocket, connectionStarted); addLink(identityPacket, sslsocket, connectionStarted);
} catch (Exception e) { } catch (Exception e) {
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"), e); Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"), e);
BackgroundService.RunCommand(context, service -> { Device device = KdeConnect.getInstance().getDevice(deviceId);
Device device = service.getDevice(deviceId); if (device == null) {
if (device == null) return; return;
device.unpair(); }
}); device.unpair();
} }
}); });
//Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection //Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection

View File

@ -14,34 +14,26 @@ import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.Network; import android.net.Network;
import android.net.NetworkCapabilities; import android.net.NetworkCapabilities;
import android.net.NetworkRequest; import android.net.NetworkRequest;
import android.os.Binder;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.preference.PreferenceManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider; import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.ThreadHelper;
import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardFloatingActivity; import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardFloatingActivity;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandActivity; import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandActivity;
import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin; import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin;
import org.kde.kdeconnect.Plugins.SharePlugin.SendFileActivity; import org.kde.kdeconnect.Plugins.SharePlugin.SendFileActivity;
@ -49,92 +41,34 @@ import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider;
/*
* This class (still) does 3 things:
* - Keeps the app running by creating a foreground notification.
* - Holds references to the active LinkProviders, but doesn't handle the DeviceLink those create (the KdeConnect class does that).
* - Listens for network connectivity changes and tells the LinkProviders to re-check for devices.
* It can be started by the KdeConnectBroadcastReceiver on some events or when the MainActivity is launched.
*/
public class BackgroundService extends Service { public class BackgroundService extends Service {
private static final int FOREGROUND_NOTIFICATION_ID = 1; private static final int FOREGROUND_NOTIFICATION_ID = 1;
private static BackgroundService instance; private static BackgroundService instance;
public interface DeviceListChangedCallback { private KdeConnect applicationInstance;
void onDeviceListChanged(boolean isConnectedToNonCellularNetwork);
}
public interface PluginCallback<T extends Plugin> {
void run(T plugin);
}
private final ConcurrentHashMap<String, DeviceListChangedCallback> deviceListChangedCallbacks = new ConcurrentHashMap<>();
private final ArrayList<BaseLinkProvider> linkProviders = new ArrayList<>(); private final ArrayList<BaseLinkProvider> linkProviders = new ArrayList<>();
private final ConcurrentHashMap<String, Device> devices = new ConcurrentHashMap<>();
private final HashSet<Object> discoveryModeAcquisitions = new HashSet<>();
public static BackgroundService getInstance() { public static BackgroundService getInstance() {
return instance; return instance;
} }
boolean isConnectedToNonCellularNetwork; // True when connected over wifi/usb/bluetooth/(anything other than cellular) // This indicates when connected over wifi/usb/bluetooth/(anything other than cellular)
private final MutableLiveData<Boolean> connectedToNonCellularNetwork = new MutableLiveData<>();
private boolean acquireDiscoveryMode(Object key) { public LiveData<Boolean> isConnectedToNonCellularNetwork() {
boolean wasEmpty = discoveryModeAcquisitions.isEmpty(); return connectedToNonCellularNetwork;
discoveryModeAcquisitions.add(key);
if (wasEmpty) {
onNetworkChange();
}
//Log.e("acquireDiscoveryMode",key.getClass().getName() +" ["+discoveryModeAcquisitions.size()+"]");
return wasEmpty;
} }
private void releaseDiscoveryMode(Object key) { public void updateForegroundNotification() {
boolean removed = discoveryModeAcquisitions.remove(key);
//Log.e("releaseDiscoveryMode",key.getClass().getName() +" ["+discoveryModeAcquisitions.size()+"]");
if (removed && discoveryModeAcquisitions.isEmpty()) {
cleanDevices();
}
}
private boolean isInDiscoveryMode() {
//return !discoveryModeAcquisitions.isEmpty();
return true; // Keep it always on for now
}
private final Device.PairingCallback devicePairingCallback = new Device.PairingCallback() {
@Override
public void incomingRequest() {
onDeviceListChanged();
}
@Override
public void pairingSuccessful() {
onDeviceListChanged();
}
@Override
public void pairingFailed(String error) {
onDeviceListChanged();
}
@Override
public void unpaired() {
onDeviceListChanged();
}
};
public void onDeviceListChanged() {
for (DeviceListChangedCallback callback : deviceListChangedCallbacks.values()) {
callback.onDeviceListChanged(isConnectedToNonCellularNetwork);
}
if (NotificationHelper.isPersistentNotificationEnabled(this)) { if (NotificationHelper.isPersistentNotificationEnabled(this)) {
//Update the foreground notification with the currently connected device list //Update the foreground notification with the currently connected device list
NotificationManager nm = ContextCompat.getSystemService(this, NotificationManager.class); NotificationManager nm = ContextCompat.getSystemService(this, NotificationManager.class);
@ -142,97 +76,14 @@ public class BackgroundService extends Service {
} }
} }
private void loadRememberedDevicesFromSettings() {
//Log.e("BackgroundService", "Loading remembered trusted devices");
SharedPreferences preferences = getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
Set<String> trustedDevices = preferences.getAll().keySet();
for (String deviceId : trustedDevices) {
//Log.e("BackgroundService", "Loading device "+deviceId);
if (preferences.getBoolean(deviceId, false)) {
Device device = new Device(this, deviceId);
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback);
}
}
}
private void registerLinkProviders() { private void registerLinkProviders() {
linkProviders.add(new LanLinkProvider(this)); linkProviders.add(new LanLinkProvider(this));
// linkProviders.add(new LoopbackLinkProvider(this)); // linkProviders.add(new LoopbackLinkProvider(this));
// linkProviders.add(new BluetoothLinkProvider(this)); // linkProviders.add(new BluetoothLinkProvider(this));
} }
public ArrayList<BaseLinkProvider> getLinkProviders() {
return linkProviders;
}
public Device getDevice(String id) {
if (id == null) {
return null;
}
return devices.get(id);
}
private void cleanDevices() {
ThreadHelper.execute(() -> {
for (Device d : devices.values()) {
if (!d.isPaired() && !d.isPairRequested() && !d.isPairRequestedByPeer() && !d.deviceShouldBeKeptAlive()) {
d.disconnect();
}
}
});
}
private final BaseLinkProvider.ConnectionReceiver deviceListener = new BaseLinkProvider.ConnectionReceiver() {
@Override
public void onConnectionReceived(final NetworkPacket identityPacket, final BaseLink link) {
String deviceId = identityPacket.getString("deviceId");
Device device = devices.get(deviceId);
if (device != null) {
Log.i("KDE/BackgroundService", "addLink, known device: " + deviceId);
device.addLink(identityPacket, link);
} else {
Log.i("KDE/BackgroundService", "addLink,unknown device: " + deviceId);
device = new Device(BackgroundService.this, identityPacket, link);
if (device.isPaired() || device.isPairRequested() || device.isPairRequestedByPeer()
|| link.linkShouldBeKeptAlive()
|| isInDiscoveryMode()) {
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback);
} else {
device.disconnect();
}
}
onDeviceListChanged();
}
@Override
public void onConnectionLost(BaseLink link) {
Device d = devices.get(link.getDeviceId());
Log.i("KDE/onConnectionLost", "removeLink, deviceId: " + link.getDeviceId());
if (d != null) {
d.removeLink(link);
if (!d.isReachable() && !d.isPaired()) {
//Log.e("onConnectionLost","Removing connection device because it was not paired");
devices.remove(link.getDeviceId());
d.removePairingCallback(devicePairingCallback);
}
} else {
//Log.d("KDE/onConnectionLost","Removing connection to unknown device");
}
onDeviceListChanged();
}
};
public ConcurrentHashMap<String, Device> getDevices() {
return devices;
}
public void onNetworkChange() { public void onNetworkChange() {
Log.d("KDE/BackgroundService", "onNetworkChange");
for (BaseLinkProvider a : linkProviders) { for (BaseLinkProvider a : linkProviders) {
a.onNetworkChange(); a.onNetworkChange();
} }
@ -250,22 +101,14 @@ public class BackgroundService extends Service {
} }
} }
public void addDeviceListChangedCallback(String key, DeviceListChangedCallback callback) {
deviceListChangedCallbacks.put(key, callback);
}
public void removeDeviceListChangedCallback(String key) {
deviceListChangedCallbacks.remove(key);
}
//This will called only once, even if we launch the service intent several times //This will called only once, even if we launch the service intent several times
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
Log.d("KdeConnect/BgService", "onCreate");
instance = this; instance = this;
DeviceHelper.initializeDeviceId(this); KdeConnect.getInstance().addDeviceListChangedCallback("BackgroundService", this::updateForegroundNotification);
// Register screen on listener // Register screen on listener
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
@ -281,29 +124,19 @@ public class BackgroundService extends Service {
cm.registerNetworkCallback(networkRequestBuilder.build(), new ConnectivityManager.NetworkCallback() { cm.registerNetworkCallback(networkRequestBuilder.build(), new ConnectivityManager.NetworkCallback() {
@Override @Override
public void onAvailable(Network network) { public void onAvailable(Network network) {
isConnectedToNonCellularNetwork = true; connectedToNonCellularNetwork.postValue(true);
onDeviceListChanged();
onNetworkChange(); onNetworkChange();
} }
@Override @Override
public void onLost(Network network) { public void onLost(Network network) {
isConnectedToNonCellularNetwork = false; connectedToNonCellularNetwork.postValue(false);
onDeviceListChanged();
} }
}); });
Log.i("KDE/BackgroundService", "Service not started yet, initializing..."); applicationInstance = KdeConnect.getInstance();
PluginFactory.initPluginInfo(getBaseContext());
initializeSecurityParameters();
NotificationHelper.initializeChannels(this);
loadRememberedDevicesFromSettings();
migratePluginSettings();
registerLinkProviders(); registerLinkProviders();
addConnectionListener(applicationInstance.getConnectionListener()); // Link Providers need to be already registered
//Link Providers need to be already registered
addConnectionListener(deviceListener);
for (BaseLinkProvider a : linkProviders) { for (BaseLinkProvider a : linkProviders) {
a.onStart(); a.onStart();
} }
@ -325,36 +158,11 @@ public class BackgroundService extends Service {
return networkRequestBuilder; return networkRequestBuilder;
} }
private void migratePluginSettings() {
SharedPreferences globalPrefs = PreferenceManager.getDefaultSharedPreferences(this);
for (String pluginKey : PluginFactory.getAvailablePlugins()) {
if (PluginFactory.getPluginInfo(pluginKey).supportsDeviceSpecificSettings()) {
Iterator<Device> it = devices.values().iterator();
while (it.hasNext()) {
Device device = it.next();
Plugin plugin = PluginFactory.instantiatePluginForDevice(getBaseContext(), pluginKey, device);
if (plugin == null) {
continue;
}
plugin.copyGlobalToDeviceSpecificSettings(globalPrefs);
if (!it.hasNext()) {
plugin.removeSettings(globalPrefs);
}
}
}
}
}
public void changePersistentNotificationVisibility(boolean visible) { public void changePersistentNotificationVisibility(boolean visible) {
NotificationManager nm = ContextCompat.getSystemService(this, NotificationManager.class);
if (visible) { if (visible) {
nm.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); updateForegroundNotification();
} else { } else {
stopForeground(true); Stop();
Start(this); Start(this);
} }
} }
@ -365,7 +173,7 @@ public class BackgroundService extends Service {
ArrayList<String> connectedDevices = new ArrayList<>(); ArrayList<String> connectedDevices = new ArrayList<>();
ArrayList<String> connectedDeviceIds = new ArrayList<>(); ArrayList<String> connectedDeviceIds = new ArrayList<>();
for (Device device : getDevices().values()) { for (Device device : applicationInstance.getDevices().values()) {
if (device.isReachable() && device.isPaired()) { if (device.isReachable() && device.isPaired()) {
connectedDeviceIds.add(device.getDeviceId()); connectedDeviceIds.add(device.getDeviceId());
connectedDevices.add(device.getName()); connectedDevices.add(device.getName());
@ -409,7 +217,7 @@ public class BackgroundService extends Service {
if (connectedDeviceIds.size() == 1) { if (connectedDeviceIds.size() == 1) {
String deviceId = connectedDeviceIds.get(0); String deviceId = connectedDeviceIds.get(0);
Device device = getDevice(deviceId); Device device = KdeConnect.getInstance().getDevice(deviceId);
if (device != null) { if (device != null) {
// Adding two action buttons only when there is a single device connected. // Adding two action buttons only when there is a single device connected.
// Setting up Send File Intent. // Setting up Send File Intent.
@ -432,49 +240,24 @@ public class BackgroundService extends Service {
return notification.build(); return notification.build();
} }
private void initializeSecurityParameters() {
RsaHelper.initialiseRsaKeys(this);
SslHelper.initialiseCertificate(this);
}
@Override @Override
public void onDestroy() { public void onDestroy() {
stopForeground(true); Log.d("KdeConnect/BgService", "onDestroy");
for (BaseLinkProvider a : linkProviders) { for (BaseLinkProvider a : linkProviders) {
a.onStop(); a.onStop();
} }
KdeConnect.getInstance().removeDeviceListChangedCallback("BackgroundService");
super.onDestroy(); super.onDestroy();
} }
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return new Binder(); return null;
} }
//To use the service from the gui
public interface InstanceCallback {
void onServiceStart(BackgroundService service);
}
private final static ArrayList<InstanceCallback> callbacks = new ArrayList<>();
private final static Lock mutex = new ReentrantLock(true);
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
//This will be called for each intent launch, even if the service is already started and it is reused Log.d("KDE/BackgroundService", "onStartCommand");
mutex.lock();
try {
for (InstanceCallback c : callbacks) {
c.onServiceStart(this);
}
callbacks.clear();
} finally {
mutex.unlock();
}
if (NotificationHelper.isPersistentNotificationEnabled(this)) { if (NotificationHelper.isPersistentNotificationEnabled(this)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE); startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
@ -482,43 +265,26 @@ public class BackgroundService extends Service {
startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
} }
} }
if (intent.hasExtra("refresh")) {
onNetworkChange();
}
return Service.START_STICKY; return Service.START_STICKY;
} }
private static void Start(Context c) { public static void Start(Context context) {
RunCommand(c, null); Log.d("KDE/BackgroundService", "Start");
ContextCompat.startForegroundService(context, new Intent(context, BackgroundService.class));
} }
public static void RunCommand(final Context c, final InstanceCallback callback) { public static void ForceRefreshConnections(Context context) {
ThreadHelper.execute(() -> { Log.d("KDE/BackgroundService", "ForceRefreshConnections");
if (callback != null) { Intent i = new Intent(context, BackgroundService.class);
mutex.lock(); i.putExtra("refresh", true);
try { ContextCompat.startForegroundService(context, i);
callbacks.add(callback);
} finally {
mutex.unlock();
}
}
ContextCompat.startForegroundService(c, new Intent(c, BackgroundService.class));
});
} }
public static <T extends Plugin> void RunWithPlugin(final Context c, final String deviceId, final Class<T> pluginClass, final PluginCallback<T> cb) { public void Stop() {
RunCommand(c, service -> { stopForeground(true);
Device device = service.getDevice(deviceId);
if (device == null) {
Log.e("BackgroundService", "Device " + deviceId + " not found");
return;
}
final T plugin = device.getPlugin(pluginClass);
if (plugin == null) {
Log.e("BackgroundService", "Device " + device.getName() + " does not have plugin " + pluginClass.getName());
return;
}
cb.run(plugin);
});
} }
} }

View File

@ -883,22 +883,6 @@ public class Device implements BaseLink.PacketReceiver {
} }
} }
public boolean deviceShouldBeKeptAlive() {
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
if (preferences.contains(getDeviceId())) {
//Log.e("DeviceShouldBeKeptAlive", "because it's a paired device");
return true; //Already paired
}
for (BaseLink l : links) {
if (l.linkShouldBeKeptAlive()) {
return true;
}
}
return false;
}
public List<String> getSupportedPlugins() { public List<String> getSupportedPlugins() {
return supportedPlugins; return supportedPlugins;
} }

View File

@ -15,7 +15,6 @@ import android.os.Build;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import org.kde.kdeconnect.MyApplication;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
public class IntentHelper { public class IntentHelper {
@ -27,8 +26,8 @@ public class IntentHelper {
* @param intent the Intent to be started * @param intent the Intent to be started
* @param title a title which is shown in the notification on Android 10+ * @param title a title which is shown in the notification on Android 10+
*/ */
public static void startActivityFromBackground(Context context, Intent intent, String title) { public static void startActivityFromBackgroundOrCreateNotification(Context context, Intent intent, String title) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !MyApplication.isInForeground()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !LifecycleHelper.isInForeground()) {
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE);
Notification notification = new NotificationCompat Notification notification = new NotificationCompat
.Builder(context, NotificationHelper.Channels.HIGHPRIORITY) .Builder(context, NotificationHelper.Channels.HIGHPRIORITY)

View File

@ -1,15 +1,12 @@
package org.kde.kdeconnect; package org.kde.kdeconnect.Helpers;
import android.app.Application;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner; import androidx.lifecycle.ProcessLifecycleOwner;
import org.kde.kdeconnect.UserInterface.ThemeUtil; public class LifecycleHelper {
public class MyApplication extends Application {
private static class LifecycleObserver implements DefaultLifecycleObserver { private static class LifecycleObserver implements DefaultLifecycleObserver {
private boolean inForeground = false; private boolean inForeground = false;
@ -28,16 +25,13 @@ public class MyApplication extends Application {
} }
} }
private static final LifecycleObserver foregroundTracker = new LifecycleObserver(); private final static LifecycleObserver foregroundTracker = new LifecycleObserver();
@Override
public void onCreate() {
super.onCreate();
ThemeUtil.setUserPreferredTheme(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(foregroundTracker);
}
public static boolean isInForeground() { public static boolean isInForeground() {
return foregroundTracker.isInForeground(); return foregroundTracker.isInForeground();
} }
public static void initializeObserver() {
ProcessLifecycleOwner.get().getLifecycle().addObserver(foregroundTracker);
}
} }

View File

@ -0,0 +1,174 @@
package org.kde.kdeconnect;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.LifecycleHelper;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/*
* This class holds all the active devices and makes them accessible from every other class.
* It also takes care of initializing all classes that need so when the app boots.
* It provides a ConnectionReceiver that the BackgroundService uses to ping this class every time a new DeviceLink is created.
*/
public class KdeConnect extends Application {
public interface DeviceListChangedCallback {
void onDeviceListChanged();
}
private static KdeConnect instance = null;
private final ConcurrentHashMap<String, Device> devices = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, DeviceListChangedCallback> deviceListChangedCallbacks = new ConcurrentHashMap<>();
@Override
public void onCreate() {
super.onCreate();
instance = this;
Log.d("KdeConnect/Application", "onCreate");
ThemeUtil.setUserPreferredTheme(this);
DeviceHelper.initializeDeviceId(this);
RsaHelper.initialiseRsaKeys(this);
SslHelper.initialiseCertificate(this);
PluginFactory.initPluginInfo(this);
NotificationHelper.initializeChannels(this);
LifecycleHelper.initializeObserver();
loadRememberedDevicesFromSettings();
}
@Override
public void onTerminate() {
Log.d("KdeConnect/Application", "onTerminate");
super.onTerminate();
}
public void addDeviceListChangedCallback(String key, DeviceListChangedCallback callback) {
deviceListChangedCallbacks.put(key, callback);
}
public void removeDeviceListChangedCallback(String key) {
deviceListChangedCallbacks.remove(key);
}
private void onDeviceListChanged() {
for (DeviceListChangedCallback callback : deviceListChangedCallbacks.values()) {
callback.onDeviceListChanged();
}
}
public ConcurrentHashMap<String, Device> getDevices() {
return devices;
}
public Device getDevice(String id) {
if (id == null) {
return null;
}
return devices.get(id);
}
public <T extends Plugin> T getDevicePlugin(String deviceId, Class<T> pluginClass) {
if (deviceId == null) {
return null;
}
Device device = devices.get(deviceId);
if (device == null) {
return null;
}
return device.getPlugin(pluginClass);
}
public static KdeConnect getInstance() {
return instance;
}
private void loadRememberedDevicesFromSettings() {
//Log.e("BackgroundService", "Loading remembered trusted devices");
SharedPreferences preferences = getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
Set<String> trustedDevices = preferences.getAll().keySet();
for (String deviceId : trustedDevices) {
//Log.e("BackgroundService", "Loading device "+deviceId);
if (preferences.getBoolean(deviceId, false)) {
Device device = new Device(this, deviceId);
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback);
}
}
}
private final Device.PairingCallback devicePairingCallback = new Device.PairingCallback() {
@Override
public void incomingRequest() {
onDeviceListChanged();
}
@Override
public void pairingSuccessful() {
onDeviceListChanged();
}
@Override
public void pairingFailed(String error) {
onDeviceListChanged();
}
@Override
public void unpaired() {
onDeviceListChanged();
}
};
private final BaseLinkProvider.ConnectionReceiver connectionListener = new BaseLinkProvider.ConnectionReceiver() {
@Override
public void onConnectionReceived(final NetworkPacket identityPacket, final BaseLink link) {
String deviceId = identityPacket.getString("deviceId");
Device device = devices.get(deviceId);
if (device != null) {
Log.i("KDE/Application", "addLink, known device: " + deviceId);
device.addLink(identityPacket, link);
} else {
Log.i("KDE/Application", "addLink,unknown device: " + deviceId);
device = new Device(KdeConnect.this, identityPacket, link);
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback);
}
onDeviceListChanged();
}
@Override
public void onConnectionLost(BaseLink link) {
Device d = devices.get(link.getDeviceId());
Log.i("KDE/onConnectionLost", "removeLink, deviceId: " + link.getDeviceId());
if (d != null) {
d.removeLink(link);
if (!d.isReachable() && !d.isPaired()) {
//Log.e("onConnectionLost","Removing connection device because it was not paired");
devices.remove(link.getDeviceId());
d.removePairingCallback(devicePairingCallback);
}
} else {
//Log.d("KDE/onConnectionLost","Removing connection to unknown device");
}
onDeviceListChanged();
}
};
public BaseLinkProvider.ConnectionReceiver getConnectionListener() {
return connectionListener;
}
}

View File

@ -15,7 +15,6 @@ import android.util.Log;
public class KdeConnectBroadcastReceiver extends BroadcastReceiver { public class KdeConnectBroadcastReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
//Log.e("KdeConnect", "Broadcast event: "+intent.getAction()); //Log.e("KdeConnect", "Broadcast event: "+intent.getAction());
@ -25,9 +24,7 @@ public class KdeConnectBroadcastReceiver extends BroadcastReceiver {
switch (action) { switch (action) {
case Intent.ACTION_MY_PACKAGE_REPLACED: case Intent.ACTION_MY_PACKAGE_REPLACED:
Log.i("KdeConnect", "MyUpdateReceiver"); Log.i("KdeConnect", "MyUpdateReceiver");
BackgroundService.RunCommand(context, service -> { BackgroundService.Start(context);
});
break; break;
case Intent.ACTION_PACKAGE_REPLACED: case Intent.ACTION_PACKAGE_REPLACED:
Log.i("KdeConnect", "UpdateReceiver"); Log.i("KdeConnect", "UpdateReceiver");
@ -35,27 +32,20 @@ public class KdeConnectBroadcastReceiver extends BroadcastReceiver {
Log.i("KdeConnect", "Ignoring, it's not me!"); Log.i("KdeConnect", "Ignoring, it's not me!");
return; return;
} }
BackgroundService.RunCommand(context, service -> { BackgroundService.Start(context);
});
break; break;
case Intent.ACTION_BOOT_COMPLETED: case Intent.ACTION_BOOT_COMPLETED:
Log.i("KdeConnect", "KdeConnectBroadcastReceiver"); Log.i("KdeConnect", "KdeConnectBroadcastReceiver");
BackgroundService.RunCommand(context, service -> { BackgroundService.Start(context);
});
break; break;
case WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION: case WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION:
case WifiManager.WIFI_STATE_CHANGED_ACTION: case WifiManager.WIFI_STATE_CHANGED_ACTION:
case ConnectivityManager.CONNECTIVITY_ACTION: case ConnectivityManager.CONNECTIVITY_ACTION:
Log.i("KdeConnect", "Connection state changed, trying to connect"); Log.i("KdeConnect", "Connection state changed, trying to connect");
BackgroundService.RunCommand(context, service -> { BackgroundService.ForceRefreshConnections(context);
service.onDeviceListChanged();
service.onNetworkChange();
});
break; break;
case Intent.ACTION_SCREEN_ON: case Intent.ACTION_SCREEN_ON:
BackgroundService.RunCommand(context, BackgroundService::onNetworkChange); BackgroundService.ForceRefreshConnections(context);
break; break;
default: default:
Log.i("BroadcastReceiver", "Ignoring broadcast event: " + intent.getAction()); Log.i("BroadcastReceiver", "Ignoring broadcast event: " + intent.getAction());

View File

@ -17,10 +17,9 @@ import android.view.View;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.UserInterface.MainActivity; import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect.UserInterface.PermissionsAlertDialogFragment; import org.kde.kdeconnect.UserInterface.PermissionsAlertDialogFragment;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.ActivityBigscreenBinding; import org.kde.kdeconnect_tp.databinding.ActivityBigscreenBinding;
@ -49,28 +48,32 @@ public class BigscreenActivity extends AppCompatActivity {
binding.micButton.setVisibility(View.INVISIBLE); binding.micButton.setVisibility(View.INVISIBLE);
} }
BackgroundService.RunWithPlugin(this, deviceId, BigscreenPlugin.class, plugin -> runOnUiThread(() -> { BigscreenPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, BigscreenPlugin.class);
binding.leftButton.setOnClickListener(v -> plugin.sendLeft()); if (plugin == null) {
binding.rightButton.setOnClickListener(v -> plugin.sendRight()); finish();
binding.upButton.setOnClickListener(v -> plugin.sendUp()); return;
binding.downButton.setOnClickListener(v -> plugin.sendDown()); }
binding.selectButton.setOnClickListener(v -> plugin.sendSelect());
binding.homeButton.setOnClickListener(v -> plugin.sendHome()); binding.leftButton.setOnClickListener(v -> plugin.sendLeft());
binding.micButton.setOnClickListener(v -> { binding.rightButton.setOnClickListener(v -> plugin.sendRight());
if (plugin.hasMicPermission()) { binding.upButton.setOnClickListener(v -> plugin.sendUp());
activateSTT(); binding.downButton.setOnClickListener(v -> plugin.sendDown());
} else { binding.selectButton.setOnClickListener(v -> plugin.sendSelect());
new PermissionsAlertDialogFragment.Builder() binding.homeButton.setOnClickListener(v -> plugin.sendHome());
.setTitle(plugin.getDisplayName()) binding.micButton.setOnClickListener(v -> {
.setMessage(R.string.bigscreen_optional_permission_explanation) if (plugin.hasMicPermission()) {
.setPositiveButton(R.string.ok) activateSTT();
.setNegativeButton(R.string.cancel) } else {
.setPermissions(new String[]{Manifest.permission.RECORD_AUDIO}) new PermissionsAlertDialogFragment.Builder()
.setRequestCode(MainActivity.RESULT_NEEDS_RELOAD) .setTitle(plugin.getDisplayName())
.create().show(getSupportFragmentManager(), null); .setMessage(R.string.bigscreen_optional_permission_explanation)
} .setPositiveButton(R.string.ok)
}); .setNegativeButton(R.string.cancel)
})); .setPermissions(new String[]{Manifest.permission.RECORD_AUDIO})
.setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
.create().show(getSupportFragmentManager(), null);
}
});
} }
public void activateSTT() { public void activateSTT() {
@ -89,9 +92,12 @@ public class BigscreenActivity extends AppCompatActivity {
.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); .getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
if (result.get(0) != null) { if (result.get(0) != null) {
final String deviceId = getIntent().getStringExtra("deviceId"); final String deviceId = getIntent().getStringExtra("deviceId");
BackgroundService.RunWithPlugin(this, deviceId, BigscreenPlugin.class, plugin -> BigscreenPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, BigscreenPlugin.class);
runOnUiThread(() -> plugin.sendSTT(result.get(0))) if (plugin == null) {
); finish();
return;
}
plugin.sendSTT(result.get(0));
} }
} }
} }

View File

@ -10,7 +10,7 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import org.kde.kdeconnect.BackgroundService import org.kde.kdeconnect.KdeConnect
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
class ClipboardTileService : TileService() { class ClipboardTileService : TileService() {
@ -20,12 +20,9 @@ class ClipboardTileService : TileService() {
startActivityAndCollapse(Intent(this, ClipboardFloatingActivity::class.java).apply { startActivityAndCollapse(Intent(this, ClipboardFloatingActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
var ids : List<String> = emptyList() var ids : List<String> = emptyList()
val service = BackgroundService.getInstance() ids = KdeConnect.getInstance().devices.values
if (service != null) { .filter { it.isReachable && it.isPaired }
ids = service.devices.values .map { it.deviceId }
.filter { it.isReachable && it.isPaired }
.map { it.deviceId }
}
putExtra("connectedDeviceIds", ArrayList(ids)) putExtra("connectedDeviceIds", ArrayList(ids))
}) })
} }

View File

@ -12,8 +12,7 @@ import android.view.WindowManager;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.databinding.ActivityFindMyPhoneBinding; import org.kde.kdeconnect_tp.databinding.ActivityFindMyPhoneBinding;
import java.util.Objects; import java.util.Objects;
@ -21,7 +20,7 @@ import java.util.Objects;
public class FindMyPhoneActivity extends AppCompatActivity { public class FindMyPhoneActivity extends AppCompatActivity {
static final String EXTRA_DEVICE_ID = "deviceId"; static final String EXTRA_DEVICE_ID = "deviceId";
private FindMyPhonePlugin plugin; String deviceId;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -39,8 +38,7 @@ public class FindMyPhoneActivity extends AppCompatActivity {
finish(); finish();
} }
String deviceId = getIntent().getStringExtra(EXTRA_DEVICE_ID); deviceId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
plugin = BackgroundService.getInstance().getDevice(deviceId).getPlugin(FindMyPhonePlugin.class);
Window window = this.getWindow(); Window window = this.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
@ -53,11 +51,10 @@ public class FindMyPhoneActivity extends AppCompatActivity {
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
/* FindMyPhonePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, FindMyPhonePlugin.class);
For whatever reason when Android launches this activity as a SystemAlertWindow it calls: if (plugin == null) {
onCreate(), onStart(), onResume(), onStop(), onStart(), onResume(). return;
When using BackgroundService.RunWithPlugin we get into concurrency problems and sometimes no sound will be played }
*/
plugin.startPlaying(); plugin.startPlaying();
plugin.hideNotification(); plugin.hideNotification();
} }
@ -65,7 +62,10 @@ public class FindMyPhoneActivity extends AppCompatActivity {
@Override @Override
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
FindMyPhonePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, FindMyPhonePlugin.class);
if (plugin == null) {
return;
}
plugin.stopPlaying(); plugin.stopPlaying();
} }
} }

View File

@ -25,8 +25,8 @@ import androidx.core.content.ContextCompat;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.LifecycleHelper;
import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.MyApplication;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.Plugins.PluginFactory;
@ -107,7 +107,7 @@ public class FindMyPhonePlugin extends Plugin {
@Override @Override
public boolean onPacketReceived(NetworkPacket np) { public boolean onPacketReceived(NetworkPacket np) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || MyApplication.isInForeground()) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || LifecycleHelper.isInForeground()) {
Intent intent = new Intent(context, FindMyPhoneActivity.class); Intent intent = new Intent(context, FindMyPhoneActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(FindMyPhoneActivity.EXTRA_DEVICE_ID, device.getDeviceId()); intent.putExtra(FindMyPhoneActivity.EXTRA_DEVICE_ID, device.getDeviceId());

View File

@ -5,7 +5,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.Log; import android.util.Log;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.KdeConnect;
public class FindMyPhoneReceiver extends BroadcastReceiver { public class FindMyPhoneReceiver extends BroadcastReceiver {
final static String ACTION_FOUND_IT = "org.kde.kdeconnect.Plugins.FindMyPhonePlugin.foundIt"; final static String ACTION_FOUND_IT = "org.kde.kdeconnect.Plugins.FindMyPhonePlugin.foundIt";
@ -29,7 +29,10 @@ public class FindMyPhoneReceiver extends BroadcastReceiver {
} }
String deviceId = intent.getStringExtra(EXTRA_DEVICE_ID); String deviceId = intent.getStringExtra(EXTRA_DEVICE_ID);
FindMyPhonePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, FindMyPhonePlugin.class);
BackgroundService.RunWithPlugin(context, deviceId, FindMyPhonePlugin.class, FindMyPhonePlugin::stopPlaying); if (plugin == null) {
return;
}
plugin.stopPlaying();
} }
} }

View File

@ -27,7 +27,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.accompanist.themeadapter.material3.Mdc3Theme import com.google.accompanist.themeadapter.material3.Mdc3Theme
import org.kde.kdeconnect.BackgroundService import org.kde.kdeconnect.KdeConnect
import org.kde.kdeconnect.NetworkPacket import org.kde.kdeconnect.NetworkPacket
import org.kde.kdeconnect.UserInterface.compose.KdeTextButton import org.kde.kdeconnect.UserInterface.compose.KdeTextButton
import org.kde.kdeconnect.UserInterface.compose.KdeTextField import org.kde.kdeconnect.UserInterface.compose.KdeTextField
@ -72,9 +72,12 @@ class ComposeSendActivity : AppCompatActivity() {
} catch (e: Exception) { } catch (e: Exception) {
Log.e("KDE/ComposeSend", "Exception", e) Log.e("KDE/ComposeSend", "Exception", e)
} }
BackgroundService.RunWithPlugin( val plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin::class.java)
this, deviceId, MousePadPlugin::class.java if (plugin == null) {
) { plugin: MousePadPlugin -> plugin.sendKeyboardPacket(np) } finish();
return;
}
plugin.sendKeyboardPacket(np);
} }
private fun sendComposed() { private fun sendComposed() {

View File

@ -14,7 +14,7 @@ import android.view.View;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
public class KeyListenerView extends View { public class KeyListenerView extends View {
@ -89,7 +89,11 @@ public class KeyListenerView extends View {
} }
private void sendKeyPressPacket(final NetworkPacket np) { private void sendKeyPressPacket(final NetworkPacket np) {
BackgroundService.RunWithPlugin(getContext(), deviceId, MousePadPlugin.class, plugin -> plugin.sendKeyboardPacket(np)); MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
return;
}
plugin.sendKeyboardPacket(np);
} }
@Override @Override

View File

@ -22,10 +22,12 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; import org.kde.kdeconnect.UserInterface.PluginSettingsActivity;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
@ -115,7 +117,12 @@ public class MousePadActivity
final float nX = X; final float nX = X;
final float nY = Y; final float nY = Y;
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> plugin.sendMouseDelta(nX, nY)); MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendMouseDelta(nX, nY);
} }
@Override @Override
@ -235,24 +242,30 @@ public class MousePadActivity
startActivity(intent); startActivity(intent);
return true; return true;
} else if (id == R.id.menu_show_keyboard) { } else if (id == R.id.menu_show_keyboard) {
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> { MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin.isKeyboardEnabled()) { if (plugin == null) {
showKeyboard(); finish();
} else { return true;
Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT); }
toast.show(); if (plugin.isKeyboardEnabled()) {
} showKeyboard();
}); } else {
Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT);
toast.show();
}
return true; return true;
} else if (id == R.id.menu_open_compose_send) { } else if (id == R.id.menu_open_compose_send) {
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> { MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin.isKeyboardEnabled()) { if (plugin == null) {
showCompose(); finish();
} else { return true;
Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT); }
toast.show(); if (plugin.isKeyboardEnabled()) {
} showCompose();
}); } else {
Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT);
toast.show();
}
return true; return true;
} else { } else {
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
@ -288,20 +301,23 @@ public class MousePadActivity
mCurrentX = event.getX(); mCurrentX = event.getX();
mCurrentY = event.getY(); mCurrentY = event.getY();
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> { MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
float deltaX = (mCurrentX - mPrevX) * displayDpiMultiplier * mCurrentSensitivity; if (plugin == null) {
float deltaY = (mCurrentY - mPrevY) * displayDpiMultiplier * mCurrentSensitivity; finish();
return true;
}
// Run the mouse delta through the pointer acceleration profile float deltaX = (mCurrentX - mPrevX) * displayDpiMultiplier * mCurrentSensitivity;
mPointerAccelerationProfile.touchMoved(deltaX, deltaY, event.getEventTime()); float deltaY = (mCurrentY - mPrevY) * displayDpiMultiplier * mCurrentSensitivity;
mouseDelta = mPointerAccelerationProfile.commitAcceleratedMouseDelta(mouseDelta);
plugin.sendMouseDelta(mouseDelta.x, mouseDelta.y); // Run the mouse delta through the pointer acceleration profile
mPointerAccelerationProfile.touchMoved(deltaX, deltaY, event.getEventTime());
mouseDelta = mPointerAccelerationProfile.commitAcceleratedMouseDelta(mouseDelta);
mPrevX = mCurrentX; plugin.sendMouseDelta(mouseDelta.x, mouseDelta.y);
mPrevY = mCurrentY;
});
mPrevX = mCurrentX;
mPrevY = mCurrentY;
break; break;
} }
@ -361,7 +377,12 @@ public class MousePadActivity
@Override @Override
public void onLongPress(MotionEvent e) { public void onLongPress(MotionEvent e) {
getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendSingleHold); MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendSingleHold();
} }
@Override @Override
@ -388,7 +409,12 @@ public class MousePadActivity
@Override @Override
public boolean onDoubleTap(MotionEvent e) { public boolean onDoubleTap(MotionEvent e) {
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendDoubleClick); MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return true;
}
plugin.sendDoubleClick();
return true; return true;
} }
@ -438,19 +464,39 @@ public class MousePadActivity
private void sendLeftClick() { private void sendLeftClick() {
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendLeftClick); MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendLeftClick();
} }
private void sendMiddleClick() { private void sendMiddleClick() {
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendMiddleClick); MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendMiddleClick();
} }
private void sendRightClick() { private void sendRightClick() {
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, MousePadPlugin::sendRightClick); MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendRightClick();
} }
private void sendScroll(final float y) { private void sendScroll(final float y) {
BackgroundService.RunWithPlugin(this, deviceId, MousePadPlugin.class, plugin -> plugin.sendScroll(0, y)); MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendScroll(0, y);
} }
private void showKeyboard() { private void showKeyboard() {

View File

@ -19,11 +19,11 @@ import androidx.appcompat.app.AppCompatActivity;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.SafeTextChecker; import org.kde.kdeconnect.Helpers.SafeTextChecker;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.UserInterface.List.EntryItemWithIcon; import org.kde.kdeconnect.UserInterface.List.EntryItemWithIcon;
import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.List.SectionItem; import org.kde.kdeconnect.UserInterface.List.SectionItem;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.ActivitySendkeystrokesBinding; import org.kde.kdeconnect_tp.databinding.ActivitySendkeystrokesBinding;
@ -89,7 +89,7 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
// If we trust the sending app, check if there is only one device paired / reachable... // If we trust the sending app, check if there is only one device paired / reachable...
if (contentIsOkay) { if (contentIsOkay) {
List<Device> reachableDevices = BackgroundService.getInstance().getDevices().values().stream() List<Device> reachableDevices = KdeConnect.getInstance().getDevices().values().stream()
.filter(Device::isReachable) .filter(Device::isReachable)
.limit(2) // we only need the first two; if its more than one, we need to show the user the device-selection .limit(2) // we only need the first two; if its more than one, we need to show the user the device-selection
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -103,16 +103,9 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
} }
} }
KdeConnect.getInstance().addDeviceListChangedCallback("SendKeystrokesToHostActivity", () -> runOnUiThread(this::updateDeviceList));
// subscribe to new connected devices BackgroundService.ForceRefreshConnections(this); // force a network re-discover
BackgroundService.RunCommand(this, service -> {
service.onNetworkChange();
service.addDeviceListChangedCallback("SendKeystrokesToHostActivity", unused -> updateDeviceList());
});
// list all currently connected devices
updateDeviceList(); updateDeviceList();
} else { } else {
Toast.makeText(getApplicationContext(), R.string.sendkeystrokes_wrong_data, Toast.LENGTH_LONG).show(); Toast.makeText(getApplicationContext(), R.string.sendkeystrokes_wrong_data, Toast.LENGTH_LONG).show();
finish(); finish();
@ -122,7 +115,7 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
@Override @Override
protected void onStop() { protected void onStop() {
BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("SendKeystrokesToHostActivity")); KdeConnect.getInstance().removeDeviceListChangedCallback("SendKeystrokesToHostActivity");
super.onStop(); super.onStop();
} }
@ -131,7 +124,12 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
if (binding.textToSend.getText() != null && (toSend = binding.textToSend.getText().toString().trim()).length() > 0) { if (binding.textToSend.getText() != null && (toSend = binding.textToSend.getText().toString().trim()).length() > 0) {
final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST); final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST);
np.set("key", toSend); np.set("key", toSend);
BackgroundService.RunWithPlugin(this, deviceId.getDeviceId(), MousePadPlugin.class, plugin -> plugin.sendKeyboardPacket(np)); MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId.getDeviceId(), MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendKeyboardPacket(np);
Toast.makeText( Toast.makeText(
getApplicationContext(), getApplicationContext(),
getString(R.string.sendkeystrokes_sent_text, toSend, deviceId.getName()), getString(R.string.sendkeystrokes_sent_text, toSend, deviceId.getName()),
@ -143,41 +141,37 @@ public class SendKeystrokesToHostActivity extends AppCompatActivity {
private void updateDeviceList() { private void updateDeviceList() {
BackgroundService.RunCommand(this, service -> { Collection<Device> devices = KdeConnect.getInstance().getDevices().values();
final ArrayList<Device> devicesList = new ArrayList<>();
final ArrayList<ListAdapter.Item> items = new ArrayList<>();
Collection<Device> devices = service.getDevices().values(); SectionItem section = new SectionItem(getString(R.string.sendkeystrokes_send_to));
final ArrayList<Device> devicesList = new ArrayList<>(); items.add(section);
final ArrayList<ListAdapter.Item> items = new ArrayList<>();
SectionItem section = new SectionItem(getString(R.string.sendkeystrokes_send_to)); for (Device d : devices) {
items.add(section); if (d.isReachable() && d.isPaired()) {
devicesList.add(d);
for (Device d : devices) { items.add(new EntryItemWithIcon(d.getName(), d.getIcon()));
if (d.isReachable() && d.isPaired()) { section.isEmpty = false;
devicesList.add(d);
items.add(new EntryItemWithIcon(d.getName(), d.getIcon()));
section.isEmpty = false;
}
} }
runOnUiThread(() -> { }
binding.devicesList.setAdapter(new ListAdapter(SendKeystrokesToHostActivity.this, items));
binding.devicesList.setOnItemClickListener((adapterView, view, i, l) -> {
Device device = devicesList.get(i - 1); // NOTE: -1 because of the title!
sendKeys(device);
this.finish(); // close the activity
});
});
// only one device is connected and we trust the text to send -> send it and close the activity. binding.devicesList.setAdapter(new ListAdapter(SendKeystrokesToHostActivity.this, items));
// Usually we already check it in `onStart` - but if the BackgroundService was not started/connected to the host binding.devicesList.setOnItemClickListener((adapterView, view, i, l) -> {
// it will not have the deviceList in memory. Use this callback as second chance (but it will flicker a bit, because the activity might Device device = devicesList.get(i - 1); // NOTE: -1 because of the title!
// already been visible and get closed again quickly) sendKeys(device);
if (devicesList.size() == 1 && contentIsOkay) { this.finish(); // close the activity
Device device = devicesList.get(0);
sendKeys(device);
this.finish(); // close the activity
}
}); });
// only one device is connected and we trust the text to send -> send it and close the activity.
// Usually we already check it in `onStart` - but if the BackgroundService was not started/connected to the host
// it will not have the deviceList in memory. Use this callback as second chance (but it will flicker a bit, because the activity might
// already been visible and get closed again quickly)
if (devicesList.size() == 1 && contentIsOkay) {
Device device = devicesList.get(0);
sendKeys(device);
this.finish(); // close the activity
}
} }
} }

View File

@ -11,8 +11,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.Device;
/** /**
* Called when the mpris media notification's buttons are pressed * Called when the mpris media notification's buttons are pressed
@ -29,7 +28,7 @@ public class MprisMediaNotificationReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
//First case: buttons send by other applications via the media session APIs //First case: buttons send by other applications via the media session APIs. They don't target a specific device.
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
//Route these buttons to the media session, which will handle them //Route these buttons to the media session, which will handle them
MediaSessionCompat mediaSession = MprisMediaSession.getMediaSession(); MediaSessionCompat mediaSession = MprisMediaSession.getMediaSession();
@ -39,13 +38,10 @@ public class MprisMediaNotificationReceiver extends BroadcastReceiver {
//Second case: buttons on the notification, which we created ourselves //Second case: buttons on the notification, which we created ourselves
//Get the correct device, the mpris plugin and the mpris player //Get the correct device, the mpris plugin and the mpris player
BackgroundService service = BackgroundService.getInstance(); String deviceId = intent.getStringExtra(EXTRA_DEVICE_ID);
if (service == null) return; MprisPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MprisPlugin.class);
Device device = service.getDevice(intent.getStringExtra(EXTRA_DEVICE_ID)); if (plugin == null) return;
if (device == null) return; MprisPlugin.MprisPlayer player = plugin.getPlayerStatus(intent.getStringExtra(EXTRA_MPRIS_PLAYER));
MprisPlugin mpris = device.getPlugin(MprisPlugin.class);
if (mpris == null) return;
MprisPlugin.MprisPlayer player = mpris.getPlayerStatus(intent.getStringExtra(EXTRA_MPRIS_PLAYER));
if (player == null) return; if (player == null) return;
//Forward the action to the player //Forward the action to the player
@ -65,7 +61,9 @@ public class MprisMediaNotificationReceiver extends BroadcastReceiver {
case ACTION_CLOSE_NOTIFICATION: case ACTION_CLOSE_NOTIFICATION:
//The user dismissed the notification: actually handle its removal correctly //The user dismissed the notification: actually handle its removal correctly
MprisMediaSession.getInstance().closeMediaNotification(); MprisMediaSession.getInstance().closeMediaNotification();
break;
} }
} }
} }
} }

View File

@ -14,6 +14,7 @@ import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build; import android.os.Build;
import android.os.Handler;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.service.notification.StatusBarNotification; import android.service.notification.StatusBarNotification;
import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.MediaMetadataCompat;
@ -27,9 +28,9 @@ import androidx.core.app.TaskStackBuilder;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.media.app.NotificationCompat.MediaStyle; import androidx.media.app.NotificationCompat.MediaStyle;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationReceiver; import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationReceiver;
import org.kde.kdeconnect.Plugins.SystemVolumePlugin.SystemVolumePlugin; import org.kde.kdeconnect.Plugins.SystemVolumePlugin.SystemVolumePlugin;
import org.kde.kdeconnect.Plugins.SystemVolumePlugin.SystemVolumeProvider; import org.kde.kdeconnect.Plugins.SystemVolumePlugin.SystemVolumeProvider;
@ -112,20 +113,20 @@ public class MprisMediaSession implements
* <p> * <p>
* Can be called multiple times, once for each device * Can be called multiple times, once for each device
* *
* @param _context The context * @param context The context
* @param mpris The mpris plugin * @param plugin The mpris plugin
* @param device The device id * @param device The device id
*/ */
public void onCreate(Context _context, MprisPlugin mpris, String device) { public void onCreate(Context context, MprisPlugin plugin, String device) {
if (mprisDevices.isEmpty()) { if (mprisDevices.isEmpty()) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(_context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.registerOnSharedPreferenceChangeListener(this); prefs.registerOnSharedPreferenceChangeListener(this);
} }
context = _context; this.context = context;
mprisDevices.add(device); mprisDevices.add(device);
mpris.setPlayerListUpdatedHandler("media_notification", this::updateMediaNotification); plugin.setPlayerListUpdatedHandler("media_notification", this::updateMediaNotification);
mpris.setPlayerStatusUpdatedHandler("media_notification", this::updateMediaNotification); plugin.setPlayerStatusUpdatedHandler("media_notification", this::updateMediaNotification);
NotificationReceiver.RunCommand(context, service -> { NotificationReceiver.RunCommand(context, service -> {
@ -137,8 +138,6 @@ public class MprisMediaSession implements
onListenerConnected(service); onListenerConnected(service);
} }
}); });
updateMediaNotification();
} }
/** /**
@ -146,13 +145,13 @@ public class MprisMediaSession implements
* <p> * <p>
* Can be called multiple times, once for each device * Can be called multiple times, once for each device
* *
* @param mpris The mpris plugin * @param plugin The mpris plugin
* @param device The device id * @param device The device id
*/ */
public void onDestroy(MprisPlugin mpris, String device) { public void onDestroy(MprisPlugin plugin, String device) {
mprisDevices.remove(device); mprisDevices.remove(device);
mpris.removePlayerStatusUpdatedHandler("media_notification"); plugin.removePlayerStatusUpdatedHandler("media_notification");
mpris.removePlayerListUpdatedHandler("media_notification"); plugin.removePlayerListUpdatedHandler("media_notification");
updateMediaNotification(); updateMediaNotification();
if (mprisDevices.isEmpty()) { if (mprisDevices.isEmpty()) {
@ -166,21 +165,19 @@ public class MprisMediaSession implements
* <p> * <p>
* Prefers playing devices/mpris players, but tries to keep displaying the same * Prefers playing devices/mpris players, but tries to keep displaying the same
* player and device, while possible. * player and device, while possible.
*
* @param service The background service
*/ */
private void updateCurrentPlayer(BackgroundService service) { private void updateCurrentPlayer() {
Pair<Device, MprisPlugin.MprisPlayer> player = findPlayer(service); Pair<Device, MprisPlugin.MprisPlayer> player = findPlayer();
//Update the last-displayed device and player //Update the last-displayed device and player
notificationDevice = player.first == null ? null : player.first.getDeviceId(); notificationDevice = player.first == null ? null : player.first.getDeviceId();
notificationPlayer = player.second; notificationPlayer = player.second;
} }
private Pair<Device, MprisPlugin.MprisPlayer> findPlayer(BackgroundService service) { private Pair<Device, MprisPlugin.MprisPlayer> findPlayer() {
//First try the previously displayed player (if still playing) or the previous displayed device (otherwise) //First try the previously displayed player (if still playing) or the previous displayed device (otherwise)
if (notificationDevice != null && mprisDevices.contains(notificationDevice)) { if (notificationDevice != null && mprisDevices.contains(notificationDevice)) {
Device device = service.getDevice(notificationDevice); Device device = KdeConnect.getInstance().getDevice(notificationDevice);
MprisPlugin.MprisPlayer player; MprisPlugin.MprisPlayer player;
if (notificationPlayer != null && notificationPlayer.isPlaying()) { if (notificationPlayer != null && notificationPlayer.isPlaying()) {
@ -194,7 +191,7 @@ public class MprisMediaSession implements
} }
// Try a different player from another device // Try a different player from another device
for (Device otherDevice : service.getDevices().values()) { for (Device otherDevice : KdeConnect.getInstance().getDevices().values()) {
MprisPlugin.MprisPlayer player = getPlayerFromDevice(otherDevice, null); MprisPlugin.MprisPlayer player = getPlayerFromDevice(otherDevice, null);
if (player != null) { if (player != null) {
return new Pair<>(otherDevice, player); return new Pair<>(otherDevice, player);
@ -205,7 +202,7 @@ public class MprisMediaSession implements
// This will succeed if it's paused: // This will succeed if it's paused:
// that allows pausing and subsequently resuming via the notification // that allows pausing and subsequently resuming via the notification
if (notificationDevice != null && mprisDevices.contains(notificationDevice)) { if (notificationDevice != null && mprisDevices.contains(notificationDevice)) {
Device device = service.getDevice(notificationDevice); Device device = KdeConnect.getInstance().getDevice(notificationDevice);
MprisPlugin.MprisPlayer player = getPlayerFromDevice(device, notificationPlayer); MprisPlugin.MprisPlayer player = getPlayerFromDevice(device, notificationPlayer);
if (player != null) { if (player != null) {
@ -244,212 +241,211 @@ public class MprisMediaSession implements
} }
private void updateRemoteDeviceVolumeControl() { private void updateRemoteDeviceVolumeControl() {
// Volume control feature is only available from Lollipop onwards SystemVolumePlugin plugin = KdeConnect.getInstance().getDevicePlugin(notificationDevice, SystemVolumePlugin.class);
BackgroundService.RunWithPlugin(context, notificationDevice, SystemVolumePlugin.class, plugin -> { if (plugin == null) {
SystemVolumeProvider systemVolumeProvider = SystemVolumeProvider.fromPlugin(plugin); return;
systemVolumeProvider.addStateListener(this); }
systemVolumeProvider.startTrackingVolumeKeys(); SystemVolumeProvider systemVolumeProvider = SystemVolumeProvider.fromPlugin(plugin);
}); systemVolumeProvider.addStateListener(this);
systemVolumeProvider.startTrackingVolumeKeys();
} }
/** /**
* Update the media control notification * Update the media control notification
*/ */
private void updateMediaNotification() { private void updateMediaNotification() {
BackgroundService.RunCommand(context, service -> {
//If the user disabled the media notification, do not show it //If the user disabled the media notification, do not show it
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (!prefs.getBoolean(context.getString(R.string.mpris_notification_key), true)) { if (!prefs.getBoolean(context.getString(R.string.mpris_notification_key), true)) {
closeMediaNotification(); closeMediaNotification();
return; return;
}
if (mediaSession == null) {
mediaSession = new MediaSessionCompat(context, MPRIS_MEDIA_SESSION_TAG);
mediaSession.setCallback(mediaSessionCallback, new Handler(context.getMainLooper()));
// Deprecated flags not required in Build.VERSION_CODES.O and later
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
}
//Make sure our information is up-to-date
updateCurrentPlayer();
//If the player disappeared (and no other playing one found), just remove the notification
if (notificationPlayer == null) {
closeMediaNotification();
return;
}
updateRemoteDeviceVolumeControl();
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(context, MprisMediaNotificationReceiver.class);
iPlay.setAction(MprisMediaNotificationReceiver.ACTION_PLAY);
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iPlay.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
PendingIntent piPlay = PendingIntent.getBroadcast(context, 0, iPlay, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
NotificationCompat.Action.Builder aPlay = new NotificationCompat.Action.Builder(
R.drawable.ic_play_white, context.getString(R.string.mpris_play), piPlay);
Intent iPause = new Intent(context, MprisMediaNotificationReceiver.class);
iPause.setAction(MprisMediaNotificationReceiver.ACTION_PAUSE);
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iPause.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
PendingIntent piPause = PendingIntent.getBroadcast(context, 0, iPause, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
NotificationCompat.Action.Builder aPause = new NotificationCompat.Action.Builder(
R.drawable.ic_pause_white, context.getString(R.string.mpris_pause), piPause);
Intent iPrevious = new Intent(context, MprisMediaNotificationReceiver.class);
iPrevious.setAction(MprisMediaNotificationReceiver.ACTION_PREVIOUS);
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iPrevious.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
PendingIntent piPrevious = PendingIntent.getBroadcast(context, 0, iPrevious, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
NotificationCompat.Action.Builder aPrevious = new NotificationCompat.Action.Builder(
R.drawable.ic_previous_white, context.getString(R.string.mpris_previous), piPrevious);
Intent iNext = new Intent(context, MprisMediaNotificationReceiver.class);
iNext.setAction(MprisMediaNotificationReceiver.ACTION_NEXT);
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iNext.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
PendingIntent piNext = PendingIntent.getBroadcast(context, 0, iNext, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
NotificationCompat.Action.Builder aNext = new NotificationCompat.Action.Builder(
R.drawable.ic_next_white, context.getString(R.string.mpris_next), piNext);
Intent iOpenActivity = new Intent(context, MprisActivity.class);
iOpenActivity.putExtra("deviceId", notificationDevice);
iOpenActivity.putExtra("player", notificationPlayer.getPlayerName());
PendingIntent piOpenActivity = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(iOpenActivity)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
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(ContextCompat.getColor(context, R.color.primary))
.setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC)
.setSubText(KdeConnect.getInstance().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.getPlayerName() + ")");
} else if (!notificationPlayer.getArtist().isEmpty()) {
notification.setContentText(notificationPlayer.getArtist() + " (" + notificationPlayer.getPlayerName() + ")");
} else if (!notificationPlayer.getAlbum().isEmpty()) {
notification.setContentText(notificationPlayer.getAlbum() + " (" + notificationPlayer.getPlayerName() + ")");
} else {
notification.setContentText(notificationPlayer.getPlayerName());
}
if (albumArt != null) {
notification.setLargeIcon(albumArt);
}
if (!notificationPlayer.isPlaying()) {
Intent iCloseNotification = new Intent(context, MprisMediaNotificationReceiver.class);
iCloseNotification.setAction(MprisMediaNotificationReceiver.ACTION_CLOSE_NOTIFICATION);
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_DEVICE_ID, notificationDevice);
iCloseNotification.putExtra(MprisMediaNotificationReceiver.EXTRA_MPRIS_PLAYER, notificationPlayer.getPlayerName());
PendingIntent piCloseNotification = PendingIntent.getBroadcast(context, 0, iCloseNotification, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
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;
}
// Documentation says that this was added in Lollipop (21) but it seems to cause crashes on < Pie (28)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (notificationPlayer.isSeekAllowed()) {
playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
} }
}
playbackState.setActions(playbackActions);
mediaSession.setPlaybackState(playbackState.build());
//Make sure our information is up-to-date //Only allow deletion if no music is notificationPlayer
updateCurrentPlayer(service); notification.setOngoing(notificationPlayer.isPlaying());
//If the player disappeared (and no other playing one found), just remove the notification //Use the MediaStyle notification, so it feels like other media players. That also allows adding actions
if (notificationPlayer == null) { MediaStyle mediaStyle = new MediaStyle();
closeMediaNotification(); if (numActions == 1) {
return; 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");
//Update the metadata and playback status //Display the notification
if (mediaSession == null) { mediaSession.setActive(true);
mediaSession = new MediaSessionCompat(context, MPRIS_MEDIA_SESSION_TAG); final NotificationManager nm = ContextCompat.getSystemService(context, NotificationManager.class);
mediaSession.setCallback(mediaSessionCallback); nm.notify(MPRIS_MEDIA_NOTIFICATION_ID, notification.build());
// Deprecated flags not required in Build.VERSION_CODES.O and later
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
}
updateRemoteDeviceVolumeControl();
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 | PendingIntent.FLAG_MUTABLE);
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 | PendingIntent.FLAG_MUTABLE);
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 | PendingIntent.FLAG_MUTABLE);
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 | PendingIntent.FLAG_MUTABLE);
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());
PendingIntent piOpenActivity = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(iOpenActivity)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
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(ContextCompat.getColor(service, R.color.primary))
.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.getBroadcast(service, 0, iCloseNotification, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
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;
}
// Documentation says that this was added in Lollipop (21) but it seems to cause crashes on < Pie (28)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (notificationPlayer.isSeekAllowed()) {
playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
}
}
playbackState.setActions(playbackActions);
mediaSession.setPlaybackState(playbackState.build());
//Only allow deletion if no music is notificationPlayer
notification.setOngoing(notificationPlayer.isPlaying());
//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 = ContextCompat.getSystemService(context, NotificationManager.class);
nm.notify(MPRIS_MEDIA_NOTIFICATION_ID, notification.build());
});
} }
public void closeMediaNotification() { public void closeMediaNotification() {

View File

@ -1,7 +1,6 @@
package org.kde.kdeconnect.Plugins.MprisPlugin; package org.kde.kdeconnect.Plugins.MprisPlugin;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@ -10,7 +9,6 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -28,12 +26,9 @@ import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Helpers.VideoUrlsHelper; import org.kde.kdeconnect.Helpers.VideoUrlsHelper;
import org.kde.kdeconnect.Helpers.VolumeHelperKt; import org.kde.kdeconnect.Helpers.VolumeHelperKt;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.MprisControlBinding; import org.kde.kdeconnect_tp.databinding.MprisControlBinding;
import org.kde.kdeconnect_tp.databinding.MprisNowPlayingBinding; import org.kde.kdeconnect_tp.databinding.MprisNowPlayingBinding;
@ -49,18 +44,9 @@ public class MprisNowPlayingFragment extends Fragment implements VolumeKeyListen
private MprisNowPlayingBinding activityMprisBinding; private MprisNowPlayingBinding activityMprisBinding;
private String deviceId; private String deviceId;
private Runnable positionSeekUpdateRunnable = null; private Runnable positionSeekUpdateRunnable = null;
private String targetPlayerName = "";
private MprisPlugin.MprisPlayer targetPlayer = null; private MprisPlugin.MprisPlayer targetPlayer = null;
private final BaseLinkProvider.ConnectionReceiver connectionReceiver = new BaseLinkProvider.ConnectionReceiver() {
@Override
public void onConnectionReceived(NetworkPacket identityPacket, BaseLink link) {
connectToPlugin(null);
}
@Override
public void onConnectionLost(BaseLink link) {
}
};
public static MprisNowPlayingFragment newInstance(String deviceId) { public static MprisNowPlayingFragment newInstance(String deviceId) {
MprisNowPlayingFragment mprisNowPlayingFragment = new MprisNowPlayingFragment(); MprisNowPlayingFragment mprisNowPlayingFragment = new MprisNowPlayingFragment();
@ -94,211 +80,217 @@ public class MprisNowPlayingFragment extends Fragment implements VolumeKeyListen
@Nullable @Nullable
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
activityMprisBinding = MprisNowPlayingBinding.inflate(inflater);
mprisControlBinding = activityMprisBinding.mprisControl;
if (activityMprisBinding == null) { deviceId = requireArguments().getString(MprisPlugin.DEVICE_ID_KEY);
activityMprisBinding = MprisNowPlayingBinding.inflate(inflater);
mprisControlBinding = activityMprisBinding.mprisControl;
String targetPlayerName = ""; targetPlayerName = "";
Intent activityIntent = requireActivity().getIntent(); Intent activityIntent = requireActivity().getIntent();
activityIntent.getStringExtra("player"); if (activityIntent.hasExtra("player")) {
targetPlayerName = activityIntent.getStringExtra("player");
activityIntent.removeExtra("player"); activityIntent.removeExtra("player");
} else if (savedInstanceState != null) {
targetPlayerName = savedInstanceState.getString("targetPlayer");
}
if (TextUtils.isEmpty(targetPlayerName)) { connectToPlugin();
if (savedInstanceState != null) {
targetPlayerName = savedInstanceState.getString("targetPlayer"); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
} String interval_time_str = prefs.getString(getString(R.string.mpris_time_key),
getString(R.string.mpris_time_default));
final int interval_time = Integer.parseInt(interval_time_str);
performActionOnClick(mprisControlBinding.loopButton, p -> {
switch (p.getLoopStatus()) {
case "None":
p.setLoopStatus("Track");
break;
case "Track":
p.setLoopStatus("Playlist");
break;
case "Playlist":
p.setLoopStatus("None");
break;
}
});
performActionOnClick(mprisControlBinding.playButton, MprisPlugin.MprisPlayer::playPause);
performActionOnClick(mprisControlBinding.shuffleButton, p -> p.setShuffle(!p.getShuffle()));
performActionOnClick(mprisControlBinding.prevButton, MprisPlugin.MprisPlayer::previous);
performActionOnClick(mprisControlBinding.rewButton, p -> targetPlayer.seek(interval_time * -1));
performActionOnClick(mprisControlBinding.ffButton, p -> p.seek(interval_time));
performActionOnClick(mprisControlBinding.nextButton, MprisPlugin.MprisPlayer::next);
performActionOnClick(mprisControlBinding.stopButton, MprisPlugin.MprisPlayer::stop);
mprisControlBinding.volumeSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
} }
deviceId = requireArguments().getString(MprisPlugin.DEVICE_ID_KEY); @Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); @Override
String interval_time_str = prefs.getString(getString(R.string.mpris_time_key), public void onStopTrackingTouch(final SeekBar seekBar) {
getString(R.string.mpris_time_default)); if (targetPlayer == null) return;
final int interval_time = Integer.parseInt(interval_time_str); targetPlayer.setVolume(seekBar.getProgress());
}
});
BackgroundService.RunCommand(requireContext(), service -> service.addConnectionListener(connectionReceiver)); positionSeekUpdateRunnable = () -> {
connectToPlugin(targetPlayerName); if (!isAdded()) return; // Fragment was already detached
if (targetPlayer != null) {
mprisControlBinding.positionSeek.setProgress((int) (targetPlayer.getPosition()));
}
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000);
};
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
performActionOnClick(mprisControlBinding.loopButton, p -> { mprisControlBinding.positionSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
switch (p.getLoopStatus()) { @Override
case "None": public void onProgressChanged(SeekBar seekBar, int progress, boolean byUser) {
p.setLoopStatus("Track"); mprisControlBinding.progressTextview.setText(milisToProgress(progress));
break; }
case "Track":
p.setLoopStatus("Playlist"); @Override
break; public void onStartTrackingTouch(SeekBar seekBar) {
case "Playlist": positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
p.setLoopStatus("None"); }
break;
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
if (targetPlayer != null) {
targetPlayer.setPosition(seekBar.getProgress());
} }
}); positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
}
});
performActionOnClick(mprisControlBinding.playButton, MprisPlugin.MprisPlayer::playPause); mprisControlBinding.nowPlayingTextview.setSelected(true);
performActionOnClick(mprisControlBinding.shuffleButton, p -> p.setShuffle(!p.getShuffle()));
performActionOnClick(mprisControlBinding.prevButton, MprisPlugin.MprisPlayer::previous);
performActionOnClick(mprisControlBinding.rewButton, p -> targetPlayer.seek(interval_time * -1));
performActionOnClick(mprisControlBinding.ffButton, p -> p.seek(interval_time));
performActionOnClick(mprisControlBinding.nextButton, MprisPlugin.MprisPlayer::next);
performActionOnClick(mprisControlBinding.stopButton, MprisPlugin.MprisPlayer::stop);
mprisControlBinding.volumeSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
BackgroundService.RunCommand(requireContext(), service -> {
if (targetPlayer == null) return;
targetPlayer.setVolume(seekBar.getProgress());
});
}
});
positionSeekUpdateRunnable = () -> {
Context context = getContext();
if (context == null) return; // Fragment was already detached
BackgroundService.RunCommand(context, service -> {
if (targetPlayer != null) {
mprisControlBinding.positionSeek.setProgress((int) (targetPlayer.getPosition()));
}
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000);
});
};
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
mprisControlBinding.positionSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean byUser) {
mprisControlBinding.progressTextview.setText(milisToProgress(progress));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable);
}
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
BackgroundService.RunCommand(requireContext(), service -> {
if (targetPlayer != null) {
targetPlayer.setPosition(seekBar.getProgress());
}
positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200);
});
}
});
mprisControlBinding.nowPlayingTextview.setSelected(true);
}
return activityMprisBinding.getRoot(); return activityMprisBinding.getRoot();
} }
private void connectToPlugin(final String targetPlayerName) {
BackgroundService.RunWithPlugin(requireContext(), deviceId, MprisPlugin.class, mpris -> {
targetPlayer = mpris.getPlayerStatus(targetPlayerName);
mpris.setPlayerStatusUpdatedHandler("activity", () -> requireActivity().runOnUiThread(() -> updatePlayerStatus(mpris)));
mpris.setPlayerListUpdatedHandler("activity", () -> {
final List<String> playerList = mpris.getPlayerList();
final ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item,
playerList.toArray(ArrayUtils.EMPTY_STRING_ARRAY)
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
requireActivity().runOnUiThread(() -> {
mprisControlBinding.playerSpinner.setAdapter(adapter);
if (playerList.isEmpty()) {
mprisControlBinding.noPlayers.setVisibility(View.VISIBLE);
mprisControlBinding.playerSpinner.setVisibility(View.GONE);
mprisControlBinding.nowPlayingTextview.setText("");
} else {
mprisControlBinding.noPlayers.setVisibility(View.GONE);
mprisControlBinding.playerSpinner.setVisibility(View.VISIBLE);
}
mprisControlBinding.playerSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> arg0, View arg1, int pos, long id) {
if (pos >= playerList.size()) return;
String player = playerList.get(pos);
if (targetPlayer != null && player.equals(targetPlayer.getPlayer())) {
return; //Player hasn't actually changed
}
targetPlayer = mpris.getPlayerStatus(player);
updatePlayerStatus(mpris);
if (targetPlayer != null && targetPlayer.isPlaying()) {
MprisMediaSession.getInstance().playerSelected(targetPlayer);
}
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
targetPlayer = null;
}
});
if (targetPlayer == null) {
//If no player is selected, try to select a playing player
targetPlayer = mpris.getPlayingPlayer();
}
//Try to select the specified player
if (targetPlayer != null) {
int targetIndex = adapter.getPosition(targetPlayer.getPlayer());
if (targetIndex >= 0) {
mprisControlBinding.playerSpinner.setSelection(targetIndex);
} else {
targetPlayer = null;
}
}
//If no player selected, select the first one (if any)
if (targetPlayer == null && !playerList.isEmpty()) {
targetPlayer = mpris.getPlayerStatus(playerList.get(0));
mprisControlBinding.playerSpinner.setSelection(0);
}
updatePlayerStatus(mpris);
});
});
});
}
@Override @Override
public void onDestroy() { public void onDestroyView() {
super.onDestroy(); disconnectFromPlugin();
BackgroundService.RunCommand(requireContext(), service -> service.removeConnectionListener(connectionReceiver)); super.onDestroyView();
} }
private void performActionOnClick(View v, MprisPlayerCallback l) { private void disconnectFromPlugin() {
v.setOnClickListener(view -> BackgroundService.RunCommand(requireContext(), service -> { MprisPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MprisPlugin.class);
if (targetPlayer == null) return; if (plugin != null) {
l.performAction(targetPlayer); plugin.removePlayerListUpdatedHandler("activity");
plugin.removePlayerStatusUpdatedHandler("activity");
}
}
private void connectToPlugin() {
MprisPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MprisPlugin.class);
if (plugin == null) {
if (isAdded()) {
requireActivity().finish();
}
return;
}
targetPlayer = plugin.getPlayerStatus(targetPlayerName);
plugin.setPlayerStatusUpdatedHandler("activity", () -> requireActivity().runOnUiThread(() -> {
updatePlayerStatus(plugin);
}));
plugin.setPlayerListUpdatedHandler("activity", () -> requireActivity().runOnUiThread(() -> {
final List<String> playerList = plugin.getPlayerList();
final ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_item,
playerList.toArray(ArrayUtils.EMPTY_STRING_ARRAY)
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mprisControlBinding.playerSpinner.setAdapter(adapter);
if (playerList.isEmpty()) {
mprisControlBinding.noPlayers.setVisibility(View.VISIBLE);
mprisControlBinding.playerSpinner.setVisibility(View.GONE);
mprisControlBinding.nowPlayingTextview.setText("");
} else {
mprisControlBinding.noPlayers.setVisibility(View.GONE);
mprisControlBinding.playerSpinner.setVisibility(View.VISIBLE);
}
mprisControlBinding.playerSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> arg0, View arg1, int pos, long id) {
if (pos >= playerList.size()) return;
String player = playerList.get(pos);
if (targetPlayer != null && player.equals(targetPlayer.getPlayerName())) {
return; //Player hasn't actually changed
}
targetPlayer = plugin.getPlayerStatus(player);
targetPlayerName = targetPlayer.getPlayerName();
updatePlayerStatus(plugin);
if (targetPlayer != null && targetPlayer.isPlaying()) {
MprisMediaSession.getInstance().playerSelected(targetPlayer);
}
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
targetPlayer = null;
}
});
if (targetPlayer == null) {
//If no player is selected, try to select a playing player
targetPlayer = plugin.getPlayingPlayer();
}
//Try to select the specified player
if (targetPlayer != null) {
int targetIndex = adapter.getPosition(targetPlayer.getPlayerName());
if (targetIndex >= 0) {
mprisControlBinding.playerSpinner.setSelection(targetIndex);
} else {
targetPlayer = null;
}
}
//If no player selected, select the first one (if any)
if (targetPlayer == null && !playerList.isEmpty()) {
targetPlayer = plugin.getPlayerStatus(playerList.get(0));
mprisControlBinding.playerSpinner.setSelection(0);
}
updatePlayerStatus(plugin);
})); }));
} }
private void updatePlayerStatus(MprisPlugin mpris) { private void performActionOnClick(View v, MprisPlayerCallback l) {
v.setOnClickListener(view -> {
if (targetPlayer == null) return;
l.performAction(targetPlayer);
});
}
private void updatePlayerStatus(MprisPlugin plugin) {
if (!isAdded()) {
//Fragment is not attached to an activity. We will crash if we try to do anything here.
return;
}
MprisPlugin.MprisPlayer playerStatus = targetPlayer; MprisPlugin.MprisPlayer playerStatus = targetPlayer;
if (playerStatus == null) { if (playerStatus == null) {
//No player with that name found, just display "empty" data //No player with that name found, just display "empty" data
playerStatus = mpris.getEmptyPlayer(); playerStatus = plugin.getEmptyPlayer();
} }
String song = playerStatus.getCurrentSong(); String song = playerStatus.getCurrentSong();
@ -433,7 +425,7 @@ public class MprisNowPlayingFragment extends Fragment implements VolumeKeyListen
@Override @Override
public void onSaveInstanceState(@NonNull Bundle outState) { public void onSaveInstanceState(@NonNull Bundle outState) {
if (targetPlayer != null) { if (targetPlayer != null) {
outState.putString("targetPlayer", targetPlayer.getPlayer()); outState.putString("targetPlayer", targetPlayerName);
} }
} }

View File

@ -11,8 +11,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.util.Log; import android.util.Log;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
@ -77,12 +75,12 @@ public class MprisPlugin extends Plugin {
return album; return album;
} }
public String getPlayer() { public String getPlayerName() {
return player; return player;
} }
boolean isSpotify() { boolean isSpotify() {
return getPlayer().equalsIgnoreCase("spotify"); return getPlayerName().equalsIgnoreCase("spotify");
} }
public String getLoopStatus() { public String getLoopStatus() {
@ -165,55 +163,55 @@ public class MprisPlugin extends Plugin {
public void playPause() { public void playPause() {
if (isPauseAllowed() || isPlayAllowed()) { if (isPauseAllowed() || isPlayAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "action", "PlayPause"); sendCommand(getPlayerName(), "action", "PlayPause");
} }
} }
public void play() { public void play() {
if (isPlayAllowed()) { if (isPlayAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "action", "Play"); sendCommand(getPlayerName(), "action", "Play");
} }
} }
public void pause() { public void pause() {
if (isPauseAllowed()) { if (isPauseAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "action", "Pause"); sendCommand(getPlayerName(), "action", "Pause");
} }
} }
public void stop() { public void stop() {
MprisPlugin.this.sendCommand(getPlayer(), "action", "Stop"); sendCommand(getPlayerName(), "action", "Stop");
} }
public void previous() { public void previous() {
if (isGoPreviousAllowed()) { if (isGoPreviousAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "action", "Previous"); sendCommand(getPlayerName(), "action", "Previous");
} }
} }
public void next() { public void next() {
if (isGoNextAllowed()) { if (isGoNextAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "action", "Next"); sendCommand(getPlayerName(), "action", "Next");
} }
} }
public void setLoopStatus(String loopStatus) { public void setLoopStatus(String loopStatus) {
MprisPlugin.this.sendCommand(getPlayer(), "setLoopStatus", loopStatus); sendCommand(getPlayerName(), "setLoopStatus", loopStatus);
} }
public void setShuffle(boolean shuffle) { public void setShuffle(boolean shuffle) {
MprisPlugin.this.sendCommand(getPlayer(), "setShuffle", shuffle); sendCommand(getPlayerName(), "setShuffle", shuffle);
} }
public void setVolume(int volume) { public void setVolume(int volume) {
if (isSetVolumeAllowed()) { if (isSetVolumeAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "setVolume", volume); sendCommand(getPlayerName(), "setVolume", volume);
} }
} }
public void setPosition(int position) { public void setPosition(int position) {
if (isSeekAllowed()) { if (isSeekAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "SetPosition", position); sendCommand(getPlayerName(), "SetPosition", position);
lastPosition = position; lastPosition = position;
lastPositionTime = System.currentTimeMillis(); lastPositionTime = System.currentTimeMillis();
@ -222,7 +220,7 @@ public class MprisPlugin extends Plugin {
public void seek(int offset) { public void seek(int offset) {
if (isSeekAllowed()) { if (isSeekAllowed()) {
MprisPlugin.this.sendCommand(getPlayer(), "Seek", offset); sendCommand(getPlayerName(), "Seek", offset);
} }
} }
} }
@ -522,7 +520,7 @@ public class MprisPlugin extends Plugin {
if (player.albumArtUrl.equals(url)) { if (player.albumArtUrl.equals(url)) {
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST); NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS_REQUEST);
np.set("player", player.getPlayer()); np.set("player", player.getPlayerName());
np.set("albumArtUrl", url); np.set("albumArtUrl", url);
device.sendPacket(np); device.sendPacket(np);
return true; return true;

View File

@ -9,7 +9,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.KdeConnect;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -58,13 +58,17 @@ public class PhotoActivity extends AppCompatActivity {
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
BackgroundService.RunWithPlugin(this, getIntent().getStringExtra("deviceId"), PhotoPlugin.class, plugin -> { String deviceId = getIntent().getStringExtra("deviceId");
if (resultCode == -1) { PhotoPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, PhotoPlugin.class);
plugin.sendPhoto(photoURI); if (plugin == null) {
} else { finish();
plugin.sendCancel(); return;
} }
}); if (resultCode == -1) {
plugin.sendPhoto(photoURI);
} else {
plugin.sendCancel();
}
finish(); finish();
} }
} }

View File

@ -24,8 +24,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.media.VolumeProviderCompat; import androidx.media.VolumeProviderCompat;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.ActivityPresenterBinding; import org.kde.kdeconnect_tp.databinding.ActivityPresenterBinding;
@ -90,16 +89,14 @@ public class PresenterActivity extends AppCompatActivity implements SensorEventL
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true);
final String deviceId = getIntent().getStringExtra("deviceId"); String deviceId = getIntent().getStringExtra("deviceId");
this.plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, PresenterPlugin.class);
BackgroundService.RunWithPlugin(this, deviceId, PresenterPlugin.class, plugin -> runOnUiThread(() -> { binding.nextButton.setOnClickListener(v -> plugin.sendNext());
this.plugin = plugin; binding.previousButton.setOnClickListener(v -> plugin.sendPrevious());
binding.nextButton.setOnClickListener(v -> plugin.sendNext()); if (plugin.isPointerSupported()) {
binding.previousButton.setOnClickListener(v -> plugin.sendPrevious()); enablePointer();
if (plugin.isPointerSupported()) { }
enablePointer();
}
}));
} }
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {

View File

@ -23,9 +23,8 @@ import androidx.core.content.ContextCompat;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.ActivityRunCommandBinding; import org.kde.kdeconnect_tp.databinding.ActivityRunCommandBinding;
@ -38,38 +37,42 @@ import java.util.Objects;
public class RunCommandActivity extends AppCompatActivity { public class RunCommandActivity extends AppCompatActivity {
private ActivityRunCommandBinding binding; private ActivityRunCommandBinding binding;
private String deviceId; private String deviceId;
private final RunCommandPlugin.CommandsChangedCallback commandsChangedCallback = this::updateView; private final RunCommandPlugin.CommandsChangedCallback commandsChangedCallback = () -> runOnUiThread(this::updateView);
private List<CommandEntry> commandItems; private List<CommandEntry> commandItems;
private void updateView() { private void updateView() {
BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> runOnUiThread(() -> { RunCommandPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, RunCommandPlugin.class);
registerForContextMenu(binding.runCommandsList); if (plugin == null) {
finish();
return;
}
commandItems = new ArrayList<>(); registerForContextMenu(binding.runCommandsList);
for (JSONObject obj : plugin.getCommandList()) {
try { commandItems = new ArrayList<>();
commandItems.add(new CommandEntry(obj.getString("name"), for (JSONObject obj : plugin.getCommandList()) {
obj.getString("command"), obj.getString("key"))); try {
} catch (JSONException e) { commandItems.add(new CommandEntry(obj.getString("name"),
Log.e("RunCommand", "Error parsing JSON", e); obj.getString("command"), obj.getString("key")));
} } catch (JSONException e) {
Log.e("RunCommand", "Error parsing JSON", e);
} }
}
Collections.sort(commandItems, Comparator.comparing(CommandEntry::getName)); Collections.sort(commandItems, Comparator.comparing(CommandEntry::getName));
ListAdapter adapter = new ListAdapter(RunCommandActivity.this, commandItems); ListAdapter adapter = new ListAdapter(RunCommandActivity.this, commandItems);
binding.runCommandsList.setAdapter(adapter); binding.runCommandsList.setAdapter(adapter);
binding.runCommandsList.setOnItemClickListener((adapterView, view1, i, l) -> binding.runCommandsList.setOnItemClickListener((adapterView, view1, i, l) ->
plugin.runCommand(commandItems.get(i).getKey())); plugin.runCommand(commandItems.get(i).getKey()));
String text = getString(R.string.addcommand_explanation); String text = getString(R.string.addcommand_explanation);
if (!plugin.canAddCommand()) { if (!plugin.canAddCommand()) {
text += "\n" + getString(R.string.addcommand_explanation2); text += "\n" + getString(R.string.addcommand_explanation2);
} }
binding.addComandExplanation.setText(text); binding.addComandExplanation.setText(text);
binding.addComandExplanation.setVisibility(commandItems.isEmpty() ? View.VISIBLE : View.GONE); binding.addComandExplanation.setVisibility(commandItems.isEmpty() ? View.VISIBLE : View.GONE);
}));
} }
@Override @Override
@ -84,27 +87,22 @@ public class RunCommandActivity extends AppCompatActivity {
getSupportActionBar().setDisplayShowHomeEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true);
deviceId = getIntent().getStringExtra("deviceId"); deviceId = getIntent().getStringExtra("deviceId");
RunCommandPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId,RunCommandPlugin.class);
boolean canAddCommands = false; if (plugin != null) {
try { if (plugin.canAddCommand()) {
canAddCommands = BackgroundService.getInstance().getDevice(deviceId).getPlugin(RunCommandPlugin.class).canAddCommand(); binding.addCommandButton.show();
} catch (Exception ignore) { } else {
binding.addCommandButton.hide();
}
binding.addCommandButton.setOnClickListener(v -> {
plugin.sendSetupPacket();
new AlertDialog.Builder(RunCommandActivity.this)
.setTitle(R.string.add_command)
.setMessage(R.string.add_command_description)
.setPositiveButton(R.string.ok, null)
.show();
});
} }
if (canAddCommands) {
binding.addCommandButton.show();
} else {
binding.addCommandButton.hide();
}
binding.addCommandButton.setOnClickListener(v -> BackgroundService.RunWithPlugin(RunCommandActivity.this, deviceId, RunCommandPlugin.class, plugin -> {
plugin.sendSetupPacket();
new AlertDialog.Builder(RunCommandActivity.this)
.setTitle(R.string.add_command)
.setMessage(R.string.add_command_description)
.setPositiveButton(R.string.ok, null)
.show();
}));
updateView(); updateView();
} }
@ -134,14 +132,23 @@ public class RunCommandActivity extends AppCompatActivity {
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> plugin.addCommandsUpdatedCallback(commandsChangedCallback)); RunCommandPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, RunCommandPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.addCommandsUpdatedCallback(commandsChangedCallback);
} }
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
BackgroundService.RunWithPlugin(this, deviceId, RunCommandPlugin.class, plugin -> plugin.removeCommandsUpdatedCallback(commandsChangedCallback)); RunCommandPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, RunCommandPlugin.class);
if (plugin == null) {
return;
}
plugin.removeCommandsUpdatedCallback(commandsChangedCallback);
} }
@Override @Override

View File

@ -23,8 +23,8 @@ import io.reactivex.Flowable
import io.reactivex.processors.ReplayProcessor import io.reactivex.processors.ReplayProcessor
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.kde.kdeconnect.BackgroundService
import org.kde.kdeconnect.Device import org.kde.kdeconnect.Device
import org.kde.kdeconnect.KdeConnect
import org.kde.kdeconnect.UserInterface.MainActivity import org.kde.kdeconnect.UserInterface.MainActivity
import org.kde.kdeconnect_tp.R import org.kde.kdeconnect_tp.R
import org.reactivestreams.FlowAdapters import org.reactivestreams.FlowAdapters
@ -92,12 +92,10 @@ class RunCommandControlsProviderService : ControlsProviderService() {
if (action is CommandAction) { if (action is CommandAction) {
val commandEntry = getCommandByControlId(controlId) val commandEntry = getCommandByControlId(controlId)
if (commandEntry != null) { if (commandEntry != null) {
val plugin = BackgroundService.getInstance().getDevice(controlId.split(":")[0]).getPlugin(RunCommandPlugin::class.java) val deviceId = controlId.split(":")[0]
val plugin = KdeConnect.getInstance().getDevicePlugin(deviceId ,RunCommandPlugin::class.java)
if (plugin != null) { if (plugin != null) {
BackgroundService.RunCommand(this) { plugin.runCommand(commandEntry.key)
plugin.runCommand(commandEntry.key)
}
consumer.accept(ControlAction.RESPONSE_OK) consumer.accept(ControlAction.RESPONSE_OK)
} else { } else {
consumer.accept(ControlAction.RESPONSE_FAIL) consumer.accept(ControlAction.RESPONSE_FAIL)
@ -141,9 +139,7 @@ class RunCommandControlsProviderService : ControlsProviderService() {
private fun getAllCommandsList(): List<CommandEntryWithDevice> { private fun getAllCommandsList(): List<CommandEntryWithDevice> {
val commandList = mutableListOf<CommandEntryWithDevice>() val commandList = mutableListOf<CommandEntryWithDevice>()
val service = BackgroundService.getInstance() ?: return commandList for (device in KdeConnect.getInstance().devices.values) {
for (device in service.devices.values) {
if (!device.isReachable) { if (!device.isReachable) {
commandList.addAll(getSavedCommandsList(device)) commandList.addAll(getSavedCommandsList(device))
continue continue
@ -169,9 +165,7 @@ class RunCommandControlsProviderService : ControlsProviderService() {
private fun getCommandByControlId(controlId: String): CommandEntryWithDevice? { private fun getCommandByControlId(controlId: String): CommandEntryWithDevice? {
val controlIdParts = controlId.split(":") val controlIdParts = controlId.split(":")
val service = BackgroundService.getInstance() ?: return null val device = KdeConnect.getInstance().getDevice(controlIdParts[0])
val device = service.getDevice(controlIdParts[0])
if (device == null || !device.isPaired) return null if (device == null || !device.isPaired) return null

View File

@ -10,9 +10,8 @@ import android.widget.TextView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
public class RunCommandUrlActivity extends AppCompatActivity { public class RunCommandUrlActivity extends AppCompatActivity {
@ -26,40 +25,38 @@ public class RunCommandUrlActivity extends AppCompatActivity {
Uri uri = getIntent().getData(); Uri uri = getIntent().getData();
String deviceId = uri.getPathSegments().get(0); String deviceId = uri.getPathSegments().get(0);
BackgroundService.RunCommand(this, service -> { final Device device = KdeConnect.getInstance().getDevice(deviceId);
final Device device = service.getDevice(deviceId);
if(device == null) { if(device == null) {
error(R.string.runcommand_nosuchdevice); error(R.string.runcommand_nosuchdevice);
return; return;
}
if (!device.isPaired()) {
error(R.string.runcommand_notpaired);
return;
}
if (!device.isReachable()) {
error(R.string.runcommand_notreachable);
return;
}
final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
if (plugin == null) {
error(R.string.runcommand_noruncommandplugin);
return;
}
plugin.runCommand(uri.getPathSegments().get(1));
RunCommandUrlActivity.this.finish();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
Vibrator vibrator = getSystemService(Vibrator.class);
if(vibrator != null && vibrator.hasVibrator()) {
vibrator.vibrate(100);
} }
}
if (!device.isPaired()) {
error(R.string.runcommand_notpaired);
return;
}
if (!device.isReachable()) {
error(R.string.runcommand_notreachable);
return;
}
final RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class);
if (plugin == null) {
error(R.string.runcommand_noruncommandplugin);
return;
}
plugin.runCommand(uri.getPathSegments().get(1));
RunCommandUrlActivity.this.finish();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
Vibrator vibrator = RunCommandUrlActivity.this.getSystemService(Vibrator.class);
if(vibrator != null && vibrator.hasVibrator()) {
vibrator.vibrate(100);
}
}
});
} catch (Exception e) { } catch (Exception e) {
Log.e("RuncommandPlugin", "Exception", e); Log.e("RuncommandPlugin", "Exception", e);
} }

View File

@ -12,10 +12,12 @@ import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.RemoteViews; import android.widget.RemoteViews;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import java.util.concurrent.ConcurrentHashMap;
public class RunCommandWidget extends AppWidgetProvider { public class RunCommandWidget extends AppWidgetProvider {
public static final String RUN_COMMAND_ACTION = "RUN_COMMAND_ACTION"; public static final String RUN_COMMAND_ACTION = "RUN_COMMAND_ACTION";
@ -35,18 +37,16 @@ public class RunCommandWidget extends AppWidgetProvider {
final String targetCommand = intent.getStringExtra(TARGET_COMMAND); final String targetCommand = intent.getStringExtra(TARGET_COMMAND);
final String targetDevice = intent.getStringExtra(TARGET_DEVICE); final String targetDevice = intent.getStringExtra(TARGET_DEVICE);
BackgroundService.RunCommand(context, service -> { RunCommandPlugin plugin = KdeConnect.getInstance().getDevicePlugin(targetDevice, RunCommandPlugin.class);
RunCommandPlugin plugin = service.getDevice(targetDevice).getPlugin(RunCommandPlugin.class);
if (plugin != null) { if (plugin != null) {
try { try {
plugin.runCommand(targetCommand); plugin.runCommand(targetCommand);
} catch (Exception ex) { } catch (Exception ex) {
Log.e("RunCommandWidget", "Error running command", ex); Log.e("RunCommandWidget", "Error running command", ex);
}
} }
}); }
} else if (intent != null && TextUtils.equals(intent.getAction(), SET_CURRENT_DEVICE)) { } else if (intent != null && TextUtils.equals(intent.getAction(), SET_CURRENT_DEVICE)) {
setCurrentDevice(context); setCurrentDevice(context);
} }
@ -70,14 +70,13 @@ public class RunCommandWidget extends AppWidgetProvider {
private void updateWidget(final Context context) { private void updateWidget(final Context context) {
if (getCurrentDevice() == null || !getCurrentDevice().isReachable()) { Device device = getCurrentDevice();
BackgroundService.RunCommand(context, service -> { if (device == null || !device.isReachable()) {
if (service.getDevices().size() > 0) ConcurrentHashMap<String, Device> devices = KdeConnect.getInstance().getDevices();
currentDeviceId = service.getDevices().elements().nextElement().getDeviceId(); if (devices.size() > 0) {
currentDeviceId = devices.elements().nextElement().getDeviceId();
updateWidgetImpl(context); }
});
} }
updateWidgetImpl(context); updateWidgetImpl(context);
@ -99,12 +98,13 @@ public class RunCommandWidget extends AppWidgetProvider {
pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
views.setOnClickPendingIntent(R.id.runcommandWidgetTitleHeader, pendingIntent); views.setOnClickPendingIntent(R.id.runcommandWidgetTitleHeader, pendingIntent);
if (getCurrentDevice() == null || !getCurrentDevice().isReachable()) { Device device = getCurrentDevice();
if (device == null || !device.isReachable()) {
views.setTextViewText(R.id.runcommandWidgetTitle, context.getString(R.string.kde_connect)); views.setTextViewText(R.id.runcommandWidgetTitle, context.getString(R.string.kde_connect));
views.setViewVisibility(R.id.run_commands_list, View.GONE); views.setViewVisibility(R.id.run_commands_list, View.GONE);
views.setViewVisibility(R.id.not_reachable_message, View.VISIBLE); views.setViewVisibility(R.id.not_reachable_message, View.VISIBLE);
} else { } else {
views.setTextViewText(R.id.runcommandWidgetTitle, getCurrentDevice().getName()); views.setTextViewText(R.id.runcommandWidgetTitle, device.getName());
views.setViewVisibility(R.id.run_commands_list, View.VISIBLE); views.setViewVisibility(R.id.run_commands_list, View.VISIBLE);
views.setViewVisibility(R.id.not_reachable_message, View.GONE); views.setViewVisibility(R.id.not_reachable_message, View.GONE);
} }
@ -129,21 +129,15 @@ public class RunCommandWidget extends AppWidgetProvider {
Log.e("RunCommandWidget", "Error updating widget", ex); Log.e("RunCommandWidget", "Error updating widget", ex);
} }
if (BackgroundService.getInstance() != null) {
BackgroundService.getInstance().addDeviceListChangedCallback("RunCommandWidget", unused -> { KdeConnect.getInstance().addDeviceListChangedCallback("RunCommandWidget", () -> {
Intent updateWidget = new Intent(context, RunCommandWidget.class); Intent updateWidget = new Intent(context, RunCommandWidget.class);
context.sendBroadcast(updateWidget); context.sendBroadcast(updateWidget);
}); });
}
} }
public static Device getCurrentDevice() { public static Device getCurrentDevice() {
return KdeConnect.getInstance().getDevice(currentDeviceId);
try {
return BackgroundService.getInstance().getDevice(currentDeviceId);
} catch (Exception ex) {
return null;
}
} }
public static void setCurrentDevice(final String DeviceId) { public static void setCurrentDevice(final String DeviceId) {

View File

@ -6,10 +6,9 @@ import android.view.Window;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.databinding.WidgetRemoteCommandPluginDialogBinding; import org.kde.kdeconnect_tp.databinding.WidgetRemoteCommandPluginDialogBinding;
import java.util.Comparator; import java.util.Comparator;
@ -27,25 +26,23 @@ public class RunCommandWidgetDeviceSelector extends AppCompatActivity {
WidgetRemoteCommandPluginDialogBinding.inflate(getLayoutInflater()); WidgetRemoteCommandPluginDialogBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot()); setContentView(binding.getRoot());
BackgroundService.RunCommand(this, service -> runOnUiThread(() -> { final List<CommandEntry> deviceItems = KdeConnect.getInstance().getDevices().values().stream()
final List<CommandEntry> deviceItems = service.getDevices().values().stream() .filter(Device::isPaired).filter(Device::isReachable)
.filter(Device::isPaired).filter(Device::isReachable) .map(device -> new CommandEntry(device.getName(), null, device.getDeviceId()))
.map(device -> new CommandEntry(device.getName(), null, device.getDeviceId())) .sorted(Comparator.comparing(CommandEntry::getName))
.sorted(Comparator.comparing(CommandEntry::getName)) .collect(Collectors.toList());
.collect(Collectors.toList());
ListAdapter adapter = new ListAdapter(RunCommandWidgetDeviceSelector.this, deviceItems); ListAdapter adapter = new ListAdapter(RunCommandWidgetDeviceSelector.this, deviceItems);
binding.runCommandsDeviceList.setAdapter(adapter); binding.runCommandsDeviceList.setAdapter(adapter);
binding.runCommandsDeviceList.setOnItemClickListener((adapterView, viewContent, i, l) -> { binding.runCommandsDeviceList.setOnItemClickListener((adapterView, viewContent, i, l) -> {
CommandEntry entry = deviceItems.get(i); CommandEntry entry = deviceItems.get(i);
RunCommandWidget.setCurrentDevice(entry.getKey()); RunCommandWidget.setCurrentDevice(entry.getKey());
Intent updateWidget = new Intent(RunCommandWidgetDeviceSelector.this, RunCommandWidget.class); Intent updateWidget = new Intent(RunCommandWidgetDeviceSelector.this, RunCommandWidget.class);
RunCommandWidgetDeviceSelector.this.sendBroadcast(updateWidget); RunCommandWidgetDeviceSelector.this.sendBroadcast(updateWidget);
finish(); finish();
}); });
}));
} }
} }

View File

@ -30,8 +30,8 @@ import androidx.recyclerview.widget.RecyclerView;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; import org.kde.kdeconnect.UserInterface.PluginSettingsActivity;
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
@ -326,21 +326,11 @@ public class SftpSettingsFragment
addStoragePreferences(preferenceCategory); addStoragePreferences(preferenceCategory);
Device device = getDeviceOrThrow(); Device device = KdeConnect.getInstance().getDevice(getDeviceId());
device.reloadPluginsFromSettings(); device.reloadPluginsFromSettings();
} }
private Device getDeviceOrThrow() {
Device device = BackgroundService.getInstance().getDevice(getDeviceId());
if (device == null) {
throw new RuntimeException("SftpSettingsFragment.getDeviceOrThrow(): No device with id: " + getDeviceId());
}
return device;
}
@Override @Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) { public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
SftpPlugin.StorageInfo newStorageInfo = (SftpPlugin.StorageInfo) newValue; SftpPlugin.StorageInfo newStorageInfo = (SftpPlugin.StorageInfo) newValue;

View File

@ -16,8 +16,7 @@ import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import java.util.ArrayList; import java.util.ArrayList;
@ -69,7 +68,12 @@ public class SendFileActivity extends AppCompatActivity {
if (uris.isEmpty()) { if (uris.isEmpty()) {
Log.w("SendFileActivity", "No files to send?"); Log.w("SendFileActivity", "No files to send?");
} else { } else {
BackgroundService.RunWithPlugin(this, mDeviceId, SharePlugin.class, plugin -> plugin.sendUriList(uris)); SharePlugin plugin = KdeConnect.getInstance().getDevicePlugin(mDeviceId, SharePlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendUriList(uris);
} }
} }
finish(); finish();

View File

@ -17,10 +17,10 @@ import androidx.appcompat.app.AppCompatActivity;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.UserInterface.List.EntryItemWithIcon; import org.kde.kdeconnect.UserInterface.List.EntryItemWithIcon;
import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.List.SectionItem; import org.kde.kdeconnect.UserInterface.List.SectionItem;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.ActivityShareBinding; import org.kde.kdeconnect_tp.databinding.ActivityShareBinding;
@ -42,19 +42,17 @@ public class ShareActivity extends AppCompatActivity {
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == R.id.menu_refresh) { if (item.getItemId() == R.id.menu_refresh) {
updateDeviceListAction(); refreshDevicesAction();
return true; return true;
} else { } else {
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
} }
private void updateDeviceListAction() { private void refreshDevicesAction() {
updateDeviceList(); BackgroundService.ForceRefreshConnections(this);
BackgroundService.RunCommand(ShareActivity.this, BackgroundService::onNetworkChange);
binding.devicesListLayout.refreshListLayout.setRefreshing(true); binding.devicesListLayout.refreshListLayout.setRefreshing(true);
binding.devicesListLayout.refreshListLayout.postDelayed(() -> { binding.devicesListLayout.refreshListLayout.postDelayed(() -> {
binding.devicesListLayout.refreshListLayout.setRefreshing(false); binding.devicesListLayout.refreshListLayout.setRefreshing(false);
}, 1500); }, 1500);
@ -69,31 +67,29 @@ public class ShareActivity extends AppCompatActivity {
return; return;
} }
BackgroundService.RunCommand(this, service -> { Collection<Device> devices = KdeConnect.getInstance().getDevices().values();
final ArrayList<Device> devicesList = new ArrayList<>();
final ArrayList<ListAdapter.Item> items = new ArrayList<>();
Collection<Device> devices = service.getDevices().values(); SectionItem section = new SectionItem(getString(R.string.share_to));
final ArrayList<Device> devicesList = new ArrayList<>(); items.add(section);
final ArrayList<ListAdapter.Item> items = new ArrayList<>();
SectionItem section = new SectionItem(getString(R.string.share_to)); for (Device d : devices) {
items.add(section); if (d.isReachable() && d.isPaired()) {
devicesList.add(d);
for (Device d : devices) { items.add(new EntryItemWithIcon(d.getName(), d.getIcon()));
if (d.isReachable() && d.isPaired()) { section.isEmpty = false;
devicesList.add(d);
items.add(new EntryItemWithIcon(d.getName(), d.getIcon()));
section.isEmpty = false;
}
} }
}
runOnUiThread(() -> { binding.devicesListLayout.devicesList.setAdapter(new ListAdapter(ShareActivity.this, items));
binding.devicesListLayout.devicesList.setAdapter(new ListAdapter(ShareActivity.this, items)); binding.devicesListLayout.devicesList.setOnItemClickListener((adapterView, view, i, l) -> {
binding.devicesListLayout.devicesList.setOnItemClickListener((adapterView, view, i, l) -> { Device device = devicesList.get(i - 1); //NOTE: -1 because of the title!
Device device = devicesList.get(i - 1); //NOTE: -1 because of the title! SharePlugin plugin = KdeConnect.getInstance().getDevicePlugin(device.getDeviceId(), SharePlugin.class);
BackgroundService.RunWithPlugin(this, device.getDeviceId(), SharePlugin.class, plugin -> plugin.share(intent)); if (plugin != null) {
finish(); plugin.share(intent);
}); }
}); finish();
}); });
} }
@ -109,7 +105,7 @@ public class ShareActivity extends AppCompatActivity {
getSupportActionBar().setDisplayShowHomeEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true);
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
binding.devicesListLayout.refreshListLayout.setOnRefreshListener(this::updateDeviceListAction); binding.devicesListLayout.refreshListLayout.setOnRefreshListener(this::refreshDevicesAction);
if (actionBar != null) { if (actionBar != null) {
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM); actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM);
} }
@ -123,22 +119,21 @@ public class ShareActivity extends AppCompatActivity {
final String deviceId = intent.getStringExtra("deviceId"); final String deviceId = intent.getStringExtra("deviceId");
if (deviceId != null) { if (deviceId != null) {
BackgroundService.RunWithPlugin(this, deviceId, SharePlugin.class, plugin -> { SharePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, SharePlugin.class);
if (plugin != null) {
plugin.share(intent); plugin.share(intent);
finish(); }
}); finish();
} else { } else {
BackgroundService.RunCommand(this, service -> { KdeConnect.getInstance().addDeviceListChangedCallback("ShareActivity", () -> runOnUiThread(this::updateDeviceList));
service.onNetworkChange(); BackgroundService.ForceRefreshConnections(this); // force a network re-discover
service.addDeviceListChangedCallback("ShareActivity", unused -> updateDeviceList());
});
updateDeviceList(); updateDeviceList();
} }
} }
@Override @Override
protected void onStop() { protected void onStop() {
BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("ShareActivity")); KdeConnect.getInstance().removeDeviceListChangedCallback("ShareActivity");
super.onStop(); super.onStop();
} }
} }

View File

@ -11,7 +11,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.Log; import android.util.Log;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.KdeConnect;
public class ShareBroadcastReceiver extends BroadcastReceiver { public class ShareBroadcastReceiver extends BroadcastReceiver {
@Override @Override
@ -35,6 +35,10 @@ public class ShareBroadcastReceiver extends BroadcastReceiver {
long jobId = intent.getLongExtra(SharePlugin.CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA, -1); long jobId = intent.getLongExtra(SharePlugin.CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA, -1);
String deviceId = intent.getStringExtra(SharePlugin.CANCEL_SHARE_DEVICE_ID_EXTRA); String deviceId = intent.getStringExtra(SharePlugin.CANCEL_SHARE_DEVICE_ID_EXTRA);
BackgroundService.RunWithPlugin(context, deviceId, SharePlugin.class, plugin -> plugin.cancelJob(jobId)); SharePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, SharePlugin.class);
if (plugin == null) {
return;
}
plugin.cancelJob(jobId);
} }
} }

View File

@ -14,8 +14,8 @@ import android.service.chooser.ChooserTarget;
import android.service.chooser.ChooserTargetService; import android.service.chooser.ChooserTargetService;
import android.util.Log; import android.util.Log;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import java.util.ArrayList; import java.util.ArrayList;
@ -26,7 +26,7 @@ public class ShareChooserTargetService extends ChooserTargetService {
public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) { public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) {
Log.d("DirectShare", "invoked"); Log.d("DirectShare", "invoked");
final List<ChooserTarget> targets = new ArrayList<>(); final List<ChooserTarget> targets = new ArrayList<>();
for (Device d : BackgroundService.getInstance().getDevices().values()) { for (Device d : KdeConnect.getInstance().getDevices().values()) {
if (d.isReachable() && d.isPaired()) { if (d.isReachable() && d.isPaired()) {
Log.d("DirectShare", d.getName()); Log.d("DirectShare", d.getName());
final String targetName = d.getName(); final String targetName = d.getName();

View File

@ -15,7 +15,6 @@ import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
@ -157,7 +156,7 @@ public class SharePlugin extends Plugin {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
IntentHelper.startActivityFromBackground(context, browserIntent, url); IntentHelper.startActivityFromBackgroundOrCreateNotification(context, browserIntent, url);
} }
private void receiveText(NetworkPacket np) { private void receiveText(NetworkPacket np) {

View File

@ -11,7 +11,6 @@ import androidx.annotation.NonNull;
import androidx.core.util.Consumer; import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
import org.kde.kdeconnect_tp.databinding.ListItemSystemvolumeBinding; import org.kde.kdeconnect_tp.databinding.ListItemSystemvolumeBinding;
@ -49,8 +48,7 @@ class SinkItemHolder extends RecyclerView.ViewHolder
@Override @Override
public void onProgressChanged(final SeekBar seekBar, int i, boolean triggeredByUser) { public void onProgressChanged(final SeekBar seekBar, int i, boolean triggeredByUser) {
if (triggeredByUser) { if (triggeredByUser) {
BackgroundService.RunCommand(seekBar.getContext(), plugin.sendVolume(sink.getName(), seekBar.getProgress());
service -> plugin.sendVolume(sink.getName(), seekBar.getProgress()));
} }
} }
@ -62,8 +60,7 @@ class SinkItemHolder extends RecyclerView.ViewHolder
@Override @Override
public void onStopTrackingTouch(final SeekBar seekBar) { public void onStopTrackingTouch(final SeekBar seekBar) {
seekBarTracking.accept(false); seekBarTracking.accept(false);
BackgroundService.RunCommand(seekBar.getContext(), plugin.sendVolume(sink.getName(), seekBar.getProgress());
service -> plugin.sendVolume(sink.getName(), seekBar.getProgress()));
} }
@Override @Override

View File

@ -6,7 +6,6 @@
package org.kde.kdeconnect.Plugins.SystemVolumePlugin; package org.kde.kdeconnect.Plugins.SystemVolumePlugin;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -20,8 +19,8 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Helpers.VolumeHelperKt; import org.kde.kdeconnect.Helpers.VolumeHelperKt;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin; import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin;
import org.kde.kdeconnect.Plugins.MprisPlugin.VolumeKeyListener; import org.kde.kdeconnect.Plugins.MprisPlugin.VolumeKeyListener;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
@ -73,19 +72,15 @@ public class SystemVolumeFragment
recyclerView.setAdapter(recyclerAdapter); recyclerView.setAdapter(recyclerAdapter);
} }
connectToPlugin(getDeviceId());
return systemVolumeFragmentBinding.getRoot(); return systemVolumeFragmentBinding.getRoot();
} }
@Override @Override
public void onAttach(@NonNull Context context) { public void onDestroyView() {
super.onAttach(context);
connectToPlugin(getDeviceId());
}
@Override
public void onDetach() {
super.onDetach();
disconnectFromPlugin(getDeviceId()); disconnectFromPlugin(getDeviceId());
super.onDestroyView();
} }
@Override @Override
@ -99,16 +94,21 @@ public class SystemVolumeFragment
} }
private void connectToPlugin(final String deviceId) { private void connectToPlugin(final String deviceId) {
BackgroundService.RunWithPlugin(requireActivity(), deviceId, SystemVolumePlugin.class, plugin -> { SystemVolumePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, SystemVolumePlugin.class);
this.plugin = plugin; if (plugin == null) {
plugin.addSinkListener(SystemVolumeFragment.this); return;
plugin.requestSinkList(); }
}); this.plugin = plugin;
plugin.addSinkListener(SystemVolumeFragment.this);
plugin.requestSinkList();
} }
private void disconnectFromPlugin(final String deviceId) { private void disconnectFromPlugin(final String deviceId) {
BackgroundService.RunWithPlugin(requireActivity(), deviceId, SystemVolumePlugin.class, plugin -> SystemVolumePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, SystemVolumePlugin.class);
plugin.removeSinkListener(SystemVolumeFragment.this)); if (plugin == null) {
return;
}
plugin.removeSinkListener(SystemVolumeFragment.this);
} }
@Override @Override

View File

@ -18,11 +18,11 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.kde.kdeconnect.BackgroundService
import org.kde.kdeconnect.Device import org.kde.kdeconnect.Device
import org.kde.kdeconnect.Device.PairingCallback import org.kde.kdeconnect.Device.PairingCallback
import org.kde.kdeconnect.Device.PluginsChangedListener import org.kde.kdeconnect.Device.PluginsChangedListener
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
import org.kde.kdeconnect.KdeConnect
import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin
import org.kde.kdeconnect.Plugins.Plugin import org.kde.kdeconnect.Plugins.Plugin
import org.kde.kdeconnect.UserInterface.List.PluginAdapter import org.kde.kdeconnect.UserInterface.List.PluginAdapter
@ -98,9 +98,7 @@ class DeviceFragment : Fragment() {
// ...and for when pairing doesn't (or can't) work // ...and for when pairing doesn't (or can't) work
errorBinding = deviceBinding.pairError errorBinding = deviceBinding.pairError
BackgroundService.RunCommand(mActivity) { device = KdeConnect.getInstance().getDevice(deviceId)
device = it.getDevice(deviceId)
}
requirePairingBinding().pairButton.setOnClickListener { requirePairingBinding().pairButton.setOnClickListener {
with(requirePairingBinding()) { with(requirePairingBinding()) {
@ -128,36 +126,35 @@ class DeviceFragment : Fragment() {
mActivity?.onDeviceSelected(null) mActivity?.onDeviceSelected(null)
} }
setHasOptionsMenu(true) setHasOptionsMenu(true)
BackgroundService.RunCommand(mActivity) { service: BackgroundService ->
device = service.getDevice(deviceId) ?: let {
Log.e(TAG, "Trying to display a device fragment but the device is not present")
mActivity?.onDeviceSelected(null)
return@RunCommand
}
mActivity?.supportActionBar?.title = device?.name
device?.addPairingCallback(pairingCallback)
device?.addPluginsChangedListener(pluginsChangedListener)
refreshUI()
}
requireDeviceBinding().pluginsList.layoutManager = requireDeviceBinding().pluginsList.layoutManager =
GridLayoutManager(requireContext(), resources.getInteger(R.integer.plugins_columns)) GridLayoutManager(requireContext(), resources.getInteger(R.integer.plugins_columns))
requireDeviceBinding().permissionsList.layoutManager = LinearLayoutManager(requireContext()) requireDeviceBinding().permissionsList.layoutManager = LinearLayoutManager(requireContext())
device?.apply {
mActivity?.supportActionBar?.title = name
addPairingCallback(pairingCallback)
addPluginsChangedListener(pluginsChangedListener)
} ?: run { // device is null
Log.e(TAG, "Trying to display a device fragment but the device is not present")
mActivity?.onDeviceSelected(null)
}
refreshUI()
return deviceBinding.root return deviceBinding.root
} }
private val pluginsChangedListener = PluginsChangedListener { refreshUI() } private val pluginsChangedListener = PluginsChangedListener { refreshUI() }
override fun onDestroyView() { override fun onDestroyView() {
BackgroundService.RunCommand(mActivity) { service: BackgroundService -> device?.apply {
val device = service.getDevice(deviceId) ?: return@RunCommand removePluginsChangedListener(pluginsChangedListener)
device.removePluginsChangedListener(pluginsChangedListener) removePairingCallback(pairingCallback)
device.removePairingCallback(pairingCallback)
} }
super.onDestroyView()
pairingBinding = null pairingBinding = null
errorBinding = null errorBinding = null
deviceBinding = null deviceBinding = null
super.onDestroyView()
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {

View File

@ -25,6 +25,7 @@ import org.apache.commons.lang3.ArrayUtils
import org.kde.kdeconnect.BackgroundService import org.kde.kdeconnect.BackgroundService
import org.kde.kdeconnect.Device import org.kde.kdeconnect.Device
import org.kde.kdeconnect.Helpers.DeviceHelper import org.kde.kdeconnect.Helpers.DeviceHelper
import org.kde.kdeconnect.KdeConnect
import org.kde.kdeconnect.Plugins.SharePlugin.ShareSettingsFragment import org.kde.kdeconnect.Plugins.SharePlugin.ShareSettingsFragment
import org.kde.kdeconnect.UserInterface.About.AboutFragment.Companion.newInstance import org.kde.kdeconnect.UserInterface.About.AboutFragment.Companion.newInstance
import org.kde.kdeconnect.UserInterface.About.getApplicationAboutData import org.kde.kdeconnect.UserInterface.About.getApplicationAboutData
@ -194,16 +195,14 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
private fun onPairResultFromNotification(deviceId: String?, pairStatus: String): String? { private fun onPairResultFromNotification(deviceId: String?, pairStatus: String): String? {
assert(deviceId != null) assert(deviceId != null)
if (pairStatus != PAIRING_PENDING) { if (pairStatus != PAIRING_PENDING) {
BackgroundService.RunCommand(this) { service: BackgroundService -> val device = KdeConnect.getInstance().getDevice(deviceId)
val device = service.getDevice(deviceId) if (device == null) {
if (device == null) { Log.w(this::class.simpleName, "Reject pairing - device no longer exists: $deviceId")
Log.w(this::class.simpleName, "Reject pairing - device no longer exists: $deviceId") return null
return@RunCommand }
} when (pairStatus) {
when (pairStatus) { PAIRING_ACCEPTED -> device.acceptPairing()
PAIRING_ACCEPTED -> device.acceptPairing() PAIRING_REJECTED -> device.rejectPairing()
PAIRING_REJECTED -> device.rejectPairing()
}
} }
} }
return if (pairStatus == PAIRING_ACCEPTED || pairStatus == PAIRING_PENDING) deviceId else null return if (pairStatus == PAIRING_ACCEPTED || pairStatus == PAIRING_PENDING) deviceId else null
@ -228,45 +227,41 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
} }
private fun updateDeviceList() { private fun updateDeviceList() {
BackgroundService.RunCommand(this@MainActivity) { service: BackgroundService -> val menu = mNavigationView.menu
val menu = mNavigationView.menu menu.clear()
menu.clear() mMapMenuToDeviceId.clear()
mMapMenuToDeviceId.clear() val devicesMenu = menu.addSubMenu(R.string.devices)
val devicesMenu = menu.addSubMenu(R.string.devices) var id = MENU_ENTRY_DEVICE_FIRST_ID
var id = MENU_ENTRY_DEVICE_FIRST_ID val devices: Collection<Device> = KdeConnect.getInstance().devices.values
val devices: Collection<Device> = service.devices.values for (device in devices) {
for (device in devices) { if (device.isReachable && device.isPaired) {
if (device.isReachable && device.isPaired) { val item = devicesMenu.add(Menu.FIRST, id++, 1, device.name)
val item = devicesMenu.add(Menu.FIRST, id++, 1, device.name) item.icon = device.icon
item.icon = device.icon item.isCheckable = true
item.isCheckable = true mMapMenuToDeviceId[item] = device.deviceId
mMapMenuToDeviceId[item] = device.deviceId
}
} }
val addDeviceItem = devicesMenu.add(Menu.FIRST, MENU_ENTRY_ADD_DEVICE, 1000, R.string.pair_new_device)
addDeviceItem.setIcon(R.drawable.ic_action_content_add_circle_outline_32dp)
addDeviceItem.isCheckable = true
val settingsItem = menu.add(Menu.FIRST, MENU_ENTRY_SETTINGS, 1000, R.string.settings)
settingsItem.setIcon(R.drawable.ic_settings_white_32dp)
settingsItem.isCheckable = true
val aboutItem = menu.add(Menu.FIRST, MENU_ENTRY_ABOUT, 1000, R.string.about)
aboutItem.setIcon(R.drawable.ic_baseline_info_24)
aboutItem.isCheckable = true
//Ids might have changed
if (mCurrentMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID) {
mCurrentMenuEntry = deviceIdToMenuEntryId(mCurrentDevice)
}
mNavigationView.setCheckedItem(mCurrentMenuEntry)
} }
val addDeviceItem = devicesMenu.add(Menu.FIRST, MENU_ENTRY_ADD_DEVICE, 1000, R.string.pair_new_device)
addDeviceItem.setIcon(R.drawable.ic_action_content_add_circle_outline_32dp)
addDeviceItem.isCheckable = true
val settingsItem = menu.add(Menu.FIRST, MENU_ENTRY_SETTINGS, 1000, R.string.settings)
settingsItem.setIcon(R.drawable.ic_settings_white_32dp)
settingsItem.isCheckable = true
val aboutItem = menu.add(Menu.FIRST, MENU_ENTRY_ABOUT, 1000, R.string.about)
aboutItem.setIcon(R.drawable.ic_baseline_info_24)
aboutItem.isCheckable = true
//Ids might have changed
if (mCurrentMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID) {
mCurrentMenuEntry = deviceIdToMenuEntryId(mCurrentDevice)
}
mNavigationView.setCheckedItem(mCurrentMenuEntry)
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
BackgroundService.RunCommand(this) { service: BackgroundService -> BackgroundService.Start(applicationContext);
service.onNetworkChange() KdeConnect.getInstance().addDeviceListChangedCallback(this::class.simpleName) { runOnUiThread { updateDeviceList() } }
service.addDeviceListChangedCallback(this::class.simpleName) { updateDeviceList() }
}
updateDeviceList() updateDeviceList()
onBackPressedDispatcher.addCallback(mainFragmentCallback) onBackPressedDispatcher.addCallback(mainFragmentCallback)
onBackPressedDispatcher.addCallback(closeDrawerCallback) onBackPressedDispatcher.addCallback(closeDrawerCallback)
@ -274,10 +269,10 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
} }
override fun onStop() { override fun onStop() {
BackgroundService.RunCommand(this) { service: BackgroundService -> service.removeDeviceListChangedCallback(this::class.simpleName) } KdeConnect.getInstance().removeDeviceListChangedCallback(this::class.simpleName)
super.onStop()
mainFragmentCallback.remove() mainFragmentCallback.remove()
closeDrawerCallback.remove() closeDrawerCallback.remove()
super.onStop()
} }
@JvmOverloads @JvmOverloads
@ -315,11 +310,9 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when { when {
requestCode == RESULT_NEEDS_RELOAD -> BackgroundService.RunCommand(this) { service: BackgroundService -> requestCode == RESULT_NEEDS_RELOAD -> {
val device = service.getDevice(mCurrentDevice) KdeConnect.getInstance().getDevice(mCurrentDevice)?.reloadPluginsFromSettings()
device.reloadPluginsFromSettings()
} }
requestCode == STORAGE_LOCATION_CONFIGURED && resultCode == RESULT_OK && data != null -> { requestCode == STORAGE_LOCATION_CONFIGURED && resultCode == RESULT_OK && data != null -> {
val uri = data.data val uri = data.data
ShareSettingsFragment.saveStorageLocationPreference(this, uri) ShareSettingsFragment.saveStorageLocationPreference(this, uri)
@ -344,17 +337,14 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
} }
//New permission granted, reload plugins //New permission granted, reload plugins
BackgroundService.RunCommand(this) { service: BackgroundService -> KdeConnect.getInstance().getDevice(mCurrentDevice)?.reloadPluginsFromSettings()
val device = service.getDevice(mCurrentDevice)
device.reloadPluginsFromSettings()
}
} }
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (DeviceHelper.KEY_DEVICE_NAME_PREFERENCE == key) { if (DeviceHelper.KEY_DEVICE_NAME_PREFERENCE == key) {
mNavViewDeviceName.text = DeviceHelper.getDeviceName(this) mNavViewDeviceName.text = DeviceHelper.getDeviceName(this)
BackgroundService.RunCommand(this) { obj: BackgroundService -> obj.onNetworkChange() } //Re-send our identity packet BackgroundService.ForceRefreshConnections(this) //Re-send our identity packet
} }
} }

View File

@ -26,6 +26,7 @@ import androidx.fragment.app.Fragment;
import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.TrustedNetworkHelper; import org.kde.kdeconnect.Helpers.TrustedNetworkHelper;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.List.PairingDeviceItem; import org.kde.kdeconnect.UserInterface.List.PairingDeviceItem;
import org.kde.kdeconnect.UserInterface.List.SectionItem; import org.kde.kdeconnect.UserInterface.List.SectionItem;
@ -59,7 +60,6 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
private TextView headerText; private TextView headerText;
private TextView noWifiHeader; private TextView noWifiHeader;
private TextView notTrustedText; private TextView notTrustedText;
private boolean isConnectedToNonCellularNetwork = true;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
@ -73,7 +73,7 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
pairingExplanationTextBinding = PairingExplanationTextBinding.inflate(inflater); pairingExplanationTextBinding = PairingExplanationTextBinding.inflate(inflater);
pairingExplanationTextNoWifiBinding = PairingExplanationTextNoWifiBinding.inflate(inflater); pairingExplanationTextNoWifiBinding = PairingExplanationTextNoWifiBinding.inflate(inflater);
devicesListBinding.refreshListLayout.setOnRefreshListener(this::updateDeviceListAction); devicesListBinding.refreshListLayout.setOnRefreshListener(this::refreshDevicesAction);
notTrustedText = pairingExplanationNotTrustedBinding.getRoot(); notTrustedText = pairingExplanationNotTrustedBinding.getRoot();
notTrustedText.setOnClickListener(null); notTrustedText.setOnClickListener(null);
@ -105,136 +105,134 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
mActivity = ((MainActivity) getActivity()); mActivity = ((MainActivity) getActivity());
} }
private void updateDeviceListAction() { private void refreshDevicesAction() {
updateDeviceList(); BackgroundService.ForceRefreshConnections(requireContext());
BackgroundService.RunCommand(mActivity, BackgroundService::onNetworkChange);
devicesListBinding.refreshListLayout.setRefreshing(true); devicesListBinding.refreshListLayout.setRefreshing(true);
devicesListBinding.refreshListLayout.postDelayed(() -> { devicesListBinding.refreshListLayout.postDelayed(() -> {
// the view might be destroyed by now if (devicesListBinding != null) { // the view might be destroyed by now
if (devicesListBinding == null) { devicesListBinding.refreshListLayout.setRefreshing(false);
return;
} }
devicesListBinding.refreshListLayout.setRefreshing(false);
}, 1500); }, 1500);
} }
private void updateDeviceList() { private void updateDeviceList() {
BackgroundService.RunCommand(mActivity, service -> mActivity.runOnUiThread(() -> { if (!isAdded()) {
//Fragment is not attached to an activity. We will crash if we try to do anything here.
return;
}
if (!isAdded()) { if (listRefreshCalledThisFrame) {
//Fragment is not attached to an activity. We will crash if we try to do anything here. // This makes sure we don't try to call list.getFirstVisiblePosition()
return; // twice per frame, because the second time the list hasn't been drawn
} // yet and it would always return 0.
return;
}
listRefreshCalledThisFrame = true;
if (listRefreshCalledThisFrame) { //Check if we're on Wi-Fi/Local network. If we still see a device, don't do anything special
// This makes sure we don't try to call list.getFirstVisiblePosition() BackgroundService service = BackgroundService.getInstance();
// twice per frame, because the second time the list hasn't been drawn if (service == null) {
// yet and it would always return 0. updateConnectivityInfoHeader(true);
return; } else {
} service.isConnectedToNonCellularNetwork().observe(this, this::updateConnectivityInfoHeader);
listRefreshCalledThisFrame = true; }
Collection<Device> devices = service.getDevices().values(); try {
boolean someDevicesReachable = false; final ArrayList<ListAdapter.Item> items = new ArrayList<>();
SectionItem connectedSection;
Resources res = getResources();
connectedSection = new SectionItem(res.getString(R.string.category_connected_devices));
items.add(connectedSection);
Collection<Device> devices = KdeConnect.getInstance().getDevices().values();
for (Device device : devices) { for (Device device : devices) {
if (device.isReachable()) { if (device.isReachable() && device.isPaired()) {
someDevicesReachable = true; items.add(new PairingDeviceItem(device, PairingFragment.this));
connectedSection.isEmpty = false;
} }
} }
if (connectedSection.isEmpty) {
devicesListBinding.devicesList.removeHeaderView(headerText); items.remove(items.size() - 1); //Remove connected devices section if empty
devicesListBinding.devicesList.removeHeaderView(noWifiHeader);
devicesListBinding.devicesList.removeHeaderView(notTrustedText);
//Check if we're on Wi-Fi/Local network. If we still see a device, don't do anything special
if (someDevicesReachable || isConnectedToNonCellularNetwork) {
if (TrustedNetworkHelper.isTrustedNetwork(getContext())) {
devicesListBinding.devicesList.addHeaderView(headerText);
} else {
devicesListBinding.devicesList.addHeaderView(notTrustedText);
}
} else {
devicesListBinding.devicesList.addHeaderView(noWifiHeader);
} }
try { SectionItem availableSection = new SectionItem(res.getString(R.string.category_not_paired_devices));
final ArrayList<ListAdapter.Item> items = new ArrayList<>(); items.add(availableSection);
for (Device device : devices) {
SectionItem connectedSection; if (device.isReachable() && !device.isPaired()) {
Resources res = getResources(); items.add(new PairingDeviceItem(device, PairingFragment.this));
availableSection.isEmpty = false;
connectedSection = new SectionItem(res.getString(R.string.category_connected_devices));
items.add(connectedSection);
for (Device device : devices) {
if (device.isReachable() && device.isPaired()) {
items.add(new PairingDeviceItem(device, PairingFragment.this));
connectedSection.isEmpty = false;
}
} }
if (connectedSection.isEmpty) { }
items.remove(items.size() - 1); //Remove connected devices section if empty if (availableSection.isEmpty && !connectedSection.isEmpty) {
} items.remove(items.size() - 1); //Remove remembered devices section if empty
SectionItem availableSection = new SectionItem(res.getString(R.string.category_not_paired_devices));
items.add(availableSection);
for (Device device : devices) {
if (device.isReachable() && !device.isPaired()) {
items.add(new PairingDeviceItem(device, PairingFragment.this));
availableSection.isEmpty = false;
}
}
if (availableSection.isEmpty && !connectedSection.isEmpty) {
items.remove(items.size() - 1); //Remove remembered devices section if empty
}
SectionItem rememberedSection = new SectionItem(res.getString(R.string.category_remembered_devices));
items.add(rememberedSection);
for (Device device : devices) {
if (!device.isReachable() && device.isPaired()) {
items.add(new PairingDeviceItem(device, PairingFragment.this));
rememberedSection.isEmpty = false;
}
}
if (rememberedSection.isEmpty) {
items.remove(items.size() - 1); //Remove remembered devices section if empty
}
//Store current scroll
int index = devicesListBinding.devicesList.getFirstVisiblePosition();
View v = devicesListBinding.devicesList.getChildAt(0);
int top = (v == null) ? 0 : (v.getTop() - devicesListBinding.devicesList.getPaddingTop());
devicesListBinding.devicesList.setAdapter(new ListAdapter(mActivity, items));
//Restore scroll
devicesListBinding.devicesList.setSelectionFromTop(index, top);
} catch (IllegalStateException e) {
//Ignore: The activity was closed while we were trying to update it
} finally {
listRefreshCalledThisFrame = false;
} }
})); SectionItem rememberedSection = new SectionItem(res.getString(R.string.category_remembered_devices));
items.add(rememberedSection);
for (Device device : devices) {
if (!device.isReachable() && device.isPaired()) {
items.add(new PairingDeviceItem(device, PairingFragment.this));
rememberedSection.isEmpty = false;
}
}
if (rememberedSection.isEmpty) {
items.remove(items.size() - 1); //Remove remembered devices section if empty
}
//Store current scroll
int index = devicesListBinding.devicesList.getFirstVisiblePosition();
View v = devicesListBinding.devicesList.getChildAt(0);
int top = (v == null) ? 0 : (v.getTop() - devicesListBinding.devicesList.getPaddingTop());
devicesListBinding.devicesList.setAdapter(new ListAdapter(mActivity, items));
//Restore scroll
devicesListBinding.devicesList.setSelectionFromTop(index, top);
} catch (IllegalStateException e) {
//Ignore: The activity was closed while we were trying to update it
} finally {
listRefreshCalledThisFrame = false;
}
} }
void updateConnectivityInfoHeader(boolean isConnectedToNonCellularNetwork) {
Collection<Device> devices = KdeConnect.getInstance().getDevices().values();
boolean someDevicesReachable = false;
for (Device device : devices) {
if (device.isReachable()) {
someDevicesReachable = true;
}
}
devicesListBinding.devicesList.removeHeaderView(headerText);
devicesListBinding.devicesList.removeHeaderView(noWifiHeader);
devicesListBinding.devicesList.removeHeaderView(notTrustedText);
if (someDevicesReachable || isConnectedToNonCellularNetwork) {
if (TrustedNetworkHelper.isTrustedNetwork(getContext())) {
devicesListBinding.devicesList.addHeaderView(headerText);
} else {
devicesListBinding.devicesList.addHeaderView(notTrustedText);
}
} else {
devicesListBinding.devicesList.addHeaderView(noWifiHeader);
}
}
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
devicesListBinding.refreshListLayout.setEnabled(true); KdeConnect.getInstance().addDeviceListChangedCallback("PairingFragment", () -> mActivity.runOnUiThread(this::updateDeviceList));
BackgroundService.RunCommand(mActivity, service -> service.addDeviceListChangedCallback("PairingFragment", newIsConnectedToNonCellularNetwork -> { BackgroundService.ForceRefreshConnections(requireContext()); // force a network re-discover
isConnectedToNonCellularNetwork = newIsConnectedToNonCellularNetwork;
updateDeviceList();
}));
updateDeviceList(); updateDeviceList();
} }
@Override @Override
public void onStop() { public void onStop() {
KdeConnect.getInstance().removeDeviceListChangedCallback("PairingFragment");
super.onStop(); super.onStop();
devicesListBinding.refreshListLayout.setEnabled(false);
BackgroundService.RunCommand(mActivity, service -> service.removeDeviceListChangedCallback("PairingFragment"));
} }
@Override @Override
@ -265,7 +263,7 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId(); int id = item.getItemId();
if (id == R.id.menu_refresh) { if (id == R.id.menu_refresh) {
updateDeviceListAction(); refreshDevicesAction();
return true; return true;
} else if (id == R.id.menu_custom_device_list) { } else if (id == R.id.menu_custom_device_list) {
startActivity(new Intent(mActivity, CustomDevicesActivity.class)); startActivity(new Intent(mActivity, CustomDevicesActivity.class));

View File

@ -13,8 +13,8 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
@ -55,7 +55,7 @@ public class PluginSettingsActivity
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragmentPlaceHolder); Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragmentPlaceHolder);
if (fragment == null) { if (fragment == null) {
if (pluginKey != null) { if (pluginKey != null) {
Device device = BackgroundService.getInstance().getDevice(deviceId); Device device = KdeConnect.getInstance().getDevice(deviceId);
if (device != null) { if (device != null) {
Plugin plugin = device.getPluginIncludingWithoutPermissions(pluginKey); Plugin plugin = device.getPluginIncludingWithoutPermissions(pluginKey);
if (plugin != null) { if (plugin != null) {

View File

@ -13,8 +13,8 @@ import androidx.annotation.NonNull;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
@ -56,7 +56,7 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
this.pluginKey = getArguments().getString(ARG_PLUGIN_KEY); this.pluginKey = getArguments().getString(ARG_PLUGIN_KEY);
this.layout = getArguments().getInt(ARG_LAYOUT); this.layout = getArguments().getInt(ARG_LAYOUT);
this.device = getDeviceOrThrow(getDeviceId()); this.device = KdeConnect.getInstance().getDevice(getDeviceId());
this.plugin = device.getPluginIncludingWithoutPermissions(pluginKey); this.plugin = device.getPluginIncludingWithoutPermissions(pluginKey);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -85,13 +85,4 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
return ((PluginSettingsActivity)requireActivity()).getDeviceId(); return ((PluginSettingsActivity)requireActivity()).getDeviceId();
} }
private Device getDeviceOrThrow(String deviceId) {
Device device = BackgroundService.getInstance().getDevice(deviceId);
if (device == null) {
throw new RuntimeException("PluginSettingsFragment.onCreatePreferences() - No device with id " + getDeviceId());
}
return device;
}
} }

View File

@ -15,8 +15,8 @@ import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
@ -77,22 +77,20 @@ public class PluginSettingsListFragment extends PreferenceFragmentCompat {
final String deviceId = getArguments().getString(ARG_DEVICE_ID); final String deviceId = getArguments().getString(ARG_DEVICE_ID);
BackgroundService.RunCommand(requireContext(), service -> { Device device = KdeConnect.getInstance().getDevice(deviceId);
final Device device = service.getDevice(deviceId); if (device == null) {
if (device == null) { final FragmentActivity activity = requireActivity();
final FragmentActivity activity = requireActivity(); activity.runOnUiThread(activity::finish);
activity.runOnUiThread(activity::finish); return;
return; }
} List<String> plugins = device.getSupportedPlugins();
List<String> plugins = device.getSupportedPlugins(); PluginFactory.sortPluginList(plugins);
PluginFactory.sortPluginList(plugins);
for (final String pluginKey : plugins) { for (final String pluginKey : plugins) {
//TODO: Use PreferenceManagers context //TODO: Use PreferenceManagers context
PluginPreference pref = new PluginPreference(requireContext(), pluginKey, device, callback); PluginPreference pref = new PluginPreference(requireContext(), pluginKey, device, callback);
preferenceScreen.addPreference(pref); preferenceScreen.addPreference(pref);
} }
});
} }
@NonNull @NonNull

View File

@ -125,8 +125,10 @@ public class SettingsFragment extends PreferenceFragmentCompat {
final boolean isChecked = (Boolean) newValue; final boolean isChecked = (Boolean) newValue;
NotificationHelper.setPersistentNotificationEnabled(context, isChecked); NotificationHelper.setPersistentNotificationEnabled(context, isChecked);
BackgroundService.RunCommand(context, BackgroundService service = BackgroundService.getInstance();
service -> service.changePersistentNotificationVisibility(isChecked)); if (service != null) {
service.changePersistentNotificationVisibility(isChecked);
}
NotificationHelper.setPersistentNotificationEnabled(context, isChecked); NotificationHelper.setPersistentNotificationEnabled(context, isChecked);