package eu.hgross.blaubot.core; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import eu.hgross.blaubot.core.acceptor.ConnectionMetaDataDTO; import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionAcceptor; import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionManagerListener; import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeaconStore; import eu.hgross.blaubot.core.connector.IBlaubotConnector; import eu.hgross.blaubot.core.connector.IncompatibleBlaubotDeviceException; import eu.hgross.blaubot.core.statemachine.BlaubotAdapterHelper; import eu.hgross.blaubot.util.Log; /** * Manager to store and retrieve Connections related to {@link IBlaubotDevice} instances. * * @author Henning Gross {@literal (mail.to@henning-gross.de)} */ public class BlaubotConnectionManager { /** * Can be used in conjunction with connectToDevice(IBlaubotDevice, int) to let the ConnectionManager * decide how much retries to use. */ public static final int AUTO_MAX_RETRIES = -1; private static final String LOG_TAG = "BlaubotConnectionManager"; private final ConcurrentHashMap<IBlaubotDevice, List<IBlaubotConnection>> connections = new ConcurrentHashMap<IBlaubotDevice, List<IBlaubotConnection>>(); private final List<IBlaubotConnectionManagerListener> connectionListeners = new ArrayList<>(); private final List<IBlaubotConnectionAcceptor> connectionAcceptors; private final List<IBlaubotConnector> connectionConnectors; private final IBlaubotConnectionManagerListener connectionListener; // manager's own listener listening on private IBlaubotBeaconStore beaconStore; /** * Creates a new {@link BlaubotConnectionManager} instance managing the given acceptors and connectors for incoming * and outgoing connections. * * @param acceptors the acceptors to handle * @param connectors the connectors to be used to connect to other devices */ public BlaubotConnectionManager(List<IBlaubotConnectionAcceptor> acceptors, List<IBlaubotConnector> connectors) { this.connectionConnectors = connectors; this.connectionAcceptors = acceptors; this.connectionListener = new IBlaubotConnectionManagerListener() { @Override public void onConnectionEstablished(IBlaubotConnection connection) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Got onConnectionEstablished: " + connection); } addConnection(connection); } @Override public void onConnectionClosed(IBlaubotConnection connection) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Got onConnectionClosed: " + connection); } removeConnection(connection); } }; // We set up a listener to each acceptor to get informed about new connections for (IBlaubotConnectionAcceptor acceptor : connectionAcceptors) { acceptor.setAcceptorListener(connectionListener); } for (IBlaubotConnector connector : connectionConnectors) { connector.setIncomingConnectionListener(connectionListener); } } protected void addConnection(IBlaubotConnection connection) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Adding connection " + connection); } List<IBlaubotConnection> deviceConnections = new CopyOnWriteArrayList<>(); this.connections.putIfAbsent(connection.getRemoteDevice(), deviceConnections); deviceConnections = this.connections.get(connection.getRemoteDevice()); deviceConnections.add(connection); connection.addConnectionListener(connectionListener); // proxy event to our listeners for (IBlaubotConnectionManagerListener listener : connectionListeners) { listener.onConnectionEstablished(connection); } } protected void removeConnection(IBlaubotConnection connection) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Removing connection for device " + connection.getRemoteDevice()); } List<IBlaubotConnection> deviceConnections = this.connections.get(connection.getRemoteDevice()); if (deviceConnections == null) { if (Log.logWarningMessages()) { Log.w(LOG_TAG, "Tried to remove a connection for device " + connection.getRemoteDevice() + " but no connection for this device was registered."); } return; } boolean removed = deviceConnections.remove(connection); if (!removed) { if (Log.logWarningMessages()) { Log.w(LOG_TAG, "Tried to remove a non existant connection for device " + connection.getRemoteDevice() + " from ConnectionManager but connection was not registered."); } } // -- list was removed // note: we leave the empty list in the map! connection.removeConnectionListener(connectionListener); // proxy event to our listeners for (IBlaubotConnectionManagerListener listener : connectionListeners) { listener.onConnectionClosed(connection); } } /** * @param blauBotDevice the list of {@link IBlaubotDevice}s of this device. * @return the connection object (a socket, bluetoothsocket ...) or null, if nothing there for blauBotDevice */ public List<IBlaubotConnection> getConnections(IBlaubotDevice blauBotDevice) { return this.connections.get(blauBotDevice); } /** * @return a list of devices with at least one active connection to our device */ public List<IBlaubotDevice> getConnectedDevices() { ArrayList<IBlaubotDevice> devices = new ArrayList<>(); // jdk8 hiccup ... https://gist.github.com/AlainODea/1375759b8720a3f9f094 Map<IBlaubotDevice, List<IBlaubotConnection>> castedMap = connections; // there could be devices with no connection -> filter them out for (IBlaubotDevice d : castedMap.keySet()) { if (!connections.get(d).isEmpty()) { devices.add(d); } } return devices; } /** * @return list of all active connections */ public List<IBlaubotConnection> getAllConnections() { ArrayList<IBlaubotConnection> allConnections = new ArrayList<>(); for (List<IBlaubotConnection> connections : this.connections.values()) { allConnections.addAll(connections); } return allConnections; } public void addConnectionListener(IBlaubotConnectionManagerListener listener) { this.connectionListeners.add(listener); } public void removeConnectionListener(IBlaubotConnectionManagerListener listener) { this.connectionListeners.remove(listener); } /** * @return list of connectors associated with this manager */ public List<IBlaubotConnector> getConnectionConnectors() { return connectionConnectors; } /** * Tries to find a connector for the given uniqueDeviceId by determinig the appropriate connectors * via the IBlaubotBeaconStores meta data (if a beaconstore was provided). * * @param uniqueDeviceId the unique device id to connect to * @return the connector able to connect to one of the device's acceptors, or null, if no acceptor meta data or connector is available for this device */ public IBlaubotConnector getConnectorForDevice(String uniqueDeviceId) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Searching an appropriate connector for device" + uniqueDeviceId); } if (beaconStore == null) { if (Log.logWarningMessages()) { Log.w(LOG_TAG, "I have no BeaconStore and therefore can't connect anywhere."); } return null; } // search for metadata final List<ConnectionMetaDataDTO> lastKnownConnectionMetaData = beaconStore.getLastKnownConnectionMetaData(uniqueDeviceId); if (lastKnownConnectionMetaData == null || lastKnownConnectionMetaData.isEmpty()) { if (Log.logErrorMessages()) { Log.e(LOG_TAG, "Never got acceptor meta data for this device: " + uniqueDeviceId + " and therefore can not chose a connector"); } return null; } final List<String> supportedConnectionTypes = BlaubotAdapterHelper.extractSupportedConnectionTypes(connectionConnectors); if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Looking for acceptors with connection types: " + supportedConnectionTypes + "; in meta data: " + lastKnownConnectionMetaData); } for (ConnectionMetaDataDTO acceptorMetaData : lastKnownConnectionMetaData) { final String connectionType = acceptorMetaData.getConnectionType(); if (supportedConnectionTypes.contains(connectionType)) { // find the connector with this connection type for (IBlaubotConnector connector : connectionConnectors) { if (connector.getSupportedAcceptorTypes().contains(connectionType)) { // choose this return connector; } } } } return null; } /** * Tries to connect to the given {@link IBlaubotDevice}. * If not successful after maxRetries, null will be returned. * The retry mechanism uses the exponential backoff method which * waiting time is configured by the {@link BlaubotAdapterConfig} * for this device's adapter. * * @param device the {@link IBlaubotDevice} to connect to * @param maxRetries max number of retries or BlaubotConnectionManager.AUTO_MAX_RETRIES to let the manager decide * @return an {@link IBlaubotConnection} or null, if no connection could be established after maxRetries */ public IBlaubotConnection connectToBlaubotDevice(IBlaubotDevice device, int maxRetries) { final IBlaubotConnector connectorForDevice = getConnectorForDevice(device.getUniqueDeviceID()); // connector could be null! if (connectorForDevice == null) { if (Log.logErrorMessages()) { Log.e(LOG_TAG, "Could not retrieve connector for device " + device); } return null; } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Using connector " + connectorForDevice + " to connect to device"); } // use technology specific timings final BlaubotAdapterConfig adapterConfig = connectorForDevice.getAdapter().getBlaubotAdapterConfig(); float backoffFactor = adapterConfig.getExponentialBackoffFactor(); int backoffTimeout = adapterConfig.getConnectorRetryTimeout(); // check max retries if (maxRetries == BlaubotConnectionManager.AUTO_MAX_RETRIES) { maxRetries = adapterConfig.getMaxConnectionRetries(); } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Trying to connect to device " + device + " using exponential backoff and max " + maxRetries + " retries."); } int outStandingRetries = maxRetries; while (outStandingRetries-- > 0) { IBlaubotConnection conn = connectToBlaubotDevice(device, connectorForDevice); if (conn != null) { return conn; } // backoff try { if (outStandingRetries == 0) break; if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Backing of - outstanding retries: " + outStandingRetries); } Thread.sleep(backoffTimeout); } catch (InterruptedException e) { throw new RuntimeException(e); } backoffTimeout *= backoffFactor; } if (Log.logWarningMessages()) { Log.w(LOG_TAG, "Connection to " + device + " could not be established after " + maxRetries + " retries."); } return null; } /** * Tries to connect to the {@link IBlaubotDevice} corresponding to the given uniqueId * by aksing all connectors if for a device object belonging to this uniqueId and trying * to connect to this device. * * If not successful after maxRetries, null will be returned. * The retry mechanism uses the exponential backoff method which * waiting time is configured by the {@link BlaubotAdapterConfig} * for this device's adapter. * * @param uniqueId the device's uniqueId * @param maxRetries max number of retries or BlaubotConnectionManager.AUTO_MAX_RETRIES to let the manager decide * @return an {@link IBlaubotConnection} or null, if no connection could be established after maxRetries */ public IBlaubotConnection connectToBlaubotDevice(String uniqueId, int maxRetries) { IBlaubotDevice device = createBlaubotDeviceFromUniqueId(uniqueId); if (device == null) { return null; } return connectToBlaubotDevice(device, maxRetries); } /** * Tries to find a connector able to connect to the device, then tries to connect. * * @param device the remote device to connect to * @param connector the connector to use * @return blaubot connection, if the connection was successful - false otherwise */ private IBlaubotConnection connectToBlaubotDevice(IBlaubotDevice device, IBlaubotConnector connector) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Trying to connect to device " + device + " using the connector: " + connector); } try { IBlaubotConnection conn = connector.connectToBlaubotDevice(device); boolean result = conn != null; if (Log.logDebugMessages()) { if (result) Log.d(LOG_TAG, "Connection was successful"); else Log.d(LOG_TAG, "Connection failed."); } return conn; } catch (IncompatibleBlaubotDeviceException e) { if (Log.logErrorMessages()) { Log.e(LOG_TAG, "Connector " + connector + " not compatible."); } } if (Log.logErrorMessages()) { Log.e(LOG_TAG, "Could not connect to remote device " + device + " with connector " + connector); } return null; } /** * Goes through all connected devices and tries to find an IBlaubotDevice instance with this uniqueId. * If the device was found, it is returned. If not, a generic BlaubotDevice instance is returned. * * @param uniqueDeviceId the unique device id to create a blaubot device for * @return the corresponding {@link IBlaubotDevice} instance */ public IBlaubotDevice createBlaubotDeviceFromUniqueId(String uniqueDeviceId) { if (uniqueDeviceId == null) { throw new NullPointerException("uniqueDeviceId can't be null"); } // check if we know this device and return or create a new instance and return for (IBlaubotDevice device : getConnectedDevices()) { if (device.getUniqueDeviceID().equals(uniqueDeviceId)) { return device; } } return new BlaubotDevice(uniqueDeviceId); } /** * Sets the beacon store to be used to get the last beacon states and connectivity meta data * * @param beaconStore the store instance */ public void setBeaconStore(IBlaubotBeaconStore beaconStore) { this.beaconStore = beaconStore; } }