/* * Copyright (C) 2013 jonas.oreland@gmail.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.runnerup.hr; import android.annotation.TargetApi; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Handler; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; import java.util.UUID; /** * Base class for BT 2.0 HR providers. It has a thread for connecting with a * bluetooth device and a thread for performing data transmission when * connected. */ @TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1) public abstract class Bt20Base extends BtHRBase { public boolean isEnabled() { return isEnabledImpl(); } public static boolean isEnabledImpl() { //noinspection SimplifiableIfStatement if (BluetoothAdapter.getDefaultAdapter() != null) return BluetoothAdapter.getDefaultAdapter().isEnabled(); return false; } public boolean startEnableIntent(Activity activity, int requestCode) { return startEnableIntentImpl(activity, requestCode); } @SuppressWarnings("SameReturnValue") public static boolean startEnableIntentImpl(Activity activity, int requestCode) { activity.startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), requestCode); return true; } public static boolean checkLibrary(@SuppressWarnings("UnusedParameters") Context ctx) { // Don't bother if createInsecureRfcommSocketToServiceRecord isn't // available //noinspection RedundantIfStatement if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD_MR1) return false; return true; } // UUID private static final UUID MY_UUID = UUID .fromString("00001101-0000-1000-8000-00805F9B34FB"); private ConnectThread connectThread; private ConnectedThread connectedThread; private int hrValue = 0; private long hrTimestamp = 0; private BluetoothAdapter btAdapter = null; // private Context context = null; public Bt20Base(@SuppressWarnings("UnusedParameters") Context ctx) { // context = ctx; } @Override public void open(Handler handler, HRClient hrClient) { this.hrClient = hrClient; this.hrClientHandler = handler; if (btAdapter == null) { btAdapter = BluetoothAdapter.getDefaultAdapter(); } if (btAdapter == null) { hrClient.onOpenResult(false); return; } hrClient.onOpenResult(true); } @Override public void close() { reset(); btAdapter = null; } private boolean mIsConnecting; private boolean mIsConnected; private boolean mIsScanning; public void disconnect() { reset(); hrClient.onDisconnectResult(true); } private void reset() { cancelThreads(); mIsConnecting = false; mIsConnected = false; mIsScanning = false; } @Override public boolean isScanning() { return mIsScanning; } @Override public boolean isConnected() { return mIsConnected; } @Override public boolean isConnecting() { return mIsConnecting; } @Override public int getHRValue() { return hrValue; } @Override public long getHRValueTimestamp() { return hrTimestamp; } @Override public HRData getHRData() { if (hrValue <= 0) { return null; } return new HRData().setHeartRate(hrValue).setTimestampEstimate(hrTimestamp); } @Override public int getBatteryLevel() { return -1; } /** * Cancels all the threads. */ private void cancelThreads() { if (connectThread != null) { connectThread.cancel(); connectThread = null; } if (connectedThread != null) { connectedThread.cancel(); connectedThread = null; } } @Override public boolean isBondingDevice() { return true; } @Override public void startScan() { if (btAdapter == null) return; mIsScanning = true; hrClientHandler.post(new Runnable() { @Override public void run() { Set<BluetoothDevice> list = new HashSet<BluetoothDevice>(); list.addAll(btAdapter.getBondedDevices()); publishDevice(list); } }); } private void publishDevice(final Set<BluetoothDevice> list) { if (list.isEmpty()) { mIsScanning = false; return; } if (mIsScanning) { BluetoothDevice dev = list.iterator().next(); list.remove(dev); hrClient.onScanResult(createDeviceRef(getProviderName(), dev)); hrClientHandler.post(new Runnable() { @Override public void run() { publishDevice(list); } }); } } @Override public void stopScan() { mIsScanning = false; } @Override public void connect(HRDeviceRef ref) { cancelThreads(); if (!isEnabledImpl()) { reportConnected(false); return; } mIsConnecting = true; connectThread = new ConnectThread(btAdapter.getRemoteDevice(ref.deviceAddress), ref.deviceName); connectThread.start(); } private synchronized void connected(final BluetoothSocket bluetoothSocket, final BluetoothDevice bluetoothDevice, final String btDeviceName) { cancelThreads(); if (hrClient != null) { hrClientHandler.post(new Runnable() { @Override public void run() { if (mIsConnecting && hrClient != null) { // Start connected thread... connectedThread = new ConnectedThread(bluetoothDevice, btDeviceName, bluetoothSocket); connectedThread.start(); } else { log("closeSocket"); closeSocket(bluetoothSocket); } } }); } else { log("closeSocket"); closeSocket(bluetoothSocket); } } private static void closeSocket(BluetoothSocket bluetoothSocket) { if (bluetoothSocket != null) { try { bluetoothSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } private static void closeStream(InputStream stream) { if (stream != null) { try { stream.close(); } catch (IOException e) { e.printStackTrace(); } } } private void reportConnected(final boolean result) { log("reportConnected(" + result + ") mIsConnecting: " + mIsConnecting + ", mIsConnected: " + mIsConnected + ", hrClient: " + hrClient); if (hrClient != null) { hrClientHandler.post(new Runnable() { @Override public void run() { boolean reset = !result; if (mIsConnecting && hrClient != null) { mIsConnected = result; mIsConnecting = false; hrClient.onConnectResult(result); } else { reset = true; } if (reset) { Bt20Base.this.reset(); } } }); } } private static BluetoothSocket tryConnect(BtHRBase base, final BluetoothDevice device, int i) throws IOException { BluetoothSocket sock = null; base.log("tryConnect(method: " + i + ")"); switch (i) { case 0: sock = device.createRfcommSocketToServiceRecord(MY_UUID); break; case 1: sock = device.createInsecureRfcommSocketToServiceRecord(MY_UUID); break; case 2: { Method m; try { //noinspection RedundantArrayCreation m = device.getClass().getMethod("createInsecureRfcommSocket", new Class[]{ int.class }); m.setAccessible(true); sock = (BluetoothSocket) m.invoke(device, 1); } catch (NoSuchMethodException | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { e.printStackTrace(); } } } if (sock == null) { throw new IOException("Create socket failed!"); } try { sock.connect(); return sock; } catch (IOException ex) { base.log("closeSocket"); closeSocket(sock); throw ex; } } /** * A thread to connect to a bluetooth device. */ private class ConnectThread extends Thread { private BluetoothSocket bluetoothSocket; private final BluetoothDevice bluetoothDevice; private final String deviceName; public ConnectThread(BluetoothDevice device, String deviceName) { setName("ConnectThread-" + device.getName()); this.bluetoothDevice = device; this.deviceName = deviceName; } @Override public void run() { if (btAdapter == null || bluetoothDevice == null || deviceName == null) { reportConnected(false); return; } for (int i = 0; i < 3; i++) { if (!(isConnecting() || isConnected())) { /* check for disconnect */ break; } try { bluetoothSocket = tryConnect(Bt20Base.this, bluetoothDevice, i); break; } catch (Exception e1) { e1.printStackTrace(); } } if (bluetoothSocket == null) { log("connect failed!"); reportConnected(false); return; } if (btAdapter == null) { log("btAdapter == null in connect thread. giving up"); closeSocket(bluetoothSocket); synchronized (Bt20Base.this) { connectThread = null; } reportConnected(false); return; } // Cancel discovery to prevent slow down btAdapter.cancelDiscovery(); // Reset the ConnectThread since we are done synchronized (Bt20Base.this) { connectThread = null; } log("connected => " + bluetoothSocket); // Start the connected thread connected(bluetoothSocket, bluetoothDevice, deviceName); bluetoothSocket = null; } /** * Cancels this thread. */ public void cancel() { closeSocket(bluetoothSocket); } } /** * This thread handles data transmission when connected. */ private class ConnectedThread extends Thread { //private final BluetoothDevice bluetoothDevice; private final BluetoothSocket bluetoothSocket; private final InputStream inputStream; //private final String deviceName; public ConnectedThread(final BluetoothDevice device, final String deviceName, final BluetoothSocket bluetoothSocket) { //this.bluetoothDevice = device; this.bluetoothSocket = bluetoothSocket; //this.deviceName = deviceName; InputStream tmp = null; try { tmp = bluetoothSocket.getInputStream(); } catch (IOException e) { log("socket.getInputStream(): " + e); closeSocket(bluetoothSocket); } inputStream = tmp; } @Override public void run() { readHR(); } private void readHR() { Integer hr[] = new Integer[1]; final int frameSize = getFrameSize(); byte[] buffer = new byte[2 * frameSize]; int bytesInBuffer = 0; // Keep listening to the inputStream while connected while (true) { try { // Read from the inputStream int bytesRead = inputStream.read(buffer, bytesInBuffer, buffer.length - bytesInBuffer); if (bytesRead == -1) { throw new IOException("EOF reached."); } bytesInBuffer += bytesRead; int bytesUsed = parseBuffer(buffer, bytesInBuffer, hr); if (hr[0] != null) { hrValue = hr[0]; hrTimestamp = System.currentTimeMillis(); if (hrValue > 0 && mIsConnecting) { log("hrValue: " + hrValue + " => reportConnected"); reportConnected(true); } if (hrValue == 0) { closeStream(inputStream); closeSocket(bluetoothSocket); if (mIsConnecting) { reportConnected(false); return; } else if (mIsConnected) { reportDisconnected(true); return; } break; } } if (bytesUsed > 0) { System.arraycopy(buffer, bytesUsed, buffer, 0, bytesInBuffer - bytesUsed); bytesInBuffer -= bytesUsed; } else if (bytesUsed == 0 && bytesInBuffer == buffer.length) { log("reset"); bytesInBuffer = 0; } } catch (IOException e) { closeStream(inputStream); closeSocket(bluetoothSocket); if (mIsConnecting) reportConnected(false); reportDisconnected(true); break; } } closeStream(inputStream); closeSocket(bluetoothSocket); } /** * Cancels this thread. */ public void cancel() { closeStream(inputStream); closeSocket(bluetoothSocket); } } private void reportDisconnected(@SuppressWarnings("SameParameterValue") final boolean ok) { log("reportDisconnect(" + ok + ")"); if (hrClientHandler != null) { hrClientHandler.post(new Runnable() { @Override public void run() { if (hrClient == null) { log("reportDisconnect() hrClient == null"); return; } hrClient.onDisconnectResult(ok); } }); } else { log("reportDisconnect() hrClientHandler == null"); } } abstract static class Bt20BaseOld extends Bt20Base { public Bt20BaseOld(Context ctx) { super(ctx); } @Override public int parseBuffer(byte[] buffer, int bytesInBuffer, Integer[] hr) { hr[0] = null; if (bytesInBuffer < getFrameSize()) return 0; int hrValue = parseBuffer(buffer); if (hrValue > 0) { hr[0] = hrValue; return bytesInBuffer; // use all of buffer } else { int index = findNextAlignment(buffer); if (index < 0) return bytesInBuffer; return index; } } public abstract int getFrameSize(); public abstract int parseBuffer(byte[] buffer); public abstract int findNextAlignment(byte buffer[]); } public abstract int getFrameSize(); public abstract int parseBuffer(byte[] buffer, int bytesInBuffer, Integer[] hr); public static class ZephyrHRM extends Bt20BaseOld { static final byte ZEPHYR_HXM_BYTE_STX = 0; static final byte ZEPHYR_HXM_BYTE_HR = 12; static final byte ZEPHYR_HXM_BYTE_CRC = 58; static final byte ZEPHYR_HXM_BYTE_ETX = 59; static final byte ZEPHYR_START_BYTE = 0x02; static final byte ZEPHYR_END_BYTE = 0x03; public static final String NAME = "Zephyr"; public ZephyrHRM(Context ctx) { super(ctx); } @Override public String getName() { return NAME; } @Override public String getProviderName() { return NAME; } @Override public int getFrameSize() { return ZEPHYR_HXM_BYTE_ETX + 1; } @Override public int parseBuffer(byte[] buffer) { // Check STX (Start of Text), ETX (End of Text) and CRC Checksum boolean ok = buffer.length > ZEPHYR_HXM_BYTE_ETX && getByte(buffer[ZEPHYR_HXM_BYTE_STX]) == ZEPHYR_START_BYTE && getByte(buffer[ZEPHYR_HXM_BYTE_ETX]) == ZEPHYR_END_BYTE && calcCrc8(buffer, 3, 55) == getByte(buffer[ZEPHYR_HXM_BYTE_CRC]); if (!ok) { log("HxM insanity! " + (buffer.length > ZEPHYR_HXM_BYTE_ETX) + " " + getByte(buffer[ZEPHYR_HXM_BYTE_STX]) + "==" + ZEPHYR_START_BYTE + " " + getByte(buffer[ZEPHYR_HXM_BYTE_ETX]) + "==" + ZEPHYR_END_BYTE + " " + "calc=" + calcCrc8(buffer, 3, 55) + " " + "given=" + getByte(buffer[ZEPHYR_HXM_BYTE_CRC])); return -1; } return getByte(buffer[ZEPHYR_HXM_BYTE_HR]); } @Override public int findNextAlignment(byte[] buffer) { for (int i = 0; i < buffer.length - 1; i++) { if (getByte(buffer[i]) == ZEPHYR_END_BYTE && getByte(buffer[i + 1]) == ZEPHYR_START_BYTE) { return i; } } return -1; } private static int calcCrc8(byte buffer[], @SuppressWarnings("SameParameterValue") int start, @SuppressWarnings("SameParameterValue") int length) { int crc = 0x0; for (int i = start; i < (start + length); i++) { crc ^= getByte(buffer[i]); for (int b = 0; b <= 7; b++) { if ((crc & 1) != 0) { crc = ((crc >> 1) ^ 0x8c); } else { crc = (crc >> 1); } } } return crc; } } public static class PolarHRM extends Bt20Base { public static final String NAME = "Polar WearLink"; public PolarHRM(Context ctx) { super(ctx); } @Override public String getName() { return NAME; } @Override public String getProviderName() { return NAME; } @Override public int getFrameSize() { return 16; } private boolean startOfMessage(byte buffer[], int bytesInBuffer, int pos) { if (bytesInBuffer < pos + 4) return false; //noinspection PointlessArithmeticExpression int b0 = getByte(buffer[pos + 0]); int b1 = getByte(buffer[pos + 1]); // len int b2 = getByte(buffer[pos + 2]); int b3 = getByte(buffer[pos + 3]); if (b0 != 0xFE) { return false; } if ((0xFF - b1) != b2) { return false; } if (b3 >= 16) { return false; } //noinspection RedundantIfStatement if (bytesInBuffer < pos + b1) return false; return true; } @Override public int parseBuffer(byte[] buffer, int bytesInBuffer, Integer hrVal[]) { hrVal[0] = null; for (int i = 0; i < bytesInBuffer; i++) { if (startOfMessage(buffer, bytesInBuffer, i)) { int bytesUsed = getByte(buffer[i + 1]); hrVal[0] = getByte(buffer[i + 5]); return bytesUsed; } } return 0; } } public static class StHRMv1 extends Bt20Base { static final int FRAME_SIZE = 17; static final int START_BYTE = 250; public static final String NAME = "SportTracker HRM v1"; public StHRMv1(Context ctx) { super(ctx); } @Override public String getName() { return NAME; } @Override public String getProviderName() { return NAME; } @Override public int getFrameSize() { return FRAME_SIZE; } private boolean startOfMessage(byte buffer[], int bytesInBuffer, int pos) { if (bytesInBuffer < pos + FRAME_SIZE) return false; //noinspection PointlessArithmeticExpression int b0 = getByte(buffer[pos + 0]); int b1 = getByte(buffer[pos + 1]); int b2 = getByte(buffer[pos + 2]); if (b0 != START_BYTE) { return false; } if ((0xFF - b1) != b2) { return false; } int len = b1 >> 2; //noinspection RedundantIfStatement if (bytesInBuffer < pos + len) { return false; } return true; } @Override public int parseBuffer(byte[] buffer, int bytesInBuffer, Integer hrVal[]) { hrVal[0] = null; for (int i = 0; i < bytesInBuffer; i++) { if (startOfMessage(buffer, bytesInBuffer, i)) { int bytesUsed = getByte(buffer[i + 1]) >> 2; hrVal[0] = getByte(buffer[i + 5]); return bytesUsed; } } return 0; } } private static int getByte(byte b) { return b & 0xFF; } public static HRDeviceRef createDeviceRef(String providerName, BluetoothDevice device) { return HRDeviceRef.create(providerName, device.getName(), device.getAddress()); } }