/* * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ package org.kde.kdeconnect; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.preference.PreferenceManager; import android.util.Base64; import android.util.Log; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.commons.lang3.StringUtils; import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BasePairingHandler; import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.UserInterface.MainActivity; import org.kde.kdeconnect_tp.R; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; public class Device implements BaseLink.PacketReceiver { private final Context context; private final String deviceId; private String name; public Certificate certificate; private int notificationId; private int protocolVersion; private DeviceType deviceType; private PairStatus pairStatus; private final CopyOnWriteArrayList pairingCallback = new CopyOnWriteArrayList<>(); private final Map pairingHandlers = new HashMap<>(); private final CopyOnWriteArrayList links = new CopyOnWriteArrayList<>(); private DevicePacketQueue packetQueue; private List supportedPlugins = new ArrayList<>(); private final ConcurrentHashMap plugins = new ConcurrentHashMap<>(); private final ConcurrentHashMap pluginsWithoutPermissions = new ConcurrentHashMap<>(); private final ConcurrentHashMap pluginsWithoutOptionalPermissions = new ConcurrentHashMap<>(); private MultiValuedMap pluginsByIncomingInterface = new ArrayListValuedHashMap<>(); private final SharedPreferences settings; private final CopyOnWriteArrayList pluginsChangedListeners = new CopyOnWriteArrayList<>(); private Set incomingCapabilities = new HashSet<>(); public boolean supportsPacketType(String type) { if (incomingCapabilities == null) { return true; } else { return incomingCapabilities.contains(type); } } public interface PluginsChangedListener { void onPluginsChanged(Device device); } public enum PairStatus { NotPaired, Paired } public enum DeviceType { Phone, Tablet, Computer, Tv; static public DeviceType FromString(String s) { if ("tablet".equals(s)) return Tablet; if ("phone".equals(s)) return Phone; if ("tv".equals(s)) return Tv; return Computer; //Default } @NonNull public String toString() { switch (this) { case Tablet: return "tablet"; case Phone: return "phone"; case Tv: return "tv"; default: return "desktop"; } } public Drawable getIcon(Context context) { int drawableId; switch (this) { case Phone: drawableId = R.drawable.ic_device_phone_32dp; break; case Tablet: drawableId = R.drawable.ic_device_tablet_32dp; break; case Tv: drawableId = R.drawable.ic_device_tv_32dp; break; default: drawableId = R.drawable.ic_device_laptop_32dp; } return ContextCompat.getDrawable(context, drawableId); } } public interface PairingCallback { void incomingRequest(); void pairingSuccessful(); void pairingFailed(String error); void unpaired(); } //Remembered trusted device, we need to wait for a incoming devicelink to communicate Device(Context context, String deviceId) { settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); //Log.e("Device","Constructor A"); this.context = context; this.deviceId = deviceId; this.name = settings.getString("deviceName", context.getString(R.string.unknown_device)); this.pairStatus = PairStatus.Paired; this.protocolVersion = DeviceHelper.ProtocolVersion; //We don't know it yet this.deviceType = DeviceType.FromString(settings.getString("deviceType", "desktop")); //Assume every plugin is supported until addLink is called and we can get the actual list supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); //Do not load plugins yet, the device is not present //reloadPluginsFromSettings(); } //Device known via an incoming connection sent to us via a devicelink, we know everything but we don't trust it yet Device(Context context, NetworkPacket np, BaseLink dl) { //Log.e("Device","Constructor B"); this.context = context; this.deviceId = np.getString("deviceId"); this.name = context.getString(R.string.unknown_device); //We read it in addLink this.pairStatus = PairStatus.NotPaired; this.protocolVersion = 0; this.deviceType = DeviceType.Computer; settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); addLink(np, dl); } public String getName() { return StringUtils.defaultString(name, context.getString(R.string.unknown_device)); } public Drawable getIcon() { return deviceType.getIcon(context); } public DeviceType getDeviceType() { return deviceType; } public String getDeviceId() { return deviceId; } public Context getContext() { return context; } //Returns 0 if the version matches, < 0 if it is older or > 0 if it is newer public int compareProtocolVersion() { return protocolVersion - DeviceHelper.ProtocolVersion; } // // Pairing-related functions // public boolean isPaired() { return pairStatus == PairStatus.Paired; } /* Asks all pairing handlers that, is pair requested? */ public boolean isPairRequested() { boolean pairRequested = false; for (BasePairingHandler ph : pairingHandlers.values()) { pairRequested = pairRequested || ph.isPairRequested(); } return pairRequested; } /* Asks all pairing handlers that, is pair requested by peer? */ public boolean isPairRequestedByPeer() { boolean pairRequestedByPeer = false; for (BasePairingHandler ph : pairingHandlers.values()) { pairRequestedByPeer = pairRequestedByPeer || ph.isPairRequestedByPeer(); } return pairRequestedByPeer; } public void addPairingCallback(PairingCallback callback) { pairingCallback.add(callback); } public void removePairingCallback(PairingCallback callback) { pairingCallback.remove(callback); } public void requestPairing() { Resources res = context.getResources(); if (isPaired()) { for (PairingCallback cb : pairingCallback) { cb.pairingFailed(res.getString(R.string.error_already_paired)); } return; } if (!isReachable()) { for (PairingCallback cb : pairingCallback) { cb.pairingFailed(res.getString(R.string.error_not_reachable)); } return; } for (BasePairingHandler ph : pairingHandlers.values()) { ph.requestPairing(); } } public void unpair() { for (BasePairingHandler ph : pairingHandlers.values()) { ph.unpair(); } unpairInternal(); // Even if there are no pairing handlers, unpair } /** * This method does not send an unpair packet, instead it unpairs internally by deleting trusted device info. * Likely to be called after sending packet from pairing handler */ private void unpairInternal() { //Log.e("Device","Unpairing (unpairInternal)"); pairStatus = PairStatus.NotPaired; SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); preferences.edit().remove(deviceId).apply(); SharedPreferences devicePreferences = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); devicePreferences.edit().clear().apply(); for (PairingCallback cb : pairingCallback) cb.unpaired(); reloadPluginsFromSettings(); } /* This method should be called after pairing is done from pairing handler. Calling this method again should not create any problem as most of the things will get over writter*/ private void pairingDone() { //Log.e("Device", "Storing as trusted, deviceId: "+deviceId); hidePairingNotification(); pairStatus = PairStatus.Paired; //Store as trusted device SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); preferences.edit().putBoolean(deviceId, true).apply(); SharedPreferences.Editor editor = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE).edit(); editor.putString("deviceName", name); editor.putString("deviceType", deviceType.toString()); editor.apply(); reloadPluginsFromSettings(); for (PairingCallback cb : pairingCallback) { cb.pairingSuccessful(); } } /* This method is called after accepting pair request form GUI */ public void acceptPairing() { Log.i("KDE/Device", "Accepted pair request started by the other device"); for (BasePairingHandler ph : pairingHandlers.values()) { ph.acceptPairing(); } } /* This method is called after rejecting pairing from GUI */ public void rejectPairing() { Log.i("KDE/Device", "Rejected pair request started by the other device"); //Log.e("Device","Unpairing (rejectPairing)"); pairStatus = PairStatus.NotPaired; for (BasePairingHandler ph : pairingHandlers.values()) { ph.rejectPairing(); } for (PairingCallback cb : pairingCallback) { cb.pairingFailed(context.getString(R.string.error_canceled_by_user)); } } // // Notification related methods used during pairing // public int getNotificationId() { return notificationId; } public void displayPairingNotification() { hidePairingNotification(); notificationId = (int) System.currentTimeMillis(); Intent intent = new Intent(getContext(), MainActivity.class); intent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId()); intent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_PENDING); PendingIntent pendingIntent = PendingIntent.getActivity(getContext(), 1, intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE); Intent acceptIntent = new Intent(getContext(), MainActivity.class); Intent rejectIntent = new Intent(getContext(), MainActivity.class); acceptIntent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId()); //acceptIntent.putExtra("notificationId", notificationId); acceptIntent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_ACCEPTED); rejectIntent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId()); //rejectIntent.putExtra("notificationId", notificationId); rejectIntent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_REJECTED); PendingIntent acceptedPendingIntent = PendingIntent.getActivity(getContext(), 2, acceptIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE); PendingIntent rejectedPendingIntent = PendingIntent.getActivity(getContext(), 4, rejectIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE); Resources res = getContext().getResources(); final NotificationManager notificationManager = ContextCompat.getSystemService(getContext(), NotificationManager.class); String verificationKeyShort = SslHelper.getVerificationKey(SslHelper.certificate, certificate).substring(8); Notification noti = new NotificationCompat.Builder(getContext(), NotificationHelper.Channels.DEFAULT) .setContentTitle(res.getString(R.string.pairing_request_from, getName())) .setContentText(res.getString(R.string.pairing_verification_code, verificationKeyShort)) .setTicker(res.getString(R.string.pair_requested)) .setSmallIcon(R.drawable.ic_notification) .addAction(R.drawable.ic_accept_pairing_24dp, res.getString(R.string.pairing_accept), acceptedPendingIntent) .addAction(R.drawable.ic_reject_pairing_24dp, res.getString(R.string.pairing_reject), rejectedPendingIntent) .setAutoCancel(true) .setDefaults(Notification.DEFAULT_ALL) .build(); NotificationHelper.notifyCompat(notificationManager, notificationId, noti); } public void hidePairingNotification() { final NotificationManager notificationManager = ContextCompat.getSystemService(getContext(), NotificationManager.class); notificationManager.cancel(notificationId); } // // ComputerLink-related functions // public boolean isReachable() { return !links.isEmpty(); } public void addLink(NetworkPacket identityPacket, BaseLink link) { if (links.isEmpty()) { packetQueue = new DevicePacketQueue(this); } //FilesHelper.LogOpenFileCount(); links.add(link); link.addPacketReceiver(this); this.protocolVersion = identityPacket.getInt("protocolVersion"); if (identityPacket.has("deviceName")) { this.name = identityPacket.getString("deviceName", this.name); SharedPreferences.Editor editor = settings.edit(); editor.putString("deviceName", this.name); editor.apply(); } if (identityPacket.has("deviceType")) { this.deviceType = DeviceType.FromString(identityPacket.getString("deviceType", "desktop")); } if (identityPacket.has("certificate")) { String certificateString = identityPacket.getString("certificate"); try { byte[] certificateBytes = Base64.decode(certificateString, 0); certificate = SslHelper.parseCertificate(certificateBytes); Log.i("KDE/Device", "Got certificate "); } catch (Exception e) { Log.e("KDE/Device", "Error getting certificate", e); } } try { SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context); byte[] privateKeyBytes = Base64.decode(globalSettings.getString("privateKey", ""), 0); PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)); link.setPrivateKey(privateKey); } catch (Exception e) { Log.e("KDE/Device", "Exception reading our own private key", e); //Should not happen } Log.i("KDE/Device", "addLink " + link.getLinkProvider().getName() + " -> " + getName() + " active links: " + links.size()); if (!pairingHandlers.containsKey(link.getName())) { BasePairingHandler.PairingHandlerCallback callback = new BasePairingHandler.PairingHandlerCallback() { @Override public void incomingRequest() { for (PairingCallback cb : pairingCallback) { cb.incomingRequest(); } } @Override public void pairingDone() { Device.this.pairingDone(); } @Override public void pairingFailed(String error) { for (PairingCallback cb : pairingCallback) { cb.pairingFailed(error); } } @Override public void unpaired() { unpairInternal(); } }; pairingHandlers.put(link.getName(), link.getPairingHandler(this, callback)); } Set outgoingCapabilities = identityPacket.getStringSet("outgoingCapabilities", null); Set incomingCapabilities = identityPacket.getStringSet("incomingCapabilities", null); if (incomingCapabilities != null && outgoingCapabilities != null) { supportedPlugins = new Vector<>(PluginFactory.pluginsForCapabilities(incomingCapabilities, outgoingCapabilities)); } else { supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); } this.incomingCapabilities = incomingCapabilities; reloadPluginsFromSettings(); } public void removeLink(BaseLink link) { //FilesHelper.LogOpenFileCount(); /* Remove pairing handler corresponding to that link too if it was the only link*/ boolean linkPresent = false; for (BaseLink bl : links) { if (bl.getName().equals(link.getName())) { linkPresent = true; break; } } if (!linkPresent) { pairingHandlers.remove(link.getName()); } link.removePacketReceiver(this); links.remove(link); Log.i("KDE/Device", "removeLink: " + link.getLinkProvider().getName() + " -> " + getName() + " active links: " + links.size()); if (links.isEmpty()) { reloadPluginsFromSettings(); if (packetQueue != null) { packetQueue.disconnected(); packetQueue = null; } } } @Override public void onPacketReceived(NetworkPacket np) { if (NetworkPacket.PACKET_TYPE_PAIR.equals(np.getType())) { Log.i("KDE/Device", "Pair packet"); for (BasePairingHandler ph : pairingHandlers.values()) { try { ph.packetReceived(np); } catch (Exception e) { Log.e("PairingPacketReceived", "Exception", e); } } } else if (isPaired()) { // pluginsByIncomingInterface may not be built yet if(pluginsByIncomingInterface.isEmpty()) { reloadPluginsFromSettings(); } Collection targetPlugins = pluginsByIncomingInterface.get(np.getType()); if (!targetPlugins.isEmpty()) { // When a key doesn't exist the multivaluemap returns an empty collection, so we don't need to check for null for (String pluginKey : targetPlugins) { Plugin plugin = plugins.get(pluginKey); try { plugin.onPacketReceived(np); } catch (Exception e) { Log.e("KDE/Device", "Exception in " + plugin.getPluginKey() + "'s onPacketReceived()", e); //try { Log.e("KDE/Device", "NetworkPacket:" + np.serialize()); } catch (Exception _) { } } } } else { Log.w("Device", "Ignoring packet with type " + np.getType() + " because no plugin can handle it"); } } else { //Log.e("KDE/onPacketReceived","Device not paired, will pass packet to unpairedPacketListeners"); // If it is pair packet, it should be captured by "if" at start // If not and device is paired, it should be captured by isPaired // Else unpair, this handles the situation when one device unpairs, but other dont know like unpairing when wi-fi is off unpair(); // The following code is NOT USED. It adds support for receiving packets from not trusted devices, but as of March 2023 no plugin implements "onUnpairedDevicePacketReceived". Collection targetPlugins = pluginsByIncomingInterface.get(np.getType()); if (!targetPlugins.isEmpty()) { for (String pluginKey : targetPlugins) { Plugin plugin = plugins.get(pluginKey); try { plugin.onUnpairedDevicePacketReceived(np); } catch (Exception e) { Log.e("KDE/Device", "Exception in " + plugin.getDisplayName() + "'s onPacketReceived() in unPairedPacketListeners", e); } } } else { Log.e("Device", "Ignoring packet with type " + np.getType() + " because no plugin can handle it"); } } } public static abstract class SendPacketStatusCallback { public abstract void onSuccess(); public abstract void onFailure(Throwable e); public void onProgressChanged(int percent) { } } private final SendPacketStatusCallback defaultCallback = new SendPacketStatusCallback() { @Override public void onSuccess() { } @Override public void onFailure(Throwable e) { Log.e("KDE/sendPacket", "Exception", e); } }; @AnyThread public void sendPacket(NetworkPacket np) { sendPacket(np, -1, defaultCallback); } @AnyThread public void sendPacket(NetworkPacket np, int replaceID) { sendPacket(np, replaceID, defaultCallback); } @WorkerThread public boolean sendPacketBlocking(NetworkPacket np) { return sendPacketBlocking(np, defaultCallback); } @AnyThread public void sendPacket(final NetworkPacket np, final SendPacketStatusCallback callback) { sendPacket(np, -1, callback); } /** * Send a packet to the device asynchronously * @param np The packet * @param replaceID If positive, replaces all unsent packets with the same replaceID * @param callback A callback for success/failure */ @AnyThread public void sendPacket(final NetworkPacket np, int replaceID, final SendPacketStatusCallback callback) { if (packetQueue == null) { callback.onFailure(new Exception("Device disconnected!")); } else { packetQueue.addPacket(np, replaceID, callback); } } /** * Check if we still have an unsent packet in the queue with the given ID. * If so, remove it from the queue and return it * @param replaceID The replace ID (must be positive) * @return The found packet, or null */ public NetworkPacket getAndRemoveUnsentPacket(int replaceID) { if (packetQueue == null) { return null; } else { return packetQueue.getAndRemoveUnsentPacket(replaceID); } } /** * Send {@code np} over one of this device's connected {@link #links}. * * @param np the packet to send * @param callback a callback that can receive realtime updates * @return true if the packet was sent ok, false otherwise * @see BaseLink#sendPacket(NetworkPacket, SendPacketStatusCallback) */ @WorkerThread public boolean sendPacketBlocking(final NetworkPacket np, final SendPacketStatusCallback callback) { /* if (!m_outgoingCapabilities.contains(np.getType()) && !NetworkPacket.protocolPacketTypes.contains(np.getType())) { Log.e("Device/sendPacket", "Plugin tried to send an undeclared packet: " + np.getType()); Log.w("Device/sendPacket", "Declared outgoing packet types: " + Arrays.toString(m_outgoingCapabilities.toArray())); } */ boolean success = false; //Make a copy to avoid concurrent modification exception if the original list changes for (final BaseLink link : links) { if (link == null) continue; //Since we made a copy, maybe somebody destroyed the link in the meanwhile success = link.sendPacket(np, callback); if (success) break; //If the link didn't call sendSuccess(), try the next one } if (!success) { Log.e("KDE/sendPacket", "No device link (of " + links.size() + " available) could send the packet. Packet " + np.getType() + " to " + name + " lost!"); } return success; } // // Plugin-related functions // @Nullable public T getPlugin(Class pluginClass) { Plugin plugin = getPlugin(Plugin.getPluginKey(pluginClass)); return (T) plugin; } @Nullable public Plugin getPlugin(String pluginKey) { return plugins.get(pluginKey); } private synchronized boolean addPlugin(final String pluginKey) { Plugin existing = plugins.get(pluginKey); if (existing != null) { if (existing.isIncompatible()) { Log.i("KDE/addPlugin", "Minimum requirements (e.g. API level) not fulfilled " + pluginKey); return false; } //Log.w("KDE/addPlugin","plugin already present:" + pluginKey); if (existing.checkOptionalPermissions()) { Log.i("KDE/addPlugin", "Optional Permissions OK " + pluginKey); pluginsWithoutOptionalPermissions.remove(pluginKey); } else { Log.e("KDE/addPlugin", "No optional permission " + pluginKey); pluginsWithoutOptionalPermissions.put(pluginKey, existing); } return true; } final Plugin plugin = PluginFactory.instantiatePluginForDevice(context, pluginKey, this); if (plugin == null) { Log.e("KDE/addPlugin", "could not instantiate plugin: " + pluginKey); return false; } if (plugin.isIncompatible()) { Log.i("KDE/addPlugin", "Minimum requirements (e.g. API level) not fulfilled " + pluginKey); return false; } plugins.put(pluginKey, plugin); if (!plugin.checkRequiredPermissions()) { Log.e("KDE/addPlugin", "No permission " + pluginKey); plugins.remove(pluginKey); pluginsWithoutPermissions.put(pluginKey, plugin); return false; } else { Log.i("KDE/addPlugin", "Permissions OK " + pluginKey); pluginsWithoutPermissions.remove(pluginKey); if (plugin.checkOptionalPermissions()) { Log.i("KDE/addPlugin", "Optional Permissions OK " + pluginKey); pluginsWithoutOptionalPermissions.remove(pluginKey); } else { Log.e("KDE/addPlugin", "No optional permission " + pluginKey); pluginsWithoutOptionalPermissions.put(pluginKey, plugin); } } try { return plugin.onCreate(); } catch (Exception e) { Log.e("KDE/addPlugin", "plugin failed to load " + pluginKey, e); return false; } } private synchronized boolean removePlugin(String pluginKey) { Plugin plugin = plugins.remove(pluginKey); if (plugin == null) { return false; } try { plugin.onDestroy(); //Log.e("removePlugin","removed " + pluginKey); } catch (Exception e) { Log.e("KDE/removePlugin", "Exception calling onDestroy for plugin " + pluginKey, e); } return true; } public void setPluginEnabled(String pluginKey, boolean value) { settings.edit().putBoolean(pluginKey, value).apply(); reloadPluginsFromSettings(); } public boolean isPluginEnabled(String pluginKey) { boolean enabledByDefault = PluginFactory.getPluginInfo(pluginKey).isEnabledByDefault(); return settings.getBoolean(pluginKey, enabledByDefault); } public void reloadPluginsFromSettings() { MultiValuedMap newPluginsByIncomingInterface = new ArrayListValuedHashMap<>(); for (String pluginKey : supportedPlugins) { PluginFactory.PluginInfo pluginInfo = PluginFactory.getPluginInfo(pluginKey); boolean pluginEnabled = false; boolean listenToUnpaired = pluginInfo.listenToUnpaired(); if ((isPaired() || listenToUnpaired) && isReachable()) { pluginEnabled = isPluginEnabled(pluginKey); } if (pluginEnabled) { boolean success = addPlugin(pluginKey); if (success) { for (String packetType : pluginInfo.getSupportedPacketTypes()) { newPluginsByIncomingInterface.put(packetType, pluginKey); } } else { removePlugin(pluginKey); } } else { removePlugin(pluginKey); } } pluginsByIncomingInterface = newPluginsByIncomingInterface; onPluginsChanged(); } public void onPluginsChanged() { for (PluginsChangedListener listener : pluginsChangedListeners) { listener.onPluginsChanged(Device.this); } } public ConcurrentHashMap getLoadedPlugins() { return plugins; } public ConcurrentHashMap getPluginsWithoutPermissions() { return pluginsWithoutPermissions; } public ConcurrentHashMap getPluginsWithoutOptionalPermissions() { return pluginsWithoutOptionalPermissions; } public void addPluginsChangedListener(PluginsChangedListener listener) { pluginsChangedListeners.add(listener); } public void removePluginsChangedListener(PluginsChangedListener listener) { pluginsChangedListeners.remove(listener); } public void disconnect() { for (BaseLink link : links) { link.disconnect(); } } 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 getSupportedPlugins() { return supportedPlugins; } }