package eu.hgross.blaubot.android.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import eu.hgross.blaubot.core.Blaubot;
import eu.hgross.blaubot.core.BlaubotConstants;
import eu.hgross.blaubot.core.IBlaubotAdapter;
import eu.hgross.blaubot.core.IBlaubotConnection;
import eu.hgross.blaubot.core.IBlaubotDevice;
import eu.hgross.blaubot.core.acceptor.ConnectionMetaDataDTO;
import eu.hgross.blaubot.core.acceptor.IBlaubotConnectionAcceptor;
import eu.hgross.blaubot.core.acceptor.IBlaubotIncomingConnectionListener;
import eu.hgross.blaubot.core.acceptor.IBlaubotListeningStateListener;
import eu.hgross.blaubot.core.acceptor.UniqueDeviceIdHelper;
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.acceptor.discovery.TimeoutList;
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;
/**
* A beacon implementation for bluetooth on android.
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
public class BlaubotBluetoothBeacon implements IBlaubotBeacon {
private static final String LOG_TAG = "BlaubotBluetoothBeacon";
private static final String BEACON_SERVICE_NAME = "BeaconService";
private IBlaubotDevice ownDevice;
private IBlaubotListeningStateListener listeningStateListener;
private IBlaubotIncomingConnectionListener acceptorListener;
private BluetoothServerSocket serverSocket;
private BeaconScanner beaconScanner;
private BeaconAcceptThread acceptThread;
private IBlaubotDiscoveryEventListener discoveryEventListener;
private boolean discoveryActivated;
private IBlaubotState currentState = null;
private UUID currentBeaconUUID;
private IBlaubotBeaconStore beaconStore;
private Blaubot blaubot;
private KingdomCensusLifecycleListener kingdomCensusLifecycleListener;
public BlaubotBluetoothBeacon() {
this.discoveryActivated = true;
}
@Override
public IBlaubotAdapter getAdapter() {
return null;
}
@Override
public synchronized void startListening() {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Starting to listen for beacon connections ...");
}
if (isStarted()) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacon already listening - stopping first ...");
}
stopListening();
}
try {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacon is starting to listen on RFCOMM with UUID " + currentBeaconUUID);
}
final BluetoothServerSocket bss = BluetoothAdapter.getDefaultAdapter().listenUsingRfcommWithServiceRecord(BEACON_SERVICE_NAME, currentBeaconUUID);
serverSocket = bss;
acceptThread = new BeaconAcceptThread();
acceptThread.start();
if (beaconScanner == null) {
beaconScanner = new BeaconScanner();
beaconScanner.start();
}
} catch (IOException e) {
// TODO: what to do if we fail here?!
serverSocket = null;
if(Log.logErrorMessages()) {
Log.e(LOG_TAG, "Got IOException on BluetoothServerSocket creation!", e);
}
throw new RuntimeException("Could not create the bluetooth ServerSocket!");
}
if (listeningStateListener != null)
listeningStateListener.onListeningStarted(this);
}
@Override
public synchronized void stopListening() {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Stopping to listen for beacon connections ...");
}
if (beaconScanner != null && beaconScanner.isAlive()) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Waiting for beacon BeaconScanner to finish ...");
}
beaconScanner.interrupt();
// should kill himself
// beaconScanner.join();
beaconScanner = null;
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "BeaconScanner finished ...");
}
}
if (serverSocket != null) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Closing BluetoothServerSocket ...");
}
try {
serverSocket.close();
serverSocket = null;
} catch (IOException e) {
if(Log.logWarningMessages()) {
Log.w(LOG_TAG, "Got IOException during close!");
}
}
}
if (acceptThread != null && acceptThread.isAlive()) {
try {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Waiting for beacon accept thread to finish ...");
}
acceptThread.interrupt();
acceptThread.join();
acceptThread = null;
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacon accept thread to finished ...");
}
} catch (InterruptedException e) {
if(Log.logWarningMessages()) {
Log.w(LOG_TAG, e);
}
}
}
if (listeningStateListener != null)
listeningStateListener.onListeningStopped(this);
}
@Override
public boolean isStarted() {
return serverSocket != null || beaconScanner != null || acceptThread != null;
}
@Override
public void setListeningStateListener(IBlaubotListeningStateListener stateListener) {
this.listeningStateListener = stateListener;
}
@Override
public void setAcceptorListener(IBlaubotIncomingConnectionListener acceptorListener) {
this.acceptorListener = acceptorListener;
}
@Override
public ConnectionMetaDataDTO getConnectionMetaData() {
// TODO: maybe beacons should not derive from acceptors anymore
return null;
}
@Override
public void setDiscoveryEventListener(IBlaubotDiscoveryEventListener discoveryEventListener) {
this.discoveryEventListener = discoveryEventListener;
}
/**
* Fulfills the {@link IBlaubotConnectionAcceptor} specification for this beacon. Simply accepts the incoming beacon
* connections, wraps them into the generalized {@link IBlaubotConnection} interface and hand them to the
* {@link IBlaubotIncomingConnectionListener} for further handling of the state information exchange.
*
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
class BeaconAcceptThread extends Thread {
private final String LOG_TAG = "BeaconAcceptThread";
@Override
public void interrupt() {
super.interrupt();
}
@Override
public void run() {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "BeaconAcceptThread started ...");
}
final BluetoothServerSocket serverSocket = BlaubotBluetoothBeacon.this.serverSocket;
while (!isInterrupted() && serverSocket != null) { // this is busy wait (for 3 to 5 iterations) - I can live
// with that.
BluetoothSocket bluetoothSocket;
try {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Waiting for incoming beacon connections ...");
}
bluetoothSocket = serverSocket.accept();
} catch (IOException e) {
if(Log.logWarningMessages()) {
Log.w(LOG_TAG, "Beacon communication failed with I/O Exception ", e);
}
// TODO: in cases where the bluetooth adapter fails (not unlikely on android) we will end up in an endless loop
continue;
}
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Got a new beacon connection from " + bluetoothSocket.getRemoteDevice());
}
String uniqueDeviceId;
try {
// we read the unique device id
DataInputStream dis = new DataInputStream(bluetoothSocket.getInputStream());
uniqueDeviceId = UniqueDeviceIdHelper.readUniqueDeviceId(dis);
// we send our unique device id
UniqueDeviceIdHelper.sendUniqueDeviceIdThroughOutputStream(ownDevice, bluetoothSocket.getOutputStream());
} catch (IOException e) {
if(Log.logErrorMessages()) {
Log.e(LOG_TAG, "Something went wrong exchanging unique device Ids");
}
try {
bluetoothSocket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
continue;
}
BlaubotBluetoothDevice bluetoothDevice = new BlaubotBluetoothDevice(uniqueDeviceId, bluetoothSocket.getRemoteDevice());
IBlaubotConnection connection = new BlaubotBluetoothConnection(bluetoothDevice, bluetoothSocket);
if (acceptorListener != null) {
acceptorListener.onConnectionEstablished(connection);
} else {
if(Log.logWarningMessages()) {
Log.w(LOG_TAG, "Got a beacon connection but no acceptor listener was there to handle it!");
}
connection.disconnect();
}
}
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "BeaconAcceptThread finished ...");
}
}
}
/**
* Iterates continuously over the paired devices and exchanges state informations over the beacon interface by
* trying to establish a connection to the device. If successful, the discovered state is propagated via the
* beacon's handleDiscoveredBlaubotDevice(..) method.
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*
*/
class BeaconScanner extends Thread {
private static final String LOG_TAG = "BeaconScanner";
private static final int MIN_WAIT_BETWEEN_BEACON_PROBES = 2000; // ms
private static final int MAX_WAIT_BETWEEN_BEACON_PROBES = 5000; // ms
/**
* Maps UniqueDeviceIds -> BluetoothMacAdresses
*/
private ConcurrentHashMap<String, String> uniqueIdToMacAddressCache = new ConcurrentHashMap<>();
@Override
public void interrupt() {
super.interrupt();
}
/**
* Creates the list of devices to be scanned.
*
* @return list of devices to scan
*/
private ArrayList<BluetoothDevice> getDevicesToScan() {
// We only check devices that are bonded and not already connected to our network to minimize
// the expensive bluetooth sdp lookup and connectivity traffic.
Set<BluetoothDevice> bondedDevices = BluetoothAdapter.getDefaultAdapter().getBondedDevices();
ArrayList<BluetoothDevice> devicesToScan = new ArrayList<>();
final Set<String> excludedUniqueIds = new HashSet<>(kingdomCensusLifecycleListener.getConnectedUniqueIds());
final Set<String> excludedMacAddresses = new HashSet<>();
for(Map.Entry<String, String> entry : uniqueIdToMacAddressCache.entrySet()) {
if(excludedUniqueIds.contains(entry.getKey())) {
continue;
}
excludedMacAddresses.add(entry.getValue());
}
for (BluetoothDevice d : bondedDevices) {
// try to filter connected devices
if(excludedMacAddresses.contains(d.getAddress())) {
// -- address of d is in the network, so skip
continue;
}
devicesToScan.add(d);
}
// After filtering we sort the devices by descending by their addresses
Collections.sort(devicesToScan, new Comparator<BluetoothDevice>() {
@Override
public int compare(BluetoothDevice lhs, BluetoothDevice rhs) {
return lhs.getAddress().compareTo(rhs.getAddress());
}
});
Collections.reverse(devicesToScan);
return devicesToScan;
}
@Override
public void run() {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "BeaconScanner started ...");
}
boolean interrupted = false; // attention: we use sleep inside the loop so isInterrupted() will fail if we
// end up in the catch-block (flag cleared)
Random random = new Random(System.currentTimeMillis());
while (!this.isInterrupted() && !interrupted && beaconScanner == Thread.currentThread()) {
if (isDiscoveryDisabled()) {
try {
Thread.sleep(500); // TODO: busy wait, not good
} catch (InterruptedException e) {
break;
}
}
ArrayList<BluetoothDevice> devicesToScan = getDevicesToScan();
if(!(this.isInterrupted() || interrupted || isDiscoveryDisabled())) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Probing " + (devicesToScan.size()) + " bonded devices on their beacon interfaces");
}
}
for (BluetoothDevice device : devicesToScan) {
if (this.isInterrupted() || interrupted || isDiscoveryDisabled())
break;
if (notAvailableDevices.contains(device)) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Skipping known dead device " + device + " (" + device.getName() + ")");
}
continue; // skip dead devices
}
BluetoothSocket socket;
try {
BlaubotConstants.BLUETOOTH_ADAPTER_LOCK.acquire();
try {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Connecting to beacon of " + device + " (" + device.getName() + ")" + " ... ");
}
socket = device.createRfcommSocketToServiceRecord(currentBeaconUUID);
if(!notAvailableDevices.contains(device)) {
socket.connect();
}
} catch (IOException e) {
socket = null;
if(Log.logWarningMessages()) {
Log.w(LOG_TAG, "Could not connect to bluetooth beacon of device " + device + " (" + device.getName() + ") -> " + e.getMessage());
}
} finally {
BlaubotConstants.BLUETOOTH_ADAPTER_LOCK.release();
}
} catch (InterruptedException e) {
break;
}
if(socket != null) {
String uniqueDeviceId;
try {
// send our uniqueDeviceId
UniqueDeviceIdHelper.sendUniqueDeviceIdThroughOutputStream(ownDevice, socket.getOutputStream());
// read the accepting device's uniqueDeviceId
DataInputStream dis = new DataInputStream(socket.getInputStream());
uniqueDeviceId = UniqueDeviceIdHelper.readUniqueDeviceId(dis);
uniqueIdToMacAddressCache.put(uniqueDeviceId, socket.getRemoteDevice().getAddress());
} catch (IOException e) {
if(Log.logErrorMessages()) {
Log.e(LOG_TAG, "Something went wrong exchanging unique device Ids");
}
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
continue;
}
BlaubotBluetoothDevice bbd = new BlaubotBluetoothDevice(uniqueDeviceId, device);
IBlaubotConnection connection = new BlaubotBluetoothConnection(bbd, socket);
final List<ConnectionMetaDataDTO> connectionMetaDataList = BlaubotAdapterHelper.getConnectionMetaDataList(BlaubotAdapterHelper.getConnectionAcceptors(blaubot.getAdapters()));
ExchangeStatesTask exchangeStatesTask = new ExchangeStatesTask(ownDevice, connection, currentState, connectionMetaDataList, beaconStore, discoveryEventListener);
exchangeStatesTask.run();
}
try {
long time = random.nextInt(MAX_WAIT_BETWEEN_BEACON_PROBES - MIN_WAIT_BETWEEN_BEACON_PROBES + 1) + MIN_WAIT_BETWEEN_BEACON_PROBES;
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Letting the bluetooth adapter breathe for " + time + " ms");
}
Thread.sleep(time);
} catch (InterruptedException e) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Interrupted while breathing.");
}
interrupted = true;
break;
}
}
}
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "BeaconScanner finished ...");
}
}
/**
* @return true if the discovery is disabled by config or explicitly
*/
private boolean isDiscoveryDisabled() {
return !discoveryActivated;
}
}
@Override
public void onConnectionStateMachineStateChanged(IBlaubotState state) {
currentState = state;
}
@Override
public void setDiscoveryActivated(boolean active) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Discovery was set to " + (active ? "active" : "inactive"));
}
this.discoveryActivated = active;
}
@Override
public void setBlaubot(Blaubot blaubot) {
this.blaubot = blaubot;
this.currentBeaconUUID = blaubot.getUuidSet().getBeaconUUID();
this.ownDevice = blaubot.getOwnDevice();
this.kingdomCensusLifecycleListener = new KingdomCensusLifecycleListener(ownDevice);
this.blaubot.addLifecycleListener(this.kingdomCensusLifecycleListener);
}
@Override
public void setBeaconStore(IBlaubotBeaconStore beaconStore) {
this.beaconStore = beaconStore;
}
private static final long NEGATIVE_LIST_TIMEOUT = 1300;
protected TimeoutList<BluetoothDevice> notAvailableDevices = new TimeoutList<>(NEGATIVE_LIST_TIMEOUT);
}