diff --git a/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java b/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java index acfc1e32..475d9966 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java +++ b/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java @@ -71,6 +71,7 @@ import java.util.TreeMap; import timber.log.Timber; public class OpenScale { + public static boolean DEBUG_MODE = false; public static final String DATABASE_NAME = "openScale.db"; @@ -542,8 +543,18 @@ public class OpenScale { return measurementDAO.getAllInRange(startCalender.getTime(), endCalender.getTime(), selectedUserId); } + public void connectToBluetoothDeviceDebugMode(String hwAddress, Handler callbackBtHandler) { + Timber.d("Trying to connect to bluetooth device [%s] in debug mode", hwAddress); + + disconnectFromBluetoothDevice(); + + btDeviceDriver = BluetoothFactory.createDebugDriver(context); + btDeviceDriver.registerCallbackHandler(callbackBtHandler); + btDeviceDriver.connect(hwAddress); + } + public boolean connectToBluetoothDevice(String deviceName, String hwAddress, Handler callbackBtHandler) { - Timber.d("Trying to connect to bluetooth device %s (%s)", hwAddress, deviceName); + Timber.d("Trying to connect to bluetooth device [%s] (%s)", hwAddress, deviceName); disconnectFromBluetoothDevice(); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java index 0ae67122..ce940eee 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothCommunication.java @@ -22,13 +22,16 @@ import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.os.Handler; import com.health.openscale.core.datatypes.ScaleMeasurement; +import java.util.ArrayList; import java.util.LinkedList; +import java.util.List; import java.util.Queue; import java.util.UUID; @@ -68,6 +71,14 @@ public abstract class BluetoothCommunication { connectionEstablished = false; } + protected List getBluetoothGattServices() { + if (bluetoothGatt == null) { + return new ArrayList<>(); + } + + return bluetoothGatt.getServices(); + } + /** * Register a callback Bluetooth handler that notify any BT_STATUS_CODE changes for GUI/CORE. * @@ -233,6 +244,13 @@ public abstract class BluetoothCommunication { bluetoothGatt.readCharacteristic(gattCharacteristic); } + protected void readBytes(UUID service, UUID characteristic, UUID descriptor) { + BluetoothGattDescriptor gattDescriptor = bluetoothGatt.getService(service) + .getCharacteristic(characteristic).getDescriptor(descriptor); + + bluetoothGatt.readDescriptor(gattDescriptor); + } + /** * Set indication flag on for the Bluetooth device. * @@ -520,8 +538,8 @@ public abstract class BluetoothCommunication { public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - Timber.d("onCharacteristicRead %s: %s", - characteristic.getUuid(), byteInHex(characteristic.getValue())); + Timber.d("onCharacteristicRead %s (status=%d): %s", + characteristic.getUuid(), status, byteInHex(characteristic.getValue())); synchronized (lock) { onBluetoothDataRead(gatt, characteristic, status); @@ -540,5 +558,19 @@ public abstract class BluetoothCommunication { onBluetoothDataChange(gatt, characteristic); } } + + @Override + public void onDescriptorRead(BluetoothGatt gatt, + BluetoothGattDescriptor descriptor, + int status) { + Timber.d("onDescriptorRead %s (status=%d): %s", + descriptor.getUuid(), status, byteInHex(descriptor.getValue())); + + synchronized (lock) { + openRequest = false; + handleRequests(); + } + } + } } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java new file mode 100644 index 00000000..3ce553e4 --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothDebug.java @@ -0,0 +1,168 @@ +/* Copyright (C) 2018 Erik Johansson + * + * 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 + */ + +package com.health.openscale.core.bluetooth; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.content.Context; + +import java.util.HashMap; +import java.util.UUID; + +import timber.log.Timber; + +public class BluetoothDebug extends BluetoothCommunication { + HashMap propertyString; + + BluetoothDebug(Context context) { + super(context); + + propertyString = new HashMap<>(); + propertyString.put(BluetoothGattCharacteristic.PROPERTY_BROADCAST, "BROADCAST"); + propertyString.put(BluetoothGattCharacteristic.PROPERTY_READ, "READ"); + propertyString.put(BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, "WRITE_NO_RESPONSE"); + propertyString.put(BluetoothGattCharacteristic.PROPERTY_WRITE, "WRITE"); + propertyString.put(BluetoothGattCharacteristic.PROPERTY_NOTIFY, "NOTIFY"); + propertyString.put(BluetoothGattCharacteristic.PROPERTY_INDICATE, "INDICATE"); + propertyString.put(BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE, "SIGNED_WRITE"); + propertyString.put(BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS, "EXTENDED_PROPS"); + } + + @Override + public String driverName() { + return "Debug"; + } + + private boolean isBlacklisted(BluetoothGattService service, BluetoothGattCharacteristic characteristic) { + // Reading this triggers a pairing request on Beurer BF710 + if (service.getUuid().equals(UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb")) + && characteristic.getUuid().equals(UUID.fromString("0000ffe5-0000-1000-8000-00805f9b34fb"))) { + return true; + } + + return false; + } + + private String propertiesToString(int properties) { + StringBuilder names = new StringBuilder(); + for (int property : propertyString.keySet()) { + if ((properties & property) != 0) { + names.append(propertyString.get(property)); + names.append(", "); + } + } + + if (names.length() == 0) { + return ""; + } + + return names.substring(0, names.length() - 2); + } + + private void logService(BluetoothGattService service, boolean included) { + Timber.d("Service %s%s", service.getUuid(), included ? " (included)" : ""); + + for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { + Timber.d("|- characteristic %s (instance %d): %s (permissions=0x%x)", + characteristic.getUuid(), characteristic.getInstanceId(), + propertiesToString(characteristic.getProperties()), characteristic.getPermissions()); + byte[] value = characteristic.getValue(); + if (value != null && value.length > 0) { + Timber.d("|--> value: %s (%s)", byteInHex(value), + characteristic.getStringValue(0).replaceAll("\\p{Cntrl}", "?")); + } + + for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) { + Timber.d("|--- descriptor %s (permissions=0x%x)", + descriptor.getUuid(), descriptor.getPermissions()); + + value = descriptor.getValue(); + if (value != null && value.length > 0) { + Timber.d("|-----> value: %s", byteInHex(value)); + } + } + } + + for (BluetoothGattService includedService : service.getIncludedServices()) { + logService(includedService, true); + } + } + + private int readServiceCharacteristics(BluetoothGattService service, int offset) { + for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { + if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) != 0 + && !isBlacklisted(service, characteristic)) { + + if (offset == 0) { + readBytes(service.getUuid(), characteristic.getUuid()); + return -1; + } + + offset -= 1; + } + + for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) { + if (offset == 0) { + readBytes(service.getUuid(), characteristic.getUuid(), descriptor.getUuid()); + return -1; + } + + offset -= 1; + } + } + + for (BluetoothGattService included : service.getIncludedServices()) { + offset = readServiceCharacteristics(included, offset); + if (offset == -1) { + return offset; + } + } + + return offset; + } + + @Override + protected boolean nextInitCmd(int stateNr) { + int offset = stateNr; + + for (BluetoothGattService service : getBluetoothGattServices()) { + offset = readServiceCharacteristics(service, offset); + if (offset == -1) { + return true; + } + } + + return false; + } + + @Override + protected boolean nextBluetoothCmd(int stateNr) { + for (BluetoothGattService service : getBluetoothGattServices()) { + logService(service, false); + } + + disconnect(false); + setBtStatus(BT_STATUS_CODE.BT_CONNECTION_LOST); + return false; + } + + @Override + protected boolean nextCleanUpCmd(int stateNr) { + return false; + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java index b6aa74db..6e72fb9d 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothFactory.java @@ -21,6 +21,10 @@ import android.content.Context; import java.util.Locale; public class BluetoothFactory { + public static BluetoothCommunication createDebugDriver(Context context) { + return new BluetoothDebug(context); + } + public static BluetoothCommunication createDeviceDriver(Context context, String deviceName) { final String name = deviceName.toLowerCase(Locale.US); diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/AboutPreferences.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/AboutPreferences.java index 59347966..d96efe5a 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/AboutPreferences.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/AboutPreferences.java @@ -24,6 +24,7 @@ import android.util.Log; import com.health.openscale.BuildConfig; import com.health.openscale.R; +import com.health.openscale.core.OpenScale; import java.io.IOException; import java.io.OutputStream; @@ -110,6 +111,7 @@ public class AboutPreferences extends PreferenceFragment { tree.close(); Timber.uproot(tree); preference.setSummary(R.string.info_is_not_enable); + OpenScale.DEBUG_MODE = false; return true; } @@ -133,6 +135,7 @@ public class AboutPreferences extends PreferenceFragment { OutputStream output = getActivity().getContentResolver().openOutputStream(uri); Timber.plant(new FileDebugTree(output)); findPreference(KEY_DEBUG_LOG).setSummary(R.string.info_is_enable); + OpenScale.DEBUG_MODE = true; Timber.d("Debug log enabled (%s v%s (%d))", getResources().getString(R.string.app_name), BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE); diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java index 6af98a88..dd30a48e 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothPreferences.java @@ -16,6 +16,7 @@ package com.health.openscale.gui.preferences; import android.app.Activity; +import android.app.AlertDialog; import android.app.Fragment; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -29,6 +30,7 @@ import android.graphics.PorterDuff; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.Message; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; @@ -45,6 +47,8 @@ import com.health.openscale.gui.utils.PermissionHelper; import java.util.HashMap; import java.util.Map; +import timber.log.Timber; + public class BluetoothPreferences extends PreferenceFragment { private static final String PREFERENCE_KEY_BLUETOOTH_SCANNER = "btScanner"; @@ -145,9 +149,9 @@ public class BluetoothPreferences extends PreferenceFragment { } private void stopDiscoveryAndLeScan() { - Timber.d("Stop discovery and LE scan"); - if (handler != null) { + Timber.d("Stop discovery"); + handler.removeCallbacksAndMessages(null); handler = null; @@ -156,6 +160,8 @@ public class BluetoothPreferences extends PreferenceFragment { } if (leScanCallback != null) { + Timber.d("Stop LE scan"); + btAdapter.stopLeScan(leScanCallback); leScanCallback = null; } @@ -165,7 +171,7 @@ public class BluetoothPreferences extends PreferenceFragment { } } - private void onDeviceFound(BluetoothDevice device) { + private void onDeviceFound(final BluetoothDevice device) { if (device.getName() == null || foundDevices.containsKey(device.getAddress())) { return; } @@ -191,9 +197,20 @@ public class BluetoothPreferences extends PreferenceFragment { prefBtDevice.setIcon(R.drawable.ic_bluetooth_disabled); prefBtDevice.setSummary(R.string.label_bt_device_no_support); prefBtDevice.setEnabled(false); + + if (OpenScale.DEBUG_MODE && device.getType() == BluetoothDevice.DEVICE_TYPE_LE) { + prefBtDevice.setEnabled(true); + prefBtDevice.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + getDebugInfo(device); + return false; + } + }); + } } - foundDevices.put(device.getAddress(), prefBtDevice.isEnabled() ? device : null); + foundDevices.put(device.getAddress(), btDevice != null ? device : null); btScanner.addPreference(prefBtDevice); } @@ -287,6 +304,37 @@ public class BluetoothPreferences extends PreferenceFragment { } } + private void getDebugInfo(final BluetoothDevice device) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle("Fetching info") + .setMessage("Please wait while we fetch extended info from the device...") + .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + OpenScale.getInstance().disconnectFromBluetoothDevice(); + dialog.dismiss(); + } + }); + + final AlertDialog dialog = builder.create(); + + Handler btHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (BluetoothCommunication.BT_STATUS_CODE.values()[msg.what]) { + case BT_CONNECTION_LOST: + OpenScale.getInstance().disconnectFromBluetoothDevice(); + dialog.dismiss(); + break; + } + } + }; + + dialog.show(); + OpenScale.getInstance().connectToBluetoothDeviceDebugMode( + device.getAddress(), btHandler); + } + private class onClickListenerDeviceSelect implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(final Preference preference) { @@ -301,12 +349,21 @@ public class BluetoothPreferences extends PreferenceFragment { btScanner.setSummary(preference.getTitle()); ((BaseAdapter)getPreferenceScreen().getRootAdapter()).notifyDataSetChanged(); + stopDiscoveryAndLeScan(); btScanner.getDialog().dismiss(); // Perform an explicit bonding with classic devices - if (device.getType() == BluetoothDevice.DEVICE_TYPE_CLASSIC - && device.getBondState() == BluetoothDevice.BOND_NONE) { - device.createBond(); + switch (device.getType()) { + case BluetoothDevice.DEVICE_TYPE_CLASSIC: + if (device.getBondState() == BluetoothDevice.BOND_NONE) { + device.createBond(); + } + break; + case BluetoothDevice.DEVICE_TYPE_LE: + if (OpenScale.DEBUG_MODE) { + getDebugInfo(device); + } + break; } return true; }