package eu.hgross.blaubot.bluetooth;
import java.io.IOException;
import java.util.UUID;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;
import eu.hgross.blaubot.core.BlaubotConnectionManager;
import eu.hgross.blaubot.core.BlaubotConstants;
import eu.hgross.blaubot.core.BlaubotDevice;
import eu.hgross.blaubot.core.IBlaubotAdapter;
import eu.hgross.blaubot.core.IUnidentifiedBlaubotDevice;
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.BeaconMessage;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeaconStore;
import eu.hgross.blaubot.util.Log;
/**
* An Acceptor handling incoming bluetooth connections.
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*/
public class BlaubotJsr82BluetoothAcceptor implements IBlaubotConnectionAcceptor {
private static final String LOG_TAG = BlaubotJsr82BluetoothAcceptor.class.toString();
private final String localDeviceBluetoothMacAddress;
private final LocalDevice localDevice;
private IBlaubotListeningStateListener listeningStateListener;
private IBlaubotIncomingConnectionListener acceptorListener;
private Jsr82BluetoothAcceptThread acceptThread = null;
private boolean started = false;
private BlaubotJsr82BluetoothAdapter blaubotBluetoothAdapter;
private IBlaubotBeaconStore beaconStore;
/**
* @param blaubotBluetoothAdapter the blaubot adapter
* @throws BluetoothStateException if we cannot acces the hardware bluetooth adapter
*/
public BlaubotJsr82BluetoothAcceptor(BlaubotJsr82BluetoothAdapter blaubotBluetoothAdapter) throws BluetoothStateException {
this.blaubotBluetoothAdapter = blaubotBluetoothAdapter;
this.localDevice = LocalDevice.getLocalDevice();
this.localDeviceBluetoothMacAddress = localDevice.getBluetoothAddress();
}
@Override
public IBlaubotAdapter getAdapter() {
return blaubotBluetoothAdapter;
}
@Override
public void setBeaconStore(IBlaubotBeaconStore beaconStore) {
this.beaconStore = beaconStore;
}
@Override
public void startListening() {
if (acceptThread != null) {
stopListening();
}
acceptThread = new Jsr82BluetoothAcceptThread(blaubotBluetoothAdapter.getUUIDSet().getAppUUID());
acceptThread.start();
}
@Override
public void stopListening() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Stop listening for bluetooth clients ...");
}
if (acceptThread != null) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Interrupting and joining acceptThread ...");
}
acceptThread.interrupt();
try {
acceptThread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "AcceptThread stopped ...");
}
}
acceptThread = null;
}
@Override
public void setListeningStateListener(IBlaubotListeningStateListener stateListener) {
this.listeningStateListener = stateListener;
}
@Override
public void setAcceptorListener(IBlaubotIncomingConnectionListener acceptorListener) {
this.acceptorListener = acceptorListener;
}
@Override
public ConnectionMetaDataDTO getConnectionMetaData() {
StringBuilder sb = new StringBuilder();
int i=0;
int l = localDeviceBluetoothMacAddress.length();
for (char c : localDeviceBluetoothMacAddress.toCharArray()) {
sb.append(c);
if ((i%2) != 0 && i!=l-1 ) {
sb.append(":");
}
i++;
}
String formattedMac = sb.toString();
Jsr82BluetoothConnectionMetaDataDTO connectionMetaDataDTO = new Jsr82BluetoothConnectionMetaDataDTO(formattedMac);
return connectionMetaDataDTO;
}
@Override
public boolean isStarted() {
return started;
}
/**
* A blaubot device that enables us to delay the uniqueDeviceId set() -> used after we received it from the connection
*/
private class UnidentifiedBlaubotDevice extends BlaubotDevice implements IUnidentifiedBlaubotDevice {
public UnidentifiedBlaubotDevice() {
super("UnidentifiedBlaubotDeviceFrom" + BlaubotJsr82BluetoothAcceptor.this);
}
@Override
public void setUniqueDeviceId(String uniqueDeviceId) {
this.uniqueDeviceId = uniqueDeviceId;
}
}
/**
* Handles initial BlauBot instance communication. Once a client connects, the connected socket is handed over to the {@link BlaubotConnectionManager} clientConnections
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*/
public class Jsr82BluetoothAcceptThread extends Thread {
private final String LOG_TAG = "Jsr82BluetoothAcceptor.Jsr82BluetoothAcceptThread";
private UUID uuid;
private StreamConnectionNotifier streamConnectionNotifier;
/**
* @param uuid The uuid to register with bluetooth SDP
*/
public Jsr82BluetoothAcceptThread(UUID uuid) {
this.uuid = uuid;
setName("jsr82-bluetooth-acceptor-accept-thread");
}
@Override
public void interrupt() {
super.interrupt();
if (this.streamConnectionNotifier == null) {
return;
}
Log.d(LOG_TAG, "Closing streamConnection ...");
try {
this.streamConnectionNotifier.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Closing streamConnection caused exception", e);
}
}
@Override
public void run() {
String reformattedUuid = uuid.toString().replace("-", "");
final String url = "btspp://localhost:" + reformattedUuid + ";name=" + BlaubotConstants.BLUETOOTH_ACCEPTORS_RFCOMM_SDP_SERVICE_NAME;
started = true;
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "BluetoothJsr82 Accept Thread starting ...");
}
try {
// we will be visible for an unlimited time: http://www.ampedrftech.com/guides/cod_definition.pdf
localDevice.setDiscoverable(DiscoveryAgent.GIAC);
} catch (BluetoothStateException e) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Failed to start GIAC discoverable state: " + e.getMessage(), e);
}
}
StreamConnectionNotifier service = null;
try {
service = (StreamConnectionNotifier) Connector.open(url, Connector.READ_WRITE);
streamConnectionNotifier = service;
} catch (IOException e) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Could not listen to RFCOMM", e);
}
started = false;
throw new RuntimeException("TODO: handle listen() failure");
}
boolean notifiedListening = false;
while (!this.isInterrupted()) {
StreamConnection socket = null;
RemoteDevice dev = null;
try {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Creating bluetooth StreamConnection for incoming blaubot connections ...");
}
if (!notifiedListening && listeningStateListener != null) {
notifiedListening = true;
listeningStateListener.onListeningStarted(BlaubotJsr82BluetoothAcceptor.this);
}
socket = service.acceptAndOpen();
dev = RemoteDevice.getRemoteDevice(socket);
} catch (IOException e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "ServerSocket accept failed.", e);
}
}
if (socket != null) {
// we got a connection gather unique id
UnidentifiedBlaubotDevice blaubotDevice;
BlaubotJsr82BluetoothConnection connection;
String readableName = null;
try {
blaubotDevice = new UnidentifiedBlaubotDevice();
connection = new BlaubotJsr82BluetoothConnection(blaubotDevice, socket);
// read the accepting device's uniqueDeviceId
String uniqueDeviceId = UniqueDeviceIdHelper.readUniqueDeviceId(connection.getDataInputStream());
blaubotDevice.setUniqueDeviceId(uniqueDeviceId);
} catch (IOException e) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Something went wrong gathering the unique device id");
}
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
continue;
}
try {
readableName = dev.getFriendlyName(false);
if (readableName != null && !readableName.isEmpty()) {
blaubotDevice.setReadableName(readableName);
} else {
blaubotDevice.setReadableName(dev.getBluetoothAddress());
}
} catch (IOException e) {
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Got connection from blaubot slave device " + dev.getBluetoothAddress() + "(" + blaubotDevice.getReadableName() + ")");
}
// retrieve their beacon message with their state and most importantly their acceptor meta data
final BeaconMessage theirBeaconMessage = BeaconMessage.fromBlaubotConnection(connection);
beaconStore.putDiscoveryEvent(theirBeaconMessage, blaubotDevice);
if (acceptorListener != null) {
acceptorListener.onConnectionEstablished(connection);
} else {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "No AcceptorListener registered to " + this + " - connection established but unknown to everyone!");
}
}
} else {
// TODO: we also end here, if we hit the limit of the max bluetooth connections on a device!!
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Socket null - no client connected. This can happen if you hit the maximum connections supported by a device's bluetooth hardware, on timeout or aborted calls.");
}
}
}
// loop finished, check wether we need to notified observers, that we started listening
// if so, notify that we are now not listening anymore
if (notifiedListening && listeningStateListener != null) {
listeningStateListener.onListeningStopped(BlaubotJsr82BluetoothAcceptor.this);
}
started = false;
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Accept Thread finished ...");
}
}
}
}