org.kde.kdeconnect.Device.java Source code

Java tutorial

Introduction

Here is the source code for org.kde.kdeconnect.Device.java

Source

/*
 * Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License or (at your option) version 3 or any later version
 * accepted by the membership of KDE e.V. (or its successor approved
 * by the membership of KDE e.V.), which shall act as a proxy
 * defined in Section 14 of version 3 of the license.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. 
*/

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.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.util.Base64;
import android.util.Log;

import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
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.MaterialActivity;
import org.kde.kdeconnect_tp.R;

import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
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.PackageReceiver {

    private final Context context;

    private final String deviceId;
    private String name;
    public PublicKey publicKey;
    public Certificate certificate;
    private int notificationId;
    private int protocolVersion;

    private DeviceType deviceType;
    private PairStatus pairStatus;

    private final CopyOnWriteArrayList<PairingCallback> pairingCallback = new CopyOnWriteArrayList<>();
    private Map<String, BasePairingHandler> pairingHandlers = new HashMap<>();

    private final CopyOnWriteArrayList<BaseLink> links = new CopyOnWriteArrayList<>();

    private List<String> m_supportedPlugins = new ArrayList<>();
    private final ConcurrentHashMap<String, Plugin> plugins = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Plugin> failedPlugins = new ConcurrentHashMap<>();
    private Map<String, ArrayList<String>> pluginsByIncomingInterface;

    private final SharedPreferences settings;

    private final CopyOnWriteArrayList<PluginsChangedListener> pluginsChangedListeners = new CopyOnWriteArrayList<>();

    public interface PluginsChangedListener {
        void onPluginsChanged(Device device);
    }

    public enum PairStatus {
        NotPaired, Paired
    }

    public enum DeviceType {
        Phone, Tablet, Computer;

        public static DeviceType FromString(String s) {
            if ("tablet".equals(s))
                return Tablet;
            if ("phone".equals(s))
                return Phone;
            return Computer; //Default
        }

        public String toString() {
            switch (this) {
            case Tablet:
                return "tablet";
            case Phone:
                return "phone";
            default:
                return "desktop";
            }
        }
    }

    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 = NetworkPackage.ProtocolVersion; //We don't know it yet
        this.deviceType = DeviceType.FromString(settings.getString("deviceType", "desktop"));

        try {
            String publicKeyStr = settings.getString("publicKey", null);
            if (publicKeyStr != null) {
                byte[] publicKeyBytes = Base64.decode(publicKeyStr, 0);
                publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes));
            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("KDE/Device", "Exception deserializing stored public key for device");
        }

        //Assume every plugin is supported until addLink is called and we can get the actual list
        m_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, NetworkPackage 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;
        this.publicKey = null;

        settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);

        addLink(np, dl);
    }

    public String getName() {
        return name != null ? name : context.getString(R.string.unknown_device);
    }

    public Drawable getIcon() {
        int drawableId;
        switch (deviceType) {
        case Phone:
            drawableId = R.drawable.ic_device_phone;
            break;
        case Tablet:
            drawableId = R.drawable.ic_device_tablet;
            break;
        default:
            drawableId = R.drawable.ic_device_laptop;
        }
        return ContextCompat.getDrawable(context, drawableId);
    }

    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 - NetworkPackage.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();

        switch (pairStatus) {
        case Paired:
            for (PairingCallback cb : pairingCallback) {
                cb.pairingFailed(res.getString(R.string.error_already_paired));
            }
            return;
        case NotPaired:
            ;
        }

        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 package, instead it unpairs internally by deleting trusted device info. . Likely to be called after sending package 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(), MaterialActivity.class);
        intent.putExtra("deviceId", getDeviceId());
        intent.putExtra("notificationId", notificationId);
        PendingIntent pendingIntent = PendingIntent.getActivity(getContext(), 0, intent,
                PendingIntent.FLAG_CANCEL_CURRENT);

        Intent acceptIntent = new Intent(getContext(), MaterialActivity.class);
        Intent rejectIntent = new Intent(getContext(), MaterialActivity.class);

        acceptIntent.putExtra("deviceId", getDeviceId());
        acceptIntent.putExtra("notificationId", notificationId);
        acceptIntent.setAction("action " + System.currentTimeMillis());
        acceptIntent.putExtra(MaterialActivity.PAIR_REQUEST_STATUS, MaterialActivity.PAIRING_ACCEPTED);

        rejectIntent.putExtra("deviceId", getDeviceId());
        rejectIntent.putExtra("notificationId", notificationId);
        rejectIntent.setAction("action " + System.currentTimeMillis());
        rejectIntent.putExtra(MaterialActivity.PAIR_REQUEST_STATUS, MaterialActivity.PAIRING_REJECTED);

        PendingIntent acceptedPendingIntent = PendingIntent.getActivity(getContext(), 2, acceptIntent,
                PendingIntent.FLAG_ONE_SHOT);
        PendingIntent rejectedPendingIntent = PendingIntent.getActivity(getContext(), 4, rejectIntent,
                PendingIntent.FLAG_ONE_SHOT);

        Resources res = getContext().getResources();

        Notification noti = new NotificationCompat.Builder(getContext())
                .setContentTitle(res.getString(R.string.pairing_request_from, getName()))
                .setContentText(res.getString(R.string.tap_to_answer)).setContentIntent(pendingIntent)
                .setTicker(res.getString(R.string.pair_requested)).setSmallIcon(R.drawable.ic_notification)
                .addAction(R.drawable.ic_accept_pairing, res.getString(R.string.pairing_accept),
                        acceptedPendingIntent)
                .addAction(R.drawable.ic_reject_pairing, res.getString(R.string.pairing_reject),
                        rejectedPendingIntent)
                .setAutoCancel(true).setDefaults(Notification.DEFAULT_ALL).build();

        final NotificationManager notificationManager = (NotificationManager) getContext()
                .getSystemService(Context.NOTIFICATION_SERVICE);
        NotificationHelper.notifyCompat(notificationManager, notificationId, noti);

        BackgroundService.addGuiInUseCounter(context);
    }

    public void hidePairingNotification() {
        final NotificationManager notificationManager = (NotificationManager) getContext()
                .getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.cancel(notificationId);
        BackgroundService.removeGuiInUseCounter(context);
    }

    //
    // ComputerLink-related functions
    //

    public boolean isReachable() {
        return !links.isEmpty();
    }

    public void addLink(NetworkPackage identityPackage, BaseLink link) {
        //FilesHelper.LogOpenFileCount();

        this.protocolVersion = identityPackage.getInt("protocolVersion");

        if (identityPackage.has("deviceName")) {
            this.name = identityPackage.getString("deviceName", this.name);
            SharedPreferences.Editor editor = settings.edit();
            editor.putString("deviceName", this.name);
            editor.apply();
        }

        if (identityPackage.has("deviceType")) {
            this.deviceType = DeviceType.FromString(identityPackage.getString("deviceType", "desktop"));
        }

        if (identityPackage.has("certificate")) {
            String certificateString = identityPackage.getString("certificate");

            try {
                byte[] certificateBytes = Base64.decode(certificateString, 0);
                certificate = SslHelper.parseCertificate(certificateBytes);
                Log.i("KDE/Device", "Got certificate ");
            } catch (Exception e) {
                e.printStackTrace();
                Log.e("KDE/Device", "Error getting certificate");

            }
        }

        links.add(link);

        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) {
            e.printStackTrace();
            Log.e("KDE/Device", "Exception reading our own private key"); //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<String> outgoingCapabilities = identityPackage.getStringSet("outgoingCapabilities", null);
        Set<String> incomingCapabilities = identityPackage.getStringSet("incomingCapabilities", null);
        if (incomingCapabilities != null && outgoingCapabilities != null) {
            m_supportedPlugins = new Vector<>(
                    PluginFactory.pluginsForCapabilities(context, incomingCapabilities, outgoingCapabilities));
        } else {
            m_supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins());
        }

        link.addPackageReceiver(this);

        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.removePackageReceiver(this);
        links.remove(link);
        Log.i("KDE/Device", "removeLink: " + link.getLinkProvider().getName() + " -> " + getName()
                + " active links: " + links.size());
        if (links.isEmpty()) {
            reloadPluginsFromSettings();
        }
    }

    @Override
    public void onPackageReceived(NetworkPackage np) {

        hackToMakeRetrocompatiblePacketTypes(np);

        if (NetworkPackage.PACKAGE_TYPE_PAIR.equals(np.getType())) {

            Log.i("KDE/Device", "Pair package");

            for (BasePairingHandler ph : pairingHandlers.values()) {
                try {
                    ph.packageReceived(np);
                } catch (Exception e) {
                    e.printStackTrace();
                    Log.e("PairingPackageReceived", "Exception");
                }
            }
        } else if (isPaired()) {

            //If capabilities are not supported, iterate all plugins
            Collection<String> targetPlugins = pluginsByIncomingInterface.get(np.getType());
            if (targetPlugins != null && !targetPlugins.isEmpty()) {
                for (String pluginKey : targetPlugins) {
                    Plugin plugin = plugins.get(pluginKey);
                    try {
                        plugin.onPackageReceived(np);
                    } catch (Exception e) {
                        e.printStackTrace();
                        Log.e("KDE/Device", "Exception in " + plugin.getPluginKey() + "'s onPackageReceived()");
                        //try { Log.e("KDE/Device", "NetworkPackage:" + np.serialize()); } catch (Exception _) { }
                    }
                }
            } else {
                Log.w("Device", "Ignoring packet with type " + np.getType() + " because no plugin can handle it");
            }
        } else {

            //Log.e("KDE/onPackageReceived","Device not paired, will pass package to unpairedPackageListeners");

            // If it is pair package, 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();

            //If capabilities are not supported, iterate all plugins
            Collection<String> targetPlugins = pluginsByIncomingInterface.get(np.getType());
            if (targetPlugins != null && !targetPlugins.isEmpty()) {
                for (String pluginKey : targetPlugins) {
                    Plugin plugin = plugins.get(pluginKey);
                    try {
                        plugin.onUnpairedDevicePackageReceived(np);
                    } catch (Exception e) {
                        e.printStackTrace();
                        Log.e("KDE/Device", "Exception in " + plugin.getDisplayName()
                                + "'s onPackageReceived() in unPairedPackageListeners");
                    }
                }
            } else {
                Log.e("Device", "Ignoring packet with type " + np.getType() + " because no plugin can handle it");
            }
        }

    }

    public static abstract class SendPackageStatusCallback {
        public abstract void onSuccess();

        public abstract void onFailure(Throwable e);

        public void onProgressChanged(int percent) {
        }
    }

    private SendPackageStatusCallback defaultCallback = new SendPackageStatusCallback() {
        @Override
        public void onSuccess() {
        }

        @Override
        public void onFailure(Throwable e) {
            if (e != null) {
                e.printStackTrace();
            } else {
                Log.e("KDE/sendPackage", "Unknown (null) exception");
            }
        }
    };

    public void sendPackage(NetworkPackage np) {
        sendPackage(np, defaultCallback);
    }

    public boolean sendPackageBlocking(NetworkPackage np) {
        return sendPackageBlocking(np, defaultCallback);
    }

    //Async
    public void sendPackage(final NetworkPackage np, final SendPackageStatusCallback callback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                sendPackageBlocking(np, callback);
            }
        }).start();
    }

    public boolean sendPackageBlocking(final NetworkPackage np, final SendPackageStatusCallback callback) {

        /*
        if (!m_outgoingCapabilities.contains(np.getType()) && !NetworkPackage.protocolPackageTypes.contains(np.getType())) {
        Log.e("Device/sendPackage", "Plugin tried to send an undeclared package: " + np.getType());
        Log.w("Device/sendPackage", "Declared outgoing package types: " + Arrays.toString(m_outgoingCapabilities.toArray()));
        }
        */

        hackToMakeRetrocompatiblePacketTypes(np);

        boolean useEncryption = (protocolVersion < LanLinkProvider.MIN_VERSION_WITH_SSL_SUPPORT
                && (!np.getType().equals(NetworkPackage.PACKAGE_TYPE_PAIR) && isPaired()));

        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
            if (useEncryption) {
                success = link.sendPackageEncrypted(np, callback, publicKey);
            } else {
                success = link.sendPackage(np, callback);
            }
            if (success)
                break; //If the link didn't call sendSuccess(), try the next one
        }

        if (!success) {
            Log.e("KDE/sendPackage", "No device link (of " + links.size()
                    + " available) could send the package. Package " + np.getType() + " to " + name + " lost!");
        }

        return success;

    }
    //
    // Plugin-related functions
    //

    public <T extends Plugin> T getPlugin(Class<T> pluginClass) {
        return (T) getPlugin(Plugin.getPluginKey(pluginClass));
    }

    public <T extends Plugin> T getPlugin(Class<T> pluginClass, boolean includeFailed) {
        return (T) getPlugin(Plugin.getPluginKey(pluginClass), includeFailed);
    }

    public Plugin getPlugin(String pluginKey) {
        return getPlugin(pluginKey, false);
    }

    public Plugin getPlugin(String pluginKey, boolean includeFailed) {
        Plugin plugin = plugins.get(pluginKey);
        if (includeFailed && plugin == null) {
            plugin = failedPlugins.get(pluginKey);
        }
        return plugin;
    }

    private synchronized boolean addPlugin(final String pluginKey) {
        Plugin existing = plugins.get(pluginKey);
        if (existing != null) {
            //Log.w("KDE/addPlugin","plugin already present:" + pluginKey);
            return true;
        }

        final Plugin plugin = PluginFactory.instantiatePluginForDevice(context, pluginKey, this);
        if (plugin == null) {
            Log.e("KDE/addPlugin", "could not instantiate plugin: " + pluginKey);
            //Can't put a null
            //failedPlugins.put(pluginKey, null);
            return false;
        }

        boolean success;
        try {
            success = plugin.onCreate();
        } catch (Exception e) {
            success = false;
            e.printStackTrace();
            Log.e("KDE/addPlugin", "Exception loading plugin " + pluginKey);
        }

        if (success) {
            //Log.e("addPlugin","added " + pluginKey);
            failedPlugins.remove(pluginKey);
            plugins.put(pluginKey, plugin);
        } else {
            Log.e("KDE/addPlugin", "plugin failed to load " + pluginKey);
            plugins.remove(pluginKey);
            failedPlugins.put(pluginKey, plugin);
        }

        return success;
    }

    private synchronized boolean removePlugin(String pluginKey) {

        Plugin plugin = plugins.remove(pluginKey);
        Plugin failedPlugin = failedPlugins.remove(pluginKey);

        if (plugin == null) {
            if (failedPlugin == null) {
                //Not found
                return false;
            }
            plugin = failedPlugin;
        }

        try {
            plugin.onDestroy();
            //Log.e("removePlugin","removed " + pluginKey);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("KDE/removePlugin", "Exception calling onDestroy for plugin " + pluginKey);
        }

        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(context, pluginKey).isEnabledByDefault();
        boolean enabled = settings.getBoolean(pluginKey, enabledByDefault);
        return enabled;
    }

    public void reloadPluginsFromSettings() {

        failedPlugins.clear();

        HashMap<String, ArrayList<String>> newPluginsByIncomingInterface = new HashMap<>();

        for (String pluginKey : m_supportedPlugins) {

            PluginFactory.PluginInfo pluginInfo = PluginFactory.getPluginInfo(context, 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 packageType : pluginInfo.getSupportedPackageTypes()) {
                        packageType = hackToMakeRetrocompatiblePacketTypes(packageType);
                        ArrayList<String> plugins = newPluginsByIncomingInterface.get(packageType);
                        if (plugins == null)
                            plugins = new ArrayList<>();
                        plugins.add(pluginKey);
                        newPluginsByIncomingInterface.put(packageType, plugins);
                    }
                }
            } else {
                removePlugin(pluginKey);
            }

        }

        pluginsByIncomingInterface = newPluginsByIncomingInterface;

        onPluginsChanged();
    }

    public void onPluginsChanged() {
        for (PluginsChangedListener listener : pluginsChangedListeners) {
            listener.onPluginsChanged(Device.this);
        }
    }

    public ConcurrentHashMap<String, Plugin> getLoadedPlugins() {
        return plugins;
    }

    public ConcurrentHashMap<String, Plugin> getFailedPlugins() {
        return failedPlugins;
    }

    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<String> getSupportedPlugins() {
        return m_supportedPlugins;
    }

    public void hackToMakeRetrocompatiblePacketTypes(NetworkPackage np) {
        if (protocolVersion >= 6)
            return;
        np.mType = np.getType().replace(".request", "");
    }

    public String hackToMakeRetrocompatiblePacketTypes(String type) {
        if (protocolVersion >= 6)
            return type;
        return type.replace(".request", "");
    }

}