/* Copyright (C) 2013 Isak Eriksson
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/ */
package bgsep.bluetooth;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
import bgsep.virtualgamepad.MainActivity;
import bgsep.bluetooth.SenderImpl;
import lib.Constants;
import lib.Protocol;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Intent;
import android.os.ParcelUuid;
import android.util.Log;
import android.widget.Toast;
/**
*
* This is the main bluetooth handler. It handles discovery of and connection to
* the server. It also notifies the GUI about connection state.
*
* @author Isak Eriksson (isak.eriksson@mail.com) & Linus Lindgren
* (linlind@student.chalmers.se)
*
*/
public class BluetoothHandler extends Thread {
private Activity activity;
private static final String TAG = "Gamepad";
private BluetoothAdapter adapter;
private BluetoothSocket socket;
private OutputStream outputStream;
private InputStream inputStream;
private UUID ExpectedUUID;
private SenderImpl si;
private boolean allowAutoConnect;
private Toast mainToast;
private String serverName;
private static final int SLEEP_BETWEEN_CONNECTION_ATTEMPTS = 3000;
private static final int SLEEP_BETWEEN_POLL = 2000;
private static final int SLEEP_BEFORE_STARTING_POLL = 100;
private enum State {
IDLE, CONNECTING, WAITING_FOR_ACCEPT, CONNECTED, START_CONNECTING
}
private State state;
public static final int BLUETOOTH_REQUEST_CODE = 1;
public BluetoothHandler(Activity activity) {
setName("BluetoothHandler");
ExpectedUUID = java.util.UUID.fromString(Protocol.SERVER_UUID);
state = State.IDLE;
allowAutoConnect = true;
this.activity = activity;
mainToast = new Toast((MainActivity) activity);
si = new SenderImpl(this);
start();
}
public void disconnect(boolean expected, String message) {
state = State.IDLE;
if (expected) {
Log.d(TAG, "disconnecting from server");
si.sendCloseMessage("Disconnected by user");
notifyDisconnected(message);
} else {
notifyDisconnected(message);
}
try {
Thread.sleep(Constants.SLEEP_BETWEEN_NOTIFY_AND_CLOSE);
outputStream.close();
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException e) {
Log.d(TAG, "no connection to server");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* Sends an array of bytes to the server.
*
* @param data
* the data that will be sent to the server
*/
public synchronized void send(byte[] data) {
try {
outputStream.write(data);
return;
} catch (IOException e) {
Log.d(TAG, "Unable to send data (" + e.getMessage() + "). The server seems to be down, stopping communication..");
disconnect(false, "Connection interrupted");
} catch (NullPointerException e) {
Log.d(TAG, "No connection to server, stopping communication..");
disconnect(false, "Disconnected");
}
state = State.IDLE;
}
/**
* Stops the attempt to find and connect to a server.
*/
public void cancelConnectionAttempt() {
Log.d(TAG, "cancelling connection attempt");
((MainActivity) activity).serverDisconnected();
state = State.IDLE;
}
public void startThread() {
if (!isConnected()) {
state = State.START_CONNECTING;
}
}
public void autoConnect() {
if (allowAutoConnect) {
allowAutoConnect = false;
startThread();
}
}
// public boolean isStarted() {
// return false;
// }
public boolean isConnected() {
return (socket != null && state == State.CONNECTED);
}
private void notifyConnecting() {
activity.runOnUiThread(new Runnable() {
public void run() {
((MainActivity) activity).indicateConnecting();
}
});
}
@Override
public void run() {
while (!interrupted()) {
switch (state) {
case START_CONNECTING:
state = State.CONNECTING;
notifyConnecting();
if (initBluetoothAdapter()) {
startConnectionAttempt();
} else {
notifyDisconnected("Bluetooth not available");
}
break;
case WAITING_FOR_ACCEPT:
readFromServer();
break;
default:
// do nothing...
break;
}
try {
Thread.sleep(SLEEP_BEFORE_STARTING_POLL);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (!interrupted() && state == State.CONNECTED) {
readFromServer();
if (state == State.CONNECTED)
si.poll();
Log.d(TAG, "poll");
try {
Thread.sleep(SLEEP_BETWEEN_POLL);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private void readFromServer() {
try {
if ((state == State.CONNECTED || state == State.WAITING_FOR_ACCEPT) && inputStream.available() > 0) {
int data = inputStream.read();
if (data == Protocol.MESSAGE_TYPE_CLOSE) {
disconnect(false, "Disconnected from server");
} else if (data == Protocol.MESSAGE_TYPE_SERVER_FULL) {
disconnect(false, "Server is full");
} else if (data == Protocol.MESSAGE_TYPE_CONNECTION_ACCEPTED) {
notifyConnected(serverName);
state = State.CONNECTED;
}
}
} catch (IOException e) {
Log.d(TAG, "unable to read incomming data");
}
}
private void startConnectionAttempt() {
Log.d(TAG, "connecting...");
if (!connectToServer()) {
state = State.IDLE;
} else {
state = State.WAITING_FOR_ACCEPT;
si.sendNameMessage(adapter.getName());
Log.d(TAG, "connection established, waiting for accept..");
}
}
private boolean checkForServer(BluetoothDevice d) {
ParcelUuid[] uuids = d.getUuids();
if (uuids == null) {
return false;
}
for (ParcelUuid uuid : uuids) {
if (uuid.toString().equals(ExpectedUUID.toString())) {
Log.d(TAG, "Found a gamepad host at device" + d.getName() + " (" + d.getAddress() + ")");
return true;
}
}
return false;
}
private boolean connectToBoundedDevices() {
Log.d(TAG, adapter.getBondedDevices().size() + " bounded devices");
for (BluetoothDevice d : adapter.getBondedDevices()) {
if (d != null) {
Log.d(TAG, "\t" + d.getName());
if (checkForServer(d)) {
Log.d(TAG, "Connecting to server..");
boolean connected = connect(d.getAddress());
if (connected) {
serverName = d.getName();
return true;
}
} else {
Log.d(TAG, "start fetching with Sdp on bonded device " + d.getName() + " - " + d.getAddress());
d.fetchUuidsWithSdp();
}
}
}
return false;
}
/**
* Loops through all bounded devices and connects to the device which is
* running the Virtual Gamepad Host. If a server is not found on a bounded
* device, an SDP discovery is started.
*
*/
private boolean connectToServer() {
Log.d(TAG, "searching for servers..");
if (adapter.getBondedDevices() == null) {
notifyDisconnected("Bluetooth not available");
return false;
}
if (adapter.getBondedDevices().size() == 0) {
notifyNoServerFound();
}
while (true) { // start connecting
if (connectToBoundedDevices()) {
return true;
}
try {
Thread.sleep(SLEEP_BETWEEN_CONNECTION_ATTEMPTS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (state != State.CONNECTING) {
return false;
}
}
}
private boolean initBluetoothAdapter() {
adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
Log.d(TAG, "No bluetooth adapter detected! Stopping thread!");
state = State.IDLE;
return false;
} else {
Log.d(TAG, "Bluetooth adapter \"" + adapter.getName() + "\" detected");
}
if (adapter.isEnabled()) {
Log.d(TAG, "Bluetooth device is enabled");
} else {
Log.d(TAG, "Bluetooth device is disabled");
Log.d(TAG, "Enabling bluetooth device..");
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
activity.startActivityForResult(enableBtIntent, BLUETOOTH_REQUEST_CODE);
}
return adapter.isEnabled();
}
private boolean connect(final String address) {
if (adapter == null || address == null) {
Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
return false;
}
final BluetoothDevice device = adapter.getRemoteDevice(address);
if (device == null) {
Log.w(TAG, "Device not found. Unable to connect.");
return false;
}
try {
socket = device.createInsecureRfcommSocketToServiceRecord(ExpectedUUID);
socket.connect();
outputStream = socket.getOutputStream();
inputStream = socket.getInputStream();
if (socket.isConnected()) {
return true;
} else {
return false;
}
} catch (IOException e) {
System.out.println("unable to connect to server: " + e.getMessage());
return false;
}
}
/**
* This is used to display a message to the user about changed connection
* state.
*
* @param text
*/
private void showToast(final CharSequence text) {
System.out.println("toastar!");
activity.runOnUiThread(new Runnable() {
public void run() {
mainToast.cancel();
mainToast = Toast.makeText(activity, text, Toast.LENGTH_SHORT);
mainToast.show();
}
});
}
private void notifyConnected(String name) {
showToast("Connected to " + name);
activity.runOnUiThread(new Runnable() {
public void run() {
((MainActivity) activity).serverConnected();
}
});
}
private void notifyDisconnected(String message) {
showToast(message);
activity.runOnUiThread(new Runnable() {
public void run() {
((MainActivity) activity).serverDisconnected();
}
});
}
private void notifyNoServerFound() {
Log.d(TAG, "no servers found!");
showToast("No server found");
activity.runOnUiThread(new Runnable() {
public void run() {
((MainActivity) activity).serverDisconnected();
}
});
}
}