package eu.hgross.blaubot.geobeacon; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import eu.hgross.blaubot.core.IBlaubotConnection; import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionAcceptor; import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionListener; import eu.hgross.blaubot.core.acceptor.IBlaubotIncomingConnectionListener; import eu.hgross.blaubot.core.acceptor.discovery.TimeoutList; import eu.hgross.blaubot.core.statemachine.BlaubotAdapterHelper; import eu.hgross.blaubot.messaging.BlaubotMessage; import eu.hgross.blaubot.messaging.BlaubotMessageReceiver; import eu.hgross.blaubot.messaging.BlaubotMessageSender; import eu.hgross.blaubot.messaging.IBlaubotMessageListener; import eu.hgross.blaubot.util.Log; /** * A simple server that receives messages from beacons containing their state and connection metadata * and some geolocation informations. * This information is then used to inform nearby, connected beacons about status updates */ public class GeoBeaconServer { private static final String LOG_TAG = "GeoBeaconServer"; private Object startStopMonitor = new Object(); /** * The acceptors by which the server can be reached */ private final List<IBlaubotConnectionAcceptor> acceptors; /** * UniqueDeviceId -> Last GeoBeaconMessage */ private TimeoutList<GeoBeaconMessage> geoBeaconMessages; /** * the radius in KM in which devices are notified about "nearby" devices */ private final double geoRadius; /** * by uniquedeviceid and beaconUuid */ private Set<GeoBeaconServerClient> clients; /** * This set contains all clients, for which an "initial message set" was sent. * This means if a client connects for the first time, it is not in this set. * If this client sends a GeoBeaconMessage, we gather all GeoBeaconMEssages * of devices nearby to this client and send them to the client and add the * client to this set. * If the client disconnects, it is removed from the set. */ private Set<GeoBeaconServerClient> sentInitialMessageSet; /** * Creates the server using the given acceptors. * Make sure to add the corresponding connectors and connection data to the GeoBeacon. * @param geoRadius the radius in KM in which devices are notified about "nearby" devices * @param acceptors the acceptors to use */ public GeoBeaconServer(double geoRadius, IBlaubotConnectionAcceptor... acceptors) { this.geoRadius = geoRadius; this.acceptors = Arrays.asList(acceptors); this.geoBeaconMessages = new TimeoutList(GeoBeaconConstants.MAX_AGE_BEACON_MESSAGES); this.clients = Collections.newSetFromMap(new ConcurrentHashMap<GeoBeaconServerClient, Boolean>()); this.sentInitialMessageSet = Collections.newSetFromMap(new ConcurrentHashMap<GeoBeaconServerClient, Boolean>()); for (IBlaubotConnectionAcceptor acceptor : acceptors) { acceptor.setAcceptorListener(acceptorListener); } } /** * Called when a GeoLocationBeacon sends data. */ private IBlaubotMessageListener messageListener = new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { final GeoBeaconMessage geoBeaconMessage = GeoBeaconUtil.blaubotMessageToGeoBeaconMessage(blaubotMessage); geoBeaconMessages.report(geoBeaconMessage); if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Got GeoBeaconMessage from " + geoBeaconMessage.getBeaconMessage().getUniqueDeviceId() + ": " + geoBeaconMessage); } notifyBeacons(geoBeaconMessage); } }; /** * Called when a client disconnects. */ private IBlaubotConnectionListener disconnectListener = new IBlaubotConnectionListener() { @Override public void onConnectionClosed(IBlaubotConnection connection) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "GeoBeaconClient disconnected: " + connection.getRemoteDevice().getUniqueDeviceID()); } GeoBeaconServerClient toRemove = null; // find our client and remove from set on disconnect for (GeoBeaconServerClient curCl : clients) { if (curCl.getConnection().getRemoteDevice().getUniqueDeviceID().equals(connection.getRemoteDevice().getUniqueDeviceID())) { curCl.getMessageReceiver().removeMessageListener(messageListener); toRemove = curCl; } } if (toRemove != null) { clients.remove(toRemove); sentInitialMessageSet.remove(toRemove); } } }; /** * Handles all incoming connections */ private final IBlaubotIncomingConnectionListener acceptorListener = new IBlaubotIncomingConnectionListener() { @Override public void onConnectionEstablished(IBlaubotConnection connection) { connection.addConnectionListener(disconnectListener); GeoBeaconServerClient client = new GeoBeaconServerClient(connection); client.getMessageReceiver().addMessageListener(messageListener); clients.add(client); client.activate(); if (Log.logDebugMessages()) { Log.d(LOG_TAG, "New GeoBeaconClient: " + client); Log.d(LOG_TAG, "Items: " + geoBeaconMessages.getItems()); } // notifyOneBeacon(client); } }; /** * Starts the beacon server */ public void startBeaconServer() { final boolean allStarted = BlaubotAdapterHelper.startedCount(acceptors, null) == acceptors.size(); if (allStarted) { return; } synchronized (startStopMonitor) { BlaubotAdapterHelper.startAcceptors(acceptors); } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "GeoBeaconServer started."); } } /** * Stops the beacon server */ public void stopBeaconServer() { if (BlaubotAdapterHelper.startedCount(acceptors, null) == 0) { return; } synchronized (startStopMonitor) { BlaubotAdapterHelper.stopAcceptors(acceptors); } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "GeoBeaconServer stopped."); } } /** * Given a message notifies connected beacons nearby to the coordinates given in the message. * * @param message the message */ private void notifyBeacons(GeoBeaconMessage message) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Notifying clients ..."); } // we gather all beacon messages in a radius around message's radius and // then we check if a client exists for each of the gathered messages // if yes, we send the message to these clients // there is a special case: If a client freshly connected, he does not know // anything about the other devices around him after sending "message" // for this case we have a set that contains all clients for which a // "initial message set" was send. If the client for message.beaconmessage.unqiueDeviceId // is not in this set, we send all messages that we gathered before to this client // and add this client to the set // filter by nearby beacons (geodata) and beaconUUID Collection<GeoBeaconMessage> nearbyBeaconMessages = gatherNearbyBeaconMessages(message); Collection<GeoBeaconServerClient> nearbyClients = getClientsByBeaconMessageCollection(nearbyBeaconMessages); // send the just received message to all nearby clients for (GeoBeaconServerClient client : nearbyClients) { String uniqueDeviceID = client.getConnection().getRemoteDevice().getUniqueDeviceID(); if (uniqueDeviceID.equals(message.getBeaconMessage().getUniqueDeviceId())) { continue; // don't echo } BlaubotMessage msg = GeoBeaconUtil.geoBeaconMessageToBlaubotMessage(message); client.getMessageSender().sendMessage(msg); } // get the client that send the message GeoBeaconServerClient sender = null; for (GeoBeaconServerClient client : clients) { if (client.getConnection().getRemoteDevice().equals(message.getBeaconMessage().getUniqueDeviceId())) { sender = client; break; } } if (sender != null && !sentInitialMessageSet.contains(sender)) { // the sender never got a full update of all nearby messages, so we will send them to him for (GeoBeaconMessage geoBeaconMessage : nearbyBeaconMessages) { // send the just received message to all nearby clients String uniqueDeviceID = sender.getConnection().getRemoteDevice().getUniqueDeviceID(); if (uniqueDeviceID.equals(geoBeaconMessage.getBeaconMessage().getUniqueDeviceId())) { continue; // don't echo the just received msg } BlaubotMessage msg = GeoBeaconUtil.geoBeaconMessageToBlaubotMessage(geoBeaconMessage); sender.getMessageSender().sendMessage(msg); } } } /** * Given a collection of GeoBeaconMessages, this methods returns the collection of connected clients * for this messages. * * @param beaconMessages the beacon messages to get the clients for * @return the client collection */ private Collection<GeoBeaconServerClient> getClientsByBeaconMessageCollection(Collection<GeoBeaconMessage> beaconMessages) { final Set<String> uniqueDeviceIdSet = new HashSet<>(); for (GeoBeaconMessage message : beaconMessages) { uniqueDeviceIdSet.add(message.getBeaconMessage().getUniqueDeviceId()); } final Set<GeoBeaconServerClient> clientSet = new HashSet<>(); for (GeoBeaconServerClient geoBeaconServerClient : clients) { String uniqueDeviceID = geoBeaconServerClient.getConnection().getRemoteDevice().getUniqueDeviceID(); if (uniqueDeviceIdSet.contains(uniqueDeviceID)) { clientSet.add(geoBeaconServerClient); } } return clientSet; } /** * Based on the configured radius gathers all nearby GeoBeaconMessages around message. * Also filters by beacon uuid of message. * * @param message the message (center) * @return the list of messages surrounding message by the defined radius */ private Collection<GeoBeaconMessage> gatherNearbyBeaconMessages(GeoBeaconMessage message) { ArrayList<GeoBeaconMessage> list = new ArrayList<>(); for (GeoBeaconMessage geoBeaconMessage : geoBeaconMessages.getItems()) { if (!geoBeaconMessage.getBeaconUuid().equals(message.getBeaconUuid())) { continue; // wrong uuid } GeoData geoData = geoBeaconMessage.getGeoData(); if (geoData != null && message.getGeoData() != null) { double distance = GeoBeaconUtil.distanceBetweenGeoBeaconMessages(geoData, message.getGeoData()); if (distance <= geoRadius) { // if it is nearby, add list.add(geoBeaconMessage); } } else { Log.w(LOG_TAG, "No geodata available. Ignoring GEO_RADIUS and putting it into the list"); list.add(geoBeaconMessage); } } return list; } /** * Notifies one beacon (client) about the messages received from nearby devices (if any). * * @param geoBeaconServerClient */ private void notifyOneBeacon(GeoBeaconServerClient geoBeaconServerClient) { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Notifying one client ..."); } final BlaubotMessageSender messageSender = geoBeaconServerClient.getMessageSender(); // TODO filter by nearby beacons (geodata) and beaconUUID // send all messages for now for (GeoBeaconMessage geoBeaconMessage : geoBeaconMessages.getItems()) { final BlaubotMessage blaubotMessage = GeoBeaconUtil.geoBeaconMessageToBlaubotMessage(geoBeaconMessage); messageSender.sendMessage(blaubotMessage); } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Done notifying one client."); } } /** * Returns the acceptors on which this server is listening on. * * @return the list of acceptors */ public List<IBlaubotConnectionAcceptor> getAcceptors() { return acceptors; } /** * Given an IBlaubotConnection, this objects holds the sender and receiver. */ private static class GeoBeaconServerClient { private IBlaubotConnection connection; private BlaubotMessageSender messageSender; private BlaubotMessageReceiver messageReceiver; public GeoBeaconServerClient(IBlaubotConnection connection) { this.connection = connection; this.connection.addConnectionListener(new IBlaubotConnectionListener() { @Override public void onConnectionClosed(IBlaubotConnection connection) { deactivate(); } }); this.messageReceiver = new BlaubotMessageReceiver(connection); this.messageSender = new BlaubotMessageSender(connection); } /** * Activates sender and receiver. */ public void activate() { messageSender.activate(); messageReceiver.activate(); } /** * Deactivates sender and receiver. */ public void deactivate() { if (messageSender != null) { messageSender.deactivate(null); } if (messageReceiver != null) { messageReceiver.deactivate(null); } } public IBlaubotConnection getConnection() { return connection; } public BlaubotMessageSender getMessageSender() { return messageSender; } public BlaubotMessageReceiver getMessageReceiver() { return messageReceiver; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; GeoBeaconServerClient that = (GeoBeaconServerClient) o; return connection.equals(that.connection); } @Override public int hashCode() { return connection.hashCode(); } } }