/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package nl.rhinofly.twelvetalk; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.util.Arrays; import java.util.UUID; import android.app.Application; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; /** * This class does all the work for setting up and managing Bluetooth * connections with other devices. It has a thread that listens for * incoming connections, a thread for connecting with a device, a * thread for performing data transmissions when connected and a thread for polling. */ public class TTBluetoothService { // Debugging private static final String TAG = "BluetoothChatService"; private boolean D = true; // Name for the SDP record when creating server socket private static final String NAME_SECURE = "BluetoothChatSecure"; // Unique UUID for this application (NXT) private static final UUID MY_UUID_SECURE = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); //TO OTHER DEVICE //private static final UUID MY_UUID_SECURE = UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66"); // Member fields private final BluetoothAdapter mAdapter; private final Handler toHandler; private AcceptThread mSecureAcceptThread; private ConnectThread mConnectThread; private ConnectedThread mConnectedThread; private PollingThread mPollingThread; private int mState; private TTApplication TTApp; private BluetoothSocket mmSocket; private BluetoothDevice connectToDevice; // Constants that indicate the current connection state public static final int STATE_NONE = 0; // we're doing nothing public static final int STATE_LISTEN = 1; // now listening for incoming connections public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection public static final int STATE_CONNECTED = 3; // now connected to a remote device /** * Constructor. Prepares a new BluetoothChat session. * @param context The UI Activity Context * @param handler A Handler to send messages back to the UI Activity */ public TTBluetoothService(Handler handler, Application app, BluetoothDevice device) { mAdapter = BluetoothAdapter.getDefaultAdapter(); mState = STATE_NONE; this.TTApp = (TTApplication) app; this.D = TTApp.getPrefs().getBoolean("debug", false); toHandler = handler; connectToDevice = device; } /** * Set the current state of the chat connection * @param state An integer defining the current connection state */ private synchronized void setState(int state) { if (D) sendLogMessage("setState() " + mState + " -> " + state); mState = state; // Give the new state to the Handler so the UI Activity can update toHandler.obtainMessage(TTMainActivity.MESSAGE_STATE_CHANGE, state, -1).sendToTarget(); } /** * Return the current connection state. */ public synchronized int getState() { return mState; } /** * Start the BT service. Specifically start AcceptThread to begin a * session in listening (server) mode. Called by the Activity onResume() */ public synchronized void start() { if (D)sendLogMessage("start"); // Cancel any thread attempting to make a connection if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;} // Cancel any thread currently running a connection if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} setState(STATE_LISTEN); // Start the thread to listen on a BluetoothServerSocket if (mSecureAcceptThread == null) { mSecureAcceptThread = new AcceptThread(true); mSecureAcceptThread.start(); } } /** * Start the ConnectThread to initiate a connection to a remote device. * @param device The BluetoothDevice to connect */ public synchronized void connect() { if (D) sendLogMessage("connecting to: " + connectToDevice); // Cancel any thread attempting to make a connection if (mState == STATE_CONNECTING) { if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;} } // Cancel any thread currently running a connection if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} // Start the thread to connect with the given device mConnectThread = new ConnectThread(); mConnectThread.start(); setState(STATE_CONNECTING); } /** * Start the ConnectedThread to begin managing a Bluetooth connection * @param socket The BluetoothSocket on which the connection was made * @param device The BluetoothDevice that has been connected */ public synchronized void connected(BluetoothSocket socket,final String socketType) { if (D) sendLogMessage("connected, Socket Type:" + socketType); // Cancel the thread that completed the connection if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;} // Cancel any thread currently running a connection if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} // Cancel the accept thread because we only want to connect to one device if (mSecureAcceptThread != null) { mSecureAcceptThread.cancel(); mSecureAcceptThread = null; } // Start the thread to manage the connection and perform transmissions mConnectedThread = new ConnectedThread(socket, socketType,this.TTApp); mConnectedThread.start(); // Send the name of the connected device back to the UI Activity Message msg = toHandler.obtainMessage(TTMainActivity.MESSAGE_CONNECTED); Bundle bundle = new Bundle(); bundle.putString(TTMainActivity.DEVICE_NAME, connectToDevice.getName()); msg.setData(bundle); toHandler.sendMessage(msg); setState(STATE_CONNECTED); } /** * Stop all threads */ public synchronized void stop() { if (D) sendLogMessage("stop"); if (mConnectThread != null) { mConnectThread.cancel(); mConnectThread = null; } if (mConnectedThread != null) { mConnectedThread.cancel(); mConnectedThread = null; } if (mPollingThread != null) { //mPollingThread.cancel(); mPollingThread = null; } if (mSecureAcceptThread != null) { mSecureAcceptThread.cancel(); mSecureAcceptThread = null; } setState(STATE_NONE); } /** * Write to the ConnectedThread in an unsynchronized manner * @param out The bytes to write * @see ConnectedThread#write(byte[]) */ public void write(byte[] out) { // Create temporary object ConnectedThread r; // Synchronize a copy of the ConnectedThread synchronized (this) { if (mState != STATE_CONNECTED) return; r = mConnectedThread; } // Perform the write unsynchronized r.write(out); } private void sendHandlerMessage(int what,String mes){ Message msg = toHandler.obtainMessage(what); Bundle bundle = new Bundle(); bundle.putString(TTMainActivity.MESSAGE, mes); msg.setData(bundle); toHandler.sendMessage(msg); return; } private void sendLogMessage(String mes){ this.sendHandlerMessage(TTMainActivity.MESSAGE_LOG, mes); return; } /** * Indicate that the connection attempt failed and notify the UI Activity. */ private void connectionFailed() { sendLogMessage("Unable to connect device"); // Start the service but wait a bit first.. try { Thread.currentThread().sleep(3000); } catch (InterruptedException e) { sendLogMessage("Reconnect sleep interupted"); } TTBluetoothService.this.connect(); } /** * Indicate that the connection was lost and notify the UI Activity. */ private void connectionLost() { // Start the service again but wait a bit first.. try { Thread.currentThread().sleep(3000); } catch (InterruptedException e) { sendLogMessage("Reconnect sleep interupted"); } TTBluetoothService.this.connect(); } private byte[] hexStringToByteArray(String s) { int len = s.length(); byte[] data = null; try { data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16)); } }catch(Exception e){ //ignore } return data; } /** * This thread runs while listening for incoming connections. It behaves * like a server-side client. It runs until a connection is accepted * (or until cancelled). */ private class AcceptThread extends Thread { // The local server socket private final BluetoothServerSocket mmServerSocket; private String mSocketType; public AcceptThread(boolean secure) { BluetoothServerSocket tmp = null; mSocketType = secure ? "Secure":"Insecure"; // Create a new listening server socket try { tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE, MY_UUID_SECURE); } catch (IOException e) { sendLogMessage("Socket Type: " + mSocketType + "listen() failed"); } mmServerSocket = tmp; } public void run() { if (D) sendLogMessage("Socket Type: " + mSocketType + "BEGIN mAcceptThread" + this); setName("AcceptThread" + mSocketType); BluetoothSocket socket = null; // Listen to the server socket if we're not connected while (mState != STATE_CONNECTED) { try { // This is a blocking call and will only return on a // successful connection or an exception socket = mmServerSocket.accept(); } catch (IOException e) { sendLogMessage("Socket Type: " + mSocketType + "accept() failed"); break; } // If a connection was accepted if (socket != null) { synchronized (TTBluetoothService.this) { switch (mState) { case STATE_LISTEN: case STATE_CONNECTING: // Situation normal. Start the connected thread. connected(socket, mSocketType); break; case STATE_NONE: case STATE_CONNECTED: // Either not ready or already connected. Terminate new socket. try { socket.close(); } catch (IOException e) { sendLogMessage("Could not close unwanted socket"); } break; } } } } if (D) Log.i(TAG, "END mAcceptThread, socket Type: " + mSocketType); } public void cancel() { if (D) Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this); try { mmServerSocket.close(); } catch (IOException e) { Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e); } } } /** * This thread runs while attempting to make an outgoing connection * with a device. It runs straight through; the connection either * succeeds or fails. */ private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private String mSocketType = "secure"; public ConnectThread() { BluetoothSocket tmp = null; // Get a BluetoothSocket for a connection with the // given BluetoothDevice try { tmp = connectToDevice.createRfcommSocketToServiceRecord(MY_UUID_SECURE); } catch (IOException e) { sendLogMessage("secure socket create() failed"); } mmSocket = tmp; } public void run() { sendLogMessage("BEGIN mConnectThread SocketType:" + mSocketType); setName("ConnectThread" + mSocketType); // Always cancel discovery because it will slow down a connection mAdapter.cancelDiscovery(); // Make a connection to the BluetoothSocket try { // This is a blocking call and will only return on a // successful connection or an exception mmSocket.connect(); } catch (IOException e) { sendLogMessage(e.getMessage()); try { mmSocket.close(); } catch (IOException e2) { sendLogMessage("unable to close() " + mSocketType + " socket during connection failure"); } connectionFailed(); return; } // Reset the ConnectThread because we're done synchronized (TTBluetoothService.this) { mConnectThread = null; } // Start the connected thread connected(mmSocket, mSocketType); } public void cancel() { try { mmSocket.close(); } catch (IOException e) { sendLogMessage("close() of connect " + mSocketType + " socket failed"); } } } /** * This thread runs during a connection with a remote device. * It handles all incoming and outgoing transmissions. */ private class ConnectedThread extends Thread { private final InputStream mmInStream; private final OutputStream mmOutStream; private TTApplication TTApp; public ConnectedThread(BluetoothSocket socket, String socketType, TTApplication app) { sendLogMessage("create ConnectedThread: " + socketType); mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; TTApp = (TTApplication) app; // Get the BluetoothSocket input and output streams try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { sendLogMessage("temp sockets not created"); } mmInStream = tmpIn; mmOutStream = tmpOut; } //FIXME: messy code, need to do a bit of cleanup public void run() { Log.i(TAG, "BEGIN mConnectedThread"); byte[] bBuffer = new byte[1024]; ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int bytes; byte firstByte; String ignoreString = this.TTApp.getPrefs().getString("byteString", "0300021340").replaceAll(" ",""); mPollingThread = new PollingThread(this.TTApp); //but only start if user wants to if (TTApp.getPrefs().getBoolean("enablePolling",false)){ mPollingThread.start(); } // Keep listening to the InputStream while connected while (true) { try { // read until -1 is encountered and stick the read bytes in bBuffer, also keep a tab on the number // of bytes read while ((bytes = mmInStream.read(bBuffer, 0, bBuffer.length)) != -1) { //buffer.write(bBuffer, 0, bytes); // get first byte, it tells the size of usefull bytes returned, the rest are padding zero's (NXT always sends 64 bytes, // which is also its maximum, so only first byte us ); firstByte = bBuffer[0]; // NOTE: This fails for int usefullLength = (int)firstByte ; //sendLogMessage("Received: "+bytes+" sending:" +usefullLength); buffer.write(bBuffer, 0, usefullLength); // and send that stream back to the handler (when it does not meet the ignore string of bytes) if (!Arrays.equals(buffer.toByteArray(),hexStringToByteArray(ignoreString))){ toHandler.obtainMessage(TTMainActivity.MESSAGE_READ, bytes, -1, buffer.toByteArray()).sendToTarget(); } buffer.reset(); } } catch (IOException e) { sendHandlerMessage(TTMainActivity.MESSAGE_DISCONNECTED,""); connectionLost(); break; } } } //converts a 'hex'string like "00A0BF" for instance into byte[] {0x00,0xA0,0xBf} /** * Write to the connected OutStream. * @param buffer The bytes to write */ public void write(byte[] buffer) { try { mmOutStream.write(buffer); // Share the sent message back to the UI Activity toHandler.obtainMessage(TTMainActivity.MESSAGE_WRITE, -1, -1, buffer) .sendToTarget(); } catch (IOException e) { sendLogMessage("Exception during write"); } } public void cancel() { try { sendLogMessage("Closing connected socket"); mmSocket.close(); } catch (IOException e) { sendLogMessage("close() of connected socket failed"); } } } /** * This thread runs during a connection with a remote device. * */ private class PollingThread extends Thread { private final OutputStream mmOutStream; private TTApplication TTApp; private boolean runPoll = true; public PollingThread(Application app) { sendHandlerMessage(TTMainActivity.MESSAGE_POLL_CONNECTED, "started PollingThread"); TTApp = (TTApplication) app; OutputStream tmpOut = null; // Get the BluetoothSocket input and output streams try { tmpOut = mmSocket.getOutputStream(); } catch (IOException e) { sendLogMessage("temp sockets not created"); } mmOutStream = tmpOut; } //FIXME: messy code, need to do a bit of cleanup public void run() { Log.i(TAG, "BEGIN mPollingThread"); /* * 0x05 0x00 0x00 0x13 0x0A 0x00 0x01 0x05 0x00 (grootte van nxt bericht) 0x00 (direct message, reply required) 0x13 (MessageRead) 0x0A (Mailbox waar het bericht in wacht, dit is mailbox 1 voor de master, ook wel mailbox 10 genoemd) 0x00 (Mailbox waar het naartoe moet, niet echt boeiend maar wel slim om op 0 te houden) 0x01 (Het bericht moet verwijderd worden) * */ String ignoreString = this.TTApp.getPrefs().getString("pollingByteString", "050000130A0001").replaceAll(" ",""); byte[] pollMess = hexStringToByteArray(ignoreString); // Keep listening to the InputStream while connected while (true && runPoll) { try { write(pollMess); sleep(Integer.parseInt(this.TTApp.getPrefs().getString("polltime", "1000"))); }catch (IOException io) { sendLogMessage("disconnected"); connectionLost(); // Start the service over to restart listening mode TTBluetoothService.this.start(); break; }catch (InterruptedException ie){ sendLogMessage("interruption"); break; } } } /** * Write to the connected OutStream. * @param buffer The bytes to write */ public void write(byte[] buffer) throws IOException{ mmOutStream.write(buffer); // Share the sent message back to the UI Activity if the user wants to if (TTApp.getPrefs().getBoolean("pollingInDebug",false)){ toHandler.obtainMessage(TTMainActivity.MESSAGE_POLL, -1, -1, buffer).sendToTarget(); } } public boolean isConnected(){ return mmOutStream != null; } public void cancel() { this.runPoll = false; } } public void startBluetooth(){ if (mConnectedThread == null){ mPollingThread = new PollingThread(this.TTApp); mPollingThread.start(); sendHandlerMessage(TTMainActivity.MESSAGE_LOG, "Restarting"); } } public void stopBluetooth(){ if (mConnectedThread != null){ mConnectedThread.cancel(); mConnectedThread = null; sendHandlerMessage(TTMainActivity.MESSAGE_DISCONNECTED,"Stopped"); } } public void startPoll(){ if (mPollingThread == null){ mPollingThread = new PollingThread(this.TTApp); mPollingThread.start(); sendHandlerMessage(TTMainActivity.MESSAGE_POLL_CONNECTED, "Polling resumed"); } } public void stopPoll(){ if (mPollingThread != null && mPollingThread.isConnected()){ mPollingThread.cancel(); mPollingThread = null; sendHandlerMessage(TTMainActivity.MESSAGE_POLL_DISCONNECTED, "Polling suspended"); } } }