Merge remote-tracking branch 'mjb/master'
[fw/altos] / altosdroid / src / org / altusmetrum / AltosDroid / BluetoothChatService.java
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package org.altusmetrum.AltosDroid;
18
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.util.UUID;
23 import android.bluetooth.BluetoothAdapter;
24 import android.bluetooth.BluetoothDevice;
25 import android.bluetooth.BluetoothSocket;
26 import android.content.Context;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.Message;
30 import android.util.Log;
31
32 /**
33  * This class does all the work for setting up and managing Bluetooth
34  * connections with other devices. It has a thread that listens for
35  * incoming connections, a thread for connecting with a device, and a
36  * thread for performing data transmissions when connected.
37  */
38 public class BluetoothChatService {
39     // Debugging
40     private static final String TAG = "BluetoothChatService";
41     private static final boolean D = true;
42
43     // Member fields
44     private final BluetoothAdapter mAdapter;
45     private final Handler mHandler;
46     private ConnectThread mConnectThread;
47     private ConnectedThread mConnectedThread;
48     private int mState;
49
50     // Constants that indicate the current connection state
51     public static final int STATE_NONE = 0;       // we're doing nothing
52     public static final int STATE_READY = 1;
53     public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
54     public static final int STATE_CONNECTED = 3;  // now connected to a remote device
55
56     /**
57      * Constructor. Prepares a new BluetoothChat session.
58      * @param context  The UI Activity Context
59      * @param handler  A Handler to send messages back to the UI Activity
60      */
61     public BluetoothChatService(Context context, Handler handler) {
62         mAdapter = BluetoothAdapter.getDefaultAdapter();
63         mState = STATE_NONE;
64         mHandler = handler;
65     }
66
67     /**
68      * Set the current state of the chat connection
69      * @param state  An integer defining the current connection state
70      */
71     private synchronized void setState(int state) {
72         if (D) Log.d(TAG, "setState() " + mState + " -> " + state);
73         mState = state;
74
75         // Give the new state to the Handler so the UI Activity can update
76         mHandler.obtainMessage(AltosDroid.MESSAGE_STATE_CHANGE, state, -1).sendToTarget();
77     }
78
79     /**
80      * Return the current connection state. */
81     public synchronized int getState() {
82         return mState;
83     }
84
85     /**
86      * Start the chat service. Specifically start AcceptThread to begin a
87      * session in listening (server) mode. Called by the Activity onResume() */
88     public synchronized void start() {
89         if (D) Log.d(TAG, "start");
90
91         // Cancel any thread attempting to make a connection
92         if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
93
94         // Cancel any thread currently running a connection
95         if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
96
97         setState(STATE_READY);
98
99     }
100
101     /**
102      * Start the ConnectThread to initiate a connection to a remote device.
103      * @param device  The BluetoothDevice to connect
104      */
105     public synchronized void connect(BluetoothDevice device) {
106         if (D) Log.d(TAG, "connect to: " + device);
107
108         // Cancel any thread attempting to make a connection
109         if (mState == STATE_CONNECTING) {
110             if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
111         }
112
113         // Cancel any thread currently running a connection
114         if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
115
116         // Start the thread to connect with the given device
117         mConnectThread = new ConnectThread(device);
118         mConnectThread.start();
119         setState(STATE_CONNECTING);
120     }
121
122     /**
123      * Start the ConnectedThread to begin managing a Bluetooth connection
124      * @param socket  The BluetoothSocket on which the connection was made
125      * @param device  The BluetoothDevice that has been connected
126      */
127     public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
128         if (D) Log.d(TAG, "connected");
129
130         // Cancel the thread that completed the connection
131         if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
132
133         // Cancel any thread currently running a connection
134         if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
135
136         // Start the thread to manage the connection and perform transmissions
137         mConnectedThread = new ConnectedThread(socket);
138         mConnectedThread.start();
139
140         // Send the name of the connected device back to the UI Activity
141         Message msg = mHandler.obtainMessage(AltosDroid.MESSAGE_DEVICE_NAME);
142         Bundle bundle = new Bundle();
143         bundle.putString(AltosDroid.DEVICE_NAME, device.getName());
144         msg.setData(bundle);
145         mHandler.sendMessage(msg);
146
147         setState(STATE_CONNECTED);
148     }
149
150     /**
151      * Stop all threads
152      */
153     public synchronized void stop() {
154         if (D) Log.d(TAG, "stop");
155
156         if (mConnectThread != null) {
157             mConnectThread.cancel();
158             mConnectThread = null;
159         }
160
161         if (mConnectedThread != null) {
162             mConnectedThread.cancel();
163             mConnectedThread = null;
164         }
165
166         setState(STATE_NONE);
167     }
168
169     /**
170      * Write to the ConnectedThread in an unsynchronized manner
171      * @param out The bytes to write
172      * @see ConnectedThread#write(byte[])
173      */
174     public void write(byte[] out) {
175         // Create temporary object
176         ConnectedThread r;
177         // Synchronize a copy of the ConnectedThread
178         synchronized (this) {
179             if (mState != STATE_CONNECTED) return;
180             r = mConnectedThread;
181         }
182         // Perform the write unsynchronized
183         r.write(out);
184     }
185
186     /**
187      * Indicate that the connection attempt failed and notify the UI Activity.
188      */
189     private void connectionFailed() {
190         // Send a failure message back to the Activity
191         Message msg = mHandler.obtainMessage(AltosDroid.MESSAGE_TOAST);
192         Bundle bundle = new Bundle();
193         bundle.putString(AltosDroid.TOAST, "Unable to connect device");
194         msg.setData(bundle);
195         mHandler.sendMessage(msg);
196
197         // Start the service over to restart listening mode
198         BluetoothChatService.this.start();
199     }
200
201     /**
202      * Indicate that the connection was lost and notify the UI Activity.
203      */
204     private void connectionLost() {
205         // Send a failure message back to the Activity
206         Message msg = mHandler.obtainMessage(AltosDroid.MESSAGE_TOAST);
207         Bundle bundle = new Bundle();
208         bundle.putString(AltosDroid.TOAST, "Device connection was lost");
209         msg.setData(bundle);
210         mHandler.sendMessage(msg);
211
212         // Start the service over to restart listening mode
213         BluetoothChatService.this.start();
214     }
215
216
217     /**
218      * This thread runs while attempting to make an outgoing connection
219      * with a device. It runs straight through; the connection either
220      * succeeds or fails.
221      */
222     private class ConnectThread extends Thread {
223         private final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
224         private final BluetoothSocket mmSocket;
225         private final BluetoothDevice mmDevice;
226
227                 public ConnectThread(BluetoothDevice device) {
228             mmDevice = device;
229             BluetoothSocket tmp = null;
230
231             try {
232                                 tmp = mmDevice.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
233                         } catch (IOException e) {
234                                 e.printStackTrace();
235                         }
236             mmSocket = tmp;
237         }
238
239         public void run() {
240             Log.i(TAG, "BEGIN mConnectThread");
241             setName("ConnectThread");
242
243             // Always cancel discovery because it will slow down a connection
244             mAdapter.cancelDiscovery();
245
246             // Make a connection to the BluetoothSocket
247             try {
248                 // This is a blocking call and will only return on a
249                 // successful connection or an exception
250                 mmSocket.connect();
251             } catch (IOException e) {
252                 // Close the socket
253                 try {
254                     mmSocket.close();
255                 } catch (IOException e2) {
256                     Log.e(TAG, "unable to close() socket during connection failure", e2);
257                 }
258                 connectionFailed();
259                 return;
260             }
261
262             // Reset the ConnectThread because we're done
263             synchronized (BluetoothChatService.this) {
264                 mConnectThread = null;
265             }
266
267             // Start the connected thread
268             connected(mmSocket, mmDevice);
269         }
270
271         public void cancel() {
272             try {
273                 mmSocket.close();
274             } catch (IOException e) {
275                 Log.e(TAG, "close() of connect socket failed", e);
276             }
277         }
278     }
279
280     /**
281      * This thread runs during a connection with a remote device.
282      * It handles all incoming and outgoing transmissions.
283      */
284     private class ConnectedThread extends Thread {
285         private final BluetoothSocket mmSocket;
286         private final InputStream mmInStream;
287         private final OutputStream mmOutStream;
288
289         public ConnectedThread(BluetoothSocket socket) {
290             Log.d(TAG, "create ConnectedThread");
291             mmSocket = socket;
292             InputStream tmpIn = null;
293             OutputStream tmpOut = null;
294
295             // Get the BluetoothSocket input and output streams
296             try {
297                 tmpIn = socket.getInputStream();
298                 tmpOut = socket.getOutputStream();
299             } catch (IOException e) {
300                 Log.e(TAG, "temp sockets not created", e);
301             }
302
303             mmInStream = tmpIn;
304             mmOutStream = tmpOut;
305         }
306
307         public void run() {
308             Log.i(TAG, "BEGIN mConnectedThread");
309             byte[] buffer = new byte[1024];
310             int bytes;
311
312             // Keep listening to the InputStream while connected
313             while (true) {
314                 try {
315                     // Read from the InputStream
316                     bytes = mmInStream.read(buffer);
317
318                     // Send the obtained bytes to the UI Activity
319                     mHandler.obtainMessage(AltosDroid.MESSAGE_READ, bytes, -1, buffer.clone())
320                             .sendToTarget();
321                 } catch (IOException e) {
322                     Log.e(TAG, "disconnected", e);
323                     connectionLost();
324                     break;
325                 }
326             }
327         }
328
329         /**
330          * Write to the connected OutStream.
331          * @param buffer  The bytes to write
332          */
333         public void write(byte[] buffer) {
334             try {
335                 mmOutStream.write(buffer);
336                 mmOutStream.write('\n');
337
338                 // Share the sent message back to the UI Activity
339                 mHandler.obtainMessage(AltosDroid.MESSAGE_WRITE, -1, -1, buffer)
340                         .sendToTarget();
341             } catch (IOException e) {
342                 Log.e(TAG, "Exception during write", e);
343             }
344         }
345
346         public void cancel() {
347             try {
348                 mmSocket.close();
349             } catch (IOException e) {
350                 Log.e(TAG, "close() of connect socket failed", e);
351             }
352         }
353     }
354 }