File indexing completed on 2024-12-22 04:41:42

0001 /*
0002  * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 package org.kde.kdeconnect;
0008 
0009 import android.app.Notification;
0010 import android.app.NotificationManager;
0011 import android.app.PendingIntent;
0012 import android.content.Context;
0013 import android.content.Intent;
0014 import android.content.SharedPreferences;
0015 import android.content.res.Resources;
0016 import android.graphics.drawable.Drawable;
0017 import android.util.Log;
0018 
0019 import androidx.annotation.AnyThread;
0020 import androidx.annotation.NonNull;
0021 import androidx.annotation.Nullable;
0022 import androidx.annotation.WorkerThread;
0023 import androidx.core.app.NotificationCompat;
0024 import androidx.core.content.ContextCompat;
0025 
0026 import org.apache.commons.collections4.MultiValuedMap;
0027 import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
0028 import org.kde.kdeconnect.Backends.BaseLink;
0029 import org.kde.kdeconnect.Helpers.DeviceHelper;
0030 import org.kde.kdeconnect.Helpers.NotificationHelper;
0031 import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
0032 import org.kde.kdeconnect.Plugins.Plugin;
0033 import org.kde.kdeconnect.Plugins.PluginFactory;
0034 import org.kde.kdeconnect.UserInterface.MainActivity;
0035 import org.kde.kdeconnect_tp.R;
0036 
0037 import java.io.IOException;
0038 import java.security.cert.Certificate;
0039 import java.security.cert.CertificateException;
0040 import java.util.Arrays;
0041 import java.util.Collection;
0042 import java.util.Collections;
0043 import java.util.Comparator;
0044 import java.util.List;
0045 import java.util.Vector;
0046 import java.util.concurrent.ConcurrentHashMap;
0047 import java.util.concurrent.CopyOnWriteArrayList;
0048 
0049 public class Device implements BaseLink.PacketReceiver {
0050 
0051     private final Context context;
0052 
0053     final DeviceInfo deviceInfo;
0054 
0055     private int notificationId;
0056     PairingHandler pairingHandler;
0057     private final CopyOnWriteArrayList<PairingHandler.PairingCallback> pairingCallbacks = new CopyOnWriteArrayList<>();
0058     private final CopyOnWriteArrayList<BaseLink> links = new CopyOnWriteArrayList<>();
0059     private DevicePacketQueue packetQueue;
0060     private List<String> supportedPlugins;
0061     private final ConcurrentHashMap<String, Plugin> plugins = new ConcurrentHashMap<>();
0062     private final ConcurrentHashMap<String, Plugin> pluginsWithoutPermissions = new ConcurrentHashMap<>();
0063     private final ConcurrentHashMap<String, Plugin> pluginsWithoutOptionalPermissions = new ConcurrentHashMap<>();
0064     private MultiValuedMap<String, String> pluginsByIncomingInterface = new ArrayListValuedHashMap<>();
0065     private final SharedPreferences settings;
0066     private final CopyOnWriteArrayList<PluginsChangedListener> pluginsChangedListeners = new CopyOnWriteArrayList<>();
0067     private String connectivityType;
0068 
0069     public boolean supportsPacketType(String type) {
0070         if (deviceInfo.incomingCapabilities == null) {
0071             return true;
0072         } else {
0073             return deviceInfo.incomingCapabilities.contains(type);
0074         }
0075     }
0076 
0077     public String getConnectivityType() {
0078         return connectivityType;
0079     }
0080 
0081     public interface PluginsChangedListener {
0082         void onPluginsChanged(@NonNull Device device);
0083     }
0084 
0085     /**
0086      * Constructor for remembered, already-trusted devices.
0087      * Given the deviceId, it will load the other properties from SharedPreferences.
0088      */
0089     Device(@NonNull Context context, @NonNull String deviceId) throws CertificateException {
0090         this.context = context;
0091         this.settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
0092         this.deviceInfo = DeviceInfo.loadFromSettings(context, deviceId, settings);
0093         this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.Paired);
0094         this.supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); // Assume all are supported until we receive capabilities
0095         this.connectivityType =  "";
0096         Log.i("Device","Loading trusted device: " + deviceInfo.name);
0097     }
0098 
0099     /**
0100      * Constructor for devices discovered but not trusted yet.
0101      * Gets the DeviceInfo by calling link.getDeviceInfo() on the link passed.
0102      * This constructor also calls addLink() with the link you pass to it, since it's not legal to have an unpaired Device with 0 links.
0103      */
0104     Device(@NonNull Context context, @NonNull BaseLink link) {
0105         this.context = context;
0106         this.deviceInfo = link.getDeviceInfo();
0107         this.settings = context.getSharedPreferences(deviceInfo.id, Context.MODE_PRIVATE);
0108         this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.NotPaired);
0109         this.supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); // Assume all are supported until we receive capabilities
0110         this.connectivityType = link.getLinkProvider().getName();
0111         Log.i("Device","Creating untrusted device: "+ deviceInfo.name);
0112         addLink(link);
0113     }
0114 
0115     public String getName() {
0116         return deviceInfo.name;
0117     }
0118 
0119     public Drawable getIcon() {
0120         return  deviceInfo.type.getIcon(context);
0121     }
0122 
0123     public DeviceType getDeviceType() {
0124         return deviceInfo.type;
0125     }
0126 
0127     public String getDeviceId() {
0128         return deviceInfo.id;
0129     }
0130 
0131     public Certificate getCertificate() {
0132         return deviceInfo.certificate;
0133     }
0134 
0135     public Context getContext() {
0136         return context;
0137     }
0138 
0139     //Returns 0 if the version matches, < 0 if it is older or > 0 if it is newer
0140     public int compareProtocolVersion() {
0141         return deviceInfo.protocolVersion - DeviceHelper.ProtocolVersion;
0142     }
0143 
0144 
0145     //
0146     // Pairing-related functions
0147     //
0148 
0149     public boolean isPaired() {
0150         return pairingHandler.getState() == PairingHandler.PairState.Paired;
0151     }
0152 
0153     public boolean isPairRequested() {
0154         return pairingHandler.getState() == PairingHandler.PairState.Requested;
0155     }
0156 
0157     public boolean isPairRequestedByPeer() {
0158         return pairingHandler.getState() == PairingHandler.PairState.RequestedByPeer;
0159     }
0160 
0161     public void addPairingCallback(PairingHandler.PairingCallback callback) {
0162         pairingCallbacks.add(callback);
0163     }
0164 
0165     public void removePairingCallback(PairingHandler.PairingCallback callback) {
0166         pairingCallbacks.remove(callback);
0167     }
0168 
0169     public void requestPairing() {
0170         pairingHandler.requestPairing();
0171     }
0172 
0173     public void unpair() {
0174         pairingHandler.unpair();
0175     }
0176 
0177     /* This method is called after accepting pair request form GUI */
0178     public void acceptPairing() {
0179         Log.i("KDE/Device", "Accepted pair request started by the other device");
0180         pairingHandler.acceptPairing();
0181     }
0182 
0183     /* This method is called after rejecting pairing from GUI */
0184     public void cancelPairing() {
0185         Log.i("KDE/Device", "This side cancelled the pair request");
0186         pairingHandler.cancelPairing();
0187     }
0188 
0189     PairingHandler.PairingCallback pairingCallback = new PairingHandler.PairingCallback() {
0190         @Override
0191         public void incomingPairRequest() {
0192             displayPairingNotification();
0193             for (PairingHandler.PairingCallback cb : pairingCallbacks) {
0194                 cb.incomingPairRequest();
0195             }
0196         }
0197 
0198         @Override
0199         public void pairingSuccessful() {
0200             Log.i("Device", "pairing successful, adding to trusted devices list");
0201 
0202             hidePairingNotification();
0203 
0204             // Store current device certificate so we can check it in the future (TOFU)
0205             deviceInfo.saveInSettings(Device.this.settings);
0206 
0207             // Store as trusted device
0208             SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
0209             preferences.edit().putBoolean(deviceInfo.id, true).apply();
0210 
0211             try {
0212                 reloadPluginsFromSettings();
0213 
0214                 for (PairingHandler.PairingCallback cb : pairingCallbacks) {
0215                     cb.pairingSuccessful();
0216                 }
0217             } catch (Exception e) {
0218                 Log.e("PairingHandler", "Exception in pairingSuccessful. Not unpairing because saving the trusted device succeeded");
0219                 e.printStackTrace();
0220             }
0221         }
0222 
0223         @Override
0224         public void pairingFailed(String error) {
0225             hidePairingNotification();
0226             for (PairingHandler.PairingCallback cb : pairingCallbacks) {
0227                 cb.pairingFailed(error);
0228             }
0229         }
0230 
0231         @Override
0232         public void unpaired() {
0233             Log.i("Device", "unpaired, removing from trusted devices list");
0234             SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
0235             preferences.edit().remove(deviceInfo.id).apply();
0236 
0237             SharedPreferences devicePreferences = context.getSharedPreferences(deviceInfo.id, Context.MODE_PRIVATE);
0238             devicePreferences.edit().clear().apply();
0239 
0240             for (PairingHandler.PairingCallback cb : pairingCallbacks) {
0241                 cb.unpaired();
0242             }
0243 
0244             reloadPluginsFromSettings();
0245         }
0246     };
0247 
0248     //
0249     // Notification related methods used during pairing
0250     //
0251 
0252     public void displayPairingNotification() {
0253 
0254         hidePairingNotification();
0255 
0256         notificationId = (int) System.currentTimeMillis();
0257 
0258         Intent intent = new Intent(getContext(), MainActivity.class);
0259         intent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId());
0260         intent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_PENDING);
0261         PendingIntent pendingIntent = PendingIntent.getActivity(getContext(), 1, intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
0262 
0263         Intent acceptIntent = new Intent(getContext(), MainActivity.class);
0264         Intent rejectIntent = new Intent(getContext(), MainActivity.class);
0265 
0266         acceptIntent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId());
0267         acceptIntent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_ACCEPTED);
0268 
0269         rejectIntent.putExtra(MainActivity.EXTRA_DEVICE_ID, getDeviceId());
0270         rejectIntent.putExtra(MainActivity.PAIR_REQUEST_STATUS, MainActivity.PAIRING_REJECTED);
0271 
0272         PendingIntent acceptedPendingIntent = PendingIntent.getActivity(getContext(), 2, acceptIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE);
0273         PendingIntent rejectedPendingIntent = PendingIntent.getActivity(getContext(), 4, rejectIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE);
0274 
0275         Resources res = getContext().getResources();
0276 
0277         final NotificationManager notificationManager = ContextCompat.getSystemService(getContext(), NotificationManager.class);
0278 
0279         String verificationKeyShort = SslHelper.getVerificationKey(SslHelper.certificate, deviceInfo.certificate).substring(8);
0280 
0281         Notification noti = new NotificationCompat.Builder(getContext(), NotificationHelper.Channels.DEFAULT)
0282                 .setContentTitle(res.getString(R.string.pairing_request_from, getName()))
0283                 .setContentText(res.getString(R.string.pairing_verification_code, verificationKeyShort))
0284                 .setTicker(res.getString(R.string.pair_requested))
0285                 .setSmallIcon(R.drawable.ic_notification)
0286                 .setContentIntent(pendingIntent)
0287                 .addAction(R.drawable.ic_accept_pairing_24dp, res.getString(R.string.pairing_accept), acceptedPendingIntent)
0288                 .addAction(R.drawable.ic_reject_pairing_24dp, res.getString(R.string.pairing_reject), rejectedPendingIntent)
0289                 .setAutoCancel(true)
0290                 .setDefaults(Notification.DEFAULT_ALL)
0291                 .build();
0292 
0293         NotificationHelper.notifyCompat(notificationManager, notificationId, noti);
0294     }
0295 
0296     public void hidePairingNotification() {
0297         final NotificationManager notificationManager = ContextCompat.getSystemService(getContext(),
0298                 NotificationManager.class);
0299         notificationManager.cancel(notificationId);
0300     }
0301 
0302     //
0303     // Link-related functions
0304     //
0305 
0306     public boolean isReachable() {
0307         return !links.isEmpty();
0308     }
0309 
0310     public void addLink(BaseLink link) {
0311         if (links.isEmpty()) {
0312             packetQueue = new DevicePacketQueue(this);
0313         }
0314         //FilesHelper.LogOpenFileCount();
0315 
0316         links.add(link);
0317 
0318         List linksToSort = Arrays.asList(links.toArray());
0319         Collections.sort(linksToSort, (Comparator<BaseLink>) (o1, o2) -> Integer.compare(o2.getLinkProvider().getPriority(), o1.getLinkProvider().getPriority()));
0320         links.clear();
0321         links.addAll(linksToSort);
0322 
0323         link.addPacketReceiver(this);
0324 
0325         boolean hasChanges = updateDeviceInfo(link.getDeviceInfo());
0326 
0327         if (hasChanges || links.size() == 1) {
0328             reloadPluginsFromSettings();
0329         }
0330     }
0331 
0332     public void removeLink(BaseLink link) {
0333         //FilesHelper.LogOpenFileCount();
0334 
0335         link.removePacketReceiver(this);
0336         links.remove(link);
0337         Log.i("KDE/Device", "removeLink: " + link.getLinkProvider().getName() + " -> " + getName() + " active links: " + links.size());
0338         if (links.isEmpty()) {
0339             reloadPluginsFromSettings();
0340             if (packetQueue != null) {
0341                 packetQueue.disconnected();
0342                 packetQueue = null;
0343             }
0344         }
0345     }
0346 
0347     public boolean updateDeviceInfo(@NonNull DeviceInfo newDeviceInfo) {
0348 
0349         boolean hasChanges = false;
0350         if (!deviceInfo.name.equals(newDeviceInfo.name) || deviceInfo.type != newDeviceInfo.type) {
0351             hasChanges = true;
0352             deviceInfo.name = newDeviceInfo.name;
0353             deviceInfo.type = newDeviceInfo.type;
0354             if (isPaired()) {
0355                 deviceInfo.saveInSettings(settings);
0356             }
0357         }
0358 
0359         if (deviceInfo.outgoingCapabilities != newDeviceInfo.outgoingCapabilities ||
0360                 deviceInfo.incomingCapabilities != newDeviceInfo.incomingCapabilities) {
0361             if (newDeviceInfo.outgoingCapabilities != null && newDeviceInfo.incomingCapabilities != null) {
0362                 hasChanges = true;
0363                 Log.i("updateDeviceInfo", "Updating supported plugins according to new capabilities");
0364                 supportedPlugins = new Vector<>(PluginFactory.pluginsForCapabilities(newDeviceInfo.incomingCapabilities, newDeviceInfo.outgoingCapabilities));
0365             }
0366         }
0367 
0368         return hasChanges;
0369     }
0370 
0371     @Override
0372     public void onPacketReceived(@NonNull NetworkPacket np) {
0373 
0374         DeviceStats.countReceived(getDeviceId(), np.getType());
0375 
0376         if (NetworkPacket.PACKET_TYPE_PAIR.equals(np.getType())) {
0377             Log.i("KDE/Device", "Pair packet");
0378             pairingHandler.packetReceived(np);
0379         } else if (isPaired()) {
0380             // pluginsByIncomingInterface may not be built yet
0381             if(pluginsByIncomingInterface.isEmpty()) {
0382                 reloadPluginsFromSettings();
0383             }
0384 
0385             Collection<String> targetPlugins = pluginsByIncomingInterface.get(np.getType());
0386             if (!targetPlugins.isEmpty()) { // When a key doesn't exist the multivaluemap returns an empty collection, so we don't need to check for null
0387                 for (String pluginKey : targetPlugins) {
0388                     Plugin plugin = plugins.get(pluginKey);
0389                     try {
0390                         plugin.onPacketReceived(np);
0391                     } catch (Exception e) {
0392                         Log.e("KDE/Device", "Exception in " + plugin.getPluginKey() + "'s onPacketReceived()", e);
0393                         //try { Log.e("KDE/Device", "NetworkPacket:" + np.serialize()); } catch (Exception _) { }
0394                     }
0395                 }
0396             } else {
0397                 Log.w("Device", "Ignoring packet with type " + np.getType() + " because no plugin can handle it");
0398             }
0399         } else {
0400 
0401             //Log.e("KDE/onPacketReceived","Device not paired, will pass packet to unpairedPacketListeners");
0402 
0403             // If it is pair packet, it should be captured by "if" at start
0404             // If not and device is paired, it should be captured by isPaired
0405             // Else unpair, this handles the situation when one device unpairs, but other dont know like unpairing when wi-fi is off
0406 
0407             unpair();
0408 
0409             // 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".
0410             Collection<String> targetPlugins = pluginsByIncomingInterface.get(np.getType());
0411             if (!targetPlugins.isEmpty()) {
0412                 for (String pluginKey : targetPlugins) {
0413                     Plugin plugin = plugins.get(pluginKey);
0414                     try {
0415                         plugin.onUnpairedDevicePacketReceived(np);
0416                     } catch (Exception e) {
0417                         Log.e("KDE/Device", "Exception in " + plugin.getDisplayName() + "'s onPacketReceived() in unPairedPacketListeners", e);
0418                     }
0419                 }
0420             } else {
0421                 Log.e("Device", "Ignoring packet with type " + np.getType() + " because no plugin can handle it");
0422             }
0423         }
0424     }
0425 
0426     public static abstract class SendPacketStatusCallback {
0427         public abstract void onSuccess();
0428 
0429         public abstract void onFailure(Throwable e);
0430 
0431         public void onPayloadProgressChanged(int percent) {
0432         }
0433     }
0434 
0435     private final SendPacketStatusCallback defaultCallback = new SendPacketStatusCallback() {
0436         @Override
0437         public void onSuccess() {
0438         }
0439 
0440         @Override
0441         public void onFailure(Throwable e) {
0442             Log.e("KDE/sendPacket", "Exception", e);
0443         }
0444     };
0445 
0446     @AnyThread
0447     public void sendPacket(@NonNull NetworkPacket np) {
0448         sendPacket(np, -1, defaultCallback);
0449     }
0450 
0451     @AnyThread
0452     public void sendPacket(@NonNull NetworkPacket np, int replaceID) {
0453         sendPacket(np, replaceID, defaultCallback);
0454     }
0455 
0456     @WorkerThread
0457     public boolean sendPacketBlocking(@NonNull NetworkPacket np) {
0458         return sendPacketBlocking(np, defaultCallback);
0459     }
0460 
0461     @AnyThread
0462     public void sendPacket(@NonNull final NetworkPacket np, @NonNull final SendPacketStatusCallback callback) {
0463         sendPacket(np, -1, callback);
0464     }
0465 
0466     /**
0467      * Send a packet to the device asynchronously
0468      * @param np The packet
0469      * @param replaceID If positive, replaces all unsent packets with the same replaceID
0470      * @param callback A callback for success/failure
0471      */
0472     @AnyThread
0473     public void sendPacket(@NonNull final NetworkPacket np, int replaceID, @NonNull final SendPacketStatusCallback callback) {
0474         if (packetQueue == null) {
0475             callback.onFailure(new Exception("Device disconnected!"));
0476         } else {
0477             packetQueue.addPacket(np, replaceID, callback);
0478         }
0479     }
0480 
0481     /**
0482      * Check if we still have an unsent packet in the queue with the given ID.
0483      * If so, remove it from the queue and return it
0484      * @param replaceID The replace ID (must be positive)
0485      * @return The found packet, or null
0486      */
0487     public NetworkPacket getAndRemoveUnsentPacket(int replaceID) {
0488         if (packetQueue == null) {
0489             return null;
0490         } else {
0491             return packetQueue.getAndRemoveUnsentPacket(replaceID);
0492         }
0493     }
0494 
0495     @WorkerThread
0496     public boolean sendPacketBlocking(@NonNull final NetworkPacket np, @NonNull final SendPacketStatusCallback callback) {
0497         return sendPacketBlocking(np, callback, false);
0498     }
0499 
0500     /**
0501      * Send {@code np} over one of this device's connected {@link #links}.
0502      *
0503      * @param np                        the packet to send
0504      * @param callback                  a callback that can receive realtime updates
0505      * @param sendPayloadFromSameThread when set to true and np contains a Payload, this function
0506      *                                  won't return until the Payload has been received by the
0507      *                                  other end, or times out after 10 seconds
0508      * @return true if the packet was sent ok, false otherwise
0509      * @see BaseLink#sendPacket(NetworkPacket, SendPacketStatusCallback, boolean)
0510      */
0511     @WorkerThread
0512     public boolean sendPacketBlocking(@NonNull final NetworkPacket np, @NonNull final SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) {
0513 
0514         boolean success = false;
0515         for (final BaseLink link : links) {
0516             if (link == null) continue;
0517             try {
0518                 success = link.sendPacket(np, callback, sendPayloadFromSameThread);
0519             } catch (IOException e) {
0520                 e.printStackTrace();
0521             }
0522             DeviceStats.countSent(getDeviceId(), np.getType(), success);
0523             if (success) break;
0524         }
0525 
0526         if (!success) {
0527             Log.e("KDE/sendPacket", "No device link (of " + links.size() + " available) could send the packet. Packet " + np.getType() + " to " + deviceInfo.name + " lost!");
0528         }
0529 
0530         return success;
0531 
0532     }
0533     //
0534     // Plugin-related functions
0535     //
0536 
0537     @Nullable
0538     public <T extends Plugin> T getPlugin(Class<T> pluginClass) {
0539         Plugin plugin = getPlugin(Plugin.getPluginKey(pluginClass));
0540         return (T) plugin;
0541     }
0542 
0543     @Nullable
0544     public Plugin getPlugin(String pluginKey) {
0545         return plugins.get(pluginKey);
0546     }
0547 
0548     @Nullable
0549     public Plugin getPluginIncludingWithoutPermissions(String pluginKey) {
0550         Plugin p = plugins.get(pluginKey);
0551         if (p == null) {
0552             p = pluginsWithoutPermissions.get(pluginKey);
0553         }
0554         return p;
0555     }
0556 
0557     private synchronized boolean addPlugin(final String pluginKey) {
0558         Plugin existing = plugins.get(pluginKey);
0559         if (existing != null) {
0560 
0561             if (!existing.isCompatible()) {
0562                 Log.d("KDE/addPlugin", "Minimum requirements (e.g. API level) not fulfilled " + pluginKey);
0563                 return false;
0564             }
0565 
0566             //Log.w("KDE/addPlugin","plugin already present:" + pluginKey);
0567             if (existing.checkOptionalPermissions()) {
0568                 Log.d("KDE/addPlugin", "Optional Permissions OK " + pluginKey);
0569                 pluginsWithoutOptionalPermissions.remove(pluginKey);
0570             } else {
0571                 Log.d("KDE/addPlugin", "No optional permission " + pluginKey);
0572                 pluginsWithoutOptionalPermissions.put(pluginKey, existing);
0573             }
0574             return true;
0575         }
0576 
0577         final Plugin plugin = PluginFactory.instantiatePluginForDevice(context, pluginKey, this);
0578         if (plugin == null) {
0579             Log.e("KDE/addPlugin", "could not instantiate plugin: " + pluginKey);
0580             return false;
0581         }
0582 
0583         if (!plugin.isCompatible()) {
0584             Log.d("KDE/addPlugin", "Minimum requirements (e.g. API level) not fulfilled " + pluginKey);
0585             return false;
0586         }
0587 
0588         if (!plugin.checkRequiredPermissions()) {
0589             Log.d("KDE/addPlugin", "No permission " + pluginKey);
0590             plugins.remove(pluginKey);
0591             pluginsWithoutPermissions.put(pluginKey, plugin);
0592             return false;
0593         } else {
0594             Log.d("KDE/addPlugin", "Permissions OK " + pluginKey);
0595             plugins.put(pluginKey, plugin);
0596             pluginsWithoutPermissions.remove(pluginKey);
0597             if (plugin.checkOptionalPermissions()) {
0598                 Log.d("KDE/addPlugin", "Optional Permissions OK " + pluginKey);
0599                 pluginsWithoutOptionalPermissions.remove(pluginKey);
0600             } else {
0601                 Log.d("KDE/addPlugin", "No optional permission " + pluginKey);
0602                 pluginsWithoutOptionalPermissions.put(pluginKey, plugin);
0603             }
0604         }
0605 
0606         try {
0607             return plugin.onCreate();
0608         } catch (Exception e) {
0609             Log.e("KDE/addPlugin", "plugin failed to load " + pluginKey, e);
0610             return false;
0611         }
0612     }
0613 
0614     private synchronized boolean removePlugin(String pluginKey) {
0615 
0616         Plugin plugin = plugins.remove(pluginKey);
0617 
0618         if (plugin == null) {
0619             return false;
0620         }
0621 
0622         try {
0623             plugin.onDestroy();
0624             //Log.e("removePlugin","removed " + pluginKey);
0625         } catch (Exception e) {
0626             Log.e("KDE/removePlugin", "Exception calling onDestroy for plugin " + pluginKey, e);
0627         }
0628 
0629         return true;
0630     }
0631 
0632     public void setPluginEnabled(String pluginKey, boolean value) {
0633         settings.edit().putBoolean(pluginKey, value).apply();
0634         reloadPluginsFromSettings();
0635     }
0636 
0637     public boolean isPluginEnabled(String pluginKey) {
0638         boolean enabledByDefault = PluginFactory.getPluginInfo(pluginKey).isEnabledByDefault();
0639         return settings.getBoolean(pluginKey, enabledByDefault);
0640     }
0641 
0642     public void reloadPluginsFromSettings() {
0643         Log.i("Device", deviceInfo.name +": reloading plugins");
0644         MultiValuedMap<String, String> newPluginsByIncomingInterface = new ArrayListValuedHashMap<>();
0645 
0646         for (String pluginKey : supportedPlugins) {
0647             PluginFactory.PluginInfo pluginInfo = PluginFactory.getPluginInfo(pluginKey);
0648 
0649             boolean pluginEnabled = false;
0650             boolean listenToUnpaired = pluginInfo.listenToUnpaired();
0651             if ((isPaired() || listenToUnpaired) && isReachable()) {
0652                 pluginEnabled = isPluginEnabled(pluginKey);
0653             }
0654 
0655             if (pluginEnabled) {
0656                 boolean success = addPlugin(pluginKey);
0657                 if (success) {
0658                     for (String packetType : pluginInfo.getSupportedPacketTypes()) {
0659                         newPluginsByIncomingInterface.put(packetType, pluginKey);
0660                     }
0661                 } else {
0662                     removePlugin(pluginKey);
0663                 }
0664             } else {
0665                 removePlugin(pluginKey);
0666             }
0667         }
0668 
0669         pluginsByIncomingInterface = newPluginsByIncomingInterface;
0670 
0671         onPluginsChanged();
0672     }
0673 
0674     public void onPluginsChanged() {
0675         for (PluginsChangedListener listener : pluginsChangedListeners) {
0676             listener.onPluginsChanged(Device.this);
0677         }
0678     }
0679 
0680     public ConcurrentHashMap<String, Plugin> getLoadedPlugins() {
0681         return plugins;
0682     }
0683 
0684     public ConcurrentHashMap<String, Plugin> getPluginsWithoutPermissions() {
0685         return pluginsWithoutPermissions;
0686     }
0687 
0688     public ConcurrentHashMap<String, Plugin> getPluginsWithoutOptionalPermissions() {
0689         return pluginsWithoutOptionalPermissions;
0690     }
0691 
0692     public void addPluginsChangedListener(PluginsChangedListener listener) {
0693         pluginsChangedListeners.add(listener);
0694     }
0695 
0696     public void removePluginsChangedListener(PluginsChangedListener listener) {
0697         pluginsChangedListeners.remove(listener);
0698     }
0699 
0700     public void disconnect() {
0701         for (BaseLink link : links) {
0702             link.disconnect();
0703         }
0704     }
0705 
0706     public List<String> getSupportedPlugins() {
0707         return supportedPlugins;
0708     }
0709 
0710 }