eu.fistar.sdcs.pa.PAManagerService.java Source code

Java tutorial

Introduction

Here is the source code for eu.fistar.sdcs.pa.PAManagerService.java

Source

/**
 * Copyright (C) 2014 Consorzio Roma Ricerche
 * All rights reserved
 *
 * This file is part of the Protocol Adapter software, available at
 * https://github.com/theIoTLab/ProtocolAdapter .
 *
 * The Protocol Adapter is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either 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://opensource.org/licenses/LGPL-3.0
 *
 * Contact Consorzio Roma Ricerche (protocoladapter@gmail.com)
 */

package eu.fistar.sdcs.pa;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;

import eu.fistar.sdcs.pa.common.Capabilities;
import eu.fistar.sdcs.pa.common.IProtocolAdapter;
import eu.fistar.sdcs.pa.common.IProtocolAdapterListener;
import eu.fistar.sdcs.pa.common.PAAndroidConstants;
import eu.fistar.sdcs.pa.common.PAAndroidConstants.*;
import eu.fistar.sdcs.pa.common.DeviceDescription;
import eu.fistar.sdcs.pa.common.Observation;
import eu.fistar.sdcs.pa.common.IDeviceAdapterListener;
import eu.fistar.sdcs.pa.common.da.IDeviceAdapter;

/**
 * This class is the implementation of the Protocol Adapter. It is a bound service which can,
 * in turn, bind other services (the Device Adapters). It implements both the IProtocolAdapter
 * interface for communication with the application, and the IDeviceAdapterListener interface for
 * communication with Device Adapters.
 *
 * @author Marcello Morena
 * @author Alexandru Serbanati
 */
public class PAManagerService extends Service {

    // SharedPreferences related constants
    private final static String SHPREF_FILENAME = "listSync";
    private final static String SHPREF_WHITELIST_NAME = "whitelist";
    private final static String SHPREF_BLACKLIST_NAME = "blacklist";

    // Variables for storing white/black lists
    private final List<String> blacklist = new CopyOnWriteArrayList<String>();
    private final List<String> whitelist = new CopyOnWriteArrayList<String>();

    // Variables for Device Adapter management
    private Map<String, Capabilities> availableDAs = new HashMap<String, Capabilities>(); // <[DA ID], [DACapabilities]>
    private Map<String, IDeviceAdapter> connectedDAs = new HashMap<String, IDeviceAdapter>(); // <[DA ID], [DAInstance]>
    private Map<String, DAConnection> daConnections = new HashMap<String, DAConnection>();

    // Variables for Application Management
    private IProtocolAdapterListener appApi;

    // Variables for Protocol Adapter management
    private boolean firstStart = true;

    // Implementation of the Protocol Adapter API (IProtocolAdapter) to pass to the Application
    private final IProtocolAdapter.Stub appEndpoint = new IProtocolAdapter.Stub() {

        /**
         * Returns a list of all the devices connected at the moment in all Device Adapters.
         *
         * @return A List containing the DeviceDescription of all the connected devices
         */
        @Override
        public List<DeviceDescription> getConnectedDevices() throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Fetching connected devices");

            List<DeviceDescription> connectedDev = new ArrayList<DeviceDescription>();

            // Scan all connected DAs
            for (String tmpDaName : connectedDAs.keySet()) {

                // Retrieve the Capabilities of the DA
                Capabilities cap = availableDAs.get(tmpDaName);

                // If the DA has access to connected devices, add its devices to the general list
                if (cap.isCommunicationInitiator()) {
                    connectedDev.addAll(connectedDAs.get(tmpDaName).getConnectedDevices());
                }
            }

            // Return the list of devices
            return connectedDev;
        }

        /**
         * Returns a map containing the Device ID of all the devices paired with the smartphone that can
         * be handled by at least one DA as the key, and a list of DA IDs of DA that can handle that
         * device as the value.
         *
         * @return A map containing the Device ID of all the paired devices as the key, and a list of DA
         * IDs that can handle that device as the value (Map<String, List<String>>).
         */
        @Override
        public Map<String, List<String>> getDADevices() throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Fetching paired devices address");

            Map<String, List<String>> daDev = new HashMap<String, List<String>>();

            // For each connected DA...
            for (IDeviceAdapter tmpDa : connectedDAs.values()) {

                // ... retrieve the Capabilities of the DA...
                Capabilities cap = tmpDa.getDACapabilities();

                // ... then if it can provide the paired devices...
                if (cap.canProvideAvailableDevice()) {

                    // ... retrieve all the managed paired devices and the DA ID...
                    List<String> daPairedDev = tmpDa.getPairedDevicesAddress();
                    String daId = cap.getPackageName();

                    // ... and for each device...
                    for (String dev : daPairedDev) {

                        // ... check if it's already contained in the original HashMap...
                        List<String> daHandlingDevice = daDev.get(dev);

                        // ... if so, then add the actual DA to the list of DAs handling this device
                        if (daHandlingDevice != null) {
                            daHandlingDevice.add(daId);
                        }
                        // Otherwise create a new entry in the map for the device
                        else {
                            daHandlingDevice = new ArrayList<String>();
                            daHandlingDevice.add(daId);
                            daDev.put(dev, daHandlingDevice);
                        }
                    }
                }
            }

            // Return the HashMap of the devices
            return daDev;
        }

        /**
         * Returns a list of devices that can be detected with a scanning.
         *
         * @return A list containing the Device ID of all the discovered devices
         */
        @Override
        public List<String> detectDevices() throws RemoteException {
            // TODO Issue #3 Evaluate whether method detectDevices is really needed and, if so, evaluate whether it should be made asynchronous.
            throw new UnsupportedOperationException("This method is not working yet!");
        }

        /**
         * Set the specific configuration of a device managed by the Device Adapter passing a data
         * structure with key-value pairs containing all possible configuration parameters and
         * their values, together with the device ID. This should be done before starting the Device
         * Adapter, otherwise standard configuration will be used. Depending on capabilities, this
         * could also be invoked when the DA is already running.
         *
         * @param config The configuration for the device in the form of a key/value set (String/String)
         * @param devId The device ID (the MAC Address)
         */
        @Override
        public void setDeviceConfig(Map config, String devId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Fetching paired devices address");

            // Scan all connected DAs
            for (String tmpDaName : connectedDAs.keySet()) {

                // Retrieve the Capabilities of the DA
                Capabilities cap = availableDAs.get(tmpDaName);

                // If the DA supports device configuration at runtime, try to configure the device
                if (cap.getDeviceConfigurationType() == Capabilities.CONFIG_RUNTIME_ONLY
                        || cap.getDeviceConfigurationType() == Capabilities.CONFIG_STARTUP_AND_RUNTIME) {
                    connectedDAs.get(tmpDaName).setDeviceConfig(config, devId);
                }
            }
        }

        /**
         * Start the Device Adapter operations. This will cause the PA to bind the DA's service
         * and start the DA.
         *
         * @param daId The Device Adapter ID
         */
        @Override
        public void startDA(String daId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Starting Device Adapter " + daId);

            // Retrieve the Capabilities object for the specified DA
            Capabilities daCap = daId != null ? availableDAs.get(daId) : null;

            // Start the specified DA using the correct action in the Intent
            if (daCap != null) {
                Intent intent;
                String action = daCap.getActionName();
                String pkg = daCap.getPackageName();
                intent = new Intent().setComponent(new ComponentName(pkg, action));
                bindService(intent, new DAConnection(), Context.BIND_AUTO_CREATE);
            }

        }

        /**
         * Stop the Device Adapter operations. This will cause the PA to stop the DA and unbind the
         * related service.
         *
         * @param daId The Device Adapter ID
         */
        @Override
        public void stopDA(String daId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Stopping Device Adapter " + daId);

            // Retrieve the api endpoint of the specified DA
            IDeviceAdapter tmpDa = daId != null ? connectedDAs.get(daId) : null;

            if (tmpDa != null) {
                // Stop the operation of the Device Adapter
                tmpDa.stop();

                // Get the DAConnection object from the Map
                DAConnection conn = daConnections.get(daId);

                if (conn != null) {
                    // Unbind the Device Adapter
                    unbindService(conn);

                    // Remove the DA from the Maps
                    connectedDAs.remove(daId);
                    daConnections.remove(daId);
                }
            }

        }

        /**
         * Return a Map with all the available DAs in the system. The keys of the Map are the DAs'
         * ID and the values are the related Capabilities object.
         *
         * @return A Map with DA identifiers as key and Capabilities object as value
         */
        @Override
        public Map getAvailableDAs() throws RemoteException {
            return availableDAs;
        }

        /**
         * Return the object describing the capabilities of the specified DA.
         *
         * @param daId ID of the DA
         * @return An instance of the Capabilities object containing all the capabilities of the device
         */
        @Override
        public Capabilities getDACapabilities(String daId) throws RemoteException {
            return daId != null ? availableDAs.get(daId) : null;
        }

        /**
         * Connect to the device whose MAC Address is passed as an argument.
         *
         * @param devId The Device ID
         */
        @Override
        public void connectDev(String devId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Connecting to the specific device " + devId);

            // Retrieve the map of the devices and the list of the DAs handling the specified device
            Map<String, List<String>> devices = getDADevices();
            List<String> daHandlingDevice = devices.get(devId);

            // Check whether there is exactly one DA handling the specified device
            if (daHandlingDevice != null && daHandlingDevice.size() == 1) {

                // Retrieve the Capabilities and the endpoint of that DA
                Capabilities cap = availableDAs.get(daHandlingDevice.get(0));
                IDeviceAdapter da = connectedDAs.get(daHandlingDevice.get(0));

                // If the DA supports connection initiation, then connect to the specified device
                if (cap.isCommunicationInitiator()) {
                    da.connectDev(devId);
                } else {
                    appApi.log(LOG_LEVEL.ERROR, PAAndroidConstants.PA_PACKAGE,
                            "Connection initiation is not supported by the specified device (" + devId + ")");
                    throw new RuntimeException(
                            "Connection initiation is not supported by the specified device (" + devId + ")");
                }

            } else {
                appApi.log(LOG_LEVEL.ERROR, PAAndroidConstants.PA_PACKAGE, "The device " + devId
                        + " is not present in the list or is handled by more than one Device Adapter. Try using forceConnectDev.");
                throw new RuntimeException("The device " + devId
                        + " is not present in the list or is handled by more than one Device Adapter. Try using forceConnectDev.");
            }
        }

        /**
         * Force connection to the device whose devID is passed as an argument using the specified
         * Device Adapter. This method can be used to connect a supported device that, for some
         * reasons, is not recognised by the corresponding DA.
         *
         * @param devId The Device ID
         * @param daId The ID of the Device Adapter
         */
        @Override
        public void forceConnectDev(String devId, String daId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG,
                    "Forcing connection to the specific device " + devId + " via " + daId);

            // Retrieve the specified DA and its capabilities
            Capabilities cap = availableDAs.get(daId);
            IDeviceAdapter da = connectedDAs.get(daId);

            // If the DA is available in the system...
            if (cap != null) {
                // ... and it is connected...
                if (da != null) {
                    // ... and it supports connection initiation
                    if (cap.isCommunicationInitiator()) {
                        // ... then try connecting to the device
                        da.forceConnectDev(devId);
                    } else {
                        appApi.log(LOG_LEVEL.ERROR, PAAndroidConstants.PA_PACKAGE,
                                "Connection initiation is not supported by the specified device (" + devId + ")");
                        throw new RuntimeException(
                                "Connection initiation is not supported by the specified device (" + devId + ")");
                    }
                } else {
                    appApi.log(LOG_LEVEL.ERROR, PAAndroidConstants.PA_PACKAGE,
                            "The specified Device Adapter " + devId + " is not connected!");
                    throw new RuntimeException("The specified Device Adapter " + devId + " is not connected!");
                }
            } else {
                appApi.log(LOG_LEVEL.ERROR, PAAndroidConstants.PA_PACKAGE,
                        "The specified Device Adapter " + devId + " is not available in the system!");
                throw new RuntimeException(
                        "The specified Device Adapter " + devId + " is not available in the system!");
            }
        }

        /**
         * Disconnect from the device whose MAC Address is passed as an argument.
         *
         * @param devId The Device ID
         */
        @Override
        public void disconnectDev(String devId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Disconnecting device " + devId);

            // Scan all connected DAs
            for (String tmpDaName : connectedDAs.keySet()) {

                // Retrieve the Capabilities of the DA
                Capabilities cap = availableDAs.get(tmpDaName);

                // If the DA supports connection initiation, connect to the specified device
                if (cap.isCommunicationInitiator()) {
                    connectedDAs.get(tmpDaName).disconnectDev(devId);
                }
            }
        }

        /**
         * Receive a binder from the Application representing its interface.
         *
         * @param application The IBinder of the application
         */
        @Override
        public void registerPAListener(IBinder application) throws RemoteException {
            appApi = IProtocolAdapterListener.Stub.asInterface(application);
            // TODO If more initialization or initial actions are needed after the application registered itself, just do them here
        }

        /**
         * Add a device to the Device Adapter whitelist, passing its device ID as an argument.
         * Note that this insertion will persist, even through Device Adapter reboots, until
         * the device it's removed from the list. Every device adapter should check the format
         * of the address passed as an argument and, if it does not support that kind of
         * address, it can safely ignore that address.
         *
         * @param devId The Device ID
         */
        @Override
        public void addDeviceToWhitelist(String devId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Adding device " + devId + " to whitelist");

            // If the device ID is not valid, just do nothing
            if (devId == null || "".equals(devId))
                return;

            // Othwerwise add it to the list
            whitelist.add(devId);

            // Reflect the changes on SharedPreferences
            syncSharedPreferences();

            // Scan all connected DAs
            for (String tmpDaName : connectedDAs.keySet()) {

                // Retrieve the Capabilities of the DA
                Capabilities cap = availableDAs.get(tmpDaName);

                // If the DA supports whitelist, add the specified device to its whitelist
                if (cap.hasWhitelist()) {
                    connectedDAs.get(tmpDaName).addDeviceToWhitelist(devId);
                }
            }
        }

        /**
         * Remove from the whitelist the device whose device ID is passed as an argument.
         * If the device is not in the list, the request can be ignored.
         *
         * @param devId The Device ID
         */
        @Override
        public void removeDeviceFromWhitelist(String devId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Removing device " + devId + " from whitelist");

            // If the device ID is not valid, just do nothing
            if (devId == null || "".equals(devId))
                return;

            // Othwerwise remove it from the list
            whitelist.remove(devId);

            // Reflect the changes on SharedPreferences
            syncSharedPreferences();

            // Scan all connected DAs
            for (String tmpDaName : connectedDAs.keySet()) {

                // Retrieve the Capabilities of the DA
                Capabilities cap = availableDAs.get(tmpDaName);

                // If the DA supports whitelist, remove the specified device from whitelist
                if (cap.hasWhitelist()) {
                    connectedDAs.get(tmpDaName).removeDeviceFromWhitelist(devId);
                }
            }
        }

        /**
         * Retrieve all the devices in the whitelist of the DA. If there's no devices, an
         * empty list is returned.
         *
         * @return A list containing all the device id in the white list
         */
        @Override
        public List<String> getWhitelist() throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Retrieving the global whitelist");

            return whitelist;
        }

        /**
         * Set a list of devices in the whitelist all together, passing their device IDs as an argument.
         * Note that this insertion will persist, even through Device Adapter reboots, until
         * the devices are removed from the list. Every device adapter should check the format
         * of the address passed as an argument one by one and, if it does not support that kind of
         * address, it can safely ignore that address.
         *
         * @param mWhitelist A list containing all the device id to insert in the white list
         */
        @Override
        public void setWhitelist(List<String> mWhitelist) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Setting the global whitelist");

            // Empty the list
            emptyList(whitelist);

            if (mWhitelist != null) {
                // Add to the whitelist every element of the list passed as argument
                for (String dev : mWhitelist) {
                    if (dev != null && !"".equals(dev))
                        whitelist.add(dev);
                }
            }

            // Reflect the changes on SharedPreferences
            syncSharedPreferences();

            // Scan all connected DAs
            for (String tmpDaName : connectedDAs.keySet()) {

                // Retrieve the Capabilities of the DA
                Capabilities cap = availableDAs.get(tmpDaName);

                // If the DA supports whitelist, remove the specified device from whitelist
                if (cap.hasWhitelist()) {
                    connectedDAs.get(tmpDaName).setWhitelist(whitelist);
                }
            }
        }

        /**
         * Add a device to the Device Adapter blacklist, passing its device ID as an argument.
         * Note that this insertion will persist, even through Device Adapter reboots, until
         * the device it's removed from the list. Every device adapter should check the format
         * of the address passed as an argument and, if it does not support that kind of
         * address, it can safely ignore that address.
         *
         * @param devId The Device ID
         */
        @Override
        public void addDeviceToBlackList(String devId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Adding device " + devId + " to blacklist");

            // If the device ID is not valid, just do nothing
            if (devId == null || "".equals(devId))
                return;

            // Othwerwise add it to the list
            blacklist.add(devId);

            // Reflect the changes on SharedPreferences
            syncSharedPreferences();

            // Scan all connected DAs
            for (String tmpDaName : connectedDAs.keySet()) {

                // Retrieve the Capabilities of the DA
                Capabilities cap = availableDAs.get(tmpDaName);

                // If the DA supports blacklist, add the specified device to blacklist
                if (cap.hasBlacklist()) {
                    connectedDAs.get(tmpDaName).addDeviceToBlackList(devId);
                }
            }
        }

        /**
         * Remove from the blacklist the device whose device ID is passed as an argument.
         * If the device is not in the list, the request can be ignored.
         *
         * @param devId The Device ID
         */
        @Override
        public void removeDeviceFromBlacklist(String devId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Removing device " + devId + " from blacklist");

            // If the device ID is not valid, just do nothing
            if (devId == null || "".equals(devId))
                return;

            // Othwerwise remove it from the list
            blacklist.remove(devId);

            // Reflect the changes on SharedPreferences
            syncSharedPreferences();

            // Scan all connected DAs
            for (String tmpDaName : connectedDAs.keySet()) {

                // Retrieve the Capabilities of the DA
                Capabilities cap = availableDAs.get(tmpDaName);

                // If the DA supports blacklist, remove the specified device from blacklist
                if (cap.hasBlacklist()) {
                    connectedDAs.get(tmpDaName).removeDeviceFromBlacklist(devId);
                }
            }
        }

        /**
         * Retrieve all the devices in the blacklist of the DA. If there's no devices, an
         * empty list is returned.
         *
         * @return A list containing all the device id in the black list
         */
        @Override
        public List<String> getBlacklist() throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Retrieving the global blacklist");

            return blacklist;
        }

        /**
         * Set a list of devices in the blacklist all together, passing their device IDs as an argument.
         * Note that this insertion will persist, even through Device Adapter reboots, until
         * the devices are removed from the list. Every device adapter should check the format
         * of the address passed as an argument one by one and, if it does not support that kind of
         * address, it can safely ignore that address.
         *
         * @param mBlacklist A list containing all the device id to insert in the black list
         */
        @Override
        public void setBlackList(List<String> mBlacklist) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Setting the global blacklist");

            // Empty the list
            emptyList(blacklist);

            if (mBlacklist != null) {
                // Add to the whitelist every element of the list passed as argument
                for (String dev : mBlacklist) {
                    if (dev != null && !"".equals(dev))
                        blacklist.add(dev);
                }
            }

            // Reflect the changes on SharedPreferences
            syncSharedPreferences();

            // Scan all connected DAs
            for (String tmpDaName : connectedDAs.keySet()) {

                // Retrieve the Capabilities of the DA
                Capabilities cap = availableDAs.get(tmpDaName);

                // If the DA supports blacklist, remove the specified device from blacklist
                if (cap.hasBlacklist()) {
                    connectedDAs.get(tmpDaName).setWhitelist(blacklist);
                }
            }
        }

        /**
         * Return all the commands supported by the Device Adapter for its devices.
         *
         * @param daId ID of the DA
         * @return A list of commands supported by the Device Adapter
         */
        @Override
        public List<String> getCommandList(String daId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Retrieving command list from Device Adapter " + daId);

            List<String> commandList = new ArrayList<String>();

            // Retrieve the right DA
            IDeviceAdapter tmpDa = daId != null ? connectedDAs.get(daId) : null;
            Capabilities cap = daId != null ? availableDAs.get(daId) : null;

            // If the Device Adapter is not connected throw an Exception
            if (tmpDa == null || cap == null) {
                throw new IllegalStateException("The Device Adapter " + daId
                        + " is not connected to Protocol Adapter! If you are sure that the Device Adapter is connected, try increasing the time interval between the Device Adapter connection and the retrieving of command list.");
            }

            // Retrieve and return the Device Adapter's command list
            if (cap.supportCommands()) {
                commandList = tmpDa.getCommandList();
            }

            return commandList;
        }

        /**
         * Execute a command supported by the device. You can also specify a parameter, if the command
         * allows or requires it.
         *
         * @param command The command to execute on the device
         * @param parameter The optional parameter to pass to the device together with the command
         * @param devId The Device ID
         *
         * @throws IllegalArgumentException if the command is not supported by the Device Adapter
         */
        @Override
        public void execCommand(String command, String parameter, String devId) throws RemoteException {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Sending command to device.\n" + "Device: " + devId + "\n"
                    + "Command: " + command + "\n" + "Parameter: " + parameter);

            // Check whether the device passed as an argument is null
            if (devId == null) {
                return;
            }

            // Scan all connected DAs
            for (String tmpDaName : connectedDAs.keySet()) {

                // Retrieve the Capabilities of the DA
                Capabilities cap = availableDAs.get(tmpDaName);

                // If the DA supports sending commands to devices and is connection initiator...
                if (cap.supportCommands() && cap.isCommunicationInitiator()) {

                    List<DeviceDescription> connDevs = connectedDAs.get(tmpDaName).getConnectedDevices();

                    // ... Scan its connected devices...
                    for (DeviceDescription tmpDev : connDevs) {

                        // ... If the desired device is connected with this DA at the moment, send it the command
                        if (devId.equals(tmpDev.getDeviceID())) {
                            connectedDAs.get(tmpDaName).execCommand(command, parameter, devId);
                            break;
                        }
                    }

                }

            }
        }
    };

    /**
     * Implementation of the Device Adapter Listener API (IDeviceAdapterListener) to pass to the
     * Device Adapter
     */
    private final IDeviceAdapterListener.Stub daEndpoint = new IDeviceAdapterListener.Stub() {

        /**
         * Register a new device with the SDCS
         *
         * @param devDesc
         *      The device to register
         */
        @Override
        public void registerDevice(DeviceDescription devDesc) {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Received device description: " + devDesc.toString());

            try {
                appApi.registerDevice(devDesc);
            } catch (RemoteException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "Failed to register device with application!");
            }
        }

        /**
         * Register all the property of a given device taking care of sending one separate message for
         * each property
         *
         * @param devDesc
         *      The device whom the properties belongs to
         */
        @Override
        public void registerDeviceProperties(DeviceDescription devDesc) {
            Log.i(PAAndroidConstants.PA_LOGTAG,
                    "Received properties to register from device: " + devDesc.toString());

            try {
                appApi.registerDeviceProperties(devDesc);
            } catch (RemoteException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "Failed to register device properties with application!");
            }
        }

        /**
         * Push the data received from the DA, taking care of sending one separate message for each
         * measurement
         *
         * @param observations
         *      The measurements to push to SDCS
         *
         * @param devDesc
         *      The device whom the observation belongs to
         */
        @Override
        public void pushData(List<Observation> observations, DeviceDescription devDesc) {
            String dataStr = "";
            for (Observation obs : observations) {
                dataStr += obs.toString() + "\n";
            }
            Log.i(PAAndroidConstants.PA_LOGTAG,
                    "Received data to push\n" + "Device: " + devDesc.getDeviceID() + "Data: " + dataStr);

            try {
                appApi.pushData(observations, devDesc);
            } catch (RemoteException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "Failed to push data with application!");
            }
        }

        /**
         * Deregister the given device from the SDCS
         *
         * @param devDesc
         *      The device to use
         */
        public void deregisterDevice(DeviceDescription devDesc) {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Received device deregistration: " + devDesc.getDeviceID());

            try {
                appApi.deregisterDevice(devDesc);
            } catch (RemoteException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "Failed to deregister device with application!");
            }

        }

        /**
         * Notify a device disconnection to the upper layer
         *
         * @param devDesc The ID of the disconnected device
         */
        public void deviceDisconnected(DeviceDescription devDesc) {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Received device disconnection: " + devDesc.getDeviceID());

            try {
                appApi.deviceDisconnected(devDesc);
            } catch (RemoteException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "Failed to notify device disconnection to application!");
            }
        }

        /**
         * Receive notification from the Device Adapter of some event that happened and forward it
         * to upper layer.
         *
         * @param logLevel The severity of the event
         * @param daId The ID of the Device Adapter that generated the event
         * @param message The message associated with the event
         */
        @Override
        public void log(int logLevel, String daId, String message) throws RemoteException {
            appApi.log(logLevel, daId, message);
        }

    };

    BroadcastReceiver broadcastDiscoveryDA = new BroadcastReceiver() {
        /**
         * Receive Discovery Reply Intents from DAs and insert all info about DAs in the list of
         * available DAs
         *
         * @param context The context
         * @param intent Intent received from DA
         */
        @Override
        public void onReceive(Context context, Intent intent) {
            // Extract DA ID and DA Capabilities from the Intent
            String daId = intent.getStringExtra(DA_DISCOVERY.BUNDLE_DAID);
            Capabilities daCap = intent.getParcelableExtra(DA_DISCOVERY.BUNDLE_DACAP);

            // Insert the newly found DA inside the list of available DAs
            if (daId != null && daCap != null) {
                availableDAs.put(daId, daCap);
                Log.i(PAAndroidConstants.PA_LOGTAG,
                        "Found Device Adapter " + daCap.getFriendlyName() + " (" + daCap.getPackageName() + ")");
            }

        }
    };

    @Override
    public IBinder onBind(Intent intent) {

        // If this is the first start, do some initialization tasks
        if (firstStart) {
            try {
                // Check who is the Issuer who started the PA
                String issuer = intent.getStringExtra(SDCS_MESSAGES.EXTRA_NAME_ISSUER);
                if (issuer == null) {
                    Log.d(PAAndroidConstants.PA_LOGTAG, "No Issuer found in activation message. Using default.");
                } else {
                    Log.d(PAAndroidConstants.PA_LOGTAG, "Service was bound, Issuer is: " + issuer);
                }

            } catch (Exception ex) {
                Log.d(PAAndroidConstants.PA_LOGTAG, ex.toString());
            }

            // Retrieve the saved values for blacklist and whitelist
            restoreFromSharedPreferences();

            // Discover DAs on the system
            discoverDAs();

            // Do not allow the repetition of this phase (though this may be redundant for bound services)
            firstStart = false;

            Log.i(PAAndroidConstants.PA_LOGTAG, "Protocol Adapter up and running.");
        } else {
            Log.i(PAAndroidConstants.PA_LOGTAG, "Protocol Adapter already up and running: not restarted.");
        }

        return appEndpoint;

    }

    @Override
    public void onDestroy() {
        // Stop and unbind from binded DAs in order to avoid ServiceConnection leak
        Set<String> das = new TreeSet<String>(connectedDAs.keySet());

        for (String tmpDA : das) {
            try {
                appEndpoint.stopDA(tmpDA);
            } catch (RemoteException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "Error disconnecting from DA");
            } catch (IllegalArgumentException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "No DA to disconnect from");
            }
        }
    }

    /**
     * Discover all the DA available on the system
     */
    private void discoverDAs() {
        Log.i(PAAndroidConstants.PA_LOGTAG, "Starting Device Adapter discovery");

        // Create the Intent to broadcast
        Intent intent = new Intent(DA_DISCOVERY.REQUEST_ACTION);
        intent.putExtra(DA_DISCOVERY.BUNDLE_REPACT, DA_DISCOVERY.REPLY_ACTION);

        // Create the Intent Filter to receive broadcast replies
        IntentFilter filter = new IntentFilter();
        filter.addAction(DA_DISCOVERY.REPLY_ACTION);

        // Register the Broadcast Receiver for replies and set it to run in another thread
        HandlerThread ht = new HandlerThread("ht");
        ht.start();
        registerReceiver(broadcastDiscoveryDA, filter, null, new Handler(ht.getLooper()));

        // Send in broadcast the Intent for discovery
        sendBroadcast(intent);

        // Create a new Thread object to stop the discovery and also use it as a lock
        ScanningStopAndLock r = new ScanningStopAndLock();

        // Set the timeout for receiving replies
        r.start();

        // Wait for the scanning to be done before returning
        synchronized (r) {
            while (!r.isDiscoveryDone()) {
                try {
                    r.wait();
                } catch (InterruptedException e) {
                    // Just wait until scanning is done
                }
            }
        }

    }

    /**
     * Stop the discovery of new DAs
     */
    private void stopDADiscovery() {
        // Stop receiving intents related to DA discovery
        unregisterReceiver(broadcastDiscoveryDA);

        Log.i(PAAndroidConstants.PA_LOGTAG, "Device Adapter discovery ended");
    }

    /**
     * Remove every element on this list
     *
     * @param list The list of devices
     */
    private void emptyList(List<String> list) {

        if (list == null || list.isEmpty())
            return;

        for (String dev : list) {
            list.remove(dev);
        }

    }

    /**
     * Synchronize actual value of whitelist and blacklist with SharedPreferences
     */
    private void syncSharedPreferences() {
        // Get the SharedPreferences and prepare them for editing
        SharedPreferences settings = getSharedPreferences(SHPREF_FILENAME, 0);
        SharedPreferences.Editor editor = settings.edit();

        // Update the values of whitelist and blacklist in the SharedPreferences
        editor.putString(SHPREF_WHITELIST_NAME, new JSONArray(whitelist).toString());
        editor.putString(SHPREF_BLACKLIST_NAME, new JSONArray(blacklist).toString());

        // Commit the edits
        editor.commit();
    }

    /**
     * Restore in blacklist and whitelist every device saved in the SharedPreferences
     */
    private void restoreFromSharedPreferences() {
        JSONArray jWhitelist, jBlacklist;

        // Get access to the right SharedPreferences file
        SharedPreferences shPref = getSharedPreferences(SHPREF_FILENAME, 0);

        // Retrieve the whitelist and if there's any problem, create a new empty JSONArray
        try {
            jWhitelist = new JSONArray(shPref.getString(SHPREF_WHITELIST_NAME, "[]"));
        } catch (JSONException e) {
            jWhitelist = new JSONArray();
        }

        // Retrieve the blacklist and if there's any problem, create a new empty JSONArray
        try {
            jBlacklist = new JSONArray(shPref.getString(SHPREF_BLACKLIST_NAME, "[]"));
        } catch (JSONException e) {
            jBlacklist = new JSONArray();
        }

        // Add to whitelist every element present in JSONArray retrieved from SharedPreferences
        for (int i = 0; i < jWhitelist.length(); i++) {
            try {
                whitelist.add(jWhitelist.getString(i));
            } catch (JSONException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "Failed restoring whitelist from SharedPreferences");
            }
        }

        // Add to blacklist every element present in JSONArray retrieved from SharedPreferences
        for (int i = 0; i < jBlacklist.length(); i++) {
            try {
                blacklist.add(jBlacklist.getString(i));
            } catch (JSONException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "Failed restoring blacklist from SharedPreferences");
            }
        }
    }

    /**
     * Start all the device adapters present in the the DA's array
     */
    @SuppressWarnings("unused")
    private void startDeviceAdapters() {
        // TODO Issue #7 Decide if startDeviceAdapters method should be promoted and put inside IProtocolAdapter interface
        Log.i(PAAndroidConstants.PA_LOGTAG, "Starting all Device Adapters");

        // Start all Device Adapters that are installed
        for (Capabilities daCap : availableDAs.values()) {
            try {
                appEndpoint.startDA(daCap.getActionName());
            } catch (RemoteException e) {
                Log.w(PAAndroidConstants.PA_LOGTAG, "Device Adapter " + daCap.getFriendlyName() + " ("
                        + daCap.getPackageName() + ") failed to start!");
            }
        }

    }

    private class ScanningStopAndLock extends Thread {
        private boolean done = false;

        @Override
        public void run() {
            try {
                Thread.sleep(DA_DISCOVERY.TIMEOUT);
            } catch (InterruptedException e) {
                // Do nothing
            }
            stopDADiscovery();
            synchronized (this) {
                done = true;
                notifyAll();
            }
        }

        public boolean isDiscoveryDone() {
            return done;
        }
    }

    /**
     * Endpoint for managing the connection/disconnection of the Device Adapters
     */
    private class DAConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {

            // Retrieve the Device Adapter from the binder
            String daId = componentName.getPackageName();
            IDeviceAdapter tmpDa = IDeviceAdapter.Stub.asInterface(iBinder);

            // Add the Device Adapter to the List
            connectedDAs.put(daId, tmpDa);
            daConnections.put(daId, this);
            Log.i(PAAndroidConstants.PA_LOGTAG, "New Device Adapter connected: " + daId);

            try {
                // Register the Protocol Adapter into the Device Adapter
                tmpDa.registerDAListener(daEndpoint);
            } catch (RemoteException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "Failed to register PA inside DA " + daId);
            }

            try {
                // Get the Capabilities of the DA
                Capabilities cap = tmpDa.getDACapabilities();

                // Restore the blacklist inside the newly connected DA if it's supported
                if (cap.hasBlacklist()) {
                    tmpDa.setBlackList(blacklist);
                }

                // Restore the whitelist inside the newly connected DA if it's supported
                if (cap.hasWhitelist()) {
                    tmpDa.setWhitelist(whitelist);
                }

                // Start the newly connected DA
                tmpDa.start();
            } catch (RemoteException e) {
                Log.d(PAAndroidConstants.PA_LOGTAG, "Failed to start DA " + daId);
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

            // Get the DA ID
            String daId = componentName.getPackageName();

            Log.i(PAAndroidConstants.PA_LOGTAG, daId + " Device Adapter service disconnected.");

            // Remove the Device Adapter from the DA List
            connectedDAs.remove(daId);

            try {
                // Try to start the DA again. This way, the DA will be added again to the list of
                // connected DAs when onServiceConnected is called
                appEndpoint.startDA(daId);
                Log.i(PAAndroidConstants.PA_LOGTAG, "Restarting Device Adapter " + daId);
            } catch (RemoteException ex) {
                Log.e(PAAndroidConstants.PA_LOGTAG, daId + " Device Adapter failed to connect.");
            }
        }

    }
}