package com.openxc.interfaces.bluetooth; import java.io.IOException; import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.content.SharedPreferences; import android.os.Looper; import android.util.Log; import com.openxc.util.SupportSettingsUtils; /** * The DeviceManager collects the functions required to connect to and open a * socket to the Bluetooth device. * * The device must be previously bonded, as this class does not initiate * discovery. */ public class DeviceManager { private final static String TAG = "DeviceManager"; public static final String KNOWN_BLUETOOTH_DEVICE_PREFERENCES = "known_bluetooth_devices"; public static final String KNOWN_BLUETOOTH_DEVICE_PREF_KEY = "known_bluetooth_devices"; public static final String LAST_CONNECTED_BLUETOOTH_DEVICE_PREF_KEY = "last_connected_bluetooth_device"; public final static UUID RFCOMM_UUID = UUID.fromString( "00001101-0000-1000-8000-00805f9b34fb"); private BluetoothAdapter mBluetoothAdapter; private BluetoothSocket mSocket; private AtomicBoolean mSocketConnecting = new AtomicBoolean(false); private Context mContext; /** * The DeviceManager requires an Android Context in order to send the intent * to enable Bluetooth if it isn't already on. */ public DeviceManager(Context context) throws BluetoothException { mContext = context; if(getDefaultAdapter() == null) { String message = "This device most likely does not have " + "a Bluetooth adapter"; Log.w(TAG, message); throw new BluetoothException(message); } else { Log.d(TAG, "Initializing Bluetooth device manager"); } } private BluetoothAdapter getDefaultAdapter() { if(mBluetoothAdapter == null) { // work around an Android bug, requires that this is called before // getting the default adapter if(Looper.myLooper() == null) { Looper.prepare(); } mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); } return mBluetoothAdapter; } public void startDiscovery() { if(getDefaultAdapter() != null) { if(getDefaultAdapter().isDiscovering()) { getDefaultAdapter().cancelDiscovery(); } Log.i(TAG, "Starting Bluetooth discovery"); getDefaultAdapter().startDiscovery(); } } public BluetoothServerSocket listen() { BluetoothServerSocket tmp = null; try { // TODO use an OpenXC-specific UUID tmp = getDefaultAdapter().listenUsingRfcommWithServiceRecord( "TODO", DeviceManager.RFCOMM_UUID); } catch (IOException e) { } return tmp; } /** * Connect to the target device and open a socket. This method will block * while waiting for the device. * * Returns a socket connected to the device. */ public BluetoothSocket connect(String targetAddress) throws BluetoothException { return connect(getDefaultAdapter().getRemoteDevice(targetAddress)); } public BluetoothSocket connect(BluetoothDevice device) throws BluetoothException { if(device == null) { throw new BluetoothException("Not connecting to null Bluetooth device"); } mSocket = setupSocket(device); connectToSocket(mSocket); storeLastConnectedDevice(device); return mSocket; } /** * Immediately cancel any pending Bluetooth operations. * * The BluetoothSocket.connect() function blocks while waiting for a * connection, but it's thread safe and we can cancel that by calling * close() on it at any time. * * Importantly we don't want to close the socket any other time, because we * want to leave that up to the user of the socket - if you call close() * twice, or close Input/Output streams associated with the socket * simultaneously, it can cause a segfault due to a bug in some Android * Bluetooth stacks. Awesome! */ public void stop() { if(mSocketConnecting.get() && mSocket != null) { try { mSocket.close(); } catch(IOException e) { } } if(getDefaultAdapter() != null) { getDefaultAdapter().cancelDiscovery(); } } public Set<BluetoothDevice> getPairedDevices() { Set<BluetoothDevice> devices = new HashSet<>(); if(getDefaultAdapter() != null && getDefaultAdapter().isEnabled()) { devices = getDefaultAdapter().getBondedDevices(); } return devices; } public void storeLastConnectedDevice(BluetoothDevice device) { SharedPreferences.Editor editor = mContext.getSharedPreferences( DeviceManager.KNOWN_BLUETOOTH_DEVICE_PREFERENCES, Context.MODE_MULTI_PROCESS).edit(); editor.putString(LAST_CONNECTED_BLUETOOTH_DEVICE_PREF_KEY, device.getAddress()); editor.apply(); Log.d(TAG, "Stored last connected device: " + device.getAddress()); } public BluetoothDevice getLastConnectedDevice() { SharedPreferences preferences = mContext.getSharedPreferences(KNOWN_BLUETOOTH_DEVICE_PREFERENCES, Context.MODE_MULTI_PROCESS); String lastConnectedDeviceAddress = preferences.getString( LAST_CONNECTED_BLUETOOTH_DEVICE_PREF_KEY, null); BluetoothDevice lastConnectedDevice = null; if(lastConnectedDeviceAddress != null) { lastConnectedDevice = getDefaultAdapter().getRemoteDevice(lastConnectedDeviceAddress); } return lastConnectedDevice; } public Set<BluetoothDevice> getCandidateDevices() { Set<BluetoothDevice> candidates = new HashSet<>(); for(BluetoothDevice device : getPairedDevices()) { if(device.getName().startsWith( BluetoothVehicleInterface.DEVICE_NAME_PREFIX)) { candidates.add(device); } } SharedPreferences preferences = mContext.getSharedPreferences(KNOWN_BLUETOOTH_DEVICE_PREFERENCES, Context.MODE_MULTI_PROCESS); Set<String> detectedDevices = SupportSettingsUtils.getStringSet( preferences, KNOWN_BLUETOOTH_DEVICE_PREF_KEY, new HashSet<String>()); for(String address : detectedDevices) { if(BluetoothAdapter.checkBluetoothAddress(address)) { candidates.add(getDefaultAdapter().getRemoteDevice(address)); } } for(BluetoothDevice candidate : candidates) { Log.d(TAG, "Found previously discovered or paired OpenXC BT VI " + candidate.getAddress()); } return candidates; } public boolean isConnecting() { return mSocketConnecting.get(); } private void connectToSocket(BluetoothSocket socket) throws BluetoothException { mSocketConnecting.set(true); try { socket.connect(); if(getDefaultAdapter().isDiscovering()) { getDefaultAdapter().cancelDiscovery(); } } catch(IOException e) { String error = "Could not connect to SPP service on " + socket; Log.e(TAG, error); try { socket.close(); } catch(IOException e2) {} throw new BluetoothException(error, e); } finally { mSocketConnecting.set(false); } } /** * Open an RFCOMM socket to the Bluetooth device. * * The device may or may not actually exist, the argument is just a * reference to it. */ private BluetoothSocket setupSocket(BluetoothDevice device) throws BluetoothException { if(device == null) { Log.w(TAG, "Can't setup socket -- device is null"); throw new BluetoothException(); } Log.d(TAG, "Scanning services on " + device); BluetoothSocket socket; try { socket = device.createRfcommSocketToServiceRecord(RFCOMM_UUID); } catch(IOException e) { String error = "Unable to open a socket to device " + device; Log.w(TAG, error); throw new BluetoothException(error, e); } return socket; } }