package com.openxc.interfaces.bluetooth; import java.io.BufferedInputStream; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.concurrent.TimeUnit; import android.annotation.TargetApi; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; import com.google.common.base.MoreObjects; import com.openxc.interfaces.VehicleInterface; import com.openxc.sources.BytestreamDataSource; import com.openxc.sources.DataSourceException; import com.openxc.sources.DataSourceResourceException; import com.openxc.sources.SourceCallback; import com.openxcplatform.R; /** * A vehicle data source reading measurements from an Bluetooth-enabled * OpenXC device. * * This class tries to connect to a previously paired Bluetooth device with a * given MAC address. If found, it opens a socket to the device and streams * both read and write OpenXC messages. * * This class requires both the android.permission.BLUETOOTH and * android.permission.BLUETOOTH_ADMIN permissions. */ public class BluetoothVehicleInterface extends BytestreamDataSource implements VehicleInterface { private static final String TAG = "BluetoothVehicleInterface"; public static final String DEVICE_NAME_PREFIX = "OpenXC-VI-"; private DeviceManager mDeviceManager; private Thread mAcceptThread; private String mExplicitAddress; private String mConnectedAddress; private BufferedWriter mOutStream; private BufferedInputStream mInStream; private BluetoothSocket mSocket; private boolean mPerformAutomaticScan = true; private boolean mUsePolling = false; private boolean mSocketAccepterRunning = true; public BluetoothVehicleInterface(SourceCallback callback, Context context, String address) throws DataSourceException { super(callback, context); try { mDeviceManager = new DeviceManager(getContext()); } catch(BluetoothException e) { throw new DataSourceException( "Unable to open Bluetooth device manager", e); } IntentFilter filter = new IntentFilter( BluetoothAdapter.ACTION_DISCOVERY_FINISHED); getContext().registerReceiver(mBroadcastReceiver, filter); filter = new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED); getContext().registerReceiver(mBroadcastReceiver, filter); filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); getContext().registerReceiver(mBroadcastReceiver, filter); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); mUsePolling = preferences.getBoolean( context.getString(R.string.bluetooth_polling_key), true); Log.d(TAG, "Bluetooth device polling is " + (mUsePolling ? "enabled" : "disabled")); setAddress(address); start(); mAcceptThread = new Thread(new SocketAccepter()); mAcceptThread.start(); } public BluetoothVehicleInterface(Context context, String address) throws DataSourceException { this(null, context, address); } /** * Control whether periodic polling is used to detect a Bluetooth VI. * * This class opens a Bluetooth socket and will accept incoming connections * from a VI that can act as the Bluetooth master. For VIs that are only * able to act as slave, we have to poll for a connection occasionally to * see if it's within range. */ public void setPollingStatus(boolean enabled) { mUsePolling = enabled; } @Override public boolean setResource(String otherAddress) throws DataSourceException { boolean reconnect = false; if(isConnected()) { if(otherAddress == null) { // switch to automatic but don't break the existing connection reconnect = false; } else if(!sameResource(mConnectedAddress, otherAddress) && !sameResource(mExplicitAddress, otherAddress)) { reconnect = true; } } setAddress(otherAddress); if(reconnect) { try { if(mSocket != null) { mSocket.close(); } } catch(IOException e) { } setFastPolling(true); } return reconnect; } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public boolean isConnected() { boolean connected = false; // If we can't get the lock in 100ms, must be blocked waiting for a // connection so we consider it disconnected. try { if(mConnectionLock.readLock().tryLock(100, TimeUnit.MILLISECONDS)) { connected = super.isConnected(); if(mSocket == null) { connected = false; } else { try { connected &= mSocket.isConnected(); } catch (NoSuchMethodError e) { // Cannot get isConnected() result before API 14 // Assume previous result is correct. } } mConnectionLock.readLock().unlock(); } } catch(InterruptedException e) { } return connected; } @Override public synchronized void stop() { if(isRunning()) { try { getContext().unregisterReceiver(mBroadcastReceiver); } catch(IllegalArgumentException e) { Log.w(TAG, "Broadcast receiver not registered but we expected it to be"); } mDeviceManager.stop(); closeSocket(); super.stop(); } } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("explicitDeviceAddress", mExplicitAddress) .add("connectedDeviceAddress", mConnectedAddress) .add("socket", mSocket) .toString(); } private class SocketAccepter implements Runnable { private BluetoothServerSocket mmServerSocket; @Override public void run() { Log.d(TAG, "Socket accepter starting up"); BluetoothSocket socket = null; while(isRunning() && shouldAttemptConnection()) { while(isConnected()) { mConnectionLock.writeLock().lock(); try { mDeviceChanged.await(); } catch(InterruptedException e) { } finally { mConnectionLock.writeLock().unlock(); } } // If the BT vehicle interface has been disabled, the socket // will be disconnected and we will break out of the above // while(isConnected()) loop and land here - double check that // this interface should still be running before trying to make // another connection. if(!isRunning()) { break; } Log.d(TAG, "Initializing listening socket"); mmServerSocket = mDeviceManager.listen(); if(mmServerSocket == null) { Log.i(TAG, "Unable to listen for Bluetooth connections " + "- adapter may be off"); stopWhileBluetoothDisabled(); break; } try { Log.i(TAG, "Listening for inbound socket connections"); socket = mmServerSocket.accept(); } catch (IOException e) { } if(socket != null) { Log.i(TAG, "New inbound socket connection accepted"); manageConnectedSocket(socket); try { Log.d(TAG, "Closing listening server socket"); mmServerSocket.close(); } catch (IOException e) { } } } Log.d(TAG, "SocketAccepter is stopping"); closeSocket(); stop(); } public void stop() { try { if(mmServerSocket != null) { mmServerSocket.close(); } } catch (IOException e) { } } } @Override protected void connect() { if(!mUsePolling || !isRunning()) { return; } Log.i(TAG, "Beginning polling for Bluetooth devices"); BluetoothDevice lastConnectedDevice = mDeviceManager.getLastConnectedDevice(); BluetoothSocket newSocket = null; if(mExplicitAddress != null || !mPerformAutomaticScan) { String address = mExplicitAddress; if(address == null && lastConnectedDevice != null) { address = lastConnectedDevice.getAddress(); } if(address != null) { Log.i(TAG, "Connecting to Bluetooth device " + address); try { if(!isConnected()) { newSocket = mDeviceManager.connect(address); } } catch(BluetoothException e) { Log.w(TAG, "Unable to connect to device " + address, e); newSocket = null; } } else { Log.d(TAG, "No detected or stored Bluetooth device MAC, not attempting connection"); } } else { // Only try automatic detection of VI once, and whether or not we find // and connect to one, don't go into automatic mode again unless // manually triggered. mPerformAutomaticScan = false; Log.v(TAG, "Attempting automatic detection of Bluetooth VI"); ArrayList<BluetoothDevice> candidateDevices = new ArrayList<>( mDeviceManager.getCandidateDevices()); if(lastConnectedDevice != null) { Log.v(TAG, "First trying last connected BT VI: " + lastConnectedDevice); candidateDevices.add(0, lastConnectedDevice); } for(BluetoothDevice device : candidateDevices) { try { if(!isConnected()) { Log.i(TAG, "Attempting connection to auto-detected " + "VI " + device); newSocket = mDeviceManager.connect(device); break; } } catch(BluetoothException e) { Log.w(TAG, "Unable to connect to auto-detected device " + device, e); newSocket = null; } } if(lastConnectedDevice == null && newSocket == null && candidateDevices.size() > 0) { Log.i(TAG, "No BT VI ever connected, and none of " + "discovered devices could connect - storing " + candidateDevices.get(0).getAddress() + " as the next one to try"); mDeviceManager.storeLastConnectedDevice( candidateDevices.get(0)); } } manageConnectedSocket(newSocket); } private synchronized void manageConnectedSocket(BluetoothSocket socket) { mConnectionLock.writeLock().lock(); try { mSocket = socket; if(mSocket != null) { try { connectStreams(); connected(); mConnectedAddress = mSocket.getRemoteDevice().getAddress(); } catch(BluetoothException e) { Log.d(TAG, "Unable to open Bluetooth streams", e); disconnected(); } } else { disconnected(); } } finally { mConnectionLock.writeLock().unlock(); } } @Override protected int read(byte[] bytes) throws IOException { mConnectionLock.readLock().lock(); int bytesRead = -1; try { if(isConnected()) { bytesRead = mInStream.read(bytes, 0, bytes.length); } } finally { mConnectionLock.readLock().unlock(); } return bytesRead; } protected boolean write(byte[] bytes) { mConnectionLock.readLock().lock(); boolean success = false; try { if(isConnected()) { mOutStream.write(new String(bytes)); // TODO what if we didn't flush every time? might be faster for // sustained writes. mOutStream.flush(); success = true; } else { Log.w(TAG, "Unable to write -- not connected"); } } catch(IOException e) { Log.d(TAG, "Error writing to stream", e); } finally { mConnectionLock.readLock().unlock(); } return success; } private synchronized void closeSocket() { // The Bluetooth socket is thread safe, so we don't grab the connection // lock - we also want to forcefully break the connection NOW instead of // waiting for the lock if BT is going down try { if(mSocket != null) { mSocket.close(); Log.d(TAG, "Disconnected from the socket"); } } catch(IOException e) { Log.w(TAG, "Unable to close the socket", e); } finally { mSocket = null; } } @Override protected void disconnect() { closeSocket(); mConnectionLock.writeLock().lock(); try { try { if(mInStream != null) { mInStream.close(); Log.d(TAG, "Disconnected from the input stream"); } } catch(IOException e) { Log.w(TAG, "Unable to close the input stream", e); } finally { mInStream = null; } try { if(mOutStream != null) { mOutStream.close(); Log.d(TAG, "Disconnected from the output stream"); } } catch(IOException e) { Log.w(TAG, "Unable to close the output stream", e); } finally { mOutStream = null; } disconnected(); } finally { mConnectionLock.writeLock().unlock(); } } @Override protected String getTag() { return TAG; } private void connectStreams() throws BluetoothException { mConnectionLock.writeLock().lock(); try { try { mOutStream = new BufferedWriter(new OutputStreamWriter( mSocket.getOutputStream())); mInStream = new BufferedInputStream(mSocket.getInputStream()); Log.i(TAG, "Socket stream to vehicle interface " + "opened successfully"); } catch(IOException e) { Log.e(TAG, "Error opening streams ", e); disconnect(); throw new BluetoothException(); } } finally { mConnectionLock.writeLock().unlock(); } } private void setAddress(String address) throws DataSourceResourceException { if(address != null && !BluetoothAdapter.checkBluetoothAddress(address)) { throw new DataSourceResourceException("\"" + address + "\" is not a valid MAC address"); } mExplicitAddress = address; } private static boolean sameResource(String address, String otherAddress) { return otherAddress != null && otherAddress.equals(address); } private boolean shouldAttemptConnection() { return mSocketAccepterRunning; } private void stopWhileBluetoothDisabled() { mSocketAccepterRunning = false; stopConnectionAttempts(); } private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if(action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) || action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { // Whenever discovery finishes or another Bluetooth device connects // (i.e. it might be a car's infotainment system), take the // opportunity to try and connect to detected devices if we're not // already connected. Discovery may have been initiated by the // Enabler UI, or by some other user action or app. if(mUsePolling && !isConnected() && !mDeviceManager.isConnecting()) { Log.d(TAG, "Discovery finished or a device connected, but " + "we are not connected or attempting connections - " + "kicking off reconnection attempts"); if(mExplicitAddress == null) { mPerformAutomaticScan = true; } setFastPolling(true); } } else if(action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); switch (state) { case BluetoothAdapter.STATE_OFF: Log.d(TAG, "Bluetooth adapter turned off"); stopWhileBluetoothDisabled(); break; case BluetoothAdapter.STATE_ON: Log.d(TAG, "Bluetooth adapter turned on"); mSocketAccepterRunning = true; setFastPolling(true); mAcceptThread = new Thread(new SocketAccepter()); mAcceptThread.start(); break; } } } }; }