diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java b/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java index fef11d361..79313559e 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java @@ -16,287 +16,134 @@ package com.google.android.apps.mytracks.services.sensors; -import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.apps.mytracks.content.Sensor.SensorState; - -import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothSocket; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothProfile; +import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; -import java.io.IOException; -import java.io.InputStream; +import androidx.annotation.NonNull; + +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.content.Sensor.SensorState; + import java.util.UUID; /** - * Manages bluetooth connection. It has a thread for connecting with a bluetooth - * device and a thread for performing data transmission when connected. - * - * @author Sandor Dornbush + * Manages connection to Bluetooth LE heart rate monitor. + * TODO: shutdown connection. */ public class BluetoothConnectionManager { - public static final UUID MY_TRACKS_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); + private static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL); + private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL); + private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL); - // Message types sent to handler - public static final int MESSAGE_DEVICE_NAME = 1; - public static final int MESSAGE_READ = 2; + // Message types sent to handler + public static final int MESSAGE_DEVICE_NAME = 1; + public static final int MESSAGE_READ = 2; + public static final String KEY_DEVICE_NAME = "device_name"; - // Key for storing the device name - public static final String KEY_DEVICE_NAME = "device_name"; + private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); - private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); + private final Context context; + private final Handler handler; + private SensorState sensorState; - private final BluetoothAdapter bluetoothAdapter; - private final Handler handler; - private final MessageParser messageParser; - private SensorState sensorState; + private BluetoothGattCallback connectCallback = new BluetoothGattCallback() { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + gatt.discoverServices(); + setState(SensorState.CONNECTED); - private ConnectThread connectThread; - private ConnectedThread connectedThread; - - /** - * Constructor. - * - * @param bluetoothAdapter the bluetooth adapter - * @param handler a hander for sending messages back to the UI activity - * @param messageParser a message parser - */ - public BluetoothConnectionManager( - BluetoothAdapter bluetoothAdapter, Handler handler, MessageParser messageParser) { - this.bluetoothAdapter = bluetoothAdapter; - this.handler = handler; - this.messageParser = messageParser; - this.sensorState = SensorState.NONE; - } - - /** - * Gets the sensor state. - */ - public synchronized SensorState getSensorState() { - return sensorState; - } - - /** - * Sets the sensor state. - * - * @param sensorState the sensor state - */ - private synchronized void setState(Sensor.SensorState sensorState) { - this.sensorState = sensorState; - } - - /** - * Resets the bluetooth connection manager. - */ - public synchronized void reset() { - cancelThreads(); - setState(Sensor.SensorState.NONE); - } - - /** - * Cancels all the threads. - */ - private void cancelThreads() { - if (connectThread != null) { - connectThread.cancel(); - connectThread = null; - } - if (connectedThread != null) { - connectedThread.cancel(); - connectedThread = null; - } - } - - /** - * Connects to a bluetooth device. - * - * @param bluetoothDevice the bluetooth device - */ - public synchronized void connect(BluetoothDevice bluetoothDevice) { - Log.d(TAG, "connect to: " + bluetoothDevice); - cancelThreads(); - - connectThread = new ConnectThread(bluetoothDevice); - connectThread.start(); - setState(Sensor.SensorState.CONNECTING); - } - - /** - * Starts the ConnectedThread to read data. - * - * @param bluetoothSocket the bluetooth socket - * @param bluetoothDevice the bluetooth device - */ - private synchronized void connected( - BluetoothSocket bluetoothSocket, BluetoothDevice bluetoothDevice) { - cancelThreads(); - - connectedThread = new ConnectedThread(bluetoothSocket); - connectedThread.start(); - - // Send the device name to the handler - Message message = handler.obtainMessage(MESSAGE_DEVICE_NAME); - Bundle bundle = new Bundle(); - bundle.putString(KEY_DEVICE_NAME, bluetoothDevice.getName()); - message.setData(bundle); - handler.sendMessage(message); - - setState(Sensor.SensorState.CONNECTED); - } - - /** - * A thread to connect to a bluetooth device. - */ - private class ConnectThread extends Thread { - private final BluetoothSocket bluetoothSocket; - private final BluetoothDevice bluetoothDevice; - - public ConnectThread(BluetoothDevice device) { - setName("ConnectThread-" + device.getName()); - this.bluetoothDevice = device; - BluetoothSocket tmp = null; - - try { - tmp = bluetoothDevice.createInsecureRfcommSocketToServiceRecord(BluetoothConnectionManager.MY_TRACKS_UUID); - } catch (IOException e) { - Log.d(TAG, "Unable to create insecure connection", e); - } - - if (tmp == null) { - try { - tmp = bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.MY_TRACKS_UUID); - } catch (IOException e) { - Log.e(TAG, "Unable to get blueooth socket.", e); - } - } - - bluetoothSocket = tmp; - } - - @Override - public void run() { - if (bluetoothAdapter == null) { - BluetoothConnectionManager.this.reset(); - return; - } - // Cancel discovery to prevent slow down - bluetoothAdapter.cancelDiscovery(); - - try { - bluetoothSocket.connect(); - } catch (IOException connectException) { - Log.i(TAG, "Unable to connect.", connectException); - setState(Sensor.SensorState.DISCONNECTED); - try { - bluetoothSocket.close(); - } catch (IOException e) { - Log.e(TAG, "Unable to close blueooth socket.", e); - } - // Reset the bluetooth connection manager - BluetoothConnectionManager.this.reset(); - return; - } - - // Reset the ConnectThread since we are done - synchronized (BluetoothConnectionManager.this) { - connectThread = null; - } - - // Start the connected thread - connected(bluetoothSocket, bluetoothDevice); - } - - /** - * Cancels this thread. - */ - public void cancel() { - try { - bluetoothSocket.close(); - } catch (IOException e) { - Log.e(TAG, "Unable to close bluetooth socket.", e); - } - } - } - - /** - * This thread handles data transmission when connected. - */ - private class ConnectedThread extends Thread { - private final BluetoothSocket bluetoothSSocket; - private final InputStream inputStream; - - public ConnectedThread(BluetoothSocket bluetoothSocket) { - this.bluetoothSSocket = bluetoothSocket; - InputStream tmp = null; - - try { - tmp = bluetoothSocket.getInputStream(); - } catch (IOException e) { - Log.e(TAG, "Unable to get input stream.", e); - } - inputStream = tmp; - } - - @Override - public void run() { - byte[] buffer = new byte[messageParser.getFrameSize()]; - int bytes; // bytes read - int offset = 0; - - // Keep listening to the inputStream while connected - while (true) { - try { - // Read from the inputStream - bytes = inputStream.read(buffer, offset, messageParser.getFrameSize() - offset); - - if (bytes == -1) { throw new IOException("EOF reached."); } - - offset += bytes; - - if (offset != messageParser.getFrameSize()) { - // Partial frame received. Call read again to receive the rest. - continue; - } - - if (!messageParser.isValid(buffer)) { - int index = messageParser.findNextAlignment(buffer); - if (index == -1) { - Log.w(TAG, "Could not find any valid data. Drop data."); - offset = 0; - continue; + //Inform about status change + Message message = handler.obtainMessage(MESSAGE_DEVICE_NAME); + Bundle bundle = new Bundle(); + bundle.putString(KEY_DEVICE_NAME, gatt.getDevice().getName()); + message.setData(bundle); + handler.sendMessage(message); + return; } - Log.w(TAG, "Misaligned data. Found new message at " + index + ". Recovering..."); - offset = messageParser.getFrameSize() - index; - System.arraycopy(buffer, index, buffer, 0, offset); - continue; - } - - offset = 0; - - // Send a copy of the obtained bytes to the handler to avoid memory - // inconsistency issues - handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer.clone()).sendToTarget(); - } catch (IOException e) { - Log.i(TAG, "Bluetooth connection lost.", e); - setState(Sensor.SensorState.DISCONNECTED); - break; + Log.d(TAG, "Could not connect to bluetooth sensor: " + gatt.getDevice()); } - } + + @Override + public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) { + BluetoothGattCharacteristic characteristic = gatt + .getService(HEART_RATE_SERVICE_UUID) + .getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID); + + gatt.setCharacteristicNotification(characteristic, true); + + BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID); + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + gatt.writeDescriptor(descriptor); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) { + //DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/ + byte[] raw = characteristic.getValue(); + int index = ((raw[0] & 0x1) == 1) ? 2 : 1; + int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16; + int value = characteristic.getIntValue(format, index); + + Log.d(TAG, "Received heart beat rate: " + value); + handler.obtainMessage(MESSAGE_READ, value, value).sendToTarget(); + } + }; + + /** + * Constructor. + * + * @param handler a handler for sending messages back to the UI activity + */ + public BluetoothConnectionManager(Context context, Handler handler) { + this.context = context; + this.handler = handler; + this.sensorState = SensorState.NONE; } /** - * Cancels this thread. + * Gets the sensor state. */ - public void cancel() { - try { - bluetoothSSocket.close(); - } catch (IOException e) { - Log.e(TAG, "Unable to close bluetooth socket.", e); - } + public synchronized SensorState getSensorState() { + return sensorState; + } + + /** + * Sets the sensor state. + * + * @param sensorState the sensor state + */ + private synchronized void setState(Sensor.SensorState sensorState) { + this.sensorState = sensorState; + } + + /** + * Resets the bluetooth connection manager. + */ + public synchronized void reset() { + //TODO Disconnect + setState(Sensor.SensorState.NONE); + } + + /** + * Connects to a bluetooth device. + * + * @param bluetoothDevice the bluetooth device + */ + public synchronized void connect(BluetoothDevice bluetoothDevice) { + Log.d(TAG, "connect to: " + bluetoothDevice); + + bluetoothDevice.connectGatt(this.context, false, this.connectCallback); + setState(Sensor.SensorState.CONNECTING); } - } } diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java b/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java index 704c99bb7..b492bcfa6 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java @@ -16,6 +16,7 @@ package com.google.android.apps.mytracks.services.sensors; +import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.content.Sensor.SensorState; import com.google.android.apps.mytracks.util.PreferencesUtils; @@ -25,6 +26,7 @@ import com.google.android.maps.mytracks.R; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; +import android.content.Intent; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -35,19 +37,17 @@ import java.util.ArrayList; /** * Bluetooth sensor manager. - * + * + * TODO: Handle a BluetoothGatt.STATE_DISCONNECTED + * * @author Sandor Dornbush */ public class BluetoothSensorManager extends SensorManager { - // 1 second in milliseconds private static final long ONE_SECOND = (long) UnitConversions.S_TO_MS; private static final BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter(); private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); - /** - * Gets the default bluetooth adapter. - */ private static BluetoothAdapter getDefaultBluetoothAdapter() { // If from the main application thread, return directly if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) { @@ -87,7 +87,6 @@ public class BluetoothSensorManager extends SensorManager { } private final Context context; - private final MessageParser messageParser; private final BluetoothConnectionManager bluetoothConnectionManager; private SensorDataSet sensorDataSet = null; @@ -97,23 +96,14 @@ public class BluetoothSensorManager extends SensorManager { public void handleMessage(Message message) { switch (message.what) { case BluetoothConnectionManager.MESSAGE_DEVICE_NAME: - String deviceName = message.getData() - .getString(BluetoothConnectionManager.KEY_DEVICE_NAME); - Toast.makeText(context, context.getString(R.string.settings_sensor_connected, deviceName), - Toast.LENGTH_SHORT).show(); + String deviceName = message.getData().getString(BluetoothConnectionManager.KEY_DEVICE_NAME); + Toast.makeText(context, context.getString(R.string.settings_sensor_connected, deviceName), Toast.LENGTH_LONG).show(); break; case BluetoothConnectionManager.MESSAGE_READ: - try { - byte[] readBuf = (byte[]) message.obj; - sensorDataSet = messageParser.parseBuffer(readBuf); - Log.d(TAG, "MESSAGE_READ: " + sensorDataSet); - } catch (IllegalArgumentException e) { - sensorDataSet = null; - Log.i(TAG, "Unexpected exception on read", e); - } catch (RuntimeException e) { - sensorDataSet = null; - Log.i(TAG, "Unexpected exception on read.", e); - } + sensorDataSet = SensorDataSet.newBuilder() + .setHeartRate(Sensor.SensorData.newBuilder().setValue(message.arg1) + .setState(SensorState.SENDING)) + .build(); break; default: break; @@ -122,16 +112,11 @@ public class BluetoothSensorManager extends SensorManager { }; /** - * Constructor. - * * @param context the context - * @param messageParser the message parser */ - public BluetoothSensorManager(Context context, MessageParser messageParser) { + public BluetoothSensorManager(Context context) { this.context = context; - this.messageParser = messageParser; - bluetoothConnectionManager = new BluetoothConnectionManager( - bluetoothAdapter, messageHandler, messageParser); + bluetoothConnectionManager = new BluetoothConnectionManager(context, messageHandler); } @Override @@ -145,10 +130,9 @@ public class BluetoothSensorManager extends SensorManager { Log.w(TAG, "Bluetooth not enabled."); return; } - String address = PreferencesUtils.getString( - context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT); + String address = PreferencesUtils.getString(context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT); if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) { - Log.w(TAG, "No blueooth address."); + Log.w(TAG, "No bluetooth address."); return; } Log.w(TAG, "Connecting to bluetooth address: " + address); diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/MessageParser.java b/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/MessageParser.java deleted file mode 100644 index f8830ab8e..000000000 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/MessageParser.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2010 Google Inc. - * - * 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.google.android.apps.mytracks.services.sensors; - -import com.google.android.apps.mytracks.content.Sensor; - -/** - * An interface for parsing a byte array to a SensorData object. - * - * @author Sandor Dornbush - */ -public interface MessageParser { - - int getFrameSize(); - - Sensor.SensorDataSet parseBuffer(byte[] readBuff); - - boolean isValid(byte[] buffer); - - int findNextAlignment(byte[] buffer); -} diff --git a/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java b/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java index 6bb0c41c8..dd6ad1551 100644 --- a/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java +++ b/myTracks/src/main/java/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java @@ -88,8 +88,6 @@ public class SensorManagerFactory { * @param context the context */ private static SensorManager getSensorManager(Context context) { - //TODO Implement Bluetooth LE Sensor manager - - return null; + return new BluetoothSensorManager(context); } }