package eu.hgross.blaubot.geobeacon; import com.google.gson.Gson; import java.util.Arrays; import java.util.List; import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import eu.hgross.blaubot.core.BeaconHelper; import eu.hgross.blaubot.core.Blaubot; import eu.hgross.blaubot.core.BlaubotConstants; import eu.hgross.blaubot.core.BlaubotDevice; import eu.hgross.blaubot.core.IBlaubotAdapter; import eu.hgross.blaubot.core.IBlaubotConnection; import eu.hgross.blaubot.core.State; import eu.hgross.blaubot.core.acceptor.ConnectionMetaDataDTO; import eu.hgross.blaubot.core.acceptor.IBlaubotIncomingConnectionListener; import eu.hgross.blaubot.core.acceptor.IBlaubotListeningStateListener; import eu.hgross.blaubot.core.acceptor.discovery.BeaconMessage; import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeacon; import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeaconStore; import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotDiscoveryEventListener; import eu.hgross.blaubot.core.connector.IBlaubotConnector; import eu.hgross.blaubot.core.statemachine.states.IBlaubotState; 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 Beacon that commits its current state to a server flavoured with geolocation data * to get updates about nearby devices that do the same. * * Does not commit updates, if in passive mode. * GeoLocation data has to be commited to this beacon instance by calling setGeoData(...). * * This beacon class should be subclassed to integrate the platform specific geo location * provider into the subclass' internals. Call setGeoData() to provide this data. */ public abstract class GeoLocationBeacon implements IBlaubotBeacon { private static final String LOG_TAG = "GeoLocationBeacon"; private boolean discoveryActive; private Gson gson; private Blaubot blaubot; private IBlaubotBeaconStore beaconStore; private IBlaubotListeningStateListener listeningStateListener; private IBlaubotIncomingConnectionListener acceptorListener; private IBlaubotDiscoveryEventListener discoveryEventListener; private final List<IBlaubotConnector> connectors; /** * The last known csm state to adjust some timings for republishing. */ private State currentState; /** * Timestamp of the last beacon message publishing */ private long lastGeoBeaconMessagePublish; /** * receiver of the current connection to the beacon server */ private BlaubotMessageReceiver currentGeoBeaconConnectionMessageReceiver; /** * sender of the current connection to the beacon server */ private BlaubotMessageSender currentGeoBeaconConnectionMessageSender; /** * The current beacon message containing the state of the csm */ private BeaconMessage currentBeaconMessage; /** * The last known geo data for this device. * Will be published to the GeoBeaconServer */ private GeoData currentGeoData; /** * The executor service used to connect to the GeoBeaconServer */ private ScheduledExecutorService beaconServerConnectExecutor; private static final long CONNECT_PERIOD = 3000; private Runnable connectTask = new Runnable() { @Override public synchronized void run() { if (currentGeoBeaconConnectionMessageSender != null) { return; // do nothing, if connceted } // connect for (IBlaubotConnector connector : connectors) { final IBlaubotConnection geoBeaconServerConnection = connector.connectToBlaubotDevice(new BlaubotDevice(GeoBeaconConstants.GEO_BEACON_SERVER_UNIQUE_DEVICE_ID)); if (geoBeaconServerConnection != null) { currentGeoBeaconConnectionMessageSender = new BlaubotMessageSender(geoBeaconServerConnection); currentGeoBeaconConnectionMessageReceiver = new BlaubotMessageReceiver(geoBeaconServerConnection); currentGeoBeaconConnectionMessageReceiver.addMessageListener(messageListener); currentGeoBeaconConnectionMessageSender.activate(); currentGeoBeaconConnectionMessageReceiver.activate(); try { publishGeoBeaconMessageToServer(); } catch (Exception e) { e.printStackTrace(); } break; } } } }; /** * We need to periodically re-publish the message to the server because there is a fixed max age * for the records until they get erased. This executor will re-publish if needed */ private ScheduledExecutorService republishExecutor; private static final long REPUBLISH_TASK_PERIOD = 3000; private Runnable republishTask = new Runnable() { /** * Checks whether we need to update the state at the server because the last republish is too old * @return true, iff we should update */ private boolean doRepublish() { final long now = System.currentTimeMillis(); // we either use the max age (very slow re-publishs), if we are in a network // or we send pretty fast, if we are eager to find partners final double MAX_PERIOD_SINCE_LAST_PUBLISH = currentState != null && currentState == State.Free ? REPUBLISH_TASK_PERIOD : GeoBeaconConstants.MAX_AGE_BEACON_MESSAGES * 0.75; return (now - lastGeoBeaconMessagePublish) > MAX_PERIOD_SINCE_LAST_PUBLISH; } @Override public void run() { if (!discoveryActive) { return; } if (doRepublish()) { publishGeoBeaconMessageToServer(); } } }; /** * Gets notified by the beacon server if new interesting beacons are nearby */ private IBlaubotMessageListener messageListener = new IBlaubotMessageListener() { @Override public void onMessage(BlaubotMessage blaubotMessage) { final GeoBeaconMessage geoBeaconMessage = GeoBeaconUtil.blaubotMessageToGeoBeaconMessage(blaubotMessage); if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Got message from GeoBeaconServer: " + geoBeaconMessage); } if (discoveryEventListener != null) { // create discovery event(s) and notify final BeaconMessage beaconMessage = geoBeaconMessage.getBeaconMessage(); BeaconHelper.populateEventsFromBeaconMessage(beaconMessage, discoveryEventListener); } } }; private UUID beaconUUID; /** * @param beaconStore The beacon store holding the connection meta data for the given connectors to connect to the GeoBeaconServer's acceptors. * @param connectors connectors to be used to establish a connection to the beacon server */ public GeoLocationBeacon(IBlaubotBeaconStore beaconStore, IBlaubotConnector... connectors) { this.connectors = Arrays.asList(connectors); for (IBlaubotConnector connector : connectors) { connector.setBeaconStore(beaconStore); } this.gson = new Gson(); } @Override public void setBlaubot(Blaubot blaubot) { this.blaubot = blaubot; this.beaconUUID = blaubot.getUuidSet().getBeaconUUID(); } @Override public void setBeaconStore(IBlaubotBeaconStore beaconStore) { this.beaconStore = beaconStore; } @Override public IBlaubotAdapter getAdapter() { return null; } @Override public synchronized void startListening() { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Starting GeoLocationBeacon ..."); } if (beaconServerConnectExecutor != null) { // already started return; } // start connect thread this.beaconServerConnectExecutor = Executors.newSingleThreadScheduledExecutor(); this.beaconServerConnectExecutor.scheduleWithFixedDelay(connectTask, 0, CONNECT_PERIOD, TimeUnit.MILLISECONDS); // start re-publish thread this.republishExecutor = Executors.newSingleThreadScheduledExecutor(); this.republishExecutor.scheduleWithFixedDelay(republishTask, 0, REPUBLISH_TASK_PERIOD, TimeUnit.MILLISECONDS); // notify started if (this.listeningStateListener != null) { this.listeningStateListener.onListeningStarted(this); } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "GeoLocationBeacon started."); } } @Override public synchronized void stopListening() { if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Stopping GeoLocationBeacon ..."); } // stop connect thread if (beaconServerConnectExecutor != null) { beaconServerConnectExecutor.shutdownNow(); try { beaconServerConnectExecutor.awaitTermination(CONNECT_PERIOD * 10, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { } finally { beaconServerConnectExecutor = null; } } if (republishExecutor != null) { republishExecutor.shutdownNow(); try { republishExecutor.awaitTermination(REPUBLISH_TASK_PERIOD * 10, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { } finally { republishExecutor = null; } } // disconnect connection and deactivate sender/receiver if (this.currentGeoBeaconConnectionMessageReceiver != null) { currentGeoBeaconConnectionMessageReceiver.getBlaubotConnection().disconnect(); this.currentGeoBeaconConnectionMessageReceiver.deactivate(null); this.currentGeoBeaconConnectionMessageSender.deactivate(null); this.currentGeoBeaconConnectionMessageReceiver.removeMessageListener(messageListener); this.currentGeoBeaconConnectionMessageReceiver = null; this.currentGeoBeaconConnectionMessageSender = null; } // notify if (this.listeningStateListener != null) { this.listeningStateListener.onListeningStopped(this); } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "GeoLocationBeacon stopped."); } } @Override public synchronized boolean isStarted() { return beaconServerConnectExecutor != null; } @Override public void setListeningStateListener(IBlaubotListeningStateListener stateListener) { this.listeningStateListener = stateListener; } @Override public void setAcceptorListener(IBlaubotIncomingConnectionListener acceptorListener) { this.acceptorListener = acceptorListener; } @Override public ConnectionMetaDataDTO getConnectionMetaData() { // TODO separate the acceptor interface from the beacon interface return new ConnectionMetaDataDTO(); } @Override public void setDiscoveryEventListener(IBlaubotDiscoveryEventListener discoveryEventListener) { this.discoveryEventListener = discoveryEventListener; } @Override public void onConnectionStateMachineStateChanged(IBlaubotState state) { this.currentState = State.getStateByStatemachineClass(state.getClass()); this.currentBeaconMessage = blaubot.getConnectionStateMachine().getBeaconService().getCurrentBeaconMessage(); publishGeoBeaconMessageToServer(); } @Override public void setDiscoveryActivated(boolean active) { discoveryActive = active; } /** * Sets the current geo data * * @param geoData the best known location of this device */ public void setGeoData(GeoData geoData) { this.currentGeoData = geoData; publishGeoBeaconMessageToServer(); } /** * Builds and publishes the current GeoBeaconMessage to the server */ private void publishGeoBeaconMessageToServer() { lastGeoBeaconMessagePublish = 0; // mark that we need to re-publish if (currentGeoBeaconConnectionMessageSender == null || currentBeaconMessage == null || beaconUUID == null) { return; } if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Publishing state to beacon server ..."); } GeoBeaconMessage geoBeaconMessage = new GeoBeaconMessage(currentBeaconMessage, currentGeoData, beaconUUID.toString()); GeoBeaconMessageDTO dto = new GeoBeaconMessageDTO(geoBeaconMessage); byte[] geoMessageDtoBytes = gson.toJson(dto).getBytes(BlaubotConstants.STRING_CHARSET); BlaubotMessage msg = new BlaubotMessage(); msg.setPayload(geoMessageDtoBytes); this.currentGeoBeaconConnectionMessageSender.sendMessage(msg); this.lastGeoBeaconMessagePublish = System.currentTimeMillis(); if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Published state to beacon server."); } } }