package eu.hgross.blaubot.ethernet;
import java.io.Closeable;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;
import javax.jmdns.ServiceTypeListener;
import eu.hgross.blaubot.core.Blaubot;
import eu.hgross.blaubot.core.BlaubotConstants;
import eu.hgross.blaubot.core.BlaubotDevice;
import eu.hgross.blaubot.core.BlaubotUUIDSet;
import eu.hgross.blaubot.core.IBlaubotAdapter;
import eu.hgross.blaubot.core.IBlaubotDevice;
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.ExchangeStatesTask;
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.statemachine.BlaubotAdapterHelper;
import eu.hgross.blaubot.core.statemachine.states.IBlaubotState;
import eu.hgross.blaubot.util.KingdomCensusLifecycleListener;
import eu.hgross.blaubot.util.Log;
/**
* The bonjour beacon.
* Exploits the bonjour protocol to send beacon states.
*
* Android note:
* If you use this on Android, ensure that you acquired a MulticastLock from the WiFiManager!
* WifiManager wifi = (WifiManager) getSystemService(android.content.Context.WIFI_SERVICE);
* mMulticastLock = wifi.createMulticastLock("BlaubotMulticastLock");
* mMulticastLock.setReferenceCounted(true);
* mMulticastLock.acquire();
* and obviously don't forget to release it later.
*/
public class BlaubotBonjourBeacon implements IBlaubotBeacon, IEthernetBeacon, Closeable {
private static final long BONJOUR_LIST_DISCOVERY_INETRVAL = 5000;
public static final int SLEEP_TIME_BETWEEN_BEACON_CONNECTIONS = 100;
private final InetAddress inetAddress;
private ExecutorService executorService = Executors.newCachedThreadPool();
private static final String LOG_TAG = "BlaubotBonjourBeacon";
public static final String BONJOUR_KEY_BEACON_UUID = "BI"; // only 2 bytes allowed
public static final String BONJOUR_KEY_UNIQUE_ID = "DI"; // only 2 bytes allowed
private final int beaconPort;
private volatile boolean discoveryActivated = true;
private IBlaubotDevice ownDevice;
private BlaubotUUIDSet uuidSet;
private IBlaubotBeaconStore beaconStore;
private IBlaubotListeningStateListener listeningStateListener;
private IBlaubotIncomingConnectionListener acceptorListener;
private IBlaubotDiscoveryEventListener discoveryListener;
private Blaubot blaubot;
private JmDNS jmDns;
private volatile ServiceInfo currentServiceInfo;
private volatile EthernetBeaconAcceptThread acceptThread;
private Object startStopMonitor = new Object();
private volatile IBlaubotState currentState;
private volatile Timer timer;
private KingdomCensusLifecycleListener kingdomCensusLifecycleListener;
public BlaubotBonjourBeacon(InetAddress inetAddress, int beaconPort) {
this.beaconPort = beaconPort;
this.inetAddress = inetAddress;
}
private final ServiceListener bonjourServiceListener = new ServiceListener() {
@Override
public void serviceAdded(ServiceEvent event) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "serviceAdded " + event);
}
}
@Override
public void serviceRemoved(ServiceEvent event) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "serviceRemoved " + event);
}
}
@Override
public void serviceResolved(ServiceEvent event) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "serviceResolved" + event);
}
startBeaconExchange(event.getInfo());
}
};
/**
* Tries to connect to the beacon (if relevant beacon) and starts the state exchange, if connection was successful.
* @param serviceInfo the jmDNS service info that discovered the device
*/
private void startBeaconExchange(ServiceInfo serviceInfo) {
// -- we found another blaubot service running
String beaconUuid = serviceInfo.getPropertyString(BONJOUR_KEY_BEACON_UUID);
if (!uuidSet.getBeaconUUID().toString().equals(beaconUuid)) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Received a blaubot service event, but the beacon uuid's didn't match. There are either multiple different apps running on the network or something is wrong with your app's uuid.");
}
return;
}
// -- same beacon uuid as ours
final String uniqueDeviceId = serviceInfo.getPropertyString(BONJOUR_KEY_UNIQUE_ID);
if (uniqueDeviceId.equals(ownDevice.getUniqueDeviceID())) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Received our own service advertisement, ignoring");
}
return;
}
// -- connect and be happy
// get inet addr and port
final Inet4Address[] inet4Addresses = serviceInfo.getInet4Addresses();
if (inet4Addresses.length < 1) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Could not get the inet addr for " + uniqueDeviceId + "'s beacon");
}
return;
}
final int remoteBeaconPort = serviceInfo.getPort();
InetAddress remoteDeviceAddr = inet4Addresses[0]; // take first
// try to connect, then exchange states via tcp/ip
IBlaubotDevice remoteDevice = new BlaubotDevice(uniqueDeviceId);
Socket clientSocket;
try {
clientSocket = new Socket(remoteDeviceAddr, remoteBeaconPort);
BlaubotEthernetUtils.sendOwnUniqueIdThroughSocket(ownDevice, clientSocket);
BlaubotEthernetConnection connection = new BlaubotEthernetConnection(remoteDevice, clientSocket);
final List<ConnectionMetaDataDTO> ownAcceptorsMetaDataList = BlaubotAdapterHelper.getConnectionMetaDataList(BlaubotAdapterHelper.getConnectionAcceptors(blaubot.getAdapters()));
ExchangeStatesTask exchangeStatesTask = new ExchangeStatesTask(ownDevice, connection, currentState, ownAcceptorsMetaDataList, beaconStore, discoveryListener);
exchangeStatesTask.run();
} catch (IOException e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Connection to " + remoteDevice + "'s beacon failed: " + e.getMessage(), e);
}
}
}
@Override
public void setBlaubot(Blaubot blaubot) {
this.blaubot = blaubot;
this.ownDevice = blaubot.getOwnDevice();
this.uuidSet = blaubot.getUuidSet();
this.kingdomCensusLifecycleListener = new KingdomCensusLifecycleListener(ownDevice);
this.blaubot.addLifecycleListener(kingdomCensusLifecycleListener);
}
@Override
public void setBeaconStore(IBlaubotBeaconStore beaconStore) {
this.beaconStore = beaconStore;
}
@Override
public IBlaubotAdapter getAdapter() {
return null;
}
@Override
public void startListening() {
synchronized (startStopMonitor) {
if (isStarted()) {
return;
}
// only on first start, check jdmDns state
if (this.jmDns == null) {
try {
this.jmDns = JmDNS.create(inetAddress);
this.jmDns.addServiceListener(BlaubotConstants.BLAUBOT_BEACON_BONJOUR_SERVICE_NAME, bonjourServiceListener);
this.jmDns.addServiceTypeListener(new ServiceTypeListener() {
@Override
public void serviceTypeAdded(ServiceEvent event) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "serviceTypeAdded " + event);
}
}
@Override
public void subTypeForServiceTypeAdded(ServiceEvent event) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "subTypeForServiceTypeAdded " + event);
}
}
});
} catch (IOException e) {
// should already be handled by the creation of the inetAddress
throw new RuntimeException(e);
}
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacon is starting to listen for incoming connections on port " + beaconPort);
}
acceptThread = new EthernetBeaconAcceptThread(acceptorListener, this);
acceptThread.start();
// build new service info
final HashMap<String, String> values = new HashMap<>();
values.put(BONJOUR_KEY_BEACON_UUID, uuidSet.getBeaconUUID().toString());
values.put(BONJOUR_KEY_UNIQUE_ID, ownDevice.getUniqueDeviceID());
ServiceInfo serviceInfo = ServiceInfo.create(BlaubotConstants.BLAUBOT_BEACON_BONJOUR_SERVICE_NAME, ownDevice.getUniqueDeviceID(), beaconPort, 0, 0, values);
// register new service info
try {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Registering new Bonjour service entry ...");
}
jmDns.registerService(serviceInfo);
currentServiceInfo = serviceInfo;
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "New Bonjour service entry registered.");
}
} catch (IOException e) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Failed to register serviceInfo for bonjour", e);
}
throw new RuntimeException(e);
}
if (listeningStateListener != null) {
listeningStateListener.onListeningStarted(this);
}
}
}
@Override
public void stopListening() {
synchronized (startStopMonitor) {
if (!isStarted()) {
return;
}
// unregister old
if (currentServiceInfo != null) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Unregistering old Bonjour service entry ...");
}
jmDns.unregisterService(currentServiceInfo);
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Unregistered old Bonjour service entry.");
}
}
if (acceptThread != null && acceptThread.isAlive()) {
try {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Waiting for beacon accept thread to finish ...");
}
acceptThread.interrupt();
acceptThread.join();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacon accept thread to finished ...");
}
} catch (InterruptedException e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, e);
}
}
acceptThread = null;
}
if (listeningStateListener != null) {
listeningStateListener.onListeningStopped(this);
}
}
}
@Override
public boolean isStarted() {
return acceptThread != null && acceptThread.isAlive();
}
@Override
public void setListeningStateListener(IBlaubotListeningStateListener stateListener) {
this.listeningStateListener = stateListener;
}
@Override
public void setAcceptorListener(IBlaubotIncomingConnectionListener acceptorListener) {
this.acceptorListener = acceptorListener;
}
@Override
public ConnectionMetaDataDTO getConnectionMetaData() {
return null;
}
@Override
public void setDiscoveryEventListener(IBlaubotDiscoveryEventListener discoveryEventListener) {
this.discoveryListener = discoveryEventListener;
}
@Override
public void onConnectionStateMachineStateChanged(IBlaubotState state) {
this.currentState = state;
}
@Override
public void setDiscoveryActivated(boolean active) {
synchronized (startStopMonitor) {
if (!active) {
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
}
} else {
if (timer == null) {
timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (jmDns != null && isStarted()) {
final ServiceInfo[] list = jmDns.list(BlaubotConstants.BLAUBOT_BEACON_BONJOUR_SERVICE_NAME);
for (ServiceInfo serviceInfo : createRelevantDevicesList(list)) {
startBeaconExchange(serviceInfo);
try {
Thread.sleep(SLEEP_TIME_BETWEEN_BEACON_CONNECTIONS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, 0, BONJOUR_LIST_DISCOVERY_INETRVAL);
}
}
this.discoveryActivated = active;
}
}
/**
* Based on the bonjour informations, filters the list of service infos to the most relevant
* devices (filter out connected devices, ...)
* @param serviceInfos the bonjour data
* @return the filtered list of serviceInfo objects
*/
private List<ServiceInfo> createRelevantDevicesList(ServiceInfo[] serviceInfos) {
final Set<String> connectedUniqueIds = kingdomCensusLifecycleListener.getConnectedUniqueIds();
ArrayList<ServiceInfo> filtered = new ArrayList<>();
for(ServiceInfo serviceInfo : Arrays.asList(serviceInfos)) {
final String beaconUUID = serviceInfo.getPropertyString(BONJOUR_KEY_BEACON_UUID);
final String uniqueDeviceID = serviceInfo.getPropertyString(BONJOUR_KEY_UNIQUE_ID);
if(beaconUUID == null || uniqueDeviceID == null) {
// System.out.println("skip: " + serviceInfo);
continue;
}
if(beaconUUID.toString().equals(beaconUUID) && !connectedUniqueIds.contains(uniqueDeviceID)) {
filtered.add(serviceInfo);
} else {
// System.out.println("skip: " + serviceInfo);
}
}
return filtered;
}
@Override
public Thread getAcceptThread() {
return acceptThread;
}
@Override
public int getBeaconPort() {
return beaconPort;
}
@Override
public void close() throws IOException {
if (this.executorService != null) {
this.executorService.shutdown();
}
if (this.jmDns != null) {
this.jmDns.unregisterAllServices();
this.jmDns.close();
}
}
}