diff --git a/android_app/app/build.gradle b/android_app/app/build.gradle index a8dbcb39..6b634129 100644 --- a/android_app/app/build.gradle +++ b/android_app/app/build.gradle @@ -2,12 +2,12 @@ apply plugin: 'com.android.application' apply plugin: "androidx.navigation.safeargs" android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { applicationId "com.health.openscale" testApplicationId "com.health.openscale.test" minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 31 versionCode 54 versionName "2.3.5" @@ -132,11 +132,11 @@ android { dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation 'com.google.android.material:material:1.5.0-alpha02' + implementation 'com.google.android.material:material:1.5.0-alpha04' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.recyclerview:recyclerview:1.2.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.1' implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.navigation:navigation-fragment:2.3.5' implementation 'androidx.navigation:navigation-ui:2.3.5' @@ -148,7 +148,7 @@ dependencies { // Simple CSV implementation 'com.j256.simplecsv:simplecsv:2.6' // Blessed Android - implementation 'com.github.weliem:blessed-android:1.30' + implementation 'com.github.weliem:blessed-android:2.1.0' // CustomActivityOnCrash implementation 'cat.ereza:customactivityoncrash:2.3.0' // AppIntro @@ -159,7 +159,7 @@ dependencies { annotationProcessor 'androidx.room:room-compiler:2.3.0' androidTestImplementation 'androidx.room:room-testing:2.3.0' // Timber - implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'com.jakewharton.timber:timber:5.0.1' // Local unit tests testImplementation 'junit:junit:4.13.1' // Instrumented unit tests 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 d57ff36b..a57435bc 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 @@ -641,6 +641,22 @@ public class OpenScale { return true; } + public boolean setBluetoothDeviceUserIndex(int appUserId, int scaleUserIndex, Handler uiHandler) { + if (btDeviceDriver == null) { + return false; + } + btDeviceDriver.selectScaleUserIndexForAppUserId(appUserId, scaleUserIndex, uiHandler); + return true; + } + + public boolean setBluetoothDeviceUserConsent(int appUserId, int scaleUserConsent, Handler uiHandler) { + if (btDeviceDriver == null) { + return false; + } + btDeviceDriver.setScaleUserConsent(appUserId, scaleUserConsent, uiHandler); + return true; + } + public LiveData> getScaleMeasurementsLiveData() { int selectedUserId = getSelectedScaleUserId(); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerBF105.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerBF105.java new file mode 100644 index 00000000..51537d63 --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerBF105.java @@ -0,0 +1,138 @@ +/* Copyright (C) 2019 olie.xdev + * + * 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 + */ + +/* +* Based on source-code by weliem/blessed-android +*/ + +package com.health.openscale.core.bluetooth; + +import static com.welie.blessed.BluetoothBytesParser.FORMAT_UINT16; +import static com.welie.blessed.BluetoothBytesParser.FORMAT_UINT8; + +import android.content.Context; + +import com.welie.blessed.BluetoothBytesParser; + +import java.util.UUID; + +import timber.log.Timber; + +public class BluetoothBeurerBF105 extends BluetoothStandardWeightProfile { + private static final UUID SERVICE_BF105_CUSTOM = BluetoothGattUuid.fromShortCode(0xffff); + private static final UUID SERVICE_BF105_IMG = BluetoothGattUuid.fromShortCode(0xffc0); + + private static final UUID CHARACTERISTIC_SCALE_SETTINGS = BluetoothGattUuid.fromShortCode(0x0000); + private static final UUID CHARACTERISTIC_USER_LIST = BluetoothGattUuid.fromShortCode(0x0001); + private static final UUID CHARACTERISTIC_INITIALS = BluetoothGattUuid.fromShortCode(0x0002); + private static final UUID CHARACTERISTIC_TARGET_WEIGHT = BluetoothGattUuid.fromShortCode(0x0003); + private static final UUID CHARACTERISTIC_ACTIVITY_LEVEL = BluetoothGattUuid.fromShortCode(0x0004); + private static final UUID CHARACTERISTIC_REFER_WEIGHT_BF = BluetoothGattUuid.fromShortCode(0x000b); + private static final UUID CHARACTERISTIC_BT_MODULE = BluetoothGattUuid.fromShortCode(0x0005); + private static final UUID CHARACTERISTIC_TAKE_MEASUREMENT = BluetoothGattUuid.fromShortCode(0x0006); + private static final UUID CHARACTERISTIC_TAKE_GUEST_MEASUREMENT = BluetoothGattUuid.fromShortCode(0x0007); + private static final UUID CHARACTERISTIC_BEURER_I = BluetoothGattUuid.fromShortCode(0x0008); + private static final UUID CHARACTERISTIC_UPPER_LOWER_BODY = CHARACTERISTIC_BEURER_I; + private static final UUID CHARACTERISTIC_BEURER_II = BluetoothGattUuid.fromShortCode(0x0009); + private static final UUID CHARACTERISTIC_BEURER_III = BluetoothGattUuid.fromShortCode(0x000a); + private static final UUID CHARACTERISTIC_IMG_IDENTIFY = BluetoothGattUuid.fromShortCode(0xffc1); + private static final UUID CHARACTERISTIC_IMG_BLOCK = BluetoothGattUuid.fromShortCode(0xffc2); + + + public BluetoothBeurerBF105(Context context) { + super(context); + } + + @Override + public String driverName() { + return "Beurer BF105"; + } + + @Override + protected int getVendorSpecificMaxUserCount() { + return 10; + } + + @Override + protected void writeUserDataToScale() { + writeTargetWeight(); + super.writeUserDataToScale(); + } + + @Override + public void onBluetoothNotify(UUID characteristic, byte[] value) { + if (characteristic.equals(CHARACTERISTIC_USER_LIST)) { + handleVendorSpecificUserList(value); + } + else { + super.onBluetoothNotify(characteristic, value); + } + } + + @Override + protected void setNotifyVendorSpecificUserList() { + if (setNotificationOn(SERVICE_BF105_CUSTOM, CHARACTERISTIC_USER_LIST)) { + Timber.d("setNotifyVendorSpecificUserList() OK"); + } + else { + Timber.d("setNotifyVendorSpecificUserList() FAILED"); + } + } + + @Override + protected synchronized void requestVendorSpecificUserList() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + parser.setIntValue(0, FORMAT_UINT8); + writeBytes(SERVICE_BF105_CUSTOM, CHARACTERISTIC_USER_LIST, + parser.getValue()); + } + + @Override + protected void writeActivityLevel() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + int activityLevel = this.selectedUser.getActivityLevel().toInt() + 1; + Timber.d(String.format("activityLevel: %d", activityLevel)); + parser.setIntValue(activityLevel, FORMAT_UINT8); + writeBytes(SERVICE_BF105_CUSTOM, CHARACTERISTIC_ACTIVITY_LEVEL, + parser.getValue()); + } + + protected void writeTargetWeight() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + int targetWeight = (int) this.selectedUser.getGoalWeight(); + parser.setIntValue(targetWeight, FORMAT_UINT16); + writeBytes(SERVICE_BF105_CUSTOM, CHARACTERISTIC_TARGET_WEIGHT, + parser.getValue()); + } + + @Override + protected void writeInitials() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + String initials = getInitials(this.selectedUser.getUserName()); + Timber.d("Initials: " + initials); + parser.setString(initials); + writeBytes(SERVICE_BF105_CUSTOM, CHARACTERISTIC_INITIALS, + parser.getValue()); + } + + @Override + protected synchronized void requestMeasurement() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + parser.setIntValue(0, FORMAT_UINT8); + writeBytes(SERVICE_BF105_CUSTOM, CHARACTERISTIC_TAKE_MEASUREMENT, + parser.getValue()); + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerBF600.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerBF600.java new file mode 100644 index 00000000..2406bc30 --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerBF600.java @@ -0,0 +1,119 @@ +/* Copyright (C) 2019 olie.xdev + * + * 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 + */ + + /* + * Based on source-code by weliem/blessed-android + */ +package com.health.openscale.core.bluetooth; + +import static com.welie.blessed.BluetoothBytesParser.FORMAT_UINT8; + +import android.content.Context; + +import com.health.openscale.core.utils.Converters; +import com.welie.blessed.BluetoothBytesParser; + +import java.util.UUID; + +import timber.log.Timber; + +public class BluetoothBeurerBF600 extends BluetoothStandardWeightProfile { + private static final UUID SERVICE_BEURER_CUSTOM_BF600 = BluetoothGattUuid.fromShortCode(0xfff0); + private static final UUID CHARACTERISTIC_BEURER_BF600_SCALE_SETTING = BluetoothGattUuid.fromShortCode(0xfff1); + private static final UUID CHARACTERISTIC_BEURER_BF600_USER_LIST = BluetoothGattUuid.fromShortCode(0xfff2); + private static final UUID CHARACTERISTIC_BEURER_BF600_ACTIVITY_LEVEL = BluetoothGattUuid.fromShortCode(0xfff3); + private static final UUID CHARACTERISTIC_BEURER_BF600_TAKE_MEASUREMENT = BluetoothGattUuid.fromShortCode(0xfff4); + private static final UUID CHARACTERISTIC_BEURER_BF600_REFER_WEIGHT_BF = BluetoothGattUuid.fromShortCode(0xfff5); + private static final UUID CHARACTERISTIC_BEURER_BF850_INITIALS = BluetoothGattUuid.fromShortCode(0xfff6); + + private String deviceName; + + public BluetoothBeurerBF600(Context context, String name) { + super(context); + deviceName = name; + } + + @Override + public String driverName() { + return "Beurer " + deviceName; + } + + @Override + protected int getVendorSpecificMaxUserCount() { + return 8; + } + + @Override + protected void writeActivityLevel() { + Converters.ActivityLevel al = selectedUser.getActivityLevel(); + BluetoothBytesParser parser = new BluetoothBytesParser(new byte[]{0}); + parser.setIntValue(al.toInt() + 1, FORMAT_UINT8, 0); + Timber.d(String.format("setCurrentUserData Activity level: %d", al.toInt() + 1)); + writeBytes(SERVICE_BEURER_CUSTOM_BF600, + CHARACTERISTIC_BEURER_BF600_ACTIVITY_LEVEL, parser.getValue()); + } + + @Override + protected void writeInitials() { + if (haveCharacteristic(SERVICE_BEURER_CUSTOM_BF600, CHARACTERISTIC_BEURER_BF850_INITIALS)) { + BluetoothBytesParser parser = new BluetoothBytesParser(); + String initials = getInitials(this.selectedUser.getUserName()); + Timber.d("Initials: " + initials); + parser.setString(initials); + writeBytes(SERVICE_BEURER_CUSTOM_BF600, CHARACTERISTIC_BEURER_BF850_INITIALS, + parser.getValue()); + } + } + + @Override + protected void requestMeasurement() { + BluetoothBytesParser parser = new BluetoothBytesParser(new byte[]{0}); + parser.setIntValue(0x00, FORMAT_UINT8, 0); + Timber.d(String.format("requestMeasurement BEURER 0xFFF4 magic: 0x00")); + writeBytes(SERVICE_BEURER_CUSTOM_BF600, + CHARACTERISTIC_BEURER_BF600_TAKE_MEASUREMENT, parser.getValue()); + } + + @Override + protected void setNotifyVendorSpecificUserList() { + if (setNotificationOn(SERVICE_BEURER_CUSTOM_BF600, + CHARACTERISTIC_BEURER_BF600_USER_LIST)) { + Timber.d("setNotifyVendorSpecificUserList() OK"); + } + else { + Timber.d("setNotifyVendorSpecificUserList() FAILED"); + } + } + + @Override + protected synchronized void requestVendorSpecificUserList() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + parser.setIntValue(0x00, FORMAT_UINT8); + writeBytes(SERVICE_BEURER_CUSTOM_BF600, CHARACTERISTIC_BEURER_BF600_USER_LIST, + parser.getValue()); + stopMachineState(); + } + + @Override + public void onBluetoothNotify(UUID characteristic, byte[] value) { + if (characteristic.equals(CHARACTERISTIC_BEURER_BF600_USER_LIST)) { + handleVendorSpecificUserList(value); + } + else { + super.onBluetoothNotify(characteristic, value); + } + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerBF950.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerBF950.java new file mode 100644 index 00000000..740bf595 --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothBeurerBF950.java @@ -0,0 +1,48 @@ +/* Copyright (C) 2019 olie.xdev + * + * 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 + */ + + /* + * Based on source-code by weliem/blessed-android + */ +package com.health.openscale.core.bluetooth; + +import android.content.Context; + +import timber.log.Timber; + +public class BluetoothBeurerBF950 extends BluetoothBeurerBF105 { + private String deviceName; + + public BluetoothBeurerBF950(Context context, String name) { + super(context); + deviceName = name; + } + + @Override + public String driverName() { + return deviceName; + } + + @Override + protected int getVendorSpecificMaxUserCount() { + return 8; + } + + @Override + protected void writeTargetWeight() { + Timber.d("Target Weight not supported on " + deviceName); + } +} 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 bcda7d21..50d80645 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 @@ -16,6 +16,9 @@ package com.health.openscale.core.bluetooth; +import static android.bluetooth.BluetoothGatt.GATT_SUCCESS; +import static android.content.Context.LOCATION_SERVICE; + import android.Manifest; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.le.ScanResult; @@ -29,20 +32,19 @@ import androidx.core.content.ContextCompat; import com.health.openscale.R; import com.health.openscale.core.datatypes.ScaleMeasurement; -import com.welie.blessed.BluetoothCentral; -import com.welie.blessed.BluetoothCentralCallback; +import com.welie.blessed.BluetoothCentralManager; +import com.welie.blessed.BluetoothCentralManagerCallback; import com.welie.blessed.BluetoothPeripheral; import com.welie.blessed.BluetoothPeripheralCallback; +import com.welie.blessed.ConnectionState; +import com.welie.blessed.GattStatus; +import com.welie.blessed.HciStatus; +import com.welie.blessed.WriteType; import java.util.UUID; import timber.log.Timber; -import static android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT; -import static android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE; -import static android.content.Context.LOCATION_SERVICE; -import static com.welie.blessed.BluetoothPeripheral.GATT_SUCCESS; - public abstract class BluetoothCommunication { public enum BT_STATUS { RETRIEVE_SCALE_DATA, @@ -53,7 +55,9 @@ public abstract class BluetoothCommunication { CONNECTION_LOST, NO_DEVICE_FOUND, UNEXPECTED_ERROR, - SCALE_MESSAGE + SCALE_MESSAGE, + CHOOSE_SCALE_USER, + ENTER_SCALE_USER_CONSENT, } private int stepNr; @@ -64,7 +68,7 @@ public abstract class BluetoothCommunication { private Handler callbackBtHandler; private Handler disconnectHandler; - private BluetoothCentral central; + private BluetoothCentralManager central; private BluetoothPeripheral btPeripheral; public BluetoothCommunication(Context context) @@ -73,7 +77,20 @@ public abstract class BluetoothCommunication { this.disconnectHandler = new Handler(); this.stepNr = 0; this.stopped = false; - this.central = new BluetoothCentral(context, bluetoothCentralCallback, new Handler(Looper.getMainLooper())); + this.central = new BluetoothCentralManager(context, bluetoothCentralCallback, new Handler(Looper.getMainLooper())); + } + + protected boolean needReConnect() { + if (callbackBtHandler == null) { + return true; + } + if (btPeripheral != null) { + ConnectionState state = btPeripheral.getState(); + if (state.equals(ConnectionState.CONNECTED) || state.equals(ConnectionState.CONNECTING)) { + return false; + } + } + return true; } /** @@ -119,6 +136,20 @@ public abstract class BluetoothCommunication { } } + protected void chooseScaleUserUi(Object userList) { + if (callbackBtHandler != null) { + callbackBtHandler.obtainMessage( + BT_STATUS.CHOOSE_SCALE_USER.ordinal(), userList).sendToTarget(); + } + } + + protected void enterScaleUserConsentUi(int appScaleUserId, int scaleUserIndex) { + if (callbackBtHandler != null) { + callbackBtHandler.obtainMessage( + BT_STATUS.ENTER_SCALE_USER_CONSENT.ordinal(), appScaleUserId, scaleUserIndex).sendToTarget(); + } + } + /** * Send message to openScale user * @@ -162,11 +193,17 @@ public abstract class BluetoothCommunication { */ protected void onBluetoothDiscovery(BluetoothPeripheral peripheral) { } + /** + * Stopped current state machine + */ protected synchronized void stopMachineState() { Timber.d("Stop machine state"); stopped = true; } + /** + * resume current state machine + */ protected synchronized void resumeMachineState() { Timber.d("Resume machine state"); stopped = false; @@ -190,11 +227,23 @@ public abstract class BluetoothCommunication { } } + /** + * This function jump to a specific step number + * @param nr the step number which the state machine should jump to. + */ protected synchronized void jumpNextToStepNr(int nr) { Timber.d("Jump next to step nr " + nr); stepNr = nr; } + /** + * This function return the current step number + * @return the current step number + */ + protected synchronized int getStepNr() { + return stepNr; + } + /** * This function jumps to the step newStepNr only if the current step equals curStepNr, * i.e. if the next step (stepNr) is 1 above curStepNr @@ -221,6 +270,17 @@ public abstract class BluetoothCommunication { Timber.d("Jumped back one step to " + stepNr); } + /** + * Check if specific characteristic exists on Bluetooth device. + * + * @param service the Bluetooth UUID service + * @param characteristic the Bluetooth UUID characteristic + * @return true if characteristic exists + */ + protected boolean haveCharacteristic(UUID service, UUID characteristic) { + return btPeripheral.getCharacteristic(service, characteristic) != null; + } + /** * Write a byte array to a Bluetooth device. * @@ -241,7 +301,7 @@ public abstract class BluetoothCommunication { protected void writeBytes(UUID service, UUID characteristic, byte[] bytes, boolean noResponse) { Timber.d("Invoke write bytes [" + byteInHex(bytes) + "] on " + BluetoothGattUuid.prettyPrint(characteristic)); btPeripheral.writeCharacteristic(btPeripheral.getCharacteristic(service, characteristic), bytes, - noResponse ? WRITE_TYPE_NO_RESPONSE : WRITE_TYPE_DEFAULT); + noResponse ? WriteType.WITHOUT_RESPONSE : WriteType.WITH_RESPONSE); } /** @@ -274,14 +334,27 @@ public abstract class BluetoothCommunication { * Set notification flag on for the Bluetooth device. * * @param characteristic the Bluetooth UUID characteristic + * @return true if the operation was enqueued, false if the characteristic doesn't support notification or indications or */ - protected void setNotificationOn(UUID service, UUID characteristic) { + protected boolean setNotificationOn(UUID service, UUID characteristic) { Timber.d("Invoke set notification on " + BluetoothGattUuid.prettyPrint(characteristic)); if(btPeripheral.getService(service) != null) { - stopMachineState(); BluetoothGattCharacteristic currentTimeCharacteristic = btPeripheral.getCharacteristic(service, characteristic); - btPeripheral.setNotify(currentTimeCharacteristic, true); + if (currentTimeCharacteristic != null) { + boolean notifySet; + try { + notifySet = btPeripheral.setNotify(currentTimeCharacteristic, true); + } + catch (IllegalArgumentException e){ + notifySet = false; + }; + if (notifySet) { + stopMachineState(); + return true; + } + } } + return false; } /** @@ -303,6 +376,14 @@ public abstract class BluetoothCommunication { disconnectHandler.removeCallbacksAndMessages(null); } + public void selectScaleUserIndexForAppUserId(int appUserId, int scaleUserIndex, Handler uiHandler) { + Timber.d("Set scale user index for app user id: Not implemented!"); + } + + public void setScaleUserConsent(int appUserId, int scaleUserConsent, Handler uiHandler) { + Timber.d("Set scale user consent for app user id: Not implemented!"); + } + /** * Convert a byte array to hex for debugging purpose * @@ -373,8 +454,8 @@ public abstract class BluetoothCommunication { } @Override - public void onNotificationStateUpdate(BluetoothPeripheral peripheral, BluetoothGattCharacteristic characteristic, int status) { - if( status == GATT_SUCCESS) { + public void onNotificationStateUpdate(BluetoothPeripheral peripheral, BluetoothGattCharacteristic characteristic, GattStatus status) { + if( status.value == GATT_SUCCESS) { if(peripheral.isNotifying(characteristic)) { Timber.d(String.format("SUCCESS: Notify set for %s", characteristic.getUuid())); resumeMachineState(); @@ -385,8 +466,8 @@ public abstract class BluetoothCommunication { } @Override - public void onCharacteristicWrite(BluetoothPeripheral peripheral, byte[] value, BluetoothGattCharacteristic characteristic, int status) { - if( status == GATT_SUCCESS) { + public void onCharacteristicWrite(BluetoothPeripheral peripheral, byte[] value, BluetoothGattCharacteristic characteristic, GattStatus status) { + if( status.value == GATT_SUCCESS) { Timber.d(String.format("SUCCESS: Writing <%s> to <%s>", byteInHex(value), characteristic.getUuid().toString())); nextMachineStep(); @@ -396,14 +477,14 @@ public abstract class BluetoothCommunication { } @Override - public void onCharacteristicUpdate(final BluetoothPeripheral peripheral, byte[] value, final BluetoothGattCharacteristic characteristic, final int status) { + public void onCharacteristicUpdate(final BluetoothPeripheral peripheral, byte[] value, final BluetoothGattCharacteristic characteristic, GattStatus status) { resetDisconnectTimer(); onBluetoothNotify(characteristic.getUuid(), value); } }; // Callback for central - private final BluetoothCentralCallback bluetoothCentralCallback = new BluetoothCentralCallback() { + private final BluetoothCentralManagerCallback bluetoothCentralCallback = new BluetoothCentralManagerCallback() { @Override public void onConnectedPeripheral(BluetoothPeripheral peripheral) { @@ -415,18 +496,18 @@ public abstract class BluetoothCommunication { } @Override - public void onConnectionFailed(BluetoothPeripheral peripheral, final int status) { - Timber.e(String.format("connection '%s' failed with status %d", peripheral.getName(), status )); + public void onConnectionFailed(BluetoothPeripheral peripheral, HciStatus status) { + Timber.e(String.format("connection '%s' failed with status %d", peripheral.getName(), status.value)); setBluetoothStatus(BT_STATUS.CONNECTION_LOST); - if (status == 8) { + if (status.value == 8) { sendMessage(R.string.info_bluetooth_connection_error_scale_offline, 0); } } @Override - public void onDisconnectedPeripheral(final BluetoothPeripheral peripheral, final int status) { - Timber.d(String.format("disconnected '%s' with status %d", peripheral.getName(), status)); + public void onDisconnectedPeripheral(final BluetoothPeripheral peripheral, HciStatus status) { + Timber.d(String.format("disconnected '%s' with status %d", peripheral.getName(), status.value)); } @Override @@ -482,6 +563,20 @@ public abstract class BluetoothCommunication { }, 1000); } + protected boolean reConnectPreviousPeripheral(Handler uiHandler) { + if (btPeripheral == null) { + return false; + } + if (btPeripheral.getState() != ConnectionState.DISCONNECTED) { + disconnect(); + } + if (callbackBtHandler == null) { + registerCallbackHandler(uiHandler); + } + connect(btPeripheral.getAddress()); + return true; + } + private void resetDisconnectTimer() { disconnectHandler.removeCallbacksAndMessages(null); disconnectWithDelay(); 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 da3013a6..0802df38 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 @@ -42,14 +42,6 @@ public class BluetoothFactory { || name.equals("BF700".toLowerCase(Locale.US))) { return new BluetoothBeurerSanitas(context, BluetoothBeurerSanitas.DeviceType.BEURER_BF710); } - /*if (name.startsWith("BEURER BF600".toLowerCase(Locale.US)) - || name.startsWith("BEURER BF850".toLowerCase(Locale.US)) - || name.startsWith("BF600".toLowerCase(Locale.US)) - || name.startsWith("BF850".toLowerCase(Locale.US)) - || name.startsWith("BF-600".toLowerCase(Locale.US)) - || name.startsWith("BF-850".toLowerCase(Locale.US))) { - return new BluetoothStandardWeightProfile(context); - }*/ if (name.equals("openScale".toLowerCase(Locale.US))) { return new BluetoothCustomOpenScale(context); } @@ -122,6 +114,15 @@ public class BluetoothFactory { if (deviceName.equals("ADV")) { return new BluetoothOKOK(context); } + if (deviceName.equals("BF105")) { + return new BluetoothBeurerBF105(context); + } + if (deviceName.equals("BF600") || deviceName.equals("BF850")) { + return new BluetoothBeurerBF600(context, deviceName); + } + if (deviceName.equals("SBF77") || deviceName.equals("BF950")) { + return new BluetoothBeurerBF950(context, deviceName); + } return null; } } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothGattUuid.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothGattUuid.java index cb82e842..7facd805 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothGattUuid.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothGattUuid.java @@ -93,6 +93,7 @@ public class BluetoothGattUuid { public static final UUID CHARACTERISTIC_CHANGE_INCREMENT = fromShortCode(0x2a99); public static final UUID CHARACTERISTIC_USER_CONTROL_POINT = fromShortCode(0x2A9F); public static final UUID CHARACTERISTIC_USER_AGE = fromShortCode(0x2A80); + public static final UUID CHARACTERISTIC_USER_DATE_OF_BIRTH = fromShortCode(0x2A85); public static final UUID CHARACTERISTIC_USER_GENDER = fromShortCode(0x2A8C); public static final UUID CHARACTERISTIC_USER_HEIGHT = fromShortCode(0x2A8E); diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK.java index c77877fa..5cff0e4c 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOKOK.java @@ -1,15 +1,15 @@ package com.health.openscale.core.bluetooth; +import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.content.Context; -import android.bluetooth.le.ScanFilter; import android.os.Handler; import android.os.Looper; import android.util.SparseArray; import com.health.openscale.core.datatypes.ScaleMeasurement; -import com.welie.blessed.BluetoothCentral; -import com.welie.blessed.BluetoothCentralCallback; +import com.welie.blessed.BluetoothCentralManager; +import com.welie.blessed.BluetoothCentralManagerCallback; import com.welie.blessed.BluetoothPeripheral; import org.jetbrains.annotations.NotNull; @@ -28,8 +28,8 @@ public class BluetoothOKOK extends BluetoothCommunication { private static final int IDX_IMPEDANCE_LSB = 11; private static final int IDX_CHECKSUM = 12; - private BluetoothCentral central; - private final BluetoothCentralCallback btCallback = new BluetoothCentralCallback() { + private BluetoothCentralManager central; + private final BluetoothCentralManagerCallback btCallback = new BluetoothCentralManagerCallback() { @Override public void onDiscoveredPeripheral(@NotNull BluetoothPeripheral peripheral, @NotNull ScanResult scanResult) { SparseArray manufacturerSpecificData = scanResult.getScanRecord().getManufacturerSpecificData(); @@ -63,7 +63,7 @@ public class BluetoothOKOK extends BluetoothCommunication { public BluetoothOKOK(Context context) { super(context); - central = new BluetoothCentral(context, btCallback, new Handler(Looper.getMainLooper())); + central = new BluetoothCentralManager(context, btCallback, new Handler(Looper.getMainLooper())); } @Override diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothStandardWeightProfile.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothStandardWeightProfile.java index 1345c89b..d87efe0e 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothStandardWeightProfile.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothStandardWeightProfile.java @@ -19,42 +19,65 @@ */ package com.health.openscale.core.bluetooth; -import android.content.Context; - -import com.health.openscale.core.OpenScale; -import com.health.openscale.core.datatypes.ScaleMeasurement; -import com.health.openscale.core.datatypes.ScaleUser; -import com.welie.blessed.BluetoothBytesParser; - -import java.util.Calendar; -import java.util.Date; -import java.util.UUID; - -import timber.log.Timber; - import static com.welie.blessed.BluetoothBytesParser.FORMAT_UINT16; import static com.welie.blessed.BluetoothBytesParser.FORMAT_UINT8; -public class BluetoothStandardWeightProfile extends BluetoothCommunication { - private int CURRENT_USER_CONSENT = 3289; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.util.Pair; + +import com.health.openscale.R; +import com.health.openscale.core.OpenScale; +import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.health.openscale.core.datatypes.ScaleUser; +import com.health.openscale.core.utils.Converters; +import com.welie.blessed.BluetoothBytesParser; + +import java.text.DateFormat; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Random; +import java.util.UUID; +import java.util.Vector; + +import timber.log.Timber; + +public abstract class BluetoothStandardWeightProfile extends BluetoothCommunication { // UDS control point codes - private static final byte UDS_CP_REGISTER_NEW_USER = 0x01; - private static final byte UDS_CP_CONSENT = 0x02; - private static final byte UDS_CP_DELETE_USER_DATA = 0x03; - private static final byte UDS_CP_LIST_ALL_USERS = 0x04; - private static final byte UDS_CP_DELETE_USERS = 0x05; - private static final byte UDS_CP_RESPONSE = 0x20; + protected static final byte UDS_CP_REGISTER_NEW_USER = 0x01; + protected static final byte UDS_CP_CONSENT = 0x02; + protected static final byte UDS_CP_DELETE_USER_DATA = 0x03; + protected static final byte UDS_CP_LIST_ALL_USERS = 0x04; + protected static final byte UDS_CP_DELETE_USERS = 0x05; + protected static final byte UDS_CP_RESPONSE = 0x20; // UDS response codes - private static final byte UDS_CP_RESP_VALUE_SUCCESS = 0x01; - private static final byte UDS_CP_RESP_OP_CODE_NOT_SUPPORTED = 0x02; - private static final byte UDS_CP_RESP_INVALID_PARAMETER = 0x03; - private static final byte UDS_CP_RESP_OPERATION_FAILED = 0x04; - private static final byte UDS_CP_RESP_USER_NOT_AUTHORIZED = 0x05; + protected static final byte UDS_CP_RESP_VALUE_SUCCESS = 0x01; + protected static final byte UDS_CP_RESP_OP_CODE_NOT_SUPPORTED = 0x02; + protected static final byte UDS_CP_RESP_INVALID_PARAMETER = 0x03; + protected static final byte UDS_CP_RESP_OPERATION_FAILED = 0x04; + protected static final byte UDS_CP_RESP_USER_NOT_AUTHORIZED = 0x05; + + SharedPreferences prefs; + protected boolean registerNewUser; + ScaleUser selectedUser; + ScaleMeasurement previousMeasurement; + protected boolean haveBatteryService; + protected Vector scaleUserList; public BluetoothStandardWeightProfile(Context context) { super(context); + this.prefs = PreferenceManager.getDefaultSharedPreferences(context); + this.selectedUser = OpenScale.getInstance().getSelectedScaleUser(); + this.registerNewUser = false; + previousMeasurement = null; + haveBatteryService = false; + scaleUserList = new Vector(); } @Override @@ -62,42 +85,121 @@ public class BluetoothStandardWeightProfile extends BluetoothCommunication { return "Bluetooth Standard Weight Profile"; } + protected abstract int getVendorSpecificMaxUserCount(); + + private enum SM_STEPS { + START, + READ_DEVICE_MANUFACTURER, + READ_DEVICE_MODEL, + WRITE_CURRENT_TIME, + SET_NOTIFY_WEIGHT_MEASUREMENT, + SET_NOTIFY_BODY_COMPOSITION_MEASUREMENT, + SET_NOTIFY_CHANGE_INCREMENT, + SET_INDICATION_USER_CONTROL_POINT, + SET_NOTIFY_BATTERY_LEVEL, + READ_BATTERY_LEVEL, + SET_NOTIFY_VENDOR_SPECIFIC_USER_LIST, + REQUEST_VENDOR_SPECIFIC_USER_LIST, + REGISTER_NEW_SCALE_USER, + SELECT_SCALE_USER, + SET_SCALE_USER_DATA, + REQUEST_MEASUREMENT, + MAX_STEP + } + @Override protected boolean onNextStep(int stepNr) { - switch (stepNr) { - case 0: - // Read manufacturer and model number from the Device Information Service + if (stepNr > SM_STEPS.MAX_STEP.ordinal()) { + Timber.d( "WARNING: stepNr == " + stepNr + " outside range, must be from 0 to " + SM_STEPS.MAX_STEP.ordinal()); + stepNr = SM_STEPS.MAX_STEP.ordinal(); + } + SM_STEPS step = SM_STEPS.values()[stepNr]; + Timber.d("stepNr: " + stepNr + " " + step); + + switch (step) { + case START: + break; + case READ_DEVICE_MANUFACTURER: + // Read manufacturer from the Device Information Service readBytes(BluetoothGattUuid.SERVICE_DEVICE_INFORMATION, BluetoothGattUuid.CHARACTERISTIC_MANUFACTURER_NAME_STRING); + break; + case READ_DEVICE_MODEL: + // Read model number from the Device Information Service readBytes(BluetoothGattUuid.SERVICE_DEVICE_INFORMATION, BluetoothGattUuid.CHARACTERISTIC_MODEL_NUMBER_STRING); break; - case 1: - // Write the current time - BluetoothBytesParser parser = new BluetoothBytesParser(); - parser.setCurrentTime(Calendar.getInstance()); - writeBytes(BluetoothGattUuid.SERVICE_CURRENT_TIME, BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME, parser.getValue()); + case WRITE_CURRENT_TIME: + writeCurrentTime(); break; - case 2: + case SET_NOTIFY_WEIGHT_MEASUREMENT: // Turn on notification for Weight Service setNotificationOn(BluetoothGattUuid.SERVICE_WEIGHT_SCALE, BluetoothGattUuid.CHARACTERISTIC_WEIGHT_MEASUREMENT); break; - case 3: + case SET_NOTIFY_BODY_COMPOSITION_MEASUREMENT: // Turn on notification for Body Composition Service setNotificationOn(BluetoothGattUuid.SERVICE_BODY_COMPOSITION, BluetoothGattUuid.CHARACTERISTIC_BODY_COMPOSITION_MEASUREMENT); break; - case 4: - // Turn on notification for User Data Service + case SET_NOTIFY_CHANGE_INCREMENT: + // Turn on notification for User Data Service Change Increment setNotificationOn(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_CHANGE_INCREMENT); - setNotificationOn(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT); break; - case 5: + case SET_INDICATION_USER_CONTROL_POINT: + // Turn on notification for User Control Point + setIndicationOn(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT); + break; + case SET_NOTIFY_BATTERY_LEVEL: // Turn on notifications for Battery Service - setNotificationOn(BluetoothGattUuid.SERVICE_BATTERY_LEVEL, BluetoothGattUuid.CHARACTERISTIC_BATTERY_LEVEL); + if (setNotificationOn(BluetoothGattUuid.SERVICE_BATTERY_LEVEL, BluetoothGattUuid.CHARACTERISTIC_BATTERY_LEVEL)) { + haveBatteryService = true; + } + else { + haveBatteryService = false; + } break; - case 6: - final ScaleUser selectedUser = OpenScale.getInstance().getSelectedScaleUser(); - registerUser(CURRENT_USER_CONSENT); - setUser(selectedUser.getId(), CURRENT_USER_CONSENT); + case READ_BATTERY_LEVEL: + // read Battery Service + if (haveBatteryService) { + readBytes(BluetoothGattUuid.SERVICE_BATTERY_LEVEL, BluetoothGattUuid.CHARACTERISTIC_BATTERY_LEVEL); + } + break; + case SET_NOTIFY_VENDOR_SPECIFIC_USER_LIST: + setNotifyVendorSpecificUserList(); + break; + case REQUEST_VENDOR_SPECIFIC_USER_LIST: + scaleUserList.clear(); + requestVendorSpecificUserList(); + stopMachineState(); + break; + case REGISTER_NEW_SCALE_USER: + int userId = this.selectedUser.getId(); + int consentCode = getUserScaleConsent(userId); + int userIndex = getUserScaleIndex(userId); + if (consentCode == -1 || userIndex == -1) { + registerNewUser = true; + } + if (registerNewUser) { + Random randomFactory = new Random(); + consentCode = randomFactory.nextInt(10000); + storeUserScaleConsentCode(userId, consentCode); + registerUser(consentCode); + stopMachineState(); + } + break; + case SELECT_SCALE_USER: + Timber.d("Select user on scale!"); + setUser(this.selectedUser.getId()); + stopMachineState(); + break; + case SET_SCALE_USER_DATA: + if (registerNewUser) { + writeUserDataToScale(); + } + break; + case REQUEST_MEASUREMENT: + if (registerNewUser) { + requestMeasurement(); + sendMessage(R.string.info_step_on_scale_for_reference, 0); + } break; default: return false; @@ -106,6 +208,15 @@ public class BluetoothStandardWeightProfile extends BluetoothCommunication { return true; } + protected void writeUserDataToScale() { + writeBirthday(); + writeGender(); + writeHeight(); + writeActivityLevel(); + writeInitials(); + setChangeIncrement(); + } + @Override public void onBluetoothNotify(UUID characteristic, byte[] value) { BluetoothBytesParser parser = new BluetoothBytesParser(value); @@ -123,6 +234,9 @@ public class BluetoothStandardWeightProfile extends BluetoothCommunication { else if(characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_BATTERY_LEVEL)) { int batteryLevel = parser.getIntValue(FORMAT_UINT8); Timber.d(String.format("Received battery level %d%%", batteryLevel)); + if (batteryLevel <= 10) { + sendMessage(R.string.info_scale_low_battery, batteryLevel); + } } else if(characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_MANUFACTURER_NAME_STRING)) { String manufacturer = parser.getStringValue(0); @@ -133,40 +247,77 @@ public class BluetoothStandardWeightProfile extends BluetoothCommunication { Timber.d(String.format("Received modelnumber: %s", modelNumber)); } else if(characteristic.equals(BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT)) { - if(value[0]==UDS_CP_RESPONSE) { - switch (value[1]) { - case UDS_CP_REGISTER_NEW_USER: - if (value[2] == UDS_CP_RESP_VALUE_SUCCESS) { - int userIndex = value[3]; - Timber.d(String.format("Created user %d", userIndex)); - } else { - Timber.e("ERROR: could not register new user"); - } - break; - case UDS_CP_CONSENT: - if (value[2] == UDS_CP_RESP_VALUE_SUCCESS) { - Timber.d("Success user consent"); - } else if (value[2] == UDS_CP_RESP_USER_NOT_AUTHORIZED) { - Timber.e("Not authorized"); - } - break; - default: - Timber.e("Unhandled response"); - break; - } - } - } else { - Timber.d(String.format("Got data: <%s>", byteInHex(value))); + handleUserControlPointNotify(value); + } + else { + Timber.d(String.format("Notification from unhandled characteristic: %s, value: [%s]", + characteristic.toString(), byteInHex(value))); } } - private void handleWeightMeasurement(byte[] value) { + protected void handleUserControlPointNotify(byte[] value) { + if(value[0]==UDS_CP_RESPONSE) { + switch (value[1]) { + case UDS_CP_LIST_ALL_USERS: + Timber.d("UDS_CP_LIST_ALL_USERS value [" + byteInHex(value) + "]"); + break; + case UDS_CP_REGISTER_NEW_USER: + if (value[2] == UDS_CP_RESP_VALUE_SUCCESS) { + int userIndex = value[3]; + int userId = this.selectedUser.getId(); + Timber.d(String.format("UDS_CP_REGISTER_NEW_USER: Created scale user index: " + + "%d (app user id: %d)", userIndex, userId)); + storeUserScaleIndex(userId, userIndex); + resumeMachineState(); + } else { + Timber.e("UDS_CP_REGISTER_NEW_USER: ERROR: could not register new scale user, code: " + value[2]); + } + break; + case UDS_CP_CONSENT: + if (registerNewUser) { + Timber.d("UDS_CP_CONSENT: registerNewUser==true, value[2] == " + value[2]); + resumeMachineState(); + break; + } + if (value[2] == UDS_CP_RESP_VALUE_SUCCESS) { + Timber.d("UDS_CP_CONSENT: Success user consent"); + resumeMachineState(); + } else if (value[2] == UDS_CP_RESP_USER_NOT_AUTHORIZED) { + Timber.e("UDS_CP_CONSENT: Not authorized"); + enterScaleUserConsentUi(this.selectedUser.getId(), getUserScaleIndex(this.selectedUser.getId())); + } + else { + Timber.e("UDS_CP_CONSENT: unhandled, code: " + value[2]); + } + break; + default: + Timber.e("CHARACTERISTIC_USER_CONTROL_POINT: Unhandled response code " + + value[1] + " value [" + byteInHex(value) + "]"); + break; + } + } + else { + Timber.d("CHARACTERISTIC_USER_CONTROL_POINT: non-response code " + value[0] + + " value [" + byteInHex(value) + "]"); + } + } + + protected ScaleMeasurement weightMeasurementToScaleMeasurement(byte[] value) { + String prefix = "weightMeasurementToScaleMeasurement() "; + Timber.d(String.format(prefix + "value: [%s]", byteInHex(value))); BluetoothBytesParser parser = new BluetoothBytesParser(value); + final int flags = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8); boolean isKg = (flags & 0x01) == 0; final boolean timestampPresent = (flags & 0x02) > 0; final boolean userIDPresent = (flags & 0x04) > 0; final boolean bmiAndHeightPresent = (flags & 0x08) > 0; + Timber.d(String.format(prefix + "flags: 0x%02x ", flags) + + "[" + (isKg ? "SI" : "Imperial") + + (timestampPresent ? ", timestamp" : "") + + (userIDPresent ? ", userID" : "") + + (bmiAndHeightPresent ? ", bmiAndHeight" : "") + + "], " + String.format("reserved flags: 0x%02x ", flags & 0xf0)); ScaleMeasurement scaleMeasurement = new ScaleMeasurement(); @@ -175,29 +326,54 @@ public class BluetoothStandardWeightProfile extends BluetoothCommunication { // Get weight float weightValue = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * weightMultiplier; + Timber.d(prefix+ "weight: " + weightValue); scaleMeasurement.setWeight(weightValue); if(timestampPresent) { Date timestamp = parser.getDateTime(); + Timber.d(prefix + "timestamp: " + timestamp.toString()); scaleMeasurement.setDateTime(timestamp); } if(userIDPresent) { - int userID = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8); - Timber.d(String.format("User id: %d", userID)); + int scaleUserIndex = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8); + int userID = getUserIdFromScaleIndex(scaleUserIndex); + Timber.d(String.format(prefix + "scale user index: %d (app user id: %d)", scaleUserIndex, userID)); + if (userID != -1) { + scaleMeasurement.setUserId(userID); + } + + if (registerNewUser) { + Timber.d(String.format(prefix + "Setting initial weight for user %s to: %s and registerNewUser to false", userID, + weightValue)); + if (selectedUser.getId() == userID) { + this.selectedUser.setInitialWeight(weightValue); + OpenScale.getInstance().updateScaleUser(selectedUser); + } + registerNewUser = false; + } } if(bmiAndHeightPresent) { float BMI = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.1f; + Timber.d(prefix + "BMI: " + BMI); float heightInMeters = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.001f; + Timber.d(prefix + "heightInMeters: " + heightInMeters); } Timber.d(String.format("Got weight: %s", weightValue)); - addScaleMeasurement(scaleMeasurement); + return scaleMeasurement; } - private void handleBodyCompositionMeasurement(byte[] value) { + protected void handleWeightMeasurement(byte[] value) { + mergeWithPreviousScaleMeasurement(weightMeasurementToScaleMeasurement(value)); + } + + protected ScaleMeasurement bodyCompositionMeasurementToScaleMeasurement(byte[] value) { + String prefix = "bodyCompositionMeasurementToScaleMeasurement() "; + Timber.d(String.format(prefix + "value: [%s]", byteInHex(value))); BluetoothBytesParser parser = new BluetoothBytesParser(value); + final int flags = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16); boolean isKg = (flags & 0x0001) == 0; float massMultiplier = (float) (isKg ? 0.005 : 0.01); @@ -213,78 +389,187 @@ public class BluetoothStandardWeightProfile extends BluetoothCommunication { boolean weightPresent = (flags & 0x0400) > 0; boolean heightPresent = (flags & 0x0800) > 0; boolean multiPacketMeasurement = (flags & 0x1000) > 0; + Timber.d(String.format(prefix + "flags: 0x%02x ", flags) + + "[" + (isKg ? "SI" : "Imperial") + + (timestampPresent ? ", timestamp" : "") + + (userIDPresent ? ", userID" : "") + + (bmrPresent ? ", bmr" : "") + + (musclePercentagePresent ? ", musclePercentage" : "") + + (muscleMassPresent ? ", muscleMass" : "") + + (fatFreeMassPresent ? ", fatFreeMass" : "") + + (softLeanMassPresent ? ", softLeanMass" : "") + + (bodyWaterMassPresent ? ", bodyWaterMass" : "") + + (impedancePresent ? ", impedance" : "") + + (weightPresent ? ", weight" : "") + + (heightPresent ? ", height" : "") + + (multiPacketMeasurement ? ", multiPacketMeasurement" : "") + + "], " + String.format("reserved flags: 0x%04x ", flags & 0xe000)); ScaleMeasurement scaleMeasurement = new ScaleMeasurement(); float bodyFatPercentage = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.1f; + Timber.d(prefix + "bodyFatPercentage: " + bodyFatPercentage); scaleMeasurement.setFat(bodyFatPercentage); // Read timestamp if present if (timestampPresent) { Date timestamp = parser.getDateTime(); + Timber.d(prefix + "timestamp: " + timestamp.toString()); scaleMeasurement.setDateTime(timestamp); } // Read userID if present if (userIDPresent) { - int userID = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8); - Timber.d(String.format("user id: %d", userID)); + int scaleUserIndex = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8); + int userID = getUserIdFromScaleIndex(scaleUserIndex); + Timber.d(String.format(prefix + "scale user index: %d (app user id: %d)", scaleUserIndex, userID)); + if (userID != -1) { + scaleMeasurement.setUserId(userID); + } } // Read bmr if present if (bmrPresent) { int bmrInJoules = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16); int bmrInKcal = Math.round(((bmrInJoules / 4.1868f) * 10.0f) / 10.0f); + Timber.d(prefix + "bmrInJoules: " + bmrInJoules + " bmrInKcal: " + bmrInKcal); } // Read musclePercentage if present if (musclePercentagePresent) { float musclePercentage = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.1f; + Timber.d(prefix + "musclePercentage: " + musclePercentage); scaleMeasurement.setMuscle(musclePercentage); } // Read muscleMass if present if (muscleMassPresent) { float muscleMass = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + Timber.d(prefix + "muscleMass: " + muscleMass); } // Read fatFreeMassPresent if present if (fatFreeMassPresent) { float fatFreeMass = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + Timber.d(prefix + "fatFreeMass: " + fatFreeMass); } // Read softleanMass if present + float softLeanMass = 0.0f; if (softLeanMassPresent) { - float softLeanMass = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + softLeanMass = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + Timber.d(prefix + "softLeanMass: " + softLeanMass); } // Read bodyWaterMass if present if (bodyWaterMassPresent) { float bodyWaterMass = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + Timber.d(prefix + "bodyWaterMass: " + bodyWaterMass); scaleMeasurement.setWater(bodyWaterMass); } // Read impedance if present if (impedancePresent) { float impedance = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.1f; + Timber.d(prefix + "impedance: " + impedance); } // Read weight if present + float weightValue = 0.0f; if (weightPresent) { - float weightValue = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + weightValue = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * massMultiplier; + Timber.d(prefix + "weightValue: " + weightValue); scaleMeasurement.setWeight(weightValue); } + else { + if (previousMeasurement != null) { + weightValue = previousMeasurement.getWeight(); + if (weightValue > 0) { + weightPresent = true; + } + } + } + + // calculate lean body mass and bone mass + if (weightPresent && softLeanMassPresent) { + float fatMass = weightValue * bodyFatPercentage / 100.0f; + float leanBodyMass = weightValue - fatMass; + float boneMass = leanBodyMass - softLeanMass; + scaleMeasurement.setLbm(leanBodyMass); + scaleMeasurement.setBone(boneMass); + } // Read height if present if (heightPresent) { float heightValue = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16); + Timber.d(prefix + "heightValue: " + heightValue); + } + + if (multiPacketMeasurement) { + Timber.e(prefix + "multiPacketMeasurement not supported!"); } Timber.d(String.format("Got body composition: %s", byteInHex(value))); - addScaleMeasurement(scaleMeasurement); + return scaleMeasurement; } - private void registerUser(int consentCode) { + protected void handleBodyCompositionMeasurement(byte[] value) { + mergeWithPreviousScaleMeasurement(bodyCompositionMeasurementToScaleMeasurement(value)); + } + + /** + * Bluetooth scales usually implement both "Weight Scale Feature" and "Body Composition Feature". + * It seems that scale first transmits weight measurement (with user index and timestamp) and + * later transmits body composition measurement (without user index and timestamp). + * If previous measurement contains user index and new measurements does not then merge them and + * store as one. + * disconnect() function must store previousMeasurement to openScale db (if present). + * + * @param newMeasurement the scale data that should be merged with previous measurement or + * stored as previous measurement. + */ + protected void mergeWithPreviousScaleMeasurement(ScaleMeasurement newMeasurement) { + if (previousMeasurement == null) { + if (newMeasurement.getUserId() == -1) { + addScaleMeasurement(newMeasurement); + } + else { + previousMeasurement = newMeasurement; + } + } + else { + if ((newMeasurement.getUserId() == -1) && (previousMeasurement.getUserId() != -1)) { + previousMeasurement.merge(newMeasurement); + addScaleMeasurement(previousMeasurement); + previousMeasurement = null; + } + else { + addScaleMeasurement(previousMeasurement); + if (newMeasurement.getUserId() == -1) { + addScaleMeasurement(newMeasurement); + previousMeasurement = null; + } + else { + previousMeasurement = newMeasurement; + } + } + } + } + + @Override + public void disconnect() { + if (previousMeasurement != null) { + addScaleMeasurement(previousMeasurement); + previousMeasurement = null; + } + super.disconnect(); + } + + protected abstract void setNotifyVendorSpecificUserList(); + + protected abstract void requestVendorSpecificUserList(); + + protected void registerUser(int consentCode) { BluetoothBytesParser parser = new BluetoothBytesParser(new byte[]{0,0,0}); parser.setIntValue(UDS_CP_REGISTER_NEW_USER, FORMAT_UINT8,0); parser.setIntValue(consentCode, FORMAT_UINT16,1); @@ -292,7 +577,7 @@ public class BluetoothStandardWeightProfile extends BluetoothCommunication { writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT, parser.getValue()); } - private void setUser(int userIndex, int consentCode) { + protected void setUser(int userIndex, int consentCode) { BluetoothBytesParser parser = new BluetoothBytesParser(new byte[]{0,0,0,0}); parser.setIntValue(UDS_CP_CONSENT,FORMAT_UINT8,0); parser.setIntValue(userIndex, FORMAT_UINT8,1); @@ -300,4 +585,269 @@ public class BluetoothStandardWeightProfile extends BluetoothCommunication { Timber.d(String.format("setUser userIndex: %d, consentCode: %d", userIndex, consentCode)); writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT, parser.getValue()); } + + protected synchronized void setUser(int userId) { + int userIndex = getUserScaleIndex(userId); + int consentCode = getUserScaleConsent(userId); + Timber.d(String.format("setting: userId %d, userIndex: %d, consent Code: %d ", userId, userIndex, consentCode)); + setUser(userIndex, consentCode); + } + + protected void deleteUser(int userIndex, int consentCode) { + setUser(userIndex, consentCode); + deleteUser(); + } + + protected void deleteUser() { + BluetoothBytesParser parser = new BluetoothBytesParser(new byte[] { 0 }); + parser.setIntValue(UDS_CP_DELETE_USER_DATA, FORMAT_UINT8, 0); + writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_CONTROL_POINT, + parser.getValue()); + } + + protected void writeCurrentTime() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + parser.setCurrentTime(Calendar.getInstance()); + writeBytes(BluetoothGattUuid.SERVICE_CURRENT_TIME, BluetoothGattUuid.CHARACTERISTIC_CURRENT_TIME, + parser.getValue()); + } + + protected void writeBirthday() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + parser.setDateTime(dateToCalender(this.selectedUser.getBirthday())); + writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_DATE_OF_BIRTH, + Arrays.copyOfRange(parser.getValue(), 0, 3)); + } + + protected Calendar dateToCalender(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + return calendar; + } + + protected void writeGender() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + int gender = this.selectedUser.getGender().toInt(); + Timber.d(String.format("gender: %d", gender)); + parser.setIntValue(gender, FORMAT_UINT8); + writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_GENDER, + parser.getValue()); + } + + protected void writeHeight() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + int height = (int) this.selectedUser.getBodyHeight(); + Timber.d(String.format("height: %d", height)); + parser.setIntValue(height, FORMAT_UINT16); + writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_USER_HEIGHT, + parser.getValue()); + } + + protected void writeActivityLevel() { + Timber.d("Write user activity level not implemented!"); + } + + protected void writeInitials() { + Timber.d("Write user initials not implemented!"); + } + + protected void setChangeIncrement() { + BluetoothBytesParser parser = new BluetoothBytesParser(); + parser.setIntValue(1, FORMAT_UINT8); + writeBytes(BluetoothGattUuid.SERVICE_USER_DATA, BluetoothGattUuid.CHARACTERISTIC_CHANGE_INCREMENT, + parser.getValue()); + } + + protected void requestMeasurement() { + Timber.d("Take measurement command not implemented!"); + } + + protected synchronized void storeUserScaleConsentCode(int userId, int consentCode) { + prefs.edit().putInt("userConsentCode" + userId, consentCode).apply(); + } + + protected synchronized int getUserScaleConsent(int userId) { + return prefs.getInt("userConsentCode" + userId, -1); + } + + protected synchronized void storeUserScaleIndex(int userId, int userIndex) { + int currentUserIndex = getUserScaleIndex(userId); + if (currentUserIndex != -1) { + prefs.edit().putInt("userIdFromUserScaleIndex" + currentUserIndex, -1); + } + prefs.edit().putInt("userScaleIndex" + userId, userIndex).apply(); + if (userIndex != -1) { + prefs.edit().putInt("userIdFromUserScaleIndex" + userIndex, userId).apply(); + } + } + + protected synchronized int getUserIdFromScaleIndex(int userScaleIndex) { + return prefs.getInt("userIdFromUserScaleIndex" + userScaleIndex, -1); + } + + protected synchronized int getUserScaleIndex(int userId) { + return prefs.getInt("userScaleIndex" + userId, -1); + } + + protected void reconnectOrSetSmState(SM_STEPS requestedState, SM_STEPS minState, Handler uiHandler) { + if (needReConnect()) { + jumpNextToStepNr(SM_STEPS.START.ordinal()); + stopMachineState(); + reConnectPreviousPeripheral(uiHandler); + return; + } + if (getStepNr() > minState.ordinal()) { + jumpNextToStepNr(requestedState.ordinal()); + } + resumeMachineState(); + } + + @Override + public void selectScaleUserIndexForAppUserId(int appUserId, int scaleUserIndex, Handler uiHandler) { + Timber.d("Select scale user index from UI: user id: " + appUserId + ", scale user index: " + scaleUserIndex); + if (scaleUserIndex == -1) { + reconnectOrSetSmState(SM_STEPS.REGISTER_NEW_SCALE_USER, SM_STEPS.REGISTER_NEW_SCALE_USER, uiHandler); + } + else { + storeUserScaleIndex(appUserId, scaleUserIndex); + if (getUserScaleConsent(appUserId) == -1) { + enterScaleUserConsentUi(appUserId, scaleUserIndex); + } + else { + reconnectOrSetSmState(SM_STEPS.SELECT_SCALE_USER, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler); + } + } + } + + @Override + public void setScaleUserConsent(int appUserId, int scaleUserConsent, Handler uiHandler) { + Timber.d("set scale user consent from UI: user id: " + appUserId + ", scale user consent: " + scaleUserConsent); + storeUserScaleConsentCode(appUserId, scaleUserConsent); + if (scaleUserConsent == -1) { + reconnectOrSetSmState(SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler); + } + else { + reconnectOrSetSmState(SM_STEPS.SELECT_SCALE_USER, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler); + } + } + + protected void handleVendorSpecificUserList(byte[] value) { + Timber.d(String.format("Got user data: <%s>", byteInHex(value))); + BluetoothBytesParser parser = new BluetoothBytesParser(value); + int userListStatus = parser.getIntValue(FORMAT_UINT8); + if (userListStatus == 2) { + Timber.d("scale have no users!"); + storeUserScaleConsentCode(selectedUser.getId(), -1); + storeUserScaleIndex(selectedUser.getId(), -1); + jumpNextToStepNr(SM_STEPS.REGISTER_NEW_SCALE_USER.ordinal()); + resumeMachineState(); + return; + } + else if (userListStatus == 1) { + for (int i = 0; i < scaleUserList.size(); i++) { + if (i == 0) { + Timber.d("scale user list:"); + } + Timber.d("\n" + (i + 1) + ". " + scaleUserList.get(i)); + } + if ((scaleUserList.size() == 0)) { + storeUserScaleConsentCode(selectedUser.getId(), -1); + storeUserScaleIndex(selectedUser.getId(), -1); + jumpNextToStepNr(SM_STEPS.REGISTER_NEW_SCALE_USER.ordinal()); + resumeMachineState(); + return; + } + if (getUserScaleIndex(selectedUser.getId()) == -1 || getUserScaleConsent(selectedUser.getId()) == -1) { + chooseExistingScaleUser(scaleUserList); + return; + } + resumeMachineState(); + return; + } + int index = parser.getIntValue(FORMAT_UINT8); + String initials = parser.getStringValue(); + int end = 3 > initials.length() ? initials.length() : 3; + initials = initials.substring(0, end); + if (initials.length() == 3) { + if (initials.charAt(0) == 0xff && initials.charAt(1) == 0xff && initials.charAt(2) == 0xff) { + initials = ""; + } + } + parser.setOffset(5); + int year = parser.getIntValue(FORMAT_UINT16); + int month = parser.getIntValue(FORMAT_UINT8); + int day = parser.getIntValue(FORMAT_UINT8); + int height = parser.getIntValue(FORMAT_UINT8); + int gender = parser.getIntValue(FORMAT_UINT8); + int activityLevel = parser.getIntValue(FORMAT_UINT8); + GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day); + ScaleUser scaleUser = new ScaleUser(); + scaleUser.setUserName(initials); + scaleUser.setBirthday(calendar.getTime()); + scaleUser.setBodyHeight(height); + scaleUser.setGender(Converters.Gender.fromInt(gender)); + scaleUser.setActivityLevel(Converters.ActivityLevel.fromInt(activityLevel - 1)); + scaleUser.setId(index); + scaleUserList.add(scaleUser); + if (scaleUserList.size() == getVendorSpecificMaxUserCount()) { + if (getUserScaleIndex(selectedUser.getId()) == -1 || getUserScaleConsent(selectedUser.getId()) == -1) { + chooseExistingScaleUser(scaleUserList); + return; + } + resumeMachineState(); + } + } + + protected void chooseExistingScaleUser(Vector userList) { + final DateFormat dateFormat = DateFormat.getDateInstance(); + int choicesCount = userList.size(); + if (userList.size() < getVendorSpecificMaxUserCount()) { + choicesCount = userList.size() + 1; + } + CharSequence[] choiceStrings = new String[choicesCount]; + int indexArray[] = new int[choicesCount]; + int selectedItem = -1; + for (int i = 0; i < userList.size(); ++i) { + ScaleUser u = userList.get(i); + String name = u.getUserName(); + choiceStrings[i] = (name.length() > 0 ? name : String.format("P%02d", u.getId())) + + " " + context.getString(u.getGender().isMale() ? R.string.label_male : R.string.label_female).toLowerCase() + + " " + context.getString(R.string.label_height).toLowerCase() + ":" + u.getBodyHeight() + + " " + context.getString(R.string.label_birthday).toLowerCase() + ":" + dateFormat.format(u.getBirthday()) + + " " + context.getString(R.string.label_activity_level).toLowerCase() + ":" + (u.getActivityLevel().toInt() + 1); + indexArray[i] = u.getId(); + } + if (userList.size() < getVendorSpecificMaxUserCount()) { + choiceStrings[userList.size()] = context.getString(R.string.info_create_new_user_on_scale); + indexArray[userList.size()] = -1; + } + Pair choices = new Pair(choiceStrings, indexArray); + chooseScaleUserUi(choices); + } + + protected String getInitials(String fullName) { + if (fullName == null || fullName.isEmpty() || fullName.chars().allMatch(Character::isWhitespace)) { + return getDefaultInitials(); + } + return buildInitialsStringFrom(fullName).toUpperCase(); + } + + private String getDefaultInitials() { + int userId = this.selectedUser.getId(); + int userIndex = getUserScaleIndex(userId); + return "P" + userIndex + " "; + } + + private String buildInitialsStringFrom(String fullName) { + String[] name = fullName.trim().split(" +"); + String initials = ""; + for (int i = 0; i < 3; i++) { + if (i < name.length && name[i] != "") { + initials += name[i].charAt(0); + } else { + initials += " "; + } + } + return initials; + } } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java b/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java index ef330cb9..72d7700f 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java @@ -32,12 +32,18 @@ import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; +import android.text.Editable; import android.text.Html; +import android.text.InputFilter; +import android.text.InputType; import android.text.TextUtils; +import android.text.TextWatcher; import android.text.method.LinkMovementMethod; +import android.util.Pair; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -651,10 +657,92 @@ public class MainActivity extends AppCompatActivity Timber.e("Bluetooth scale message error: " + ex); } break; + case CHOOSE_SCALE_USER: + chooseScaleUser(msg); + break; + case ENTER_SCALE_USER_CONSENT: + enterScaleUserConsent(msg); + break; } } }; + private void chooseScaleUser(Message msg) { + AlertDialog.Builder mBuilder = new AlertDialog.Builder(MainActivity.this); + Pair choices = (Pair)msg.obj; + + mBuilder.setTitle(getResources().getString(R.string.info_select_scale_user)); + mBuilder.setSingleChoiceItems(choices.first, -1 , new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialoginterface, int i) { + Timber.d("UI selected " + i + ": " + choices.first[i] + " P-0" + choices.second[i]); + OpenScale.getInstance().setBluetoothDeviceUserIndex(OpenScale.getInstance().getSelectedScaleUser().getId(), choices.second[i], callbackBtHandler); + dialoginterface.dismiss(); + } + }); + mBuilder.setNegativeButton(getResources().getString(R.string.label_cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialoginterface, int i) { + dialoginterface.dismiss(); + } + }); + + AlertDialog mDialog = mBuilder.create(); + mDialog.show(); + } + + private void enterScaleUserConsent(Message msg) { + final int appUserId = msg.arg1; + final int scaleUserIndex = msg.arg2; + final int[] consentCode = {-1}; + + AlertDialog.Builder mBuilder = new AlertDialog.Builder(MainActivity.this); + mBuilder.setTitle(getResources().getString(R.string.info_enter_consent_code_for_scale_user, Integer.toString(scaleUserIndex))); + + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + InputFilter[] filterArray = new InputFilter[1]; + filterArray[0] = new InputFilter.LengthFilter(4); + input.setFilters(filterArray); + mBuilder.setView(input); + + mBuilder.setPositiveButton(getResources().getString(R.string.label_ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialoginterface, int i) { + OpenScale.getInstance().setBluetoothDeviceUserConsent(appUserId, consentCode[0], callbackBtHandler); + dialoginterface.dismiss(); + } + }); + mBuilder.setNegativeButton(getResources().getString(R.string.label_cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialoginterface, int i) { + OpenScale.getInstance().setBluetoothDeviceUserConsent(appUserId, -1, callbackBtHandler); + dialoginterface.dismiss(); + } + }); + + AlertDialog mDialog = mBuilder.create(); + mDialog.show(); + + mDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + input.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + @Override + public void afterTextChanged(Editable s) { + try { + consentCode[0] = Integer.parseInt(s.toString()); + Timber.d("consent code set to " + consentCode[0] + "(" + s.toString() + ")"); + mDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); + } catch(NumberFormatException nfe) { + Timber.d("Could not parse " + nfe); + mDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + } + } + }); + } + private void setBluetoothStatusIcon(int iconResource) { bluetoothStatusIcon = iconResource; bluetoothStatus.setIcon(getResources().getDrawable(bluetoothStatusIcon)); diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java index efc12ee8..0ee9d2e6 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BluetoothSettingsFragment.java @@ -57,8 +57,8 @@ import com.health.openscale.core.bluetooth.BluetoothCommunication; import com.health.openscale.core.bluetooth.BluetoothFactory; import com.health.openscale.gui.utils.ColorUtil; import com.health.openscale.gui.utils.PermissionHelper; -import com.welie.blessed.BluetoothCentral; -import com.welie.blessed.BluetoothCentralCallback; +import com.welie.blessed.BluetoothCentralManager; +import com.welie.blessed.BluetoothCentralManagerCallback; import com.welie.blessed.BluetoothPeripheral; import java.util.HashMap; @@ -76,7 +76,7 @@ public class BluetoothSettingsFragment extends Fragment { private TextView txtSearching; private ProgressBar progressBar; private Handler progressHandler; - private BluetoothCentral central; + private BluetoothCentralManager central; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -123,7 +123,7 @@ public class BluetoothSettingsFragment extends Fragment { return formatDeviceName(device.getName(), device.getAddress()); } - private final BluetoothCentralCallback bluetoothCentralCallback = new BluetoothCentralCallback() { + private final BluetoothCentralManagerCallback bluetoothCentralCallback = new BluetoothCentralManagerCallback() { @Override public void onDiscoveredPeripheral(BluetoothPeripheral peripheral, ScanResult scanResult) { new Handler().post(new Runnable() { @@ -139,7 +139,7 @@ public class BluetoothSettingsFragment extends Fragment { deviceListView.removeAllViews(); foundDevices.clear(); - central = new BluetoothCentral(requireContext(), bluetoothCentralCallback, new Handler(Looper.getMainLooper())); + central = new BluetoothCentralManager(requireContext(), bluetoothCentralCallback, new Handler(Looper.getMainLooper())); central.scanForPeripherals(); txtSearching.setVisibility(View.VISIBLE); diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index defdfea5..cc2b2035 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -164,6 +164,8 @@ Max. number of concurrent scale users reached Please step barefoot on the scale for reference measurements Please step barefoot on the scale + Select scale user + Enter PIN/consent code for scale user %s Measuring weight: %.2f This scale has not been paired!\n\nHold the button on the bottom of the scale to switch it to pairing mode, and then reconnect to retrieve the device password. Pairing succeeded!\n\nReconnect to retrieve measurement data. @@ -283,4 +285,5 @@ Next Back Done + Create new user on scale. \ No newline at end of file