/*
* Copyright (C) 2011 Cuong Bui
*
* 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 com.android.server.location;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.UUID;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
public class BTGPSService {
private static final boolean D = true;
private static final String TAG = "BTGPSService";
private static final UUID BT_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private final BluetoothAdapter mAdapter;
private final Handler mHandler;
private WatchdogThread mWatchdogThread = null;
private ConnectThread mConnectThread = null;
private ConnectedThread mConnectedThread = null;
private final int mMaxNMEABuffer=4096;
private final char[] buffer = new char[mMaxNMEABuffer];
int bytes;
private long refreshRate = 1000;
private long lastActivity = 0;
// MAX_ACTIVITY_TIMEOUT * refresh time window should have at least one activity.
private int MAX_ACTIVITY_TIMEOUT = 5;
// Maximum connect retry attempt
private int MAX_RECONNECT_RETRIES = 5;
// time window for one single connection (ms). socket connect timeout is around 12 sec
private int MAX_CONNECT_TIMEOUT = 13000;
// last connected device. is used to auto reconnect.
private BluetoothDevice lastConnectedDevice=null;
private int mState = 0;
// 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
public synchronized void setRefreshRate(long r) {
refreshRate = r;
}
public synchronized long getRefreshRate() {
return refreshRate;
}
public BTGPSService(Handler h) {
mAdapter = BluetoothAdapter.getDefaultAdapter();
mHandler = h;
}
private void sendMessage(int message, int arg, Object obj) {
Message m = Message.obtain(mHandler, message);
m.arg1 = arg;
m.obj = obj;
mHandler.sendMessage(m);
}
private void handleFailedConnection() {
if (getServiceState() != STATE_NONE) {
if (D) Log.d(TAG, "Connection failed with status != 0. try to reconnect");
connect(lastConnectedDevice);
} else {
if (D) Log.d(TAG, "Connection stopped with status = 0.");
}
}
/**
* 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) Log.d(TAG, "setState() " + mState + " -> " + state);
mState = state;
if (mState == STATE_NONE) {
sendMessage(BTGpsLocationProvider.GPS_STATUS_UPDATE, 0, null);
} else if (mState == STATE_CONNECTED) {
sendMessage(BTGpsLocationProvider.GPS_STATUS_UPDATE, 1, null);
}
}
/**
* Return the current connection state. */
public synchronized int getServiceState() {
return mState;
}
/**
* Start the chat service. Specifically start AcceptThread to begin a
* session in listening (server) mode. Called by the Activity onResume() */
public synchronized void start() {
if (D) Log.d(TAG, "start");
if (!mAdapter.isEnabled()) {
setState(STATE_NONE);
return;
}
// 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 ConnectThread to initiate a connection to a remote device.
* @param device The BluetoothDevice to connect
*/
public synchronized boolean connect(BluetoothDevice device) {
lastConnectedDevice = device;
if (D) Log.d(TAG, "connect to: " + device);
// Cancel any thread attempting to make a connection
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
if (mWatchdogThread != null) {
mWatchdogThread.cancel();
mWatchdogThread = null;
}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
// Helper thread that monitors and retries to connect after time out
mWatchdogThread = new WatchdogThread(device);
mWatchdogThread.start();
return true;
}
/**
* 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) {
// reset connect thread
if (mConnectThread != null) mConnectThread = null;
// kill watchdog, since we are connected
if (mWatchdogThread != null) {
mWatchdogThread.cancel();
mWatchdogThread = null;
}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
// Start the thread to manage the connection and perform transmissions
mConnectedThread = new ConnectedThread(socket);
mConnectedThread.start();
setState(STATE_CONNECTED);
}
/**
* Stop all threads
*/
public synchronized void stop() {
if (D) Log.d(TAG, "Stopping btsvc, Set state to None");
setState(STATE_NONE);
if (mWatchdogThread != null) {
if (D) Log.d(TAG, "Cancelling watchdog thread");
mWatchdogThread.cancel();
mWatchdogThread = null;
}
if (mConnectThread != null) {
if (D) Log.d(TAG, "Cancelling connect thread");
mConnectThread.cancel();
mConnectThread = null;
}
if (mConnectedThread != null) {
if (D) Log.d(TAG, "Cancelling connected thread");
mConnectedThread.cancel();
mConnectedThread = null;
}
}
/**
* 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;
}
r.write(out);
}
/**
* 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 BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
private String mSocketType;
public ConnectThread(BluetoothDevice device) {
mmDevice = device;
}
private void closeSocket() {
if (D) Log.d(TAG, getId()+":close socket");
if (mmSocket == null) {
Log.e(TAG, getId()+":Socket not ready. Aborting Close");
return;
}
try {
mmSocket.close();
mmSocket = null;
} catch (IOException e) {
Log.e(TAG, getId()+":close() of connect " + mSocketType + " socket failed", e);
}
}
public void run() {
Log.i(TAG, getId() + ":begin mConnectThread");
BluetoothSocket tmp = null;
// Always cancel discovery because it will slow down a connection
try {
tmp = mmDevice.createRfcommSocketToServiceRecord(BT_UUID);
} catch (IOException e) {
Log.e(TAG, "Socket create() failed", e);
return;
}
mmSocket = tmp;
// Make a connection to the BluetoothSocket
if (mAdapter.isEnabled()) mAdapter.cancelDiscovery();
try {
// This is a blocking call and will only return on a
// successful connection or an exception
if (D) Log.d(TAG, getId() + ":Connecting to socket...");
mmSocket.connect();
if (D) Log.d(TAG, "connected with remote device: "
+ mmDevice.getName() + " at address " + mmDevice.getAddress());
connected(mmSocket);
} catch (IOException e) {
Log.w(TAG, getId() + ":connect failed.", e);
return;
}
}
public synchronized void cancel() {
closeSocket();
}
}
/**
* This thread runs during a connection with a remote device.
* It handles all incoming and outgoing transmissions.
*/
private class ConnectedThread extends Thread {
private BluetoothSocket mmSocket;
private InputStream mmInStream;
private OutputStream mmOutStream;
private boolean cancelled = false;
private void closeSocket() {
if (D) Log.d(TAG, getId()+":close socket");
if (mmSocket == null) {
Log.e(TAG, getId()+":Socket not ready. Aborting Close");
return;
}
try {
mmSocket.close();
mmSocket = null;
} catch (IOException e) {
Log.e(TAG, getId()+": close() of connect socket failed", e);
}
}
public ConnectedThread(BluetoothSocket socket) {
Log.d(TAG, getId() + ":begin ConnectedThread");
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// Get the BluetoothSocket input and output streams
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
if (mmSocket == null || mmInStream == null) {
Log.e(TAG, "Input stream or socket is null. Aborting thread");
return;
}
if (D) Log.d(TAG, getId() + ":BEGIN mConnectedThread");
java.util.Arrays.fill(buffer, (char) ' ');
// reset refresh rate to 1000
refreshRate = 1000;
lastActivity = 0;
BufferedReader reader = new BufferedReader(new InputStreamReader(mmInStream));
// Keep listening to the InputStream while connected
while (true) {
try {
if (reader.ready()) {
bytes = reader.read(buffer, 0, mMaxNMEABuffer);
Message msg = mHandler.obtainMessage(
BTGpsLocationProvider.GPS_DATA_AVAILABLE,buffer);
lastActivity = System.currentTimeMillis();
msg.arg1 = bytes;
mHandler.sendMessage(msg);
}
if (lastActivity != 0 && (System.currentTimeMillis() - lastActivity) >
MAX_ACTIVITY_TIMEOUT*refreshRate) {
Log.w(TAG, getId() + ":BT activity timeout.");
closeSocket();
handleFailedConnection();
return;
}
try {
// get default sleep time
Thread.sleep(getRefreshRate());
} catch (InterruptedException e) {
if (cancelled) {
closeSocket();
return;
}
}
} catch (IOException e) {
Log.w(TAG, getId() + ":disconnected.", e);
closeSocket();
handleFailedConnection();
return;
}
}
}
/**
* Write to the connected OutStream.
* @param buffer The bytes to write
*/
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
mmOutStream.flush();
} catch (IOException e) {
Log.e(TAG, "Exception during write", e);
}
}
public void cancel() {
try {
if (mmSocket == null) {
Log.e(TAG, "Input stream null. Aborting Cacnel");
return;
}
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
} finally {
cancelled = true;
interrupt();
}
}
}
/*
* Thread that starts the connection thread an monitors it.
* Thread will be cancelled if timeot occurs
*/
private class WatchdogThread extends Thread {
private final BluetoothDevice btdevice;
private int retries = 0;
private boolean sleep = false;
private boolean cancelled = false;
public WatchdogThread(BluetoothDevice dev) {
btdevice = dev;
}
public void run() {
while(retries < MAX_RECONNECT_RETRIES) {
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
mConnectThread = new ConnectThread(btdevice);
mConnectThread.start();
setState(STATE_CONNECTING);
// monitor connection and cancel if timeout
if (D) Log.d(TAG, getId() + ":Waiting " + MAX_CONNECT_TIMEOUT
+ " (ms) for service to connect...");
try {
sleep = true;
Thread.sleep(MAX_CONNECT_TIMEOUT);
sleep = false;
if (D) Log.d(TAG, getId() + ":Connecting timeout.");
} catch (InterruptedException e) {
if (D) Log.d(TAG, getId() + ":Watchdog interrupted. probably by cancel.");
}
if (getServiceState() == STATE_CONNECTED) {
if (D) Log.d(TAG, getId() + ":Connected. aborting watchdog");
return;
}
if (cancelled) {
if (D) Log.d(TAG, getId() + ":Cancelled. aborting watchdog");
return;
}
retries++;
}
// max timeout, so stopping service
if (D) Log.d(TAG, getId() + ":Max connection retries exceeded. stopping services.");
BTGPSService.this.stop();
}
public void cancel() {
cancelled = true;
if (sleep) interrupt();
}
}
}