diff --git a/android_app/app/src/androidTest/java/com/health/openscale/gui/AddMeasurementTest.java b/android_app/app/src/androidTest/java/com/health/openscale/gui/AddMeasurementTest.java new file mode 100644 index 00000000..e0b914ed --- /dev/null +++ b/android_app/app/src/androidTest/java/com/health/openscale/gui/AddMeasurementTest.java @@ -0,0 +1,106 @@ +/* Copyright (C) 2018 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 +*/ +package com.health.openscale.gui; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.ViewInteraction; +import android.support.test.espresso.contrib.PickerActions; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.LargeTest; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.DatePicker; + +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.gui.activities.BaseAppCompatActivity; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.hamcrest.TypeSafeMatcher; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Calendar; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard; +import static android.support.test.espresso.action.ViewActions.replaceText; +import static android.support.test.espresso.action.ViewActions.scrollTo; +import static android.support.test.espresso.matcher.ViewMatchers.withClassName; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.assertEquals; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class AddMeasurementTest { + private static final double DELTA = 1e-15; + + private static Context context; + private static OpenScale openScale; + + private static final ScaleUser male = TestData.getMaleUser(); + private static final ScaleUser female = TestData.getFemaleUser(); + + @Rule + public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(MainActivity.class, false, true); + + @BeforeClass + public static void initTest() { + context = InstrumentationRegistry.getTargetContext(); + + openScale = OpenScale.getInstance(); + + openScale.addScaleUser(male); + openScale.addScaleUser(female); + + // Set first start to true to get the user add dialog + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs.edit() + .putBoolean("firstStart", false) + .putString(BaseAppCompatActivity.PREFERENCE_LANGUAGE, "en") + .putInt("selectedUserId", male.getId()) + .commit(); + } + + @AfterClass + public static void addMeasurementVerification() { + openScale.deleteScaleUser(male.getId()); + openScale.deleteScaleUser(female.getId()); + } + + @Test + public void addMeasurementTest() { + onView(withId(R.id.action_add_measurement)).perform(click()); + + ScaleMeasurement measurement = TestData.getMeasurement(1); + } +} diff --git a/android_app/app/src/androidTest/java/com/health/openscale/gui/UserAddTest.java b/android_app/app/src/androidTest/java/com/health/openscale/gui/AddUserTest.java similarity index 98% rename from android_app/app/src/androidTest/java/com/health/openscale/gui/UserAddTest.java rename to android_app/app/src/androidTest/java/com/health/openscale/gui/AddUserTest.java index f6e97f09..54a7089f 100644 --- a/android_app/app/src/androidTest/java/com/health/openscale/gui/UserAddTest.java +++ b/android_app/app/src/androidTest/java/com/health/openscale/gui/AddUserTest.java @@ -61,7 +61,7 @@ import static org.junit.Assert.assertEquals; @LargeTest @RunWith(AndroidJUnit4.class) -public class UserAddTest { +public class AddUserTest { private static final double DELTA = 1e-15; private Context context; @@ -82,7 +82,7 @@ public class UserAddTest { } @After - public void verifyUserAdd() { + public void addUserVerification() { ScaleUser user = OpenScale.getInstance().getSelectedScaleUser(); assertEquals("test", user.getUserName()); @@ -107,10 +107,12 @@ public class UserAddTest { goalDate.set(Calendar.HOUR_OF_DAY, 0); assertEquals(goalDate.getTime().getTime(), user.getGoalDate().getTime()); + + OpenScale.getInstance().deleteScaleUser(user.getId()); } @Test - public void userAddTest() { + public void addUserTest() { mActivityTestRule.launchActivity(null); ViewInteraction editText = onView( diff --git a/android_app/app/src/androidTest/java/com/health/openscale/gui/TestData.java b/android_app/app/src/androidTest/java/com/health/openscale/gui/TestData.java new file mode 100644 index 00000000..7c96be62 --- /dev/null +++ b/android_app/app/src/androidTest/java/com/health/openscale/gui/TestData.java @@ -0,0 +1,108 @@ +/* Copyright (C) 2018 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 + */ +package com.health.openscale.gui; + +import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.health.openscale.core.datatypes.ScaleUser; +import com.health.openscale.core.utils.Converters; + +import java.util.Calendar; +import java.util.Date; +import java.util.Random; + +public class TestData { + private static Random rand = new Random(); + + public static ScaleUser getMaleUser() { + ScaleUser male = new ScaleUser(); + + male.setUserName("Bob"); + male.setGender(Converters.Gender.MALE); + male.setInitialWeight(80.0f); + male.setScaleUnit(Converters.WeightUnit.KG); + male.setActivityLevel(Converters.ActivityLevel.MILD); + male.setBodyHeight(180.0f); + male.setGoalWeight(60.0f); + male.setMeasureUnit(Converters.MeasureUnit.CM); + male.setBirthday(getDateFromYears(-20)); + male.setGoalDate(getDateFromYears(2)); + + return male; + } + + public static ScaleUser getFemaleUser() { + ScaleUser female = new ScaleUser(); + + female.setUserName("Alice"); + female.setGender(Converters.Gender.FEMALE); + female.setInitialWeight(70.0f); + female.setScaleUnit(Converters.WeightUnit.LB); + female.setActivityLevel(Converters.ActivityLevel.EXTREME); + female.setBodyHeight(160.0f); + female.setGoalWeight(50.0f); + female.setMeasureUnit(Converters.MeasureUnit.INCH); + female.setBirthday(getDateFromYears(-25)); + female.setGoalDate(getDateFromYears(1)); + + return female; + } + + public static ScaleMeasurement getMeasurement(int nr) { + ScaleMeasurement measurement = new ScaleMeasurement(); + + rand.setSeed(nr); + + measurement.setDateTime(getDateFromDays(nr)); + measurement.setWeight(100.0f + getRandNumberInRange(0,50)); + measurement.setFat(30.0f + getRandNumberInRange(0,30)); + measurement.setWater(50.0f + getRandNumberInRange(0,20)); + measurement.setMuscle(40.0f + getRandNumberInRange(0,15)); + measurement.setLbm(20.0f + getRandNumberInRange(0,10)); + measurement.setBone(8.0f + getRandNumberInRange(0,50)); + measurement.setWaist(50.0f + getRandNumberInRange(0,50)); + measurement.setHip(60.0f + getRandNumberInRange(0,50)); + measurement.setChest(80.0f + getRandNumberInRange(0,50)); + measurement.setThigh(40.0f + getRandNumberInRange(0,50)); + measurement.setBiceps(30.0f + getRandNumberInRange(0,50)); + measurement.setNeck(15.0f + getRandNumberInRange(0,50)); + measurement.setCaliper1(5.0f + getRandNumberInRange(0,10)); + measurement.setCaliper2(10.0f + getRandNumberInRange(0,10)); + measurement.setCaliper3(7.0f + getRandNumberInRange(0,10)); + measurement.setComment("my comment " + nr); + + return measurement; + } + + private static Date getDateFromYears(int years) { + Calendar currentTime = Calendar.getInstance(); + + currentTime.add(Calendar.YEAR, years); + + return currentTime.getTime(); + } + + private static Date getDateFromDays(int days) { + Calendar currentTime = Calendar.getInstance(); + + currentTime.add(Calendar.DAY_OF_YEAR, days); + + return currentTime.getTime(); + } + + private static float getRandNumberInRange(int min, int max) { + return (float)(rand.nextInt(max*10 - min*10) + min*10) / 10.0f; + } +} 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 29faecf0..f992f6c7 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 @@ -101,6 +101,10 @@ public abstract class BluetoothCommunication { return bluetoothGatt.getServices(); } + protected boolean hasBluetoothGattService(UUID service) { + return bluetoothGatt != null && bluetoothGatt.getService(service) != null; + } + /** * Register a callback Bluetooth handler that notify any BT_STATUS_CODE changes for GUI/CORE. * @@ -271,6 +275,7 @@ public abstract class BluetoothCommunication { BluetoothGattCharacteristic gattCharacteristic = bluetoothGatt.getService(service) .getCharacteristic(characteristic); + Timber.d("Read characteristic %s", characteristic); bluetoothGatt.readCharacteristic(gattCharacteristic); } @@ -278,6 +283,7 @@ public abstract class BluetoothCommunication { BluetoothGattDescriptor gattDescriptor = bluetoothGatt.getService(service) .getCharacteristic(characteristic).getDescriptor(descriptor); + Timber.d("Read descriptor %s", descriptor); bluetoothGatt.readDescriptor(gattDescriptor); } @@ -369,12 +375,16 @@ public abstract class BluetoothCommunication { return ""; } + if (data.length == 0) { + return ""; + } + final StringBuilder stringBuilder = new StringBuilder(3 * data.length); for (byte byteChar : data) { stringBuilder.append(String.format("%02X ", byteChar)); } - return stringBuilder.toString(); + return stringBuilder.substring(0, stringBuilder.length() - 1); } protected byte xorChecksum(byte[] data, int offset, int length) { @@ -654,7 +664,8 @@ public abstract class BluetoothCommunication { @Override public void onServicesDiscovered(final BluetoothGatt gatt, int status) { - Timber.d("onServicesDiscovered: status=%d", status); + Timber.d("onServicesDiscovered: status=%d (%d services)", + status, gatt.getServices().size()); synchronized (lock) { cmdStepNr = 0; @@ -681,24 +692,32 @@ public abstract class BluetoothCommunication { setBtMachineState(BT_MACHINE_STATE.BT_INIT_STATE); } + private void postDelayedHandleRequests() { + // Wait a short while before starting the next operation as suggested + // on the android.jlelse.eu link above. + handler.postDelayed(new Runnable() { + @Override + public void run() { + synchronized (lock) { + openRequest = false; + handleRequests(); + } + } + }, 60); + } + @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - synchronized (lock) { - openRequest = false; - handleRequests(); - } + postDelayedHandleRequests(); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - synchronized (lock) { - openRequest = false; - handleRequests(); - } + postDelayedHandleRequests(); } @Override @@ -710,8 +729,7 @@ public abstract class BluetoothCommunication { synchronized (lock) { onBluetoothDataRead(gatt, characteristic, status); - openRequest = false; - handleRequests(); + postDelayedHandleRequests(); } } @@ -733,10 +751,7 @@ public abstract class BluetoothCommunication { Timber.d("onDescriptorRead %s (status=%d): %s", descriptor.getUuid(), status, byteInHex(descriptor.getValue())); - synchronized (lock) { - openRequest = false; - handleRequests(); - } + postDelayedHandleRequests(); } } } 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 index 3ce553e4..6d54bfa2 100644 --- 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 @@ -156,8 +156,8 @@ public class BluetoothDebug extends BluetoothCommunication { logService(service, false); } - disconnect(false); setBtStatus(BT_STATUS_CODE.BT_CONNECTION_LOST); + disconnect(false); return false; } diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java index 55215fae..3a788da6 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/BluetoothOneByone.java @@ -31,7 +31,9 @@ import timber.log.Timber; public class BluetoothOneByone extends BluetoothCommunication { private final UUID WEIGHT_MEASUREMENT_SERVICE_BODY_COMPOSITION = UUID.fromString("0000181B-0000-1000-8000-00805f9b34fb"); + private final UUID WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION = UUID.fromString("00002A9C-0000-1000-8000-00805f9b34fb"); // read, indication + private final UUID WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION_ALT = UUID.fromString("0000fff4-0000-1000-8000-00805f9b34fb"); // notify private final UUID WEIGHT_MEASUREMENT_SERVICE = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb"); private final UUID CMD_MEASUREMENT_CHARACTERISTIC = UUID.fromString("0000fff1-0000-1000-8000-00805f9b34fb"); // write only @@ -53,7 +55,17 @@ public class BluetoothOneByone extends BluetoothCommunication { switch (stateNr) { case 0: lastWeight = 0; - setIndicationOn(WEIGHT_MEASUREMENT_SERVICE_BODY_COMPOSITION, WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION, WEIGHT_MEASUREMENT_CONFIG); + + if (hasBluetoothGattService(WEIGHT_MEASUREMENT_SERVICE_BODY_COMPOSITION)) { + setIndicationOn(WEIGHT_MEASUREMENT_SERVICE_BODY_COMPOSITION, + WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION, + WEIGHT_MEASUREMENT_CONFIG); + } + else { + setNotificationOn(WEIGHT_MEASUREMENT_SERVICE, + WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION_ALT, + WEIGHT_MEASUREMENT_CONFIG); + } break; case 1: ScaleUser currentUser = OpenScale.getInstance().getSelectedScaleUser(); @@ -94,11 +106,20 @@ public class BluetoothOneByone extends BluetoothCommunication { @Override public void onBluetoothDataChange(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic gattCharacteristic) { final byte[] data = gattCharacteristic.getValue(); + if (data == null) { + return; + } + final UUID uuid = gattCharacteristic.getUuid(); // if data is valid data - if (data != null && data.length == 20) { + if (data.length == 20 + && uuid.equals(WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION)) { parseBytes(data); } + else if (data.length == 11 + && uuid.equals(WEIGHT_MEASUREMENT_CHARACTERISTIC_BODY_COMPOSITION_ALT)) { + parseBytesAlt(data); + } } private void parseBytes(byte[] weightBytes) { @@ -117,4 +138,21 @@ public class BluetoothOneByone extends BluetoothCommunication { addScaleData(scaleBtData); } } + + private void parseBytesAlt(byte[] weightBytes) { + float weight = Converters.fromUnsignedInt16Le(weightBytes, 3) / 100.0f; + boolean done = (weightBytes[9] & 0xff) == 0; + + Timber.d("weight: %.2f%s", weight, done ? " (done)" : ""); + + // This check should be a bit more elaborate, but it works for now... + if (done && weight != lastWeight) { + lastWeight = weight; + + ScaleMeasurement scaleBtData = new ScaleMeasurement(); + scaleBtData.setWeight(weight); + + addScaleData(scaleBtData); + } + } } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java index 43d21dcc..954cf324 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/fragments/GraphFragment.java @@ -37,6 +37,7 @@ import android.widget.LinearLayout; import android.widget.PopupMenu; import android.widget.RelativeLayout; import android.widget.TextView; +import android.widget.Toast; import com.health.openscale.R; import com.health.openscale.core.OpenScale; @@ -321,9 +322,16 @@ public class GraphFragment extends Fragment implements FragmentUpdateListener { floatingActionBar.removeAllViews(); - PolynomialFitter polyFitter = new PolynomialFitter( - Math.min(Integer.parseInt(prefs.getString("regressionLineOrder", "1")), - 100)); + int regressLineOrder = 1; + + try { + regressLineOrder = Integer.parseInt(prefs.getString("regressionLineOrder", "1")); + } catch (NumberFormatException e) { + Toast.makeText(getContext(), getString(R.string.error_value_required) + ":" + e.getMessage(), Toast.LENGTH_LONG).show(); + prefs.edit().putString("regressionLineOrder", "1").commit(); + } + + PolynomialFitter polyFitter = new PolynomialFitter(Math.min(regressLineOrder, 100)); for (MeasurementView view : measurementViews) { if (view instanceof FloatMeasurementView) { 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 a2529f3e..bf3cf998 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 @@ -307,7 +307,7 @@ public class BluetoothPreferences extends PreferenceFragment { super.onStart(); // Restart discovery after e.g. orientation change - if (btScanner.getDialog() != null) { + if (btScanner.getDialog() != null && btScanner.getDialog().isShowing()) { startBluetoothDiscovery(); } } diff --git a/doc/scales/excelvan_cf366ble.jpg b/doc/scales/excelvan_cf366ble.jpg new file mode 100644 index 00000000..495eaa90 Binary files /dev/null and b/doc/scales/excelvan_cf366ble.jpg differ