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 }