From d0029e80abb6c4d928006a32d8c1b0b7ad1740ae Mon Sep 17 00:00:00 2001 From: Scott Gilroy Date: Thu, 20 Oct 2016 18:46:21 -0400 Subject: [PATCH 001/157] Support for "nutrition.calories" and "nutrition.carbohydrates" in `query()` [https://github.com/dariosalvi78/cordova-plugin-health/issues/17] --- src/android/HealthPlugin.java | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 852f7bfe..b546098a 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -23,6 +23,7 @@ import com.google.android.gms.fitness.data.DataSource; import com.google.android.gms.fitness.data.DataType; import com.google.android.gms.fitness.data.Field; +import com.google.android.gms.fitness.data.Value; import com.google.android.gms.fitness.request.DataReadRequest; import com.google.android.gms.fitness.request.DataTypeCreateRequest; import com.google.android.gms.fitness.result.DataReadResult; @@ -96,7 +97,8 @@ public class HealthPlugin extends CordovaPlugin { public static Map nutritiondatatypes = new HashMap(); static { - //nutritiondatatypes.put("food", DataType.TYPE_NUTRITION); + nutritiondatatypes.put("nutrition.calories", DataType.TYPE_NUTRITION); + nutritiondatatypes.put("nutrition.carbohydrates", DataType.TYPE_NUTRITION); } public static Map customdatatypes = new HashMap(); @@ -470,6 +472,24 @@ private void query(final JSONArray args, final CallbackContext callbackContext) float distance = datapoint.getValue(Field.FIELD_DISTANCE).asFloat(); obj.put("value", distance); obj.put("unit", "m"); + } else if (DT.equals(DataType.TYPE_NUTRITION)) { + Value nutrients = datapoint.getValue(Field.FIELD_NUTRIENTS); + String field = null, unit = null; + switch (datatype) { + case "nutrition.calories": + field = Field.NUTRIENT_CALORIES; + unit = "kcal"; + break; + case "nutrition.carbohydrates": + field = Field.NUTRIENT_TOTAL_CARBS; + unit = "g"; + break; + } + if (field != null) { + float value = nutrients.getKeyValue(field); + obj.put("value", value); + obj.put("unit", unit); + } } else if (DT.equals(DataType.TYPE_CALORIES_EXPENDED)) { float calories = datapoint.getValue(Field.FIELD_CALORIES).asFloat(); obj.put("value", calories); From 8cfd54e802ce6b186bf7014350a4a2dac1fd21a1 Mon Sep 17 00:00:00 2001 From: Scott Gilroy Date: Thu, 20 Oct 2016 22:06:29 -0400 Subject: [PATCH 002/157] Support for "nutrition.calories" and "nutrition.carbohydrates" in `queryAggregated()` [https://github.com/dariosalvi78/cordova-plugin-health/issues/17] --- src/android/HealthPlugin.java | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index b546098a..2708f690 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -648,6 +648,8 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac builder.aggregate(DataType.TYPE_BASAL_METABOLIC_RATE, DataType.AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY); } else if (datatype.equalsIgnoreCase("activity")) { builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); + } else if (nutritiondatatypes.get(datatype)) { + builder.aggregate(DataType.TYPE_NUTRITION, DataType.AGGREGATE_NUTRITION_SUMMARY); } else { callbackContext.error("Datatype " + datatype + " not supported"); return; @@ -740,6 +742,9 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("activity")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); + } else if (nutritiondatatypes.get(datatype)) { + // TODO: set the correct unit for each nutrition type + retBucket.put("unit", "kcal"); } } } @@ -764,6 +769,24 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac float ncal = datapoint.getValue(Field.FIELD_AVERAGE).asFloat(); double ocal = retBucket.getDouble("value"); retBucket.put("value", ocal + ncal); + } else if (nutritiondatatypes.get(datatype)) { + Value nutrients = datapoint.getValue(Field.FIELD_NUTRIENTS); + String field = null, unit = null; + switch (datatype) { + case "nutrition.calories": + field = Field.NUTRIENT_CALORIES; + unit = "kcal"; + break; + case "nutrition.carbohydrates": + field = Field.NUTRIENT_TOTAL_CARBS; + unit = "g"; + break; + } + if (field != null) { + float value = nutrients.getKeyValue(field); + double total = retBucket.getDouble("value"); + retBucket.put("value", total + value); + } } else if (datatype.equalsIgnoreCase("activity")) { String activity = datapoint.getValue(Field.FIELD_ACTIVITY).asActivity(); JSONObject actobj = retBucket.getJSONObject("value"); From 2b06ac059e3f769a95a60a59dd5f67cf289ab276 Mon Sep 17 00:00:00 2001 From: Scott Gilroy Date: Fri, 21 Oct 2016 00:21:03 -0400 Subject: [PATCH 003/157] Support all of the Google Fit nutrient fields for query and queryAggregated [https://github.com/dariosalvi78/cordova-plugin-health/issues/17] --- src/android/HealthPlugin.java | 86 +++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 2708f690..9baa5c35 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -93,12 +93,47 @@ public class HealthPlugin extends CordovaPlugin { locationdatatypes.put("distance", DataType.TYPE_DISTANCE_DELTA); } + private static class NutrientFieldInfo { + public String field; + public String unit; + + public NutrientFieldInfo(String field, String unit) { + this.field = field; + this.unit = unit; + } + } + + //Lookup for nutrition fields and units + public static Map nutrientFields = new HashMap<>(); + + static { + nutrientFields.put("nutrition.calories", new NutrientFieldInfo(Field.NUTRIENT_CALORIES, "kcal")); + nutrientFields.put("nutrition.fat.total", new NutrientFieldInfo(Field.NUTRIENT_TOTAL_FAT, "g")); + nutrientFields.put("nutrition.fat.saturated", new NutrientFieldInfo(Field.NUTRIENT_SATURATED_FAT, "g")); + nutrientFields.put("nutrition.fat.unsaturated", new NutrientFieldInfo(Field.NUTRIENT_UNSATURATED_FAT, "g")); + nutrientFields.put("nutrition.fat.polyunsaturated", new NutrientFieldInfo(Field.NUTRIENT_POLYUNSATURATED_FAT, "g")); + nutrientFields.put("nutrition.fat.monounsaturated", new NutrientFieldInfo(Field.NUTRIENT_MONOUNSATURATED_FAT, "g")); + nutrientFields.put("nutrition.fat.trans", new NutrientFieldInfo(Field.NUTRIENT_TRANS_FAT, "g")); + nutrientFields.put("nutrition.cholesterol", new NutrientFieldInfo(Field.NUTRIENT_CHOLESTEROL, "mg")); + nutrientFields.put("nutrition.sodium", new NutrientFieldInfo(Field.NUTRIENT_SODIUM, "mg")); + nutrientFields.put("nutrition.potassium", new NutrientFieldInfo(Field.NUTRIENT_POTASSIUM, "mg")); + nutrientFields.put("nutrition.carbs.total", new NutrientFieldInfo(Field.NUTRIENT_TOTAL_CARBS, "g")); + nutrientFields.put("nutrition.dietary_fiber", new NutrientFieldInfo(Field.NUTRIENT_DIETARY_FIBER, "g")); + nutrientFields.put("nutrition.sugar", new NutrientFieldInfo(Field.NUTRIENT_SUGAR, "g")); + nutrientFields.put("nutrition.protein", new NutrientFieldInfo(Field.NUTRIENT_PROTEIN, "g")); + nutrientFields.put("nutrition.vitamin_a", new NutrientFieldInfo(Field.NUTRIENT_VITAMIN_A, "IU")); + nutrientFields.put("nutrition.vitamin_c", new NutrientFieldInfo(Field.NUTRIENT_VITAMIN_C, "mg")); + nutrientFields.put("nutrition.calcium", new NutrientFieldInfo(Field.NUTRIENT_CALCIUM, "mg")); + nutrientFields.put("nutrition.iron", new NutrientFieldInfo(Field.NUTRIENT_IRON, "mg")); + } + //Scope for read/write access to nutrition data types in Google Fit. public static Map nutritiondatatypes = new HashMap(); static { - nutritiondatatypes.put("nutrition.calories", DataType.TYPE_NUTRITION); - nutritiondatatypes.put("nutrition.carbohydrates", DataType.TYPE_NUTRITION); + for (String dataType : nutrientFields.keySet()) { + nutritiondatatypes.put(dataType, DataType.TYPE_NUTRITION); + } } public static Map customdatatypes = new HashMap(); @@ -474,21 +509,10 @@ private void query(final JSONArray args, final CallbackContext callbackContext) obj.put("unit", "m"); } else if (DT.equals(DataType.TYPE_NUTRITION)) { Value nutrients = datapoint.getValue(Field.FIELD_NUTRIENTS); - String field = null, unit = null; - switch (datatype) { - case "nutrition.calories": - field = Field.NUTRIENT_CALORIES; - unit = "kcal"; - break; - case "nutrition.carbohydrates": - field = Field.NUTRIENT_TOTAL_CARBS; - unit = "g"; - break; - } - if (field != null) { - float value = nutrients.getKeyValue(field); - obj.put("value", value); - obj.put("unit", unit); + NutrientFieldInfo fieldInfo = nutrientFields.get(datatype); + if (fieldInfo != null) { + obj.put("value", (float) nutrients.getKeyValue(fieldInfo.field)); + obj.put("unit", fieldInfo.unit); } } else if (DT.equals(DataType.TYPE_CALORIES_EXPENDED)) { float calories = datapoint.getValue(Field.FIELD_CALORIES).asFloat(); @@ -648,7 +672,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac builder.aggregate(DataType.TYPE_BASAL_METABOLIC_RATE, DataType.AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY); } else if (datatype.equalsIgnoreCase("activity")) { builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); - } else if (nutritiondatatypes.get(datatype)) { + } else if (nutritiondatatypes.get(datatype) != null) { builder.aggregate(DataType.TYPE_NUTRITION, DataType.AGGREGATE_NUTRITION_SUMMARY); } else { callbackContext.error("Datatype " + datatype + " not supported"); @@ -742,9 +766,11 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("activity")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); - } else if (nutritiondatatypes.get(datatype)) { - // TODO: set the correct unit for each nutrition type - retBucket.put("unit", "kcal"); + } else if (nutritiondatatypes.get(datatype) != null) { + NutrientFieldInfo fieldInfo = nutrientFields.get(datatype); + if (fieldInfo != null) { + retBucket.put("unit", fieldInfo.unit); + } } } } @@ -769,21 +795,11 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac float ncal = datapoint.getValue(Field.FIELD_AVERAGE).asFloat(); double ocal = retBucket.getDouble("value"); retBucket.put("value", ocal + ncal); - } else if (nutritiondatatypes.get(datatype)) { + } else if (nutritiondatatypes.get(datatype) != null) { Value nutrients = datapoint.getValue(Field.FIELD_NUTRIENTS); - String field = null, unit = null; - switch (datatype) { - case "nutrition.calories": - field = Field.NUTRIENT_CALORIES; - unit = "kcal"; - break; - case "nutrition.carbohydrates": - field = Field.NUTRIENT_TOTAL_CARBS; - unit = "g"; - break; - } - if (field != null) { - float value = nutrients.getKeyValue(field); + NutrientFieldInfo fieldInfo = nutrientFields.get(datatype); + if (fieldInfo != null) { + float value = nutrients.getKeyValue(fieldInfo.field); double total = retBucket.getDouble("value"); retBucket.put("value", total + value); } From 33a6eeb4d31d410200d4864e827e866c1ff85ab8 Mon Sep 17 00:00:00 2001 From: Scott Gilroy Date: Fri, 21 Oct 2016 00:53:22 -0400 Subject: [PATCH 004/157] Remove use of Java 7 feature. [https://github.com/dariosalvi78/cordova-plugin-health/issues/17] --- src/android/HealthPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 9baa5c35..91a9ef0d 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -104,7 +104,7 @@ public NutrientFieldInfo(String field, String unit) { } //Lookup for nutrition fields and units - public static Map nutrientFields = new HashMap<>(); + public static Map nutrientFields = new HashMap(); static { nutrientFields.put("nutrition.calories", new NutrientFieldInfo(Field.NUTRIENT_CALORIES, "kcal")); From a0b9c0dae91130ec3a714d24937b15828e61804b Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 21 Oct 2016 11:47:35 +0100 Subject: [PATCH 005/157] better readme --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8d69cbd1..eb87f09e 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ navigator.health.query({ Quirks of query() - in Google Fit calories.basal is returned as an average per day, and usually is not available in all days (may be not available in time windows smaller than 5 days or more) -- in Google Fit calories.active is computed by subtracting the basal from the total, as basal an average of the a number of days before endDate is taken (the actual number is defined in a variable, currently set to 7) +- in Google Fit calories.active is computed by subtracting the basal from the total, as basal an average of the a number of days before endDate is taken (the actual number is 7) - while Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input - when querying for activities, Google Fit is able to determine some activities automatically, while HealthKit only relies on the input of the user or of some external app - when querying for activities, calories and distance are also provided in HealthKit (units are kcal and metres) and never in Google Fit @@ -177,6 +177,8 @@ Quirks of queryAggregated() - when querying for activities, calories and distance are provided when available in HealthKit and never in Google Fit - in Android, the start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. +- when bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00 +- weeks start on Monday ### store() @@ -207,17 +209,16 @@ Quirks of store() - in iOS you cannot store the total calories, you need to specify either basal or active. If you use total calories, the active ones will be stored. - in Android you can only store active calories, as the basal are estimated automatically. If you store total calories, these will be treated as active. - in iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. -- in iOS, storing the sleep activities is not supported at the moment. +- in iOS storing the sleep activities is not supported at the moment. ## Differences between HealthKit and Google Fit -* HealthKit includes medical data (eg blood glucose), Google Fit is currently only related to fitness data +* HealthKit includes medical data (eg blood glucose), Google Fit is only related to fitness data * HealthKit provides a data model that is not extensible, Google Fit allows defining custom data types -* HealthKit allows to insert data with the unit of measurement of your choice, and automatically translates units when quiered, Google Fit stores data with a fixed unit of measurement -* HealthKit automatically counts steps and distance when you carry your phone with you, Google Fit also detects the kind of activity (sedentary, running, walking, cycling, in vehicle) +* HealthKit allows to insert data with the unit of measurement of your choice, and automatically translates units when quiered, Google Fit uses fixed units of measurement +* HealthKit automatically counts steps and distance when you carry your phone with you and if your phone has the CoreMotion chip, Google Fit also detects the kind of activity (sedentary, running, walking, cycling, in vehicle) * HealthKit automatically computes the distance only for running/walking activities, Google Fit includes bicycle also - ## External Resources * The official Apple documentation for [HealthKit can be found here](https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Framework/index.html#//apple_ref/doc/uid/TP40014707). @@ -229,24 +230,24 @@ Quirks of store() short term -- add query with buckets (see window.plugins.healthkit.querySampleTypeAggregated for HealthKit) - add delete - get steps from the "polished" Google Fit data source (see https://plus.google.com/104895513165544578271/posts/a8P62A6ejQy) - add support for HKCategory samples in HealthKit - extend the datatypes - blood pressure (KCorrelationTypeIdentifierBloodPressure, custom data type) - - food (HKCorrelationTypeIdentifierFood, TYPE_NUTRITION) - blood glucose - location (NA, TYPE_LOCATION) long term - add registration to updates (in Fit: HistoryApi#registerDataUpdateListener() ) -- store vital signs on an encrypted DB in the case of Android and remove custom datatypes (possible choice: [sqlcipher](https://www.zetetic.net/sqlcipher/sqlcipher-for-android/). The file would be stored on shared drive, and it would be shared among apps through a service. You could more simply share the file, but then how would you share the password? If shared through a service, all apps would have the same service because it's part of the plugin, so the service should not auto-start until the first app tries to bind it (see [this](http://stackoverflow.com/questions/31506177/the-same-android-service-instance-for-two-apps) for suggestions). This is sub-optimal, as all apps would have the same copy of the service (although lightweight). A better approach would be requiring an extra app, but this creates other issues like "who would publish it?", "why the user would be needed to download another app?" etc. +- store vital signs on an encrypted DB in the case of Android and remove custom datatypes. Possible choice: [sqlcipher](https://www.zetetic.net/sqlcipher/sqlcipher-for-android/). The file would be stored on shared drive, and it would be shared among apps through a service. You could more simply share the file, but then how would you share the password? If shared through a service, all apps would have the same service because it's part of the plugin, so the service should not auto-start until the first app tries to bind it (see [this](http://stackoverflow.com/questions/31506177/the-same-android-service-instance-for-two-apps) for suggestions). This is sub-optimal, as all apps would have the same copy of the service (although lightweight). A better approach would be requiring an extra app, but this creates other issues like "who would publish it?", "why the user would be needed to download another app?" etc. - add also Samsung Health as a health record for Android ## Contributions Any help is more than welcome! -I cannot program in iOS, so I would particularly appreciate someone who can give me a hand. +I don't know Objectve C and I am not interested into learning it now, so I would particularly appreciate someone who can give me a hand with the iOS part. +Particularly, I'd like to allow support for HKCategory. +Also, I would love to know from you if the plugin is currently used in any app actually available online. Just send me an email to my_username at gmail.com From d0fd05d70974e0e3605d9ccb3add84642adc2866 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 18 Nov 2016 15:35:06 +0000 Subject: [PATCH 006/157] add type to readme --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eb87f09e..cb92cf24 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,27 @@ Google Fit is limited to fitness data and, for health, custom data types are def | fat_percentage | HKQuantityTypeIdentifierBodyFatPercentage (%) | TYPE_BODY_FAT_PERCENTAGE | | gender | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | - +| nutrition | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | +| nutrition.calories | HKQuantityTypeIdentifierDietaryEnergyConsumed (kcal) | TYPE_NUTRITION, NUTRIENT_CALORIES | +| nutrition.fat.total | HKQuantityTypeIdentifierDietaryFatTotal (g) | TYPE_NUTRITION, NUTRIENT_TOTAL_FAT | +| nutrition.fat.saturated | HKQuantityTypeIdentifierDietaryFatSaturated (g) | TYPE_NUTRITION, NUTRIENT_SATURATED_FAT | +| nutrition.fat.unsaturated | NA (g) | TYPE_NUTRITION, NUTRIENT_UNSATURATED_FAT | +| nutrition.fat.polyunsaturated | HKQuantityTypeIdentifierDietaryFatPolyunsaturated (g) | TYPE_NUTRITION, NUTRIENT_POLYUNSATURATED_FAT | +| nutrition.fat.monounsaturated | HKQuantityTypeIdentifierDietaryFatMonounsaturated (g) | TYPE_NUTRITION, NUTRIENT_MONOUNSATURATED_FAT | +| nutrition.fat.trans | NA (g) | TYPE_NUTRITION, NUTRIENT_TRANS_FAT | +| nutrition.cholesterol | HKQuantityTypeIdentifierDietaryCholesterol (mg) | TYPE_NUTRITION, NUTRIENT_CHOLESTEROL | +| nutrition.sodium | HKQuantityTypeIdentifierDietarySodium (mg) | TYPE_NUTRITION, NUTRIENT_SODIUM | +| nutrition.potassium | HKQuantityTypeIdentifierDietaryPotassium (mg) | TYPE_NUTRITION, NUTRIENT_POTASSIUM | +| nutrition.carbs.total | HKQuantityTypeIdentifierDietaryCarbohydrates (g) | TYPE_NUTRITION, NUTRIENT_TOTAL_CARBS | +| nutrition.dietary_fiber | HKQuantityTypeIdentifierDietaryFiber (g) | TYPE_NUTRITION, NUTRIENT_DIETARY_FIBER | +| nutrition.sugar | HKQuantityTypeIdentifierDietarySugar (g) | TYPE_NUTRITION, NUTRIENT_SUGAR | +| nutrition.protein | HKQuantityTypeIdentifierDietaryProtein (g) | TYPE_NUTRITION, NUTRIENT_PROTEIN | +| nutrition.vitamin_a | HKQuantityTypeIdentifierDietaryVitaminA (IU) | TYPE_NUTRITION, NUTRIENT_VITAMIN_A | +| nutrition.vitamin_c | HKQuantityTypeIdentifierDietaryVitaminC (mg) | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | +| nutrition.calcium | HKQuantityTypeIdentifierDietaryCalcium (mg) | TYPE_NUTRITION, NUTRIENT_CALCIUM | +| nutrition.iron | HKQuantityTypeIdentifierDietaryIron (mg) | TYPE_NUTRITION, NUTRIENT_IRON | +| nutrition.water | HKQuantityTypeIdentifierDietaryWater (g) | NA | +| nutrition.caffeine | HKQuantityTypeIdentifierDietaryCaffeine (g) | NA | Note: units of measurements are fixed ! From 481321ac0a3b074061ec1c2c11fccac909fd265d Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 18 Nov 2016 17:24:21 +0000 Subject: [PATCH 007/157] adding query of nutrition --- README.md | 7 ++- src/android/HealthPlugin.java | 97 +++++++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cb92cf24..e8bede36 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | nutrition.vitamin_c | HKQuantityTypeIdentifierDietaryVitaminC (mg) | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | | nutrition.calcium | HKQuantityTypeIdentifierDietaryCalcium (mg) | TYPE_NUTRITION, NUTRIENT_CALCIUM | | nutrition.iron | HKQuantityTypeIdentifierDietaryIron (mg) | TYPE_NUTRITION, NUTRIENT_IRON | -| nutrition.water | HKQuantityTypeIdentifierDietaryWater (g) | NA | +| nutrition.water | HKQuantityTypeIdentifierDietaryWater (l) | TYPE_HYDRATION | | nutrition.caffeine | HKQuantityTypeIdentifierDietaryCaffeine (g) | NA | Note: units of measurements are fixed ! @@ -90,7 +90,8 @@ Returned objects can be of different types, see examples below: | fat_percentage | 31.2 | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | - +| nutrition | { item: "cheese", meal_type: "lunch", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } } | +| nutrition.X | 12.4 | ## Methods @@ -192,6 +193,8 @@ The following table shows what types are supported and examples of aggregated da | calories.active | { startDate: Date, endDate: Date, value: 3547.4, unit: 'kcal' } | | calories.basal | { startDate: Date, endDate: Date, value: 13146.1, unit: 'kcal' } | | activity | { startDate: Date, endDate: Date, value: { still: { duration: 520000, calories: 30, distance: 0 }, walking: { duration: 223000, calories: 20, distance: 15 }}, unit: 'activitySummary' } (note: duration is expressed in milliseconds, distance in metres and calories in kcal) | +| nutrition | { startDate: Date, endDate: Date, value: {}, unit: 'nutrition' } | +| nutrition.X | { startDate: Date, endDate: Date, value: 23, unit: 'mg'} | Quirks of queryAggregated() diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 91a9ef0d..19a0f341 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -131,6 +131,8 @@ public NutrientFieldInfo(String field, String unit) { public static Map nutritiondatatypes = new HashMap(); static { + nutritiondatatypes.put("nutrition", DataType.TYPE_NUTRITION); + nutritiondatatypes.put("nutrition.water", DataType.TYPE_HYDRATION); for (String dataType : nutrientFields.keySet()) { nutritiondatatypes.put(dataType, DataType.TYPE_NUTRITION); } @@ -507,12 +509,37 @@ private void query(final JSONArray args, final CallbackContext callbackContext) float distance = datapoint.getValue(Field.FIELD_DISTANCE).asFloat(); obj.put("value", distance); obj.put("unit", "m"); + } else if (DT.equals(DataType.TYPE_HYDRATION)) { + float distance = datapoint.getValue(Field.FIELD_VOLUME).asFloat(); + obj.put("value", distance); + obj.put("unit", "l"); } else if (DT.equals(DataType.TYPE_NUTRITION)) { - Value nutrients = datapoint.getValue(Field.FIELD_NUTRIENTS); - NutrientFieldInfo fieldInfo = nutrientFields.get(datatype); - if (fieldInfo != null) { - obj.put("value", (float) nutrients.getKeyValue(fieldInfo.field)); - obj.put("unit", fieldInfo.unit); + if(datatype.equalsIgnoreCase("nutrition")) { + JSONObject dob = new JSONObject(); + if(datapoint.getValue(Field.FIELD_FOOD_ITEM) != null){ + dob.put("item", datapoint.getValue(Field.FIELD_FOOD_ITEM).asString()); + } + if(datapoint.getValue(Field.FIELD_MEAL_TYPE) != null){ + int mealt = datapoint.getValue(Field.FIELD_MEAL_TYPE).asInt(); + if(mealt == Field.MEAL_TYPE_BREAKFAST) dob.put("meal_type", "breakfast"); + else if(mealt == Field.MEAL_TYPE_DINNER) dob.put("meal_type", "dinner"); + else if(mealt == Field.MEAL_TYPE_LUNCH) dob.put("meal_type", "lunch"); + else if(mealt == Field.MEAL_TYPE_SNACK) dob.put("meal_type", "snack"); + else dob.put("meal_type", "unknown"); + } + if(datapoint.getValue(Field.FIELD_NUTRIENTS) != null){ + Value v = datapoint.getValue(Field.FIELD_NUTRIENTS); + dob.put("nutrients", getNutrients(v, null)); + } + obj.put("value", dob); + obj.put("unit", "nutrition"); + } else { + Value nutrients = datapoint.getValue(Field.FIELD_NUTRIENTS); + NutrientFieldInfo fieldInfo = nutrientFields.get(datatype); + if (fieldInfo != null) { + obj.put("value", (float) nutrients.getKeyValue(fieldInfo.field)); + obj.put("unit", fieldInfo.unit); + } } } else if (DT.equals(DataType.TYPE_CALORIES_EXPENDED)) { float calories = datapoint.getValue(Field.FIELD_CALORIES).asFloat(); @@ -567,6 +594,54 @@ private void query(final JSONArray args, final CallbackContext callbackContext) } } + private JSONObject getNutrients(Value nutrientsMap, JSONObject mergewith) throws JSONException { + JSONObject nutrients; + if(mergewith != null){ + nutrients = mergewith; + } else { + nutrients = new JSONObject(); + } + mergeNutrient(Field.NUTRIENT_CALORIES, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_TOTAL_FAT, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_SATURATED_FAT, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_UNSATURATED_FAT, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_POLYUNSATURATED_FAT, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_MONOUNSATURATED_FAT, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_TRANS_FAT, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_CHOLESTEROL, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_SODIUM, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_POTASSIUM, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_TOTAL_CARBS, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_DIETARY_FIBER, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_SUGAR, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_PROTEIN, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_VITAMIN_A, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_VITAMIN_C, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_CALCIUM, nutrientsMap, nutrients); + mergeNutrient(Field.NUTRIENT_IRON, nutrientsMap, nutrients); + + return nutrients; + } + + private void mergeNutrient(String f, Value nutrientsMap, JSONObject nutrients) throws JSONException { + if(nutrientsMap.getKeyValue(f) != null) { + String n = null; + for(String name : nutrientFields.keySet()){ + if(nutrientFields.get(name).field.equalsIgnoreCase(f)){ + n = name; + break; + } + } + if(n != null) { + float val = nutrientsMap.getKeyValue(f); + if(nutrients.has(n)) { + val += nutrients.getDouble(n); + } + nutrients.put(n, val); + } + } + } + private void queryAggregated(final JSONArray args, final CallbackContext callbackContext) throws JSONException { if (!args.getJSONObject(0).has("startDate")) { callbackContext.error("Missing argument startDate"); @@ -732,6 +807,12 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("activity")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); + } else if(datatype.equalsIgnoreCase("nutrition")) { + retBucket.put("value", new JSONObject()); + retBucket.put("unit", "nutrition"); + } else if (nutritiondatatypes.get(datatype) != null) { + retBucket.put("value", new JSONObject()); + retBucket.put("unit", nutrientFields.get(datatype).unit); } } @@ -795,6 +876,12 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac float ncal = datapoint.getValue(Field.FIELD_AVERAGE).asFloat(); double ocal = retBucket.getDouble("value"); retBucket.put("value", ocal + ncal); + } else if(datatype.equalsIgnoreCase("nutrition")) { + JSONObject nutrsob = retBucket.getJSONObject("value"); + if(datapoint.getValue(Field.FIELD_NUTRIENTS) != null){ + nutrsob = getNutrients(datapoint.getValue(Field.FIELD_NUTRIENTS), nutrsob); + } + retBucket.put("value", nutrsob); } else if (nutritiondatatypes.get(datatype) != null) { Value nutrients = datapoint.getValue(Field.FIELD_NUTRIENTS); NutrientFieldInfo fieldInfo = nutrientFields.get(datatype); From e259093ae364436cd55c76f575c9e630e26a79af Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 21 Nov 2016 12:06:27 +0000 Subject: [PATCH 008/157] fixed minor bugs in query and query aggregated --- src/android/HealthPlugin.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 19a0f341..13dbd9c9 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -811,7 +811,6 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac retBucket.put("value", new JSONObject()); retBucket.put("unit", "nutrition"); } else if (nutritiondatatypes.get(datatype) != null) { - retBucket.put("value", new JSONObject()); retBucket.put("unit", nutrientFields.get(datatype).unit); } } @@ -847,6 +846,9 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("activity")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); + } else if(datatype.equalsIgnoreCase("nutrition")) { + retBucket.put("value", new JSONObject()); + retBucket.put("unit", "nutrition"); } else if (nutritiondatatypes.get(datatype) != null) { NutrientFieldInfo fieldInfo = nutrientFields.get(datatype); if (fieldInfo != null) { From cc99dfba98b7c56e03b6044fb063eae79b650745 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 21 Nov 2016 15:16:53 +0000 Subject: [PATCH 009/157] added water --- src/android/HealthPlugin.java | 14 ++++++++++++-- www/android/health.js | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 13dbd9c9..c87b53cb 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -512,7 +512,7 @@ private void query(final JSONArray args, final CallbackContext callbackContext) } else if (DT.equals(DataType.TYPE_HYDRATION)) { float distance = datapoint.getValue(Field.FIELD_VOLUME).asFloat(); obj.put("value", distance); - obj.put("unit", "l"); + obj.put("unit", "ml");// documentation says it's litres, but from experiments I get ml } else if (DT.equals(DataType.TYPE_NUTRITION)) { if(datatype.equalsIgnoreCase("nutrition")) { JSONObject dob = new JSONObject(); @@ -747,6 +747,8 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac builder.aggregate(DataType.TYPE_BASAL_METABOLIC_RATE, DataType.AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY); } else if (datatype.equalsIgnoreCase("activity")) { builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); + } else if (datatype.equalsIgnoreCase("nutrition.water")) { + builder.aggregate(DataType.TYPE_HYDRATION, DataType.AGGREGATE_HYDRATION); } else if (nutritiondatatypes.get(datatype) != null) { builder.aggregate(DataType.TYPE_NUTRITION, DataType.AGGREGATE_NUTRITION_SUMMARY); } else { @@ -807,6 +809,8 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("activity")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); + } else if (datatype.equalsIgnoreCase("nutrition.water")) { + retBucket.put("unit", "ml"); } else if(datatype.equalsIgnoreCase("nutrition")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "nutrition"); @@ -846,6 +850,8 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("activity")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); + } else if (datatype.equalsIgnoreCase("nutrition.water")) { + retBucket.put("unit", "ml"); } else if(datatype.equalsIgnoreCase("nutrition")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "nutrition"); @@ -878,7 +884,11 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac float ncal = datapoint.getValue(Field.FIELD_AVERAGE).asFloat(); double ocal = retBucket.getDouble("value"); retBucket.put("value", ocal + ncal); - } else if(datatype.equalsIgnoreCase("nutrition")) { + } else if(datatype.equalsIgnoreCase("nutrition.water")) { + float nwat = datapoint.getValue(Field.FIELD_VOLUME).asFloat(); + double owat = retBucket.getDouble("value"); + retBucket.put("value", owat + nwat); + } else if(datatype.equalsIgnoreCase("nutrition")) { JSONObject nutrsob = retBucket.getJSONObject("value"); if(datapoint.getValue(Field.FIELD_NUTRIENTS) != null){ nutrsob = getNutrients(datapoint.getValue(Field.FIELD_NUTRIENTS), nutrsob); diff --git a/www/android/health.js b/www/android/health.js index fb7adb4b..81e267b8 100644 --- a/www/android/health.js +++ b/www/android/health.js @@ -48,7 +48,25 @@ Health.prototype.query = function (opts, onSuccess, onError) { data[i].startDate = new Date(data[i].startDate); data[i].endDate = new Date(data[i].endDate); } - onSuccess(data); + // if nutrition, add water + if(opts.dataType == 'nutrition'){ + opts.dataType ='nutrition.water'; + navigator.health.query(opts, function(water){ + // merge and sort + for(var i=0; i Date: Mon, 21 Nov 2016 15:21:27 +0000 Subject: [PATCH 010/157] added store of nutrition in roadmap --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e8bede36..2265a66e 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ Quirks of store() short term +- add store of nutrition - add delete - get steps from the "polished" Google Fit data source (see https://plus.google.com/104895513165544578271/posts/a8P62A6ejQy) - add support for HKCategory samples in HealthKit From 44577c48ebdbe29550160f59dd0bca29d725540d Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 21 Nov 2016 16:06:23 +0000 Subject: [PATCH 011/157] test commit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2265a66e..126f16f9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Cordova Health Plugin + A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit. This work is based on [cordova plugin googlefit](https://github.com/2dvisio/cordova-plugin-googlefit) and on [cordova healthkit plugin](https://github.com/Telerik-Verified-Plugins/HealthKit) From a6a6e6eaf60e9cce5ccb00a66b1c01bb1a8c87d8 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 21 Nov 2016 17:36:40 +0000 Subject: [PATCH 012/157] adding nutrition.X to HK except nutrition --- README.md | 2 +- www/ios/health.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 126f16f9..ffd96b78 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | nutrition.vitamin_c | HKQuantityTypeIdentifierDietaryVitaminC (mg) | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | | nutrition.calcium | HKQuantityTypeIdentifierDietaryCalcium (mg) | TYPE_NUTRITION, NUTRIENT_CALCIUM | | nutrition.iron | HKQuantityTypeIdentifierDietaryIron (mg) | TYPE_NUTRITION, NUTRIENT_IRON | -| nutrition.water | HKQuantityTypeIdentifierDietaryWater (l) | TYPE_HYDRATION | +| nutrition.water | HKQuantityTypeIdentifierDietaryWater (ml) | TYPE_HYDRATION | | nutrition.caffeine | HKQuantityTypeIdentifierDietaryCaffeine (g) | NA | Note: units of measurements are fixed ! diff --git a/www/ios/health.js b/www/ios/health.js index 613e2602..dc86a804 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -15,6 +15,23 @@ dataTypes['weight'] = 'HKQuantityTypeIdentifierBodyMass'; dataTypes['heart_rate'] = 'HKQuantityTypeIdentifierHeartRate'; dataTypes['fat_percentage'] = 'HKQuantityTypeIdentifierBodyFatPercentage'; dataTypes['activity'] = 'HKWorkoutTypeIdentifier'; // and HKCategoryTypeIdentifierSleepAnalysis +dataTypes['nutrition.calories'] = 'HKQuantityTypeIdentifierDietaryEnergyConsumed'; +dataTypes['nutrition.fat.total'] = 'HKQuantityTypeIdentifierDietaryFatTotal'; +dataTypes['nutrition.fat.polyunsaturated'] = 'HKQuantityTypeIdentifierDietaryFatPolyunsaturated'; +dataTypes['nutrition.fat.monounsaturated'] = 'HKQuantityTypeIdentifierDietaryFatMonounsaturated'; +dataTypes['nutrition.cholesterol'] = 'HKQuantityTypeIdentifierDietaryCholesterol'; +dataTypes['nutrition.sodium'] = 'HKQuantityTypeIdentifierDietarySodium'; +dataTypes['nutrition.potassium'] = 'HKQuantityTypeIdentifierDietaryPotassium'; +dataTypes['nutrition.carbs.total'] = 'HKQuantityTypeIdentifierDietaryCarbohydrates'; +dataTypes['nutrition.dietary_fiber'] = 'HKQuantityTypeIdentifierDietaryFiber'; +dataTypes['nutrition.sugar'] = 'HKQuantityTypeIdentifierDietarySugar'; +dataTypes['nutrition.protein'] = 'HKQuantityTypeIdentifierDietaryProtein'; +dataTypes['nutrition.vitamin_a'] = 'HKQuantityTypeIdentifierDietaryVitaminA'; +dataTypes['nutrition.vitamin_c'] = 'HKQuantityTypeIdentifierDietaryVitaminC'; +dataTypes['nutrition.calcium'] = 'HKQuantityTypeIdentifierDietaryCalcium'; +dataTypes['nutrition.iron'] = 'HKQuantityTypeIdentifierDietaryIron'; +dataTypes['nutrition.water'] = 'HKQuantityTypeIdentifierDietaryWater'; +dataTypes['nutrition.caffeine'] = 'HKQuantityTypeIdentifierDietaryCaffeine'; var units = []; units['steps'] = 'count'; @@ -26,6 +43,25 @@ units['height'] = 'm'; units['weight'] = 'kg'; units['heart_rate'] = 'count/min'; units['fat_percentage'] = '%'; +units['nutrition.calories'] = 'kcal'; +units['nutrition.fat.total'] = 'g'; +units['nutrition.fat.saturated'] = 'g'; +units['nutrition.fat.polyunsaturated'] = 'g'; +units['nutrition.fat.monounsaturated'] = 'g'; +units['nutrition.cholesterol'] = 'mg'; +units['nutrition.sodium'] = 'mg'; +units['nutrition.potassium'] = 'mg'; +units['nutrition.carbs.total'] = 'g'; +units['nutrition.dietary_fiber'] = 'g'; +units['nutrition.sugar'] = 'g'; +units['nutrition.protein'] = 'g'; +units['nutrition.vitamin_a'] = 'ug'; // should be IU!! +units['nutrition.vitamin_c'] = 'mg'; +units['nutrition.calcium'] = 'mg'; +units['nutrition.iron'] = 'mg'; +units['nutrition.water'] = 'ml'; +units['nutrition.caffeine'] = 'g'; + Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); From bc424fd2d2c4a26e981d2819f1f3f987a0911605 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 22 Nov 2016 12:14:52 +0000 Subject: [PATCH 013/157] added support for nutrition in query for iOS --- README.md | 5 +++-- src/ios/HealthKit.m | 45 +++++++++++++++++++++++++++------------- www/ios/health.js | 50 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ffd96b78..cfbb32be 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | nutrition.dietary_fiber | HKQuantityTypeIdentifierDietaryFiber (g) | TYPE_NUTRITION, NUTRIENT_DIETARY_FIBER | | nutrition.sugar | HKQuantityTypeIdentifierDietarySugar (g) | TYPE_NUTRITION, NUTRIENT_SUGAR | | nutrition.protein | HKQuantityTypeIdentifierDietaryProtein (g) | TYPE_NUTRITION, NUTRIENT_PROTEIN | -| nutrition.vitamin_a | HKQuantityTypeIdentifierDietaryVitaminA (IU) | TYPE_NUTRITION, NUTRIENT_VITAMIN_A | +| nutrition.vitamin_a | HKQuantityTypeIdentifierDietaryVitaminA (mcg) | TYPE_NUTRITION, NUTRIENT_VITAMIN_A | | nutrition.vitamin_c | HKQuantityTypeIdentifierDietaryVitaminC (mg) | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | | nutrition.calcium | HKQuantityTypeIdentifierDietaryCalcium (mg) | TYPE_NUTRITION, NUTRIENT_CALCIUM | | nutrition.iron | HKQuantityTypeIdentifierDietaryIron (mg) | TYPE_NUTRITION, NUTRIENT_IRON | @@ -160,7 +160,7 @@ Quirks of query() - while Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input - when querying for activities, Google Fit is able to determine some activities automatically, while HealthKit only relies on the input of the user or of some external app - when querying for activities, calories and distance are also provided in HealthKit (units are kcal and metres) and never in Google Fit - +- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. As the conversion is not simple, it's not done automatically. ### queryAggregated() @@ -203,6 +203,7 @@ Quirks of queryAggregated() - in Android, the start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. - when bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00 - weeks start on Monday +- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. ### store() diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index ed32754b..38b7eb5a 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -16,6 +16,7 @@ static NSString *const HKPluginKeySampleType = @"sampleType"; static NSString *const HKPluginKeyAggregation = @"aggregation"; static NSString *const HKPluginKeyUnit = @"unit"; +static NSString *const HKPluginKeyUnits = @"units"; static NSString *const HKPluginKeyAmount = @"amount"; static NSString *const HKPluginKeyValue = @"value"; static NSString *const HKPluginKeyCorrelationType = @"correlationType"; @@ -1522,14 +1523,19 @@ - (void)queryCorrelationType:(CDVInvokedUrlCommand *)command { NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:[args[HKPluginKeyStartDate] longValue]]; NSDate *endDate = [NSDate dateWithTimeIntervalSince1970:[args[HKPluginKeyEndDate] longValue]]; NSString *correlationTypeString = args[HKPluginKeyCorrelationType]; - NSString *unitString = args[HKPluginKeyUnit]; + NSArray *unitsString = args[HKPluginKeyUnits]; HKCorrelationType *type = (HKCorrelationType *) [HealthKit getHKSampleType:correlationTypeString]; if (type == nil) { [HealthKit triggerErrorCallbackWithMessage:@"sampleType was invalid" command:command delegate:self.commandDelegate]; return; } - HKUnit *unit = ((unitString != nil) ? [HKUnit unitFromString:unitString] : nil); + NSMutableArray *units = [[NSMutableArray alloc] init]; + for (NSString *unitString in unitsString) { + HKUnit *unit = ((unitString != nil) ? [HKUnit unitFromString:unitString] : nil); + [units addObject:unit]; + } + // TODO check that unit is compatible with sampleType if sample type of HKQuantityType NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate]; @@ -1572,18 +1578,22 @@ - (void)queryCorrelationType:(CDVInvokedUrlCommand *)command { NSMutableArray *samples = [NSMutableArray arrayWithCapacity:correlation.objects.count]; for (HKQuantitySample *quantitySample in correlation.objects) { - // if an incompatible unit was passed, the sample is not included - if ([quantitySample.quantity isCompatibleWithUnit:unit]) { - [samples addObject:@{ - HKPluginKeyStartDate: [HealthKit stringFromDate:quantitySample.startDate], - HKPluginKeyEndDate: [HealthKit stringFromDate:quantitySample.endDate], - HKPluginKeySampleType: quantitySample.sampleType.identifier, - HKPluginKeyValue: @([quantitySample.quantity doubleValueForUnit:unit]), - HKPluginKeyUnit: unit.unitString, - HKPluginKeyMetadata: ((quantitySample.metadata != nil) ? quantitySample.metadata : @{}), - HKPluginKeyUUID: quantitySample.UUID.UUIDString + for (int i=0; i<[units count]; i++) { + HKUnit *unit = units[i]; + NSString *unitS = unitsString[i]; + if ([quantitySample.quantity isCompatibleWithUnit:unit]) { + [samples addObject:@{ + HKPluginKeyStartDate: [HealthKit stringFromDate:quantitySample.startDate], + HKPluginKeyEndDate: [HealthKit stringFromDate:quantitySample.endDate], + HKPluginKeySampleType: quantitySample.sampleType.identifier, + HKPluginKeyValue: @([quantitySample.quantity doubleValueForUnit:unit]), + HKPluginKeyUnit: unitS, + HKPluginKeyMetadata: ((quantitySample.metadata != nil) ? quantitySample.metadata : @{}), + HKPluginKeyUUID: quantitySample.UUID.UUIDString + } + ]; + break; } - ]; } } entry[HKPluginKeyObjects] = samples; @@ -1591,7 +1601,14 @@ - (void)queryCorrelationType:(CDVInvokedUrlCommand *)command { } else if ([sample isKindOfClass:[HKQuantitySample class]]) { HKQuantitySample *qsample = (HKQuantitySample *) sample; - entry[@"quantity"] = @([qsample.quantity doubleValueForUnit:unit]); + for (int i=0; i<[units count]; i++) { + HKUnit *unit = units[i]; + if ([qsample.quantity isCompatibleWithUnit:unit]) { + double quantity = [qsample.quantity doubleValueForUnit:unit]; + entry[@"quantity"] = [NSString stringWithFormat:@"%f", quantity]; + break; + } + } } else if ([sample isKindOfClass:[HKWorkout class]]) { diff --git a/www/ios/health.js b/www/ios/health.js index dc86a804..daf7798c 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -55,14 +55,13 @@ units['nutrition.carbs.total'] = 'g'; units['nutrition.dietary_fiber'] = 'g'; units['nutrition.sugar'] = 'g'; units['nutrition.protein'] = 'g'; -units['nutrition.vitamin_a'] = 'ug'; // should be IU!! +units['nutrition.vitamin_a'] = 'mcg'; units['nutrition.vitamin_c'] = 'mg'; units['nutrition.calcium'] = 'mg'; units['nutrition.iron'] = 'mg'; units['nutrition.water'] = 'ml'; units['nutrition.caffeine'] = 'g'; - Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); }; @@ -71,7 +70,12 @@ Health.prototype.requestAuthorization = function (dts, onSuccess, onError) { var HKdatatypes = []; for (var i = 0; i < dts.length; i++) { if ((dts[i] !== 'gender') && (dts[i] !== 'date_of_birth')) { // ignore gender and DOB - if (dataTypes[dts[i]]) { + if(dts[i] === 'nutrition') { + // add all nutrition stuff + for(var datatype in dataTypes){ + if (datatype.startsWith('nutrition')) HKdatatypes.push(dataTypes[datatype]); + } + } else if (dataTypes[dts[i]]) { HKdatatypes.push(dataTypes[dts[i]]); if (dts[i] === 'distance') HKdatatypes.push('HKQuantityTypeIdentifierDistanceCycling'); if (dts[i] === 'activity') HKdatatypes.push('HKCategoryTypeIdentifierSleepAnalysis'); @@ -157,6 +161,46 @@ Health.prototype.query = function (opts, onSuccess, onError) { onSuccess(result); }, onError); }, onError); + } else if (opts.dataType === 'nutrition') { + + var convertFromGrams = function(toUnit, q) { + if(toUnit == 'mcg') return q * 1000000; + if(toUnit == 'mg') return q * 1000; + if(toUnit == 'g') return q; + if(toUnit == 'kg') return q / 1000; + return q; + } + + var result = []; + window.plugins.healthkit.queryCorrelationType({ + startDate: opts.startDate, + endDate: opts.endDate, + correlationType: 'HKCorrelationTypeIdentifierFood', + units: ['g', 'ml', 'kcal'] + }, function (data) { + for(var i=0; i Date: Tue, 22 Nov 2016 17:37:18 +0000 Subject: [PATCH 014/157] added support for aggregated nutrients plus some simplifications of the code --- README.md | 6 +- src/ios/HealthKit.m | 2 + www/ios/health.js | 426 ++++++++++++++++++++++++-------------------- 3 files changed, 236 insertions(+), 198 deletions(-) diff --git a/README.md b/README.md index cfbb32be..67d2e170 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ Quirks of query() - while Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input - when querying for activities, Google Fit is able to determine some activities automatically, while HealthKit only relies on the input of the user or of some external app - when querying for activities, calories and distance are also provided in HealthKit (units are kcal and metres) and never in Google Fit -- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. As the conversion is not simple, it's not done automatically. +- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. The conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). ### queryAggregated() @@ -194,7 +194,7 @@ The following table shows what types are supported and examples of aggregated da | calories.active | { startDate: Date, endDate: Date, value: 3547.4, unit: 'kcal' } | | calories.basal | { startDate: Date, endDate: Date, value: 13146.1, unit: 'kcal' } | | activity | { startDate: Date, endDate: Date, value: { still: { duration: 520000, calories: 30, distance: 0 }, walking: { duration: 223000, calories: 20, distance: 15 }}, unit: 'activitySummary' } (note: duration is expressed in milliseconds, distance in metres and calories in kcal) | -| nutrition | { startDate: Date, endDate: Date, value: {}, unit: 'nutrition' } | +| nutrition | { startDate: Date, endDate: Date, value: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 }, unit: 'nutrition' } | | nutrition.X | { startDate: Date, endDate: Date, value: 23, unit: 'mg'} | Quirks of queryAggregated() @@ -203,7 +203,7 @@ Quirks of queryAggregated() - in Android, the start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. - when bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00 - weeks start on Monday -- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. +- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. The conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). ### store() diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index 38b7eb5a..26d16c62 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -1557,6 +1557,8 @@ - (void)queryCorrelationType:(CDVInvokedUrlCommand *)command { // common indices entry[HKPluginKeyUUID] = sample.UUID.UUIDString; + entry[HKPluginKeySourceName] = sample.source.name; + entry[HKPluginKeySourceBundleId] = sample.source.bundleIdentifier; if (sample.metadata == nil || ![NSJSONSerialization isValidJSONObject:sample.metadata]) { entry[HKPluginKeyMetadata] = @{}; } else { diff --git a/www/ios/health.js b/www/ios/health.js index daf7798c..74dbd112 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -15,6 +15,7 @@ dataTypes['weight'] = 'HKQuantityTypeIdentifierBodyMass'; dataTypes['heart_rate'] = 'HKQuantityTypeIdentifierHeartRate'; dataTypes['fat_percentage'] = 'HKQuantityTypeIdentifierBodyFatPercentage'; dataTypes['activity'] = 'HKWorkoutTypeIdentifier'; // and HKCategoryTypeIdentifierSleepAnalysis +dataTypes['nutrition'] = 'HKCorrelationTypeIdentifierFood'; dataTypes['nutrition.calories'] = 'HKQuantityTypeIdentifierDietaryEnergyConsumed'; dataTypes['nutrition.fat.total'] = 'HKQuantityTypeIdentifierDietaryFatTotal'; dataTypes['nutrition.fat.polyunsaturated'] = 'HKQuantityTypeIdentifierDietaryFatPolyunsaturated'; @@ -43,6 +44,7 @@ units['height'] = 'm'; units['weight'] = 'kg'; units['heart_rate'] = 'count/min'; units['fat_percentage'] = '%'; +units['nutrition'] = 'nutrition'; units['nutrition.calories'] = 'kcal'; units['nutrition.fat.total'] = 'g'; units['nutrition.fat.saturated'] = 'g'; @@ -162,45 +164,18 @@ Health.prototype.query = function (opts, onSuccess, onError) { }, onError); }, onError); } else if (opts.dataType === 'nutrition') { - - var convertFromGrams = function(toUnit, q) { - if(toUnit == 'mcg') return q * 1000000; - if(toUnit == 'mg') return q * 1000; - if(toUnit == 'g') return q; - if(toUnit == 'kg') return q / 1000; - return q; - } - var result = []; window.plugins.healthkit.queryCorrelationType({ - startDate: opts.startDate, - endDate: opts.endDate, - correlationType: 'HKCorrelationTypeIdentifierFood', - units: ['g', 'ml', 'kcal'] - }, function (data) { - for(var i=0; i= retval[j].startDate)) { - // add the sample to the bucket - var dur = (data[i].endDate - data[i].startDate); - var dist = data[i].distance; - var cals = data[i].calories; - if (retval[j].value[data[i].value]) { - retval[j].value[data[i].value].duration += dur; - retval[j].value[data[i].value].distance += dist; - retval[j].value[data[i].value].calories += cals; - } else { - retval[j].value[data[i].value] = { - duration: dur, - distance: dist, - calories: cals - }; - } - } - } - } - onSuccess(retval); + onSuccess(bucketize(data, opts.bucket, startD, endD, 'activitySummary', mergeActivitySamples)); + }, onError); + } else if (opts.dataType === 'nutrition') { + // query and manually aggregate + navigator.health.query(opts, function (data) { + onSuccess(bucketize(data, opts.bucket, startD, endD, 'nutrition', mergeNutritionSamples)); }, onError); } else { window.plugins.healthkit.querySampleTypeAggregated(opts, function (value) { - // merges values and adds unit - var mergeAndSuccess = function (value, previous) { - var retval = []; - for (var i = 0; i < value.length; i++) { - var sample = { - startDate: value[i].startDate, - endDate: value[i].endDate, - value: value[i].quantity, - unit: opts.unit - }; - for (var j = 0; j < previous.length; j++) { - // we expect the buckets to have the same start and end dates - if (value[i].startDate === rundists[j].startDate) { - value[i].value += previous[j].quantity; - } - } - retval.push(sample); - } - onSuccess(retval); - }; if (opts.dataType === 'distance') { // add cycled distance var rundists = value; opts.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; opts.startDate = startD; opts.endDate = endD; - window.plugins.healthkit.querySampleTypeAggregated(opts, function (v) { - mergeAndSuccess(v, rundists); + window.plugins.healthkit.querySampleTypeAggregated(opts, function (dists) { + onSuccess(prepareResults(rundists, opts.unit, dists)); }, onError); } else if (opts.dataType === 'calories') { // add basal calories @@ -367,53 +264,26 @@ Health.prototype.queryAggregated = function (opts, onSuccess, onError) { opts.sampleType = 'HKQuantityTypeIdentifierBasalEnergyBurned'; opts.startDate = startD; opts.endDate = endD; - window.plugins.healthkit.sumQuantityType(opts, function (v) { - mergeAndSuccess(v, activecals); + window.plugins.healthkit.querySampleTypeAggregated(opts, function (cals) { + onSuccess(prepareResults(activecals, opts.unit, cals)); }, onError); } else { - // refactor objects - var retval = []; - for (var i = 0; i < value.length; i++) { - var sample = { - startDate: value[i].startDate, - endDate: value[i].endDate, - value: value[i].quantity, - unit: opts.unit - }; - retval.push(sample); - } - onSuccess(retval); + //simply refactor the result and send it + onSuccess(prepareResults(value, opts.unit)); } }, onError); } } else { // ---- no bucketing, just sum if (opts.dataType === 'activity') { - var res = { - startDate: startD, - endDate: endD, - value: {}, - unit: 'activitySummary' - }; navigator.health.query(opts, function (data) { - // aggregate by activity - for (var i = 0; i < data.length; i++) { - var dur = (data[i].endDate - data[i].startDate); - var dist = data[i].distance; - var cals = data[i].calories; - if (res.value[data[i].value]) { - res.value[data[i].value].duration += dur; - res.value[data[i].value].distance += dist; - res.value[data[i].value].calories += cals; - } else { - res.value[data[i].value] = { - duration: dur, - distance: dist, - calories: cals - }; - } - } - onSuccess(res); + // manually aggregate by activity + onSuccess(aggregateIntoResult(data, 'activitySummary', mergeActivitySamples)); + }, onError); + } else if(opts.dataType === 'nutrition') { + // manually aggregate by nutrition + navigator.health.query(opts, function (data) { + onSuccess(aggregateIntoResult(data, 'nutrition', mergeNutritionSamples)); }, onError); } else { window.plugins.healthkit.sumQuantityType(opts, function (value) { @@ -509,3 +379,169 @@ cordova.addConstructor(function () { navigator.health = new Health(); return navigator.health; }); + + +// UTILITY functions + +// converts from grams into another unit +// if the unit is not specified or is not weight, then the original quantity is returned +var convertFromGrams = function(toUnit, q) { + if(toUnit == 'mcg') return q * 1000000; + if(toUnit == 'mg') return q * 1000; + if(toUnit == 'kg') return q / 1000; + return q; +} + +// refactors the result of a query into returned type +var prepareResult = function (data, unit) { + var res = { + startDate: new Date(data.startDate), + endDate: new Date(data.endDate), + value: data.quantity, + unit: unit + }; + if (data.sourceName) res.sourceName = data.sourceName; + if (data.sourceBundleId) res.sourceBundleId = data.sourceBundleId; + return res; +}; + +// refactors the result of a nutrition query into returned type +var prepareNutrition = function (data) { + var res = { + startDate: new Date(data.startDate), + endDate: new Date(data.endDate), + value: {}, + unit: 'nutrition' + }; + if (data.sourceName) res.sourceName = data.sourceName; + if (data.sourceBundleId) res.sourceBundleId = data.sourceBundleId; + if (data.metadata && data.metadata.item) res.value.item = data.metadata.item; + if (data.metadata && data.metadata.meal_type) res.value.meal_type = data.metadata.meal_type; + res.value.nutrients = {}; + for (var j=0; j res.endDate) res.endDate = data[i].endDate; + if (data[i].startDate < res.startDate) res.startDate = data[i].startDate; + merge(data[i], res); + } + return res; +} + +// takes the result of a query (data) and transforms them, also merges with (unprocessed) results of another query +var prepareResults = function (data, unit, mergeWith) { + var retval = []; + for (var i = 0; i < data.length; i++) { + var retsample = prepareResult(data[i], unit); + if (mergeWith) { // merge with existing array returned by a query + for (var j = 0; j < mergeWith.length; j++) { + // we expect the buckets to have the same start and end dates + var mergeSample = prepareResult(mergeWith[j], unit); + if (retsample.startDate.getTime() === mergeSample.startDate.getTime()) { + retsample.value += mergeSample.value; + } + } + } + retval.push(retsample); + } + return retval; +}; + +// takes the results of a query (data) and merges it into a bucketized result (returned) +var bucketize = function (data, bucket, startD, endD, unit, merge) { + var retval = []; + // create buckets + var sd; + if (bucket === 'hour') { + sd = new Date(startD.getFullYear(), startD.getMonth(), startD.getDate(), startD.getHours()); + } else if (bucket === 'day') { + sd = new Date(startD.getFullYear(), startD.getMonth(), startD.getDate()); + } else if (bucket === 'week') { + sd = new Date(startD.getTime()); + sd.setDate(startD.getDate() - (startD.getDay() === 0 ? 6 : startD.getDay() - 1)); // last monday + } else if (bucket === 'month') { + sd = new Date(startD.getFullYear(), startD.getMonth()); + } else if (bucket === 'year') { + sd = new Date(startD.getFullYear()); + } else { + throw 'Bucket not recognised ' + bucket; + } + while (sd <= endD) { + var ed; + if (bucket === 'hour') { + ed = new Date(sd.getFullYear(), sd.getMonth(), sd.getDate(), sd.getHours() + 1); + } else if (bucket === 'day') { + ed = new Date(sd.getFullYear(), sd.getMonth(), sd.getDate() + 1); + } else if (bucket === 'week') { + ed = new Date(sd.getFullYear(), sd.getMonth(), sd.getDate() + 7); + } else if (bucket === 'month') { + ed = new Date(sd.getFullYear(), sd.getMonth() + 1); + } else if (bucket === 'year') { + ed = new Date(sd.getFullYear() + 1); + } + retval.push({ + startDate: sd, + endDate: ed, + value: {}, + unit: unit + }); + sd = ed; + } + for (var i = 0; i < data.length; i++) { + // select the bucket + for (var j = 0; j < retval.length; j++) { + if ((data[i].endDate <= retval[j].endDate) && (data[i].startDate >= retval[j].startDate)) { + merge(data[i], retval[j]); + } + } + } + return retval; +}; From 67f4a7b5be7fefd12f6adae36b00ab1bc00207e4 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 22 Nov 2016 17:54:25 +0000 Subject: [PATCH 015/157] better readme --- README.md | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 67d2e170..dc9cf975 100644 --- a/README.md +++ b/README.md @@ -77,14 +77,23 @@ Google Fit is limited to fitness data and, for health, custom data types are def Note: units of measurements are fixed ! -Returned objects can be of different types, see examples below: +Returned objects contain a set of fixed fields: + +- startDate: {type: Date} a date indicating when the data point starts +- endDate: {type: Date} a date indicating when the data point ends +- sourceBundleId: {type: String} the identifier of the app that produced the data +- sourceName: {type: String} the name of the app that produced the data (as it appears to the user) +- unit: {type: String} the unit of measurement +- value: the actual value + +value can be of different types, see examples below: | data type | value | |----------------|-----------------------------------| | steps | 34 | | distance | 101.2 | | calories | 245.3 | -| activity | "walking" (note: recognised activities and their mapping to Fit / HealthKit equivalents are listed in [this file](activities_map.md)) | +| activity | "walking" (note: recognized activities and their mapping to Fit / HealthKit equivalents are listed in [this file](activities_map.md)) | | height | 185.9 | | weight | 83.3 | | heart_rate | 66 | @@ -114,9 +123,9 @@ Quirks of isAvailable() ### requestAuthorization() Requests read and write access to a set of data types. -It is recommendable to always explain why the app needs access to the data before asking the user to authorise it. +It is recommendable to always explain why the app needs access to the data before asking the user to authorize it. -This function must be called before using the query and store functions, even if the authorisation has already been given at some point in the past. +This function must be called before using the query and store functions, even if the authorization has already been given at some point in the past. ``` navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) @@ -128,8 +137,8 @@ navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) Quirks of requestAuthorization() -- In Android, it will try to get authorisation from the Google Fit APIs. It is necessary that the app's package name and the signing key are registered in the Google API console (see [here](https://developers.google.com/fit/android/get-api-key)). -- In Android, be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the autorisation will ensure that the app is connected again. +- In Android, it will try to get authorization from the Google Fit APIs. It is necessary that the app's package name and the signing key are registered in the Google API console (see [here](https://developers.google.com/fit/android/get-api-key)). +- In Android, be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the autorization will ensure that the app is connected again. - In Android 6 and over, this function will also ask for some dynamic permissions if needed (e.g. in the case of "distance", it will need access to ACCESS_FINE_LOCATION). ### query() @@ -149,7 +158,7 @@ navigator.health.query({ - startDate: {type: Date}, start date from which to get data - endDate: {type: Date}, end data to which to get the data - dataType: {type: String}, the data type to be queried (see above) -- successCallback: {type: function(data) }, called if all OK, data contains the result of the query in the form of an array of: { startDate: Date, endDate: Date, value: xxx, unit: 'xxx', sourceName: '', sourceBundleId: '' } +- successCallback: {type: function(data) }, called if all OK, data contains the result of the query in the form of an array of: { startDate: Date, endDate: Date, value: xxx, unit: 'xxx', sourceName: 'aaaa', sourceBundleId: 'bbbb' } - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem @@ -194,7 +203,7 @@ The following table shows what types are supported and examples of aggregated da | calories.active | { startDate: Date, endDate: Date, value: 3547.4, unit: 'kcal' } | | calories.basal | { startDate: Date, endDate: Date, value: 13146.1, unit: 'kcal' } | | activity | { startDate: Date, endDate: Date, value: { still: { duration: 520000, calories: 30, distance: 0 }, walking: { duration: 223000, calories: 20, distance: 15 }}, unit: 'activitySummary' } (note: duration is expressed in milliseconds, distance in metres and calories in kcal) | -| nutrition | { startDate: Date, endDate: Date, value: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 }, unit: 'nutrition' } | +| nutrition | { startDate: Date, endDate: Date, value: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 }, unit: 'nutrition' } (note: units of measurement for nutrients are fixed according to the table at the beginning of this readme) | | nutrition.X | { startDate: Date, endDate: Date, value: 23, unit: 'mg'} | Quirks of queryAggregated() @@ -240,7 +249,7 @@ Quirks of store() * HealthKit includes medical data (eg blood glucose), Google Fit is only related to fitness data * HealthKit provides a data model that is not extensible, Google Fit allows defining custom data types -* HealthKit allows to insert data with the unit of measurement of your choice, and automatically translates units when quiered, Google Fit uses fixed units of measurement +* HealthKit allows to insert data with the unit of measurement of your choice, and automatically translates units when queried, Google Fit uses fixed units of measurement * HealthKit automatically counts steps and distance when you carry your phone with you and if your phone has the CoreMotion chip, Google Fit also detects the kind of activity (sedentary, running, walking, cycling, in vehicle) * HealthKit automatically computes the distance only for running/walking activities, Google Fit includes bicycle also @@ -260,7 +269,7 @@ short term - get steps from the "polished" Google Fit data source (see https://plus.google.com/104895513165544578271/posts/a8P62A6ejQy) - add support for HKCategory samples in HealthKit - extend the datatypes - - blood pressure (KCorrelationTypeIdentifierBloodPressure, custom data type) + - blood pressure (KCorrelationTypeIdentifierBloodPressure, custom data type) - blood glucose - location (NA, TYPE_LOCATION) @@ -274,6 +283,5 @@ long term Any help is more than welcome! I don't know Objectve C and I am not interested into learning it now, so I would particularly appreciate someone who can give me a hand with the iOS part. -Particularly, I'd like to allow support for HKCategory. Also, I would love to know from you if the plugin is currently used in any app actually available online. Just send me an email to my_username at gmail.com From 32f2066331f706634cd39b5d980b6471a8b18ec8 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 22 Nov 2016 18:02:37 +0000 Subject: [PATCH 016/157] even better readme! --- README.md | 70 +++++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index dc9cf975..f8a8af05 100644 --- a/README.md +++ b/README.md @@ -39,41 +39,41 @@ Some more detailed instructions are provided [here](https://github.com/2dvisio/c As HealthKit does not allow adding custom data types, only a subset of data types supported by HealthKit has been chosen. Google Fit is limited to fitness data and, for health, custom data types are defined with the suffix of the package name of your project. -| data type | HealthKit equivalent (unit) | Google Fit equivalent | -|-----------------|---------------------------------------------------------|------------------------------------------| -| steps | HKQuantityTypeIdentifierStepCount (count) | TYPE_STEP_COUNT_DELTA | -| distance | HKQuantityTypeIdentifierDistanceWalkingRunning (m) + HKQuantityTypeIdentifierDistanceCycling (m) | TYPE_DISTANCE_DELTA | -| calories | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned(kcal) | TYPE_CALORIES_EXPENDED | -| calories.active | HKQuantityTypeIdentifierActiveEnergyBurned (kcal) | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | -| calories.basal | HKQuantityTypeIdentifierBasalEnergyBurned (kcal) | TYPE_BASAL_METABOLIC_RATE * time window | -| activity | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | -| height | HKQuantityTypeIdentifierHeight (m) | TYPE_HEIGHT | -| weight | HKQuantityTypeIdentifierBodyMass (kg) | TYPE_WEIGHT | -| heart_rate | HKQuantityTypeIdentifierHeartRate (count/min) | TYPE_HEART_RATE_BPM | -| fat_percentage | HKQuantityTypeIdentifierBodyFatPercentage (%) | TYPE_BODY_FAT_PERCENTAGE | -| gender | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | -| date_of_birth | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | -| nutrition | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | -| nutrition.calories | HKQuantityTypeIdentifierDietaryEnergyConsumed (kcal) | TYPE_NUTRITION, NUTRIENT_CALORIES | -| nutrition.fat.total | HKQuantityTypeIdentifierDietaryFatTotal (g) | TYPE_NUTRITION, NUTRIENT_TOTAL_FAT | -| nutrition.fat.saturated | HKQuantityTypeIdentifierDietaryFatSaturated (g) | TYPE_NUTRITION, NUTRIENT_SATURATED_FAT | -| nutrition.fat.unsaturated | NA (g) | TYPE_NUTRITION, NUTRIENT_UNSATURATED_FAT | -| nutrition.fat.polyunsaturated | HKQuantityTypeIdentifierDietaryFatPolyunsaturated (g) | TYPE_NUTRITION, NUTRIENT_POLYUNSATURATED_FAT | -| nutrition.fat.monounsaturated | HKQuantityTypeIdentifierDietaryFatMonounsaturated (g) | TYPE_NUTRITION, NUTRIENT_MONOUNSATURATED_FAT | -| nutrition.fat.trans | NA (g) | TYPE_NUTRITION, NUTRIENT_TRANS_FAT | -| nutrition.cholesterol | HKQuantityTypeIdentifierDietaryCholesterol (mg) | TYPE_NUTRITION, NUTRIENT_CHOLESTEROL | -| nutrition.sodium | HKQuantityTypeIdentifierDietarySodium (mg) | TYPE_NUTRITION, NUTRIENT_SODIUM | -| nutrition.potassium | HKQuantityTypeIdentifierDietaryPotassium (mg) | TYPE_NUTRITION, NUTRIENT_POTASSIUM | -| nutrition.carbs.total | HKQuantityTypeIdentifierDietaryCarbohydrates (g) | TYPE_NUTRITION, NUTRIENT_TOTAL_CARBS | -| nutrition.dietary_fiber | HKQuantityTypeIdentifierDietaryFiber (g) | TYPE_NUTRITION, NUTRIENT_DIETARY_FIBER | -| nutrition.sugar | HKQuantityTypeIdentifierDietarySugar (g) | TYPE_NUTRITION, NUTRIENT_SUGAR | -| nutrition.protein | HKQuantityTypeIdentifierDietaryProtein (g) | TYPE_NUTRITION, NUTRIENT_PROTEIN | -| nutrition.vitamin_a | HKQuantityTypeIdentifierDietaryVitaminA (mcg) | TYPE_NUTRITION, NUTRIENT_VITAMIN_A | -| nutrition.vitamin_c | HKQuantityTypeIdentifierDietaryVitaminC (mg) | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | -| nutrition.calcium | HKQuantityTypeIdentifierDietaryCalcium (mg) | TYPE_NUTRITION, NUTRIENT_CALCIUM | -| nutrition.iron | HKQuantityTypeIdentifierDietaryIron (mg) | TYPE_NUTRITION, NUTRIENT_IRON | -| nutrition.water | HKQuantityTypeIdentifierDietaryWater (ml) | TYPE_HYDRATION | -| nutrition.caffeine | HKQuantityTypeIdentifierDietaryCaffeine (g) | NA | +| data type | Unit | HealthKit equivalent (unit) | Google Fit equivalent | +|-----------------|-------|-----------------------------------------------|------------------------------------------| +| steps | count | HKQuantityTypeIdentifierStepCount | TYPE_STEP_COUNT_DELTA | +| distance | m | HKQuantityTypeIdentifierDistanceWalkingRunning + HKQuantityTypeIdentifierDistanceCycling | TYPE_DISTANCE_DELTA | +| calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | +| calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | +| calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | +| activity | | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | +| height | m | HKQuantityTypeIdentifierHeight | TYPE_HEIGHT | +| weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | +| heart_rate | count/min| HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | +| fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | +| gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | +| date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | +| nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | +| nutrition.calories | kcal | HKQuantityTypeIdentifierDietaryEnergyConsumed | TYPE_NUTRITION, NUTRIENT_CALORIES | +| nutrition.fat.total | g | HKQuantityTypeIdentifierDietaryFatTotal | TYPE_NUTRITION, NUTRIENT_TOTAL_FAT | +| nutrition.fat.saturated | g | HKQuantityTypeIdentifierDietaryFatSaturated | TYPE_NUTRITION, NUTRIENT_SATURATED_FAT | +| nutrition.fat.unsaturated | g | NA | TYPE_NUTRITION, NUTRIENT_UNSATURATED_FAT | +| nutrition.fat.polyunsaturated | g | HKQuantityTypeIdentifierDietaryFatPolyunsaturated | TYPE_NUTRITION, NUTRIENT_POLYUNSATURATED_FAT | +| nutrition.fat.monounsaturated | g | HKQuantityTypeIdentifierDietaryFatMonounsaturated | TYPE_NUTRITION, NUTRIENT_MONOUNSATURATED_FAT | +| nutrition.fat.trans | g | NA | TYPE_NUTRITION, NUTRIENT_TRANS_FAT (g) | +| nutrition.cholesterol | mg | HKQuantityTypeIdentifierDietaryCholesterol | TYPE_NUTRITION, NUTRIENT_CHOLESTEROL | +| nutrition.sodium | mg | HKQuantityTypeIdentifierDietarySodium | TYPE_NUTRITION, NUTRIENT_SODIUM | +| nutrition.potassium | mg | HKQuantityTypeIdentifierDietaryPotassium | TYPE_NUTRITION, NUTRIENT_POTASSIUM | +| nutrition.carbs.total | g | HKQuantityTypeIdentifierDietaryCarbohydrates | TYPE_NUTRITION, NUTRIENT_TOTAL_CARBS | +| nutrition.dietary_fiber | g | HKQuantityTypeIdentifierDietaryFiber | TYPE_NUTRITION, NUTRIENT_DIETARY_FIBER | +| nutrition.sugar | g | HKQuantityTypeIdentifierDietarySugar | TYPE_NUTRITION, NUTRIENT_SUGAR | +| nutrition.protein | g | HKQuantityTypeIdentifierDietaryProtein | TYPE_NUTRITION, NUTRIENT_PROTEIN | +| nutrition.vitamin_a | mcg | HKQuantityTypeIdentifierDietaryVitaminA | TYPE_NUTRITION, NUTRIENT_VITAMIN_A | +| nutrition.vitamin_c | mg | HKQuantityTypeIdentifierDietaryVitaminC | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | +| nutrition.calcium | mg | HKQuantityTypeIdentifierDietaryCalcium | TYPE_NUTRITION, NUTRIENT_CALCIUM | +| nutrition.iron | mg | HKQuantityTypeIdentifierDietaryIron | TYPE_NUTRITION, NUTRIENT_IRON | +| nutrition.water | ml | HKQuantityTypeIdentifierDietaryWater | TYPE_HYDRATION | +| nutrition.caffeine | g | HKQuantityTypeIdentifierDietaryCaffeine | NA | Note: units of measurements are fixed ! From d8a62357fdf3b0c690160105f5f550e097d0c30e Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 22 Nov 2016 18:04:43 +0000 Subject: [PATCH 017/157] remove unit from table --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8a8af05..f968284f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Some more detailed instructions are provided [here](https://github.com/2dvisio/c As HealthKit does not allow adding custom data types, only a subset of data types supported by HealthKit has been chosen. Google Fit is limited to fitness data and, for health, custom data types are defined with the suffix of the package name of your project. -| data type | Unit | HealthKit equivalent (unit) | Google Fit equivalent | +| data type | Unit | HealthKit equivalent | Google Fit equivalent | |-----------------|-------|-----------------------------------------------|------------------------------------------| | steps | count | HKQuantityTypeIdentifierStepCount | TYPE_STEP_COUNT_DELTA | | distance | m | HKQuantityTypeIdentifierDistanceWalkingRunning + HKQuantityTypeIdentifierDistanceCycling | TYPE_DISTANCE_DELTA | From 1f36088569af27397bfa9bcc653c8b60ee47551f Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 23 Nov 2016 10:37:02 +0000 Subject: [PATCH 018/157] use of standard keys for item and meal type fixes #25 --- www/ios/health.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index 74dbd112..58c744ec 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -415,8 +415,8 @@ var prepareNutrition = function (data) { }; if (data.sourceName) res.sourceName = data.sourceName; if (data.sourceBundleId) res.sourceBundleId = data.sourceBundleId; - if (data.metadata && data.metadata.item) res.value.item = data.metadata.item; - if (data.metadata && data.metadata.meal_type) res.value.meal_type = data.metadata.meal_type; + if (data.metadata && data.metadata.HKFoodType) res.value.item = data.metadata.HKFoodType; + if (data.metadata && data.metadata.HKFoodMeal) res.value.meal_type = data.metadata.HKFoodMeal; res.value.nutrients = {}; for (var j=0; j Date: Wed, 23 Nov 2016 11:23:22 +0000 Subject: [PATCH 019/157] separates isAvailable from prompting the user to download GF fixes #19 --- README.md | 19 +++++++++++++++-- src/android/HealthPlugin.java | 40 +++++++++++++++++++++++------------ www/android/health.js | 4 ++++ 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f968284f..41c42e77 100644 --- a/README.md +++ b/README.md @@ -116,9 +116,24 @@ navigator.health.isAvailable(successCallback, errorCallback) - successCallback: {type: function(available)}, if available a true is passed as argument, false otherwise - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem -Quirks of isAvailable() -- In Android, it checks both if recent Google Play Services and Google Fit are installed. If the play services are not installed, or are obsolete, it will show a pop-up suggesting to download them. If Google Fit is not installed, it will open the Play Store at the location of the Google Fit app. The plugin does not wait until the missing packages are installed, it will return immediately. +### promptInstallFit() (Android only) + +Only available on Android. + +Checks if recent Google Play Services and Google Fit are installed. +If the play services are not installed, or are obsolete, it will show a pop-up suggesting to download them. +If Google Fit is not installed, it will open the Play Store at the location of the Google Fit app. +The plugin does not wait until the missing packages are installed, it will return immediately. +If both Play Services and Google Fit are available, this function just returns without any visible effect. + +``` +navigator.health.promptInstallFit(successCallback, errorCallback) +``` + +- successCallback: {type: function()}, called if the function was called +- errorCallback: {type: function(err)}, called if something went wrong + ### requestAuthorization() diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index c87b53cb..8fdd807e 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -260,6 +260,9 @@ public boolean execute(String action, final JSONArray args, final CallbackContex if ("isAvailable".equals(action)) { isAvailable(callbackContext); return true; + } else if("promptInstallFit".equals(action)) { + promptInstall(callbackContext); + return true; } else if ("requestAuthorization".equals(action)) { requestAuthorization(args, callbackContext); return true; @@ -295,15 +298,32 @@ public void run() { return false; } - private void isAvailable(final CallbackContext callbackContext) { - //first check that the Google APIs are available + // first check that the Google APIs are available + GoogleApiAvailability gapi = GoogleApiAvailability.getInstance(); + int apiresult = gapi.isGooglePlayServicesAvailable(this.cordova.getActivity()); + if (apiresult == ConnectionResult.SUCCESS) { + // then check that Google Fit is actually installed + PackageManager pm = cordova.getActivity().getApplicationContext().getPackageManager(); + try { + pm.getPackageInfo("com.google.android.apps.fitness", PackageManager.GET_ACTIVITIES); + // Success return object + PluginResult result; + result = new PluginResult(PluginResult.Status.OK, true); + callbackContext.sendPluginResult(result); + } catch (PackageManager.NameNotFoundException e) { + Log.d(TAG, "Google Fit not installed"); + } + } + PluginResult result; + result = new PluginResult(PluginResult.Status.OK, false); + callbackContext.sendPluginResult(result); + } + + private void promptInstall(final CallbackContext callbackContext) { GoogleApiAvailability gapi = GoogleApiAvailability.getInstance(); int apiresult = gapi.isGooglePlayServicesAvailable(this.cordova.getActivity()); if (apiresult != ConnectionResult.SUCCESS) { - PluginResult result; - result = new PluginResult(PluginResult.Status.OK, false); - callbackContext.sendPluginResult(result); if (gapi.isUserResolvableError(apiresult)) { // show the dialog, but no action is performed afterwards gapi.showErrorDialogFragment(this.cordova.getActivity(), apiresult, 1000); @@ -313,25 +333,17 @@ private void isAvailable(final CallbackContext callbackContext) { PackageManager pm = cordova.getActivity().getApplicationContext().getPackageManager(); try { pm.getPackageInfo("com.google.android.apps.fitness", PackageManager.GET_ACTIVITIES); - // Success return object - PluginResult result; - result = new PluginResult(PluginResult.Status.OK, true); - callbackContext.sendPluginResult(result); } catch (PackageManager.NameNotFoundException e) { //show popup for downloading app //code from http://stackoverflow.com/questions/11753000/how-to-open-the-google-play-store-directly-from-my-android-application - try { cordova.getActivity().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.google.android.apps.fitness"))); } catch (android.content.ActivityNotFoundException anfe) { cordova.getActivity().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.apps.fitness"))); } - - PluginResult result; - result = new PluginResult(PluginResult.Status.OK, false); - callbackContext.sendPluginResult(result); } } + callbackContext.success(); } private void requestAuthorization(final JSONArray args, final CallbackContext callbackContext) throws JSONException { diff --git a/www/android/health.js b/www/android/health.js index 81e267b8..3db5cf17 100644 --- a/www/android/health.js +++ b/www/android/health.js @@ -8,6 +8,10 @@ Health.prototype.isAvailable = function (onSuccess, onError) { exec(onSuccess, onError, "health", "isAvailable", []); }; +Health.prototype.promptInstallFit = function (onSuccess, onError) { + exec(onSuccess, onError, "health", "promptInstallFit", []); +}; + Health.prototype.requestAuthorization = function (datatypes, onSuccess, onError) { exec(onSuccess, onError, "health", "requestAuthorization", datatypes); }; From 737de49fe1d9ba5899014b3039dc94bfe8816e66 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 23 Nov 2016 11:23:50 +0000 Subject: [PATCH 020/157] some more details about nutrition --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41c42e77..d732170b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | | calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | | calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | -| activity | | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | +| activity | | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | | height | m | HKQuantityTypeIdentifierHeight | TYPE_HEIGHT | | weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | | heart_rate | count/min| HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | @@ -68,7 +68,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | nutrition.dietary_fiber | g | HKQuantityTypeIdentifierDietaryFiber | TYPE_NUTRITION, NUTRIENT_DIETARY_FIBER | | nutrition.sugar | g | HKQuantityTypeIdentifierDietarySugar | TYPE_NUTRITION, NUTRIENT_SUGAR | | nutrition.protein | g | HKQuantityTypeIdentifierDietaryProtein | TYPE_NUTRITION, NUTRIENT_PROTEIN | -| nutrition.vitamin_a | mcg | HKQuantityTypeIdentifierDietaryVitaminA | TYPE_NUTRITION, NUTRIENT_VITAMIN_A | +| nutrition.vitamin_a | mcg (HK), IU (GF) | HKQuantityTypeIdentifierDietaryVitaminA | TYPE_NUTRITION, NUTRIENT_VITAMIN_A | | nutrition.vitamin_c | mg | HKQuantityTypeIdentifierDietaryVitaminC | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | | nutrition.calcium | mg | HKQuantityTypeIdentifierDietaryCalcium | TYPE_NUTRITION, NUTRIENT_CALCIUM | | nutrition.iron | mg | HKQuantityTypeIdentifierDietaryIron | TYPE_NUTRITION, NUTRIENT_IRON | @@ -184,6 +184,7 @@ Quirks of query() - while Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input - when querying for activities, Google Fit is able to determine some activities automatically, while HealthKit only relies on the input of the user or of some external app - when querying for activities, calories and distance are also provided in HealthKit (units are kcal and metres) and never in Google Fit +- when querying for nutrition, Google Fit always returns all the nutrition elements it has, while HealthKit returns only those that are stored as correlation. To be sure one gets all stored the quantities (regardless of they are stored as correlation or not), it's beter to query single nutrients. - nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. The conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). ### queryAggregated() @@ -227,6 +228,7 @@ Quirks of queryAggregated() - in Android, the start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. - when bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00 - weeks start on Monday +- when querying for nutrition, HealthKit returns only those that are stored as correlation. To be sure one gets all the stored quantities, it's beter to query single nutrients. - nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. The conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). ### store() From 3aed3b624d004e47bacf561cff15508d3477c306 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 23 Nov 2016 13:56:18 +0000 Subject: [PATCH 021/157] added filtered flag to steps for Android fixes #22 --- README.md | 12 +++++----- src/android/HealthPlugin.java | 42 +++++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d732170b..bf99d4d4 100644 --- a/README.md +++ b/README.md @@ -179,11 +179,12 @@ navigator.health.query({ Quirks of query() -- in Google Fit calories.basal is returned as an average per day, and usually is not available in all days (may be not available in time windows smaller than 5 days or more) -- in Google Fit calories.active is computed by subtracting the basal from the total, as basal an average of the a number of days before endDate is taken (the actual number is 7) -- while Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input -- when querying for activities, Google Fit is able to determine some activities automatically, while HealthKit only relies on the input of the user or of some external app -- when querying for activities, calories and distance are also provided in HealthKit (units are kcal and metres) and never in Google Fit +- in Android one can query for steps as filtered by the Google Fit app, in that case `filtered: true` must be put in the query object. +- in Google Fit calories.basal is returned as an average per day, and usually is not available in all days (may be not available in time windows smaller than 5 days or more). +- in Google Fit calories.active is computed by subtracting the basal from the total, as basal an average of the a number of days before endDate is taken (the actual number is 7). +- while Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input. +- when querying for activities, Google Fit is able to determine some activities automatically, while HealthKit only relies on the input of the user or of some external app. +- when querying for activities, calories and distance are also provided in HealthKit (units are kcal and metres) and never in Google Fit. - when querying for nutrition, Google Fit always returns all the nutrition elements it has, while HealthKit returns only those that are stored as correlation. To be sure one gets all stored the quantities (regardless of they are stored as correlation or not), it's beter to query single nutrients. - nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. The conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). @@ -224,6 +225,7 @@ The following table shows what types are supported and examples of aggregated da Quirks of queryAggregated() +- in Android, to query for steps as filtered by the Google Fit app, the flag `filtered: true` must be added into the query object. - when querying for activities, calories and distance are provided when available in HealthKit and never in Google Fit - in Android, the start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. - when bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00 diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 8fdd807e..7e4372d2 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -488,11 +488,27 @@ private void query(final JSONArray args, final CallbackContext callbackContext) } } + DataReadRequest readRequest = null; + if (DT.equals(DataType.TYPE_STEP_COUNT_DELTA) && args.getJSONObject(0).has("filtered") && args.getJSONObject(0).getBoolean("filtered")) { + // exceptional case for filtered steps + DataSource filteredStepsSource = new DataSource.Builder() + .setDataType(DataType.TYPE_STEP_COUNT_DELTA) + .setType(DataSource.TYPE_DERIVED) + .setStreamName("estimated_steps") + .setAppPackageName("com.google.android.gms") + .build(); + + readRequest = new DataReadRequest.Builder() + .setTimeRange(st, et, TimeUnit.MILLISECONDS) + .read(filteredStepsSource) + .build(); + } else { + readRequest = new DataReadRequest.Builder() + .setTimeRange(st, et, TimeUnit.MILLISECONDS) + .read(dt) + .build(); + } - DataReadRequest readRequest = new DataReadRequest.Builder() - .setTimeRange(st, et, TimeUnit.MILLISECONDS) - .read(dt) - .build(); DataReadResult dataReadResult = Fitness.HistoryApi.readData(mClient, readRequest).await(); @@ -507,9 +523,9 @@ private void query(final JSONArray args, final CallbackContext callbackContext) DataSource dataSource = datapoint.getOriginalDataSource(); if (dataSource != null) { String sourceName = dataSource.getName(); - obj.put("sourceName", sourceName); + if(sourceName != null) obj.put("sourceName", sourceName); String sourceBundleId = dataSource.getAppPackageName(); - obj.put("sourceBundleId", sourceBundleId); + if(sourceBundleId != null) obj.put("sourceBundleId", sourceBundleId); } //reference for fields: https://developers.google.com/android/reference/com/google/android/gms/fitness/data/Field.html @@ -745,12 +761,24 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } + DataReadRequest.Builder builder = new DataReadRequest.Builder(); builder.setTimeRange(st, et, TimeUnit.MILLISECONDS); int allms = (int) (et - st); if (datatype.equalsIgnoreCase("steps")) { - builder.aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA); + if (args.getJSONObject(0).has("filtered") && args.getJSONObject(0).getBoolean("filtered")){ + // exceptional case for filtered steps + DataSource filteredStepsSource = new DataSource.Builder() + .setDataType(DataType.TYPE_STEP_COUNT_DELTA) + .setType(DataSource.TYPE_DERIVED) + .setStreamName("estimated_steps") + .setAppPackageName("com.google.android.gms") + .build(); + builder.aggregate(filteredStepsSource, DataType.AGGREGATE_STEP_COUNT_DELTA); + } else { + builder.aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA); + } } else if (datatype.equalsIgnoreCase("distance")) { builder.aggregate(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA); } else if (datatype.equalsIgnoreCase("calories")) { From 3a5ced42659923dc601842c1ac5ab546ab111d7f Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 23 Nov 2016 14:00:14 +0000 Subject: [PATCH 022/157] udpated readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index bf99d4d4..85cfd9f6 100644 --- a/README.md +++ b/README.md @@ -285,8 +285,7 @@ short term - add store of nutrition - add delete -- get steps from the "polished" Google Fit data source (see https://plus.google.com/104895513165544578271/posts/a8P62A6ejQy) -- add support for HKCategory samples in HealthKit +- add support for storing HKCategory samples in HealthKit - extend the datatypes - blood pressure (KCorrelationTypeIdentifierBloodPressure, custom data type) - blood glucose From dfe2004bd91beb1be43e4de17979a93255135843 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 23 Nov 2016 14:09:22 +0000 Subject: [PATCH 023/157] needs cordova 6 at least (probably a lower version would do anyway, but better be on the safe side) --- README.md | 2 ++ package.json | 2 +- plugin.xml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85cfd9f6..4e120f25 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ This work is based on [cordova plugin googlefit](https://github.com/2dvisio/cord For an introduction about Google Fit versus HealthKit see [this very good article](https://yalantis.com/blog/how-can-healthkit-and-googlefit-help-you-develop-healthcare-and-fitness-apps/). +This plugin is kept up to date and requires a recent version of cordova (6 and on) and recent iOS and Android SDKs. + ## Warning This plugin stores health data in Google Fit, practice that is [discouraged by Google](https://developers.google.com/fit/terms). diff --git a/package.json b/package.json index 14d9c2e2..d64c7c90 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "engines": [ { "name": "cordova", - "version": ">=3.0.0" + "version": ">=6.0.0" } ] } diff --git a/plugin.xml b/plugin.xml index 5bbc8593..76bf36f7 100755 --- a/plugin.xml +++ b/plugin.xml @@ -20,7 +20,7 @@ https://github.com/dariosalvi78/cordova-plugin-health/issues - + From f779abc9752030a9e998f34aa48dfed9d2a746ef Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 23 Nov 2016 14:09:33 +0000 Subject: [PATCH 024/157] release 0.8.0 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d64c7c90..41393776 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.7.0", + "version": "0.8.0", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 76bf36f7..1ad81f6b 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.8.0"> Cordova Health From 8a249467a59142bcd39c7601a59bc1c501e66891 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 25 Nov 2016 13:29:51 +0000 Subject: [PATCH 025/157] nicer read --- README.md | 94 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 4e120f25..9d1c6d15 100644 --- a/README.md +++ b/README.md @@ -121,14 +121,15 @@ navigator.health.isAvailable(successCallback, errorCallback) ### promptInstallFit() (Android only) -Only available on Android. - Checks if recent Google Play Services and Google Fit are installed. If the play services are not installed, or are obsolete, it will show a pop-up suggesting to download them. If Google Fit is not installed, it will open the Play Store at the location of the Google Fit app. The plugin does not wait until the missing packages are installed, it will return immediately. If both Play Services and Google Fit are available, this function just returns without any visible effect. +This function is only available on Android. + + ``` navigator.health.promptInstallFit(successCallback, errorCallback) ``` @@ -155,14 +156,14 @@ navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) Quirks of requestAuthorization() - In Android, it will try to get authorization from the Google Fit APIs. It is necessary that the app's package name and the signing key are registered in the Google API console (see [here](https://developers.google.com/fit/android/get-api-key)). -- In Android, be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the autorization will ensure that the app is connected again. +- In Android, be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the authorization will ensure that the app is connected again. - In Android 6 and over, this function will also ask for some dynamic permissions if needed (e.g. in the case of "distance", it will need access to ACCESS_FINE_LOCATION). ### query() Gets all the records of a certain data type within a certain time window. -Warning: it can generate long arrays! +Warning: if the time span is big, it can generate long arrays! ``` navigator.health.query({ @@ -181,14 +182,14 @@ navigator.health.query({ Quirks of query() -- in Android one can query for steps as filtered by the Google Fit app, in that case `filtered: true` must be put in the query object. -- in Google Fit calories.basal is returned as an average per day, and usually is not available in all days (may be not available in time windows smaller than 5 days or more). -- in Google Fit calories.active is computed by subtracting the basal from the total, as basal an average of the a number of days before endDate is taken (the actual number is 7). -- while Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input. -- when querying for activities, Google Fit is able to determine some activities automatically, while HealthKit only relies on the input of the user or of some external app. -- when querying for activities, calories and distance are also provided in HealthKit (units are kcal and metres) and never in Google Fit. -- when querying for nutrition, Google Fit always returns all the nutrition elements it has, while HealthKit returns only those that are stored as correlation. To be sure one gets all stored the quantities (regardless of they are stored as correlation or not), it's beter to query single nutrients. -- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. The conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). +- In Android it is possible to query for "raw" steps or to select those as filtered by the Google Fit app. In the latter case the query object must contain the field `filtered: true`. +- In Google Fit calories.basal is returned as an average per day, and usually is not available in all days (may be not available in time windows smaller than 5 days or more). +- In Google Fit calories.active is computed by subtracting the basal calories from the total. As basal energy expenditure, an average is computed from the week before endDate. +- While Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input. +- When querying for activities, Google Fit is able to determine some activities automatically (still, walking, running, biking, in vehicle), while HealthKit only relies on the input of the user or of some external app. +- When querying for activities, calories and distance are also provided in HealthKit (units are kcal and meters) and never in Google Fit. +- When querying for nutrition, Google Fit always returns all the nutrition elements it has, while HealthKit returns only those that are stored as correlation. To be sure to get all stored the quantities (regardless of they are stored as correlation or not), it's better to query single nutrients. +- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. This is because conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). ### queryAggregated() @@ -208,11 +209,11 @@ navigator.health.queryAggregated({ - endDate: {type: Date}, end data to which to get the data - dataType: {type: String}, the data type to be queried (see below for supported data types) - bucket: {type: String}, if specified, aggregation is grouped an array of "buckets" (windows of time), supported values are: 'hour', 'day', 'week', 'month', 'year' -- successCallback: {type: function(data)}, called if all OK, data contains the result of the query, see below for returned data types +- successCallback: {type: function(data)}, called if all OK, data contains the result of the query, see below for returned data types. If no buckets is specified, the result is an object. If a bucketing strategy is specified, the result is an array. - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem Not all data types are supported for aggregated queries. -The following table shows what types are supported and examples of aggregated data: +The following table shows what types are supported and examples of the returned object: | data type | example of returned object | |-----------------|----------------------------| @@ -227,13 +228,13 @@ The following table shows what types are supported and examples of aggregated da Quirks of queryAggregated() -- in Android, to query for steps as filtered by the Google Fit app, the flag `filtered: true` must be added into the query object. -- when querying for activities, calories and distance are provided when available in HealthKit and never in Google Fit -- in Android, the start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. -- when bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00 -- weeks start on Monday -- when querying for nutrition, HealthKit returns only those that are stored as correlation. To be sure one gets all the stored quantities, it's beter to query single nutrients. -- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. The conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). +- In Android, to query for steps as filtered by the Google Fit app, the flag `filtered: true` must be added into the query object. +- When querying for activities, calories and distance are provided when available in HealthKit and never in Google Fit. +- In Android, the start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. +- When bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00. +- Weeks start on Monday. +- When querying for nutrition, HealthKit returns only those that are stored as correlation. To be sure to get all the stored quantities, it's better to query single nutrients. +- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. ### store() @@ -261,43 +262,50 @@ navigator.health.store({ Quirks of store() -- in iOS you cannot store the total calories, you need to specify either basal or active. If you use total calories, the active ones will be stored. -- in Android you can only store active calories, as the basal are estimated automatically. If you store total calories, these will be treated as active. -- in iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. -- in iOS storing the sleep activities is not supported at the moment. +- In iOS you cannot store the total calories, you need to specify either basal or active. If you use total calories, the active ones will be stored. +- In Android you can only store active calories, as the basal are estimated automatically. If you store total calories, these will be treated as active. +- In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. +- In iOS storing the sleep activities is not supported at the moment. +- Storing of nutrients is not supported at the moment. ## Differences between HealthKit and Google Fit -* HealthKit includes medical data (eg blood glucose), Google Fit is only related to fitness data -* HealthKit provides a data model that is not extensible, Google Fit allows defining custom data types -* HealthKit allows to insert data with the unit of measurement of your choice, and automatically translates units when queried, Google Fit uses fixed units of measurement -* HealthKit automatically counts steps and distance when you carry your phone with you and if your phone has the CoreMotion chip, Google Fit also detects the kind of activity (sedentary, running, walking, cycling, in vehicle) -* HealthKit automatically computes the distance only for running/walking activities, Google Fit includes bicycle also +* HealthKit includes medical data (eg blood glucose), Google Fit is only related to fitness data. +* HealthKit provides a data model that is not extensible, Google Fit allows defining custom data types. +* HealthKit allows to insert data with the unit of measurement of your choice, and automatically translates units when queried, Google Fit uses fixed units of measurement. +* HealthKit automatically counts steps and distance when you carry your phone with you and if your phone has the CoreMotion chip, Google Fit also detects the kind of activity (sedentary, running, walking, cycling, in vehicle). +* HealthKit automatically computes the distance only for running/walking activities, Google Fit includes bicycle also. ## External Resources -* The official Apple documentation for [HealthKit can be found here](https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Framework/index.html#//apple_ref/doc/uid/TP40014707). +* The official Apple documentation for HealthKit [can be found here](https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Framework/index.html#//apple_ref/doc/uid/TP40014707). * For functions that require the `unit` attribute, you can find the comprehensive list of possible units from the [Apple Developers documentation](https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HKUnit_Class/index.html#//apple_ref/doc/uid/TP40014727-CH1-SW2). -* [HealthKit constants](https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Constants/index.html), used throughout the code -* Google Fit [supported data types](https://developers.google.com/fit/android/data-types) +* [HealthKit constants](https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Constants/index.html), used throughout the code. +* Google Fit [supported data types](https://developers.google.com/fit/android/data-types). ## Roadmap -short term +short term: -- add store of nutrition -- add delete +- add storing of nutrition +- allow deletion of data points - add support for storing HKCategory samples in HealthKit -- extend the datatypes - - blood pressure (KCorrelationTypeIdentifierBloodPressure, custom data type) +- add more datatypes + - body fat percentage + - oxygen saturation + - blood pressure - blood glucose - - location (NA, TYPE_LOCATION) + - temperature + - respiratory rate -long term +long term: -- add registration to updates (in Fit: HistoryApi#registerDataUpdateListener() ) -- store vital signs on an encrypted DB in the case of Android and remove custom datatypes. Possible choice: [sqlcipher](https://www.zetetic.net/sqlcipher/sqlcipher-for-android/). The file would be stored on shared drive, and it would be shared among apps through a service. You could more simply share the file, but then how would you share the password? If shared through a service, all apps would have the same service because it's part of the plugin, so the service should not auto-start until the first app tries to bind it (see [this](http://stackoverflow.com/questions/31506177/the-same-android-service-instance-for-two-apps) for suggestions). This is sub-optimal, as all apps would have the same copy of the service (although lightweight). A better approach would be requiring an extra app, but this creates other issues like "who would publish it?", "why the user would be needed to download another app?" etc. -- add also Samsung Health as a health record for Android +- add registration to updates (in Fit: HistoryApi#registerDataUpdateListener()). +- store custom data types and vital signs on an encrypted DB in the case of Android. +Possible choice: [sqlcipher](https://www.zetetic.net/sqlcipher/sqlcipher-for-android/). +The file would be stored on shared drive, and it would be shared among apps through a service. +All apps would have the same service because it's part of the plugin, so the service should not auto-start until the first app tries to bind it (see [this](http://stackoverflow.com/questions/31506177/the-same-android-service-instance-for-two-apps) for suggestions). +- add also Samsung Health as a health record for Android. ## Contributions From 3abc24105257f77a98b29623216c7427235ac6b9 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 7 Dec 2016 10:49:26 +0000 Subject: [PATCH 026/157] added comment about not having fit installed --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 9d1c6d15..7304be95 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,7 @@ cordova plugin add cordova-plugin-health * You need to have the Google Services API downloaded in your SDK * Be sure to give your app access to the Google Fitness API, see [this](https://developers.google.com/fit/android/get-api-key) and [this](https://github.com/2dvisio/cordova-plugin-googlefit#sdk-requirements-for-compiling-the-plugin) * If you are wondering what key your compiled app is using, you can type `keytool -list -printcert -jarfile yourapp.apk` - -Some more detailed instructions are provided [here](https://github.com/2dvisio/cordova-plugin-googlefit) - +* You can use the Google Fitness API even if the user doesn't have Google Fit installed, but there has to be some other fitness app putting data into the Fitness API otherwise your queries will always be empty. See the [the original documentation](https://developers.google.com/fit/overview). ## Supported data types From f947f1e811a4af040fc65ed8664541282ca498b5 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 8 Dec 2016 13:49:42 +0000 Subject: [PATCH 027/157] use latest version of fitness API may solve #27 too --- plugin.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.xml b/plugin.xml index 1ad81f6b..f3a54aed 100755 --- a/plugin.xml +++ b/plugin.xml @@ -74,7 +74,7 @@ - + From fd97538abe9a317583112406186b70e7d0f88088 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 9 Dec 2016 09:33:17 +0000 Subject: [PATCH 028/157] allowing latest version of Play Services should fix #27 --- README.md | 1 + plugin.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7304be95..5daddebb 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ cordova plugin add cordova-plugin-health * Be sure to give your app access to the Google Fitness API, see [this](https://developers.google.com/fit/android/get-api-key) and [this](https://github.com/2dvisio/cordova-plugin-googlefit#sdk-requirements-for-compiling-the-plugin) * If you are wondering what key your compiled app is using, you can type `keytool -list -printcert -jarfile yourapp.apk` * You can use the Google Fitness API even if the user doesn't have Google Fit installed, but there has to be some other fitness app putting data into the Fitness API otherwise your queries will always be empty. See the [the original documentation](https://developers.google.com/fit/overview). +* This plugin is set to use the latest version of the Google Play Services API (``). This is done to likely guarantee the compatibility with other plugins using Google Play Services and not incurr in the , but bear in mind that a) the plugin was tested until version 9.4.0 of the APIs and b) other plugins may be using a different version of the API. If you run into an issue, check the generated gradle file (build.gradle) under `dependencies` between `// SUB-PROJECT DEPENDENCIES START` and `// SUB-PROJECT DEPENDENCIES END` and amke sure that all versions of the `com.google.android.gms:play-services-xxxx` are the same. ## Supported data types diff --git a/plugin.xml b/plugin.xml index f3a54aed..06817aca 100755 --- a/plugin.xml +++ b/plugin.xml @@ -74,7 +74,7 @@ - + From e2a4795017e392e4432c52cfd352d5b553acb6e9 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 9 Dec 2016 09:36:28 +0000 Subject: [PATCH 029/157] typo in the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5daddebb..09cb9523 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ cordova plugin add cordova-plugin-health * Be sure to give your app access to the Google Fitness API, see [this](https://developers.google.com/fit/android/get-api-key) and [this](https://github.com/2dvisio/cordova-plugin-googlefit#sdk-requirements-for-compiling-the-plugin) * If you are wondering what key your compiled app is using, you can type `keytool -list -printcert -jarfile yourapp.apk` * You can use the Google Fitness API even if the user doesn't have Google Fit installed, but there has to be some other fitness app putting data into the Fitness API otherwise your queries will always be empty. See the [the original documentation](https://developers.google.com/fit/overview). -* This plugin is set to use the latest version of the Google Play Services API (``). This is done to likely guarantee the compatibility with other plugins using Google Play Services and not incurr in the , but bear in mind that a) the plugin was tested until version 9.4.0 of the APIs and b) other plugins may be using a different version of the API. If you run into an issue, check the generated gradle file (build.gradle) under `dependencies` between `// SUB-PROJECT DEPENDENCIES START` and `// SUB-PROJECT DEPENDENCIES END` and amke sure that all versions of the `com.google.android.gms:play-services-xxxx` are the same. +* This plugin is set to use the latest version of the Google Play Services API (``). This is done to likely guarantee the compatibility with other plugins using Google Play Services, but bear in mind that a) the plugin was tested until version 9.4.0 of the APIs and b) other plugins may be using a different version of the API. If you run into an issue, check the generated gradle file (build.gradle) under `dependencies` between `// SUB-PROJECT DEPENDENCIES START` and `// SUB-PROJECT DEPENDENCIES END` and amke sure that all versions of the `com.google.android.gms:play-services-xxxx` are the same. ## Supported data types From 485f2d85a4a01e8a190d95e7c780f46a9c3c06aa Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 12 Dec 2016 16:42:18 +0000 Subject: [PATCH 030/157] release 0.8.1 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 41393776..0ab80222 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.8.0", + "version": "0.8.1", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 06817aca..96466419 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.8.1"> Cordova Health From 665427bf26ea29dd1a09d7245c2c5d6144ddbcb7 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 16 Dec 2016 09:56:58 +0000 Subject: [PATCH 031/157] added comment about overwriting datapoints --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 09cb9523..18885e77 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,7 @@ navigator.health.store({ Quirks of store() +- Google Fit doesn't allow you to overwrite data points that overlap with others already stored of the same type (see [here](https://developers.google.com/fit/android/history#manageConflicting)). At the moment there is no support for [update](https://developers.google.com/fit/android/history#updateData) nor delete. - In iOS you cannot store the total calories, you need to specify either basal or active. If you use total calories, the active ones will be stored. - In Android you can only store active calories, as the basal are estimated automatically. If you store total calories, these will be treated as active. - In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. From 6e2be28f86994516c0fba392a877411c9b0a4dbd Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 20 Dec 2016 11:05:49 +0000 Subject: [PATCH 032/157] added isAuthorized() for Fit --- README.md | 14 +++ src/android/HealthPlugin.java | 171 ++++++++++++++++++++++------------ 2 files changed, 126 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 18885e77..5f424d41 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,20 @@ Quirks of requestAuthorization() - In Android, be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the authorization will ensure that the app is connected again. - In Android 6 and over, this function will also ask for some dynamic permissions if needed (e.g. in the case of "distance", it will need access to ACCESS_FINE_LOCATION). +### isAuthorized() + +Check if the app has authorization to read/write a set of datatypes. +This function is similar to requestAuthorization() and has similar quirks. + +``` +navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) +``` + +- datatypes: {type: Array of String}, a list of data types you want to be granted access to +- successCallback: {type: function(authorized)}, if the argument is true, the app is authorized +- errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem + + ### query() Gets all the records of a certain data type within a certain time window. diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 7e4372d2..40ba26a1 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -57,13 +57,14 @@ public class HealthPlugin extends CordovaPlugin { //calling activity private CordovaInterface cordova; - //actual Google API client private GoogleApiClient mClient; - public static final int REQUEST_OAUTH = 1; - public static final LinkedList dynPerms = new LinkedList(); - public static final int REQUEST_DYN_PERMS = 2; + private static final int REQUEST_OAUTH = 1; + private CallbackContext authReqCallbackCtx; + private boolean authAutoresolve = false; + private static final LinkedList dynPerms = new LinkedList(); + private static final int REQUEST_DYN_PERMS = 2; //Scope for read/write access to activity-related data types in Google Fit. //These include activity type, calories consumed and expended, step counts, and others. @@ -150,7 +151,6 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) { this.cordova = cordova; } - private CallbackContext authReqCallbackCtx; private void authReqSuccess() { //Create custom data types @@ -195,9 +195,10 @@ public void run() { }); } - public void requestDynamicPermissions() { + private void requestDynamicPermissions() { if (dynPerms.isEmpty()) { - authReqCallbackCtx.success(); + // nothing to be done + authReqCallbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, true)); } else { LinkedList perms = new LinkedList(); for (String p : dynPerms) { @@ -206,13 +207,19 @@ public void requestDynamicPermissions() { } } if (perms.isEmpty()) { - authReqCallbackCtx.success(); + // nothing to be done + authReqCallbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, true)); } else { - cordova.requestPermissions(this, REQUEST_DYN_PERMS, perms.toArray(new String[perms.size()])); + if (authAutoresolve) { + cordova.requestPermissions(this, REQUEST_DYN_PERMS, perms.toArray(new String[perms.size()])); + } else { + authReqCallbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, false)); + } } } } + @Override public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException { if (requestCode == REQUEST_DYN_PERMS) { for (int i = 0; i < grantResults.length; i++) { @@ -230,6 +237,7 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, int } } + @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { if (requestCode == REQUEST_OAUTH) { if (resultCode == Activity.RESULT_OK) { @@ -260,11 +268,32 @@ public boolean execute(String action, final JSONArray args, final CallbackContex if ("isAvailable".equals(action)) { isAvailable(callbackContext); return true; - } else if("promptInstallFit".equals(action)) { + } else if ("promptInstallFit".equals(action)) { promptInstall(callbackContext); return true; - } else if ("requestAuthorization".equals(action)) { - requestAuthorization(args, callbackContext); + } else if ("checkAuthorization".equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + @Override + public void run() { + try { + checkAuthorization(args, callbackContext, true); + } catch (Exception ex) { + callbackContext.error(ex.getMessage()); + } + } + }); + return true; + } else if ("isAuthorized".equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + @Override + public void run() { + try { + checkAuthorization(args, callbackContext, false); + } catch (Exception ex) { + callbackContext.error(ex.getMessage()); + } + } + }); return true; } else if ("query".equals(action)) { cordova.getThreadPool().execute(new Runnable() { @@ -291,7 +320,16 @@ public void run() { }); return true; } else if ("store".equals(action)) { - store(args, callbackContext); + cordova.getThreadPool().execute(new Runnable() { + @Override + public void run() { + try { + store(args, callbackContext); + } catch (Exception ex) { + callbackContext.error(ex.getMessage()); + } + } + }); return true; } @@ -346,9 +384,10 @@ private void promptInstall(final CallbackContext callbackContext) { callbackContext.success(); } - private void requestAuthorization(final JSONArray args, final CallbackContext callbackContext) throws JSONException { + private void checkAuthorization(final JSONArray args, final CallbackContext callbackContext, final boolean autoresolve) throws JSONException { this.cordova.setActivityResultCallback(this); authReqCallbackCtx = callbackContext; + authAutoresolve = autoresolve; //reset scopes boolean bodyscope = false; @@ -398,7 +437,7 @@ public void onConnectionSuspended(int i) { message = "connection lost, service disconnected"; } else message = "connection lost, code: " + i; Log.e(TAG, message); - callbackContext.error(message); + authReqCallbackCtx.error(message); } }); @@ -406,20 +445,29 @@ public void onConnectionSuspended(int i) { new GoogleApiClient.OnConnectionFailedListener() { @Override public void onConnectionFailed(ConnectionResult result) { - Log.i(TAG, "Connection failed, cause: " + result.toString()); + Log.i(TAG, "Connection to Fit failed, cause: " + result.getErrorMessage()); if (!result.hasResolution()) { Log.e(TAG, "Connection failure has no resolution: " + result.getErrorMessage()); - callbackContext.error(result.getErrorMessage()); + authReqCallbackCtx.error(result.getErrorMessage()); + return; } else { - // The failure has a resolution. Resolve it. - // Called typically when the app is not yet authorized, and an - // authorization dialog is displayed to the user. - try { - Log.i(TAG, "Attempting to resolve failed connection"); - result.startResolutionForResult(cordova.getActivity(), REQUEST_OAUTH); - } catch (IntentSender.SendIntentException e) { - Log.e(TAG, "Exception while starting resolution activity", e); - callbackContext.error(result.getErrorMessage()); + if (authAutoresolve) { + // The failure has a resolution. Resolve it. + // Called typically when the app is not yet authorized, and an + // authorization dialog is displayed to the user. + try { + Log.i(TAG, "Attempting to resolve failed connection"); + result.startResolutionForResult(cordova.getActivity(), REQUEST_OAUTH); + } catch (IntentSender.SendIntentException e) { + Log.e(TAG, "Exception while starting resolution activity", e); + authReqCallbackCtx.error(result.getErrorMessage()); + return; + } + } else { + // probably not authorized, send false + Log.d(TAG, "Connection to Fit failed, probably because of authorization, giving up now"); + authReqCallbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, false)); + return; } } } @@ -429,6 +477,7 @@ public void onConnectionFailed(ConnectionResult result) { mClient.connect(); } + private boolean lightConnect() { this.cordova.setActivityResultCallback(this); @@ -523,9 +572,9 @@ private void query(final JSONArray args, final CallbackContext callbackContext) DataSource dataSource = datapoint.getOriginalDataSource(); if (dataSource != null) { String sourceName = dataSource.getName(); - if(sourceName != null) obj.put("sourceName", sourceName); + if (sourceName != null) obj.put("sourceName", sourceName); String sourceBundleId = dataSource.getAppPackageName(); - if(sourceBundleId != null) obj.put("sourceBundleId", sourceBundleId); + if (sourceBundleId != null) obj.put("sourceBundleId", sourceBundleId); } //reference for fields: https://developers.google.com/android/reference/com/google/android/gms/fitness/data/Field.html @@ -542,20 +591,24 @@ private void query(final JSONArray args, final CallbackContext callbackContext) obj.put("value", distance); obj.put("unit", "ml");// documentation says it's litres, but from experiments I get ml } else if (DT.equals(DataType.TYPE_NUTRITION)) { - if(datatype.equalsIgnoreCase("nutrition")) { + if (datatype.equalsIgnoreCase("nutrition")) { JSONObject dob = new JSONObject(); - if(datapoint.getValue(Field.FIELD_FOOD_ITEM) != null){ + if (datapoint.getValue(Field.FIELD_FOOD_ITEM) != null) { dob.put("item", datapoint.getValue(Field.FIELD_FOOD_ITEM).asString()); } - if(datapoint.getValue(Field.FIELD_MEAL_TYPE) != null){ + if (datapoint.getValue(Field.FIELD_MEAL_TYPE) != null) { int mealt = datapoint.getValue(Field.FIELD_MEAL_TYPE).asInt(); - if(mealt == Field.MEAL_TYPE_BREAKFAST) dob.put("meal_type", "breakfast"); - else if(mealt == Field.MEAL_TYPE_DINNER) dob.put("meal_type", "dinner"); - else if(mealt == Field.MEAL_TYPE_LUNCH) dob.put("meal_type", "lunch"); - else if(mealt == Field.MEAL_TYPE_SNACK) dob.put("meal_type", "snack"); + if (mealt == Field.MEAL_TYPE_BREAKFAST) + dob.put("meal_type", "breakfast"); + else if (mealt == Field.MEAL_TYPE_DINNER) + dob.put("meal_type", "dinner"); + else if (mealt == Field.MEAL_TYPE_LUNCH) + dob.put("meal_type", "lunch"); + else if (mealt == Field.MEAL_TYPE_SNACK) + dob.put("meal_type", "snack"); else dob.put("meal_type", "unknown"); } - if(datapoint.getValue(Field.FIELD_NUTRIENTS) != null){ + if (datapoint.getValue(Field.FIELD_NUTRIENTS) != null) { Value v = datapoint.getValue(Field.FIELD_NUTRIENTS); dob.put("nutrients", getNutrients(v, null)); } @@ -624,7 +677,7 @@ private void query(final JSONArray args, final CallbackContext callbackContext) private JSONObject getNutrients(Value nutrientsMap, JSONObject mergewith) throws JSONException { JSONObject nutrients; - if(mergewith != null){ + if (mergewith != null) { nutrients = mergewith; } else { nutrients = new JSONObject(); @@ -652,17 +705,17 @@ private JSONObject getNutrients(Value nutrientsMap, JSONObject mergewith) throws } private void mergeNutrient(String f, Value nutrientsMap, JSONObject nutrients) throws JSONException { - if(nutrientsMap.getKeyValue(f) != null) { + if (nutrientsMap.getKeyValue(f) != null) { String n = null; - for(String name : nutrientFields.keySet()){ - if(nutrientFields.get(name).field.equalsIgnoreCase(f)){ + for (String name : nutrientFields.keySet()) { + if (nutrientFields.get(name).field.equalsIgnoreCase(f)) { n = name; break; } } - if(n != null) { + if (n != null) { float val = nutrientsMap.getKeyValue(f); - if(nutrients.has(n)) { + if (nutrients.has(n)) { val += nutrients.getDouble(n); } nutrients.put(n, val); @@ -693,9 +746,9 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac String bucketType = ""; if (hasbucket) { bucketType = args.getJSONObject(0).getString("bucket"); - if(!bucketType.equalsIgnoreCase("hour") && !bucketType.equalsIgnoreCase("day")) { + if (!bucketType.equalsIgnoreCase("hour") && !bucketType.equalsIgnoreCase("day")) { customBuckets = true; - if(!bucketType.equalsIgnoreCase("week") && !bucketType.equalsIgnoreCase("month") && ! bucketType.equalsIgnoreCase("year")){ + if (!bucketType.equalsIgnoreCase("week") && !bucketType.equalsIgnoreCase("month") && !bucketType.equalsIgnoreCase("year")) { // error callbackContext.error("Bucket type " + bucketType + " not recognised"); return; @@ -753,9 +806,9 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac // so we query over a week then we take the average float basalAVG = 0; if (datatype.equalsIgnoreCase("calories.basal")) { - try{ + try { basalAVG = getBasalAVG(_et); - } catch (Exception ex){ + } catch (Exception ex) { callbackContext.error(ex.getMessage()); return; } @@ -767,7 +820,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac int allms = (int) (et - st); if (datatype.equalsIgnoreCase("steps")) { - if (args.getJSONObject(0).has("filtered") && args.getJSONObject(0).getBoolean("filtered")){ + if (args.getJSONObject(0).has("filtered") && args.getJSONObject(0).getBoolean("filtered")) { // exceptional case for filtered steps DataSource filteredStepsSource = new DataSource.Builder() .setDataType(DataType.TYPE_STEP_COUNT_DELTA) @@ -783,7 +836,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac builder.aggregate(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA); } else if (datatype.equalsIgnoreCase("calories")) { builder.aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED); - } else if(datatype.equalsIgnoreCase("calories.basal")) { + } else if (datatype.equalsIgnoreCase("calories.basal")) { builder.aggregate(DataType.TYPE_BASAL_METABOLIC_RATE, DataType.AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY); } else if (datatype.equalsIgnoreCase("activity")) { builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); @@ -851,7 +904,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac retBucket.put("unit", "activitySummary"); } else if (datatype.equalsIgnoreCase("nutrition.water")) { retBucket.put("unit", "ml"); - } else if(datatype.equalsIgnoreCase("nutrition")) { + } else if (datatype.equalsIgnoreCase("nutrition")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "nutrition"); } else if (nutritiondatatypes.get(datatype) != null) { @@ -879,7 +932,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac retBucket.put("endDate", bucket.getEndTime(TimeUnit.MILLISECONDS)); retBucketsArr.put(retBucket); } - if(!retBucket.has("value")) { + if (!retBucket.has("value")) { retBucket.put("value", 0); if (datatype.equalsIgnoreCase("steps")) { retBucket.put("unit", "count"); @@ -892,7 +945,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac retBucket.put("unit", "activitySummary"); } else if (datatype.equalsIgnoreCase("nutrition.water")) { retBucket.put("unit", "ml"); - } else if(datatype.equalsIgnoreCase("nutrition")) { + } else if (datatype.equalsIgnoreCase("nutrition")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "nutrition"); } else if (nutritiondatatypes.get(datatype) != null) { @@ -924,13 +977,13 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac float ncal = datapoint.getValue(Field.FIELD_AVERAGE).asFloat(); double ocal = retBucket.getDouble("value"); retBucket.put("value", ocal + ncal); - } else if(datatype.equalsIgnoreCase("nutrition.water")) { + } else if (datatype.equalsIgnoreCase("nutrition.water")) { float nwat = datapoint.getValue(Field.FIELD_VOLUME).asFloat(); double owat = retBucket.getDouble("value"); retBucket.put("value", owat + nwat); - } else if(datatype.equalsIgnoreCase("nutrition")) { + } else if (datatype.equalsIgnoreCase("nutrition")) { JSONObject nutrsob = retBucket.getJSONObject("value"); - if(datapoint.getValue(Field.FIELD_NUTRIENTS) != null){ + if (datapoint.getValue(Field.FIELD_NUTRIENTS) != null) { nutrsob = getNutrients(datapoint.getValue(Field.FIELD_NUTRIENTS), nutrsob); } retBucket.put("value", nutrsob); @@ -960,15 +1013,15 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } } //end of data set loop - if (datatype.equalsIgnoreCase("calories.basal")){ + if (datatype.equalsIgnoreCase("calories.basal")) { double basals = retBucket.getDouble("value"); - if(!atleastone) { + if (!atleastone) { //when no basal is available, use the daily average basals += basalAVG; retBucket.put("value", basals); } // if the bucket is not daily, it needs to be normalised - if(!hasbucket || bucketType.equalsIgnoreCase("hour")){ + if (!hasbucket || bucketType.equalsIgnoreCase("hour")) { long sst = retBucket.getLong("startDate"); long eet = retBucket.getLong("endDate"); basals = (basals / (24 * 60 * 60 * 1000)) * (eet - sst); @@ -976,10 +1029,10 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } } // end of buckets loop - for(int i=0; i< retBucketsArr.length(); i++){ + for (int i = 0; i < retBucketsArr.length(); i++) { long _sss = retBucketsArr.getJSONObject(i).getLong("startDate"); long _eee = retBucketsArr.getJSONObject(i).getLong("endDate"); - Log.d(TAG, "Bucket: "+ new Date(_sss) + " " + new Date(_eee)); + Log.d(TAG, "Bucket: " + new Date(_sss) + " " + new Date(_eee)); } if (hasbucket) callbackContext.success(retBucketsArr); else callbackContext.success(retBucket); From 6b26adad19939e78e5cbed37427d06416a319b80 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 20 Dec 2016 13:03:34 +0000 Subject: [PATCH 033/157] implements isAuthorized for iOS too fixes #31 fixes #29 --- src/ios/HealthKit.m | 8 +++++++- www/ios/health.js | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index 26d16c62..9292df30 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -570,7 +570,13 @@ - (void)checkAuthStatus:(CDVInvokedUrlCommand *)command { // if a user grants/denies read access, *only* write access. NSMutableDictionary *args = command.arguments[0]; NSString *checkType = args[HKPluginKeyType]; - HKObjectType *type = [HealthKit getHKObjectType:checkType]; + HKObjectType *type; + + if ([checkType isEqual:@"HKWorkoutTypeIdentifier"]) { + type = [HKObjectType workoutType]; + } else { + type = [HealthKit getHKObjectType:checkType]; + } __block HealthKit *bSelf = self; [self checkAuthStatusWithCallbackId:command.callbackId forType:type andCompletion:^(CDVPluginResult *result, NSString *callbackId) { diff --git a/www/ios/health.js b/www/ios/health.js index 58c744ec..9940161a 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -68,7 +68,7 @@ Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); }; -Health.prototype.requestAuthorization = function (dts, onSuccess, onError) { +var prepareDatatype4Auth = function (dts, success, error) { var HKdatatypes = []; for (var i = 0; i < dts.length; i++) { if ((dts[i] !== 'gender') && (dts[i] !== 'date_of_birth')) { // ignore gender and DOB @@ -83,19 +83,42 @@ Health.prototype.requestAuthorization = function (dts, onSuccess, onError) { if (dts[i] === 'activity') HKdatatypes.push('HKCategoryTypeIdentifierSleepAnalysis'); if (dts[i] === 'calories') HKdatatypes.push('HKQuantityTypeIdentifierBasalEnergyBurned'); } else { - onError('unknown data type ' + dts[i]); + error('unknown data type ' + dts[i]); return; } } } - if (HKdatatypes.length) { - window.plugins.healthkit.requestAuthorization({ - 'readTypes': HKdatatypes, - 'writeTypes': HKdatatypes - }, onSuccess, onError); - } else onSuccess(); + success(HKdatatypes); +} + +Health.prototype.requestAuthorization = function (dts, onSuccess, onError) { + prepareDatatype4Auth(dts, function (HKdatatypes) { + if (HKdatatypes.length) { + window.plugins.healthkit.requestAuthorization({ + 'readTypes': HKdatatypes, + 'writeTypes': HKdatatypes + }, onSuccess, onError); + } else onSuccess(); + }, onError); }; +Health.prototype.isAuthorized = function(dts, onSuccess, onError) { + prepareDatatype4Auth(dts, function (HKdatatypes) { + var check = function () { + if (HKdatatypes.length > 0) { + var dt = HKdatatypes.shift(); + window.plugins.healthkit.checkAuthStatus({ + type: dt + }, function (auth) { + if (auth === 'authorized') check(); + else onSuccess(false); + }, onError); + } else onSuccess(true); + } + check(); + }, onError); +} + Health.prototype.query = function (opts, onSuccess, onError) { var startD = opts.startDate; var endD = opts.endDate; From cb48eca49452c354b5400f02e3fab45a17873e8c Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 20 Dec 2016 13:10:47 +0000 Subject: [PATCH 034/157] fixes #28 --- src/ios/HealthKit.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index 9292df30..18f0ee90 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -1266,7 +1266,7 @@ - (void)querySampleType:(CDVInvokedUrlCommand *)command { NSDate *endDate = [NSDate dateWithTimeIntervalSince1970:[args[HKPluginKeyEndDate] longValue]]; NSString *sampleTypeString = args[HKPluginKeySampleType]; NSString *unitString = args[HKPluginKeyUnit]; - NSUInteger limit = ((args[@"limit"] != nil) ? [args[@"limit"] unsignedIntegerValue] : 100); + NSUInteger limit = ((args[@"limit"] != nil) ? [args[@"limit"] unsignedIntegerValue] : 1000); BOOL ascending = (args[@"ascending"] != nil && [args[@"ascending"] boolValue]); HKSampleType *type = [HealthKit getHKSampleType:sampleTypeString]; From a519dbdcb6f5d0cbb74fb67c15e96504d81e1289 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 20 Dec 2016 13:14:56 +0000 Subject: [PATCH 035/157] fixes #33 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5f424d41..43ce32eb 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,8 @@ navigator.health.query({ Quirks of query() +- In iOS, the amount of datapoints is limited to 1000 by default. You can override this by adding a `limit: xxx` to your query object. +- In iOS, datapoints are ordered in an descending fashion (from newer to older). You can revert this behaviour by adding `ascending: true` to your query object. - In Android it is possible to query for "raw" steps or to select those as filtered by the Google Fit app. In the latter case the query object must contain the field `filtered: true`. - In Google Fit calories.basal is returned as an average per day, and usually is not available in all days (may be not available in time windows smaller than 5 days or more). - In Google Fit calories.active is computed by subtracting the basal calories from the total. As basal energy expenditure, an average is computed from the week before endDate. From cc3b13cd031ddb8e24e121b18171a9814b76848d Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 20 Dec 2016 13:16:15 +0000 Subject: [PATCH 036/157] release 0.8.2 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0ab80222..77f1e578 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.8.1", + "version": "0.8.2", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 96466419..cabbb096 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.8.2"> Cordova Health From b1f1276803b225cf259ac64d2c685f139639c696 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 22 Dec 2016 13:42:31 +0000 Subject: [PATCH 037/157] fixes #34 --- src/android/HealthPlugin.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 40ba26a1..acdbd9b4 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -271,7 +271,8 @@ public boolean execute(String action, final JSONArray args, final CallbackContex } else if ("promptInstallFit".equals(action)) { promptInstall(callbackContext); return true; - } else if ("checkAuthorization".equals(action)) { + + } else if ("requestAuthorization".equals(action)) { cordova.getThreadPool().execute(new Runnable() { @Override public void run() { @@ -283,6 +284,18 @@ public void run() { } }); return true; + } else if ("checkAuthorization".equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + @Override + public void run() { + try { + checkAuthorization(args, callbackContext, false); + } catch (Exception ex) { + callbackContext.error(ex.getMessage()); + } + } + }); + return true; } else if ("isAuthorized".equals(action)) { cordova.getThreadPool().execute(new Runnable() { @Override From 9492b61b94acd3dd94170ef5e230c982a03bc63b Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 3 Jan 2017 11:38:36 +0000 Subject: [PATCH 038/157] fixes #35 --- README.md | 10 +++++----- plugin.xml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 43ce32eb..e991e278 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,15 @@ cordova plugin add cordova-plugin-health * Make sure your app id has the 'HealthKit' entitlement when this plugin is installed (see iOS dev center). * Also, make sure your app and AppStore description complies with these Apple review guidelines: https://developer.apple.com/app-store/review/guidelines/#healthkit -* There are [two keys](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW48) to the info.plist file, `NSHealthShareUsageDescription` and `NSHealthUpdateUsageDescription` that are assigned with an empty string by default by the plugin. You may want to put a better description there. +* There are [two keys](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW48) to be added to the info.plist file: `NSHealthShareUsageDescription` and `NSHealthUpdateUsageDescription`. These are assigned with a default string by the plugin, but you may want to contextualise them for your app. ## Requirements for Android apps -* You need to have the Google Services API downloaded in your SDK -* Be sure to give your app access to the Google Fitness API, see [this](https://developers.google.com/fit/android/get-api-key) and [this](https://github.com/2dvisio/cordova-plugin-googlefit#sdk-requirements-for-compiling-the-plugin) -* If you are wondering what key your compiled app is using, you can type `keytool -list -printcert -jarfile yourapp.apk` +* You need to have the Google Services API downloaded in your SDK. +* Be sure to give your app access to the Google Fitness API, see [this](https://developers.google.com/fit/android/get-api-key) and [this](https://github.com/2dvisio/cordova-plugin-googlefit#sdk-requirements-for-compiling-the-plugin). +* If you are wondering what key your compiled app is using, you can type `keytool -list -printcert -jarfile yourapp.apk`. * You can use the Google Fitness API even if the user doesn't have Google Fit installed, but there has to be some other fitness app putting data into the Fitness API otherwise your queries will always be empty. See the [the original documentation](https://developers.google.com/fit/overview). -* This plugin is set to use the latest version of the Google Play Services API (``). This is done to likely guarantee the compatibility with other plugins using Google Play Services, but bear in mind that a) the plugin was tested until version 9.4.0 of the APIs and b) other plugins may be using a different version of the API. If you run into an issue, check the generated gradle file (build.gradle) under `dependencies` between `// SUB-PROJECT DEPENDENCIES START` and `// SUB-PROJECT DEPENDENCIES END` and amke sure that all versions of the `com.google.android.gms:play-services-xxxx` are the same. +* This plugin is set to use the latest version of the Google Play Services API (``). This is done to likely guarantee the compatibility with other plugins using Google Play Services, but bear in mind that a) the plugin was tested until version 9.8.0 of the APIs and b) other plugins may be using a different version of the API. If you run into an issue, check the generated gradle file (build.gradle) under `dependencies` between `// SUB-PROJECT DEPENDENCIES START` and `// SUB-PROJECT DEPENDENCIES END` and make sure that all versions of the `com.google.android.gms:play-services-xxxx` are the same. ## Supported data types diff --git a/plugin.xml b/plugin.xml index cabbb096..8e838486 100755 --- a/plugin.xml +++ b/plugin.xml @@ -48,10 +48,10 @@ - + This app needs to access read your health data - + This app needs to access update your health data From 7f559227648d12b46f55d858d05d58527f475fb6 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 3 Jan 2017 11:38:51 +0000 Subject: [PATCH 039/157] just some comments --- src/android/HealthPlugin.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index acdbd9b4..7ccb292a 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -271,13 +271,12 @@ public boolean execute(String action, final JSONArray args, final CallbackContex } else if ("promptInstallFit".equals(action)) { promptInstall(callbackContext); return true; - } else if ("requestAuthorization".equals(action)) { cordova.getThreadPool().execute(new Runnable() { @Override public void run() { try { - checkAuthorization(args, callbackContext, true); + checkAuthorization(args, callbackContext, true); // with autoresolve } catch (Exception ex) { callbackContext.error(ex.getMessage()); } @@ -289,7 +288,7 @@ public void run() { @Override public void run() { try { - checkAuthorization(args, callbackContext, false); + checkAuthorization(args, callbackContext, false); // without autoresolve } catch (Exception ex) { callbackContext.error(ex.getMessage()); } From 417b21dce7067833f3e8d69decd62e3f7c4a9298 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 3 Jan 2017 11:39:58 +0000 Subject: [PATCH 040/157] release 0.8.3 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 77f1e578..edf5d883 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.8.2", + "version": "0.8.3", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 8e838486..67036103 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.8.3"> Cordova Health From 783b7051cc53f5a31343cb5eafaa02b7a09840bc Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 12 Jan 2017 15:01:54 +0000 Subject: [PATCH 041/157] stupid replication fixes #35 --- plugin.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugin.xml b/plugin.xml index 67036103..87fc490a 100755 --- a/plugin.xml +++ b/plugin.xml @@ -54,14 +54,6 @@ This app needs to access update your health data - - - - - - - - From 511a1320bd5e077993206fc8b08ecb3ddfa75ff7 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 23 Jan 2017 14:29:58 +0000 Subject: [PATCH 042/157] few corrections in the readme --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e991e278..81ae3d9a 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ Check if the app has authorization to read/write a set of datatypes. This function is similar to requestAuthorization() and has similar quirks. ``` -navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) +navigator.health.isAuthorized(datatypes, successCallback, errorCallback) ``` - datatypes: {type: Array of String}, a list of data types you want to be granted access to @@ -174,7 +174,7 @@ navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) ### query() -Gets all the records of a certain data type within a certain time window. +Gets all the data points of a certain data type within a certain time window. Warning: if the time span is big, it can generate long arrays! @@ -197,14 +197,14 @@ Quirks of query() - In iOS, the amount of datapoints is limited to 1000 by default. You can override this by adding a `limit: xxx` to your query object. - In iOS, datapoints are ordered in an descending fashion (from newer to older). You can revert this behaviour by adding `ascending: true` to your query object. -- In Android it is possible to query for "raw" steps or to select those as filtered by the Google Fit app. In the latter case the query object must contain the field `filtered: true`. -- In Google Fit calories.basal is returned as an average per day, and usually is not available in all days (may be not available in time windows smaller than 5 days or more). -- In Google Fit calories.active is computed by subtracting the basal calories from the total. As basal energy expenditure, an average is computed from the week before endDate. -- While Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input. +- In Android, it is possible to query for "raw" steps or to select those as filtered by the Google Fit app. In the latter case the query object must contain the field `filtered: true`. +- In Google Fit, calories.basal is returned as an average per day, and usually is not available in all days. +- In Google Fit, calories.active is computed by subtracting the basal calories from the total. As basal energy expenditure, an average is computed from the week before endDate. +- While Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input from some app. - When querying for activities, Google Fit is able to determine some activities automatically (still, walking, running, biking, in vehicle), while HealthKit only relies on the input of the user or of some external app. - When querying for activities, calories and distance are also provided in HealthKit (units are kcal and meters) and never in Google Fit. - When querying for nutrition, Google Fit always returns all the nutrition elements it has, while HealthKit returns only those that are stored as correlation. To be sure to get all stored the quantities (regardless of they are stored as correlation or not), it's better to query single nutrients. -- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. This is because conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). +- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. Automatic conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). ### queryAggregated() From e6de29d01a0baa1ad8a6af29c2b5e81ba6ae5a7e Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 17 Feb 2017 15:08:54 +0000 Subject: [PATCH 043/157] get calories when querying aggregated activities but only when no time bucket is specified --- src/android/HealthPlugin.java | 46 ++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 7ccb292a..6b9e4907 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -69,7 +69,6 @@ public class HealthPlugin extends CordovaPlugin { //Scope for read/write access to activity-related data types in Google Fit. //These include activity type, calories consumed and expended, step counts, and others. public static Map activitydatatypes = new HashMap(); - static { activitydatatypes.put("steps", DataType.TYPE_STEP_COUNT_DELTA); activitydatatypes.put("calories", DataType.TYPE_CALORIES_EXPENDED); @@ -79,7 +78,6 @@ public class HealthPlugin extends CordovaPlugin { //Scope for read/write access to biometric data types in Google Fit. These include heart rate, height, and weight. public static Map bodydatatypes = new HashMap(); - static { bodydatatypes.put("height", DataType.TYPE_HEIGHT); bodydatatypes.put("weight", DataType.TYPE_WEIGHT); @@ -89,7 +87,6 @@ public class HealthPlugin extends CordovaPlugin { //Scope for read/write access to location-related data types in Google Fit. These include location, distance, and speed. public static Map locationdatatypes = new HashMap(); - static { locationdatatypes.put("distance", DataType.TYPE_DISTANCE_DELTA); } @@ -851,7 +848,12 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("calories.basal")) { builder.aggregate(DataType.TYPE_BASAL_METABOLIC_RATE, DataType.AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY); } else if (datatype.equalsIgnoreCase("activity")) { - builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); + if(hasbucket) { + builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); + } else { + builder.aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED); + //here we could also get the distance: builder.aggregate(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA); + } } else if (datatype.equalsIgnoreCase("nutrition.water")) { builder.aggregate(DataType.TYPE_HYDRATION, DataType.AGGREGATE_HYDRATION); } else if (nutritiondatatypes.get(datatype) != null) { @@ -871,7 +873,11 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac builder.bucketByTime(1, TimeUnit.DAYS); } } else { - builder.bucketByTime(allms, TimeUnit.MILLISECONDS); + if (datatype.equalsIgnoreCase("activity")) { + builder.bucketByActivityType(1, TimeUnit.MILLISECONDS); + } else { + builder.bucketByTime(allms, TimeUnit.MILLISECONDS); + } } DataReadRequest readRequest = builder.build(); @@ -925,6 +931,28 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } for (Bucket bucket : dataReadResult.getBuckets()) { + + // special case of the activity without time buckets + // here the buckets contain activities and the datapoints contain calories + if(datatype.equalsIgnoreCase("activity") && !hasbucket){ + String activity = bucket.getActivity(); + float calories = 0; + int duration = (int) (bucket.getEndTime(TimeUnit.MILLISECONDS) - bucket.getStartTime(TimeUnit.MILLISECONDS)); + for (DataSet dataset : bucket.getDataSets()) { + for (DataPoint datapoint : dataset.getDataPoints()) { + calories += datapoint.getValue(Field.FIELD_CALORIES).asFloat(); + } + } + JSONObject actobj = retBucket.getJSONObject("value"); + JSONObject summary = new JSONObject(); + summary.put("duration", duration); + summary.put("calories", calories); + actobj.put(activity, summary); + retBucket.put("value", actobj); + // jump to the next iteration + continue; + } + if (hasbucket) { if (customBuckets) { //find the bucket among customs @@ -968,6 +996,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } } + // aggregate data points over the bucket boolean atleastone = false; for (DataSet dataset : bucket.getDataSets()) { @@ -1009,9 +1038,9 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } else if (datatype.equalsIgnoreCase("activity")) { String activity = datapoint.getValue(Field.FIELD_ACTIVITY).asActivity(); + int ndur = datapoint.getValue(Field.FIELD_DURATION).asInt(); JSONObject actobj = retBucket.getJSONObject("value"); JSONObject summary; - int ndur = datapoint.getValue(Field.FIELD_DURATION).asInt(); if (actobj.has(activity)) { summary = actobj.getJSONObject(activity); int odur = summary.getInt("duration"); @@ -1041,11 +1070,6 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } } // end of buckets loop - for (int i = 0; i < retBucketsArr.length(); i++) { - long _sss = retBucketsArr.getJSONObject(i).getLong("startDate"); - long _eee = retBucketsArr.getJSONObject(i).getLong("endDate"); - Log.d(TAG, "Bucket: " + new Date(_sss) + " " + new Date(_eee)); - } if (hasbucket) callbackContext.success(retBucketsArr); else callbackContext.success(retBucket); } else { From 6373d3ce1c81979ea5776eb5aa013d3e662353ba Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 17 Feb 2017 15:10:32 +0000 Subject: [PATCH 044/157] updated documentation fixes #41 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81ae3d9a..9d68beef 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ The following table shows what types are supported and examples of the returned Quirks of queryAggregated() - In Android, to query for steps as filtered by the Google Fit app, the flag `filtered: true` must be added into the query object. -- When querying for activities, calories and distance are provided when available in HealthKit and never in Google Fit. +- When querying for activities, calories and distance are provided when available in HealthKit. In Google Fit only calories are provided and only when no bucket is specified. - In Android, the start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. - When bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00. - Weeks start on Monday. From c221286414b74190b1e508796ac44b130d5c6775 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 7 Mar 2017 10:27:07 +0000 Subject: [PATCH 045/157] added isAuthorized to the Android JS --- www/android/health.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/www/android/health.js b/www/android/health.js index 3db5cf17..77d9b145 100644 --- a/www/android/health.js +++ b/www/android/health.js @@ -16,6 +16,10 @@ Health.prototype.requestAuthorization = function (datatypes, onSuccess, onError) exec(onSuccess, onError, "health", "requestAuthorization", datatypes); }; +Health.prototype.isAuthorized = function (onSuccess, onError) { + exec(onSuccess, onError, "health", "isAuthorized", []); +}; + Health.prototype.query = function (opts, onSuccess, onError) { //calories.active is done by asking all calories and subtracting the basal From 8b73b0e0244345b0bb35699d6009df21b68a0e56 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 7 Mar 2017 10:35:46 +0000 Subject: [PATCH 046/157] updated readme --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9d68beef..14113f2d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ This plugin is kept up to date and requires a recent version of cordova (6 and o ## Warning -This plugin stores health data in Google Fit, practice that is [discouraged by Google](https://developers.google.com/fit/terms). +Google discourages from using Google Fit for medical apps. +See the [official terms](https://developers.google.com/fit/terms). ## Installation @@ -21,6 +22,8 @@ Just execute this line in your project's folder: cordova plugin add cordova-plugin-health ``` +this will install the latest release. + ## Requirements for iOS apps * Make sure your app id has the 'HealthKit' entitlement when this plugin is installed (see iOS dev center). @@ -317,15 +320,15 @@ short term: long term: - add registration to updates (in Fit: HistoryApi#registerDataUpdateListener()). -- store custom data types and vital signs on an encrypted DB in the case of Android. -Possible choice: [sqlcipher](https://www.zetetic.net/sqlcipher/sqlcipher-for-android/). -The file would be stored on shared drive, and it would be shared among apps through a service. -All apps would have the same service because it's part of the plugin, so the service should not auto-start until the first app tries to bind it (see [this](http://stackoverflow.com/questions/31506177/the-same-android-service-instance-for-two-apps) for suggestions). - add also Samsung Health as a health record for Android. ## Contributions Any help is more than welcome! + I don't know Objectve C and I am not interested into learning it now, so I would particularly appreciate someone who can give me a hand with the iOS part. Also, I would love to know from you if the plugin is currently used in any app actually available online. -Just send me an email to my_username at gmail.com +Just send me an email to my_username at gmail.com. +For donations, I have a PayPal account at the same email address. + +Thanks! From bfd98166a99205a3d829d3a9ba36d2be3cedae28 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 7 Mar 2017 10:41:56 +0000 Subject: [PATCH 047/157] release 0.8.4 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index edf5d883..e77a70be 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.8.3", + "version": "0.8.4", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 87fc490a..793692e9 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.8.4"> Cordova Health From 868094331cc08d356279649c097d69b223e844e6 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 7 Mar 2017 10:47:16 +0000 Subject: [PATCH 048/157] forgot to use datatypes in isAuthorized() --- www/android/health.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/www/android/health.js b/www/android/health.js index 77d9b145..c2af6f40 100644 --- a/www/android/health.js +++ b/www/android/health.js @@ -16,11 +16,10 @@ Health.prototype.requestAuthorization = function (datatypes, onSuccess, onError) exec(onSuccess, onError, "health", "requestAuthorization", datatypes); }; -Health.prototype.isAuthorized = function (onSuccess, onError) { - exec(onSuccess, onError, "health", "isAuthorized", []); +Health.prototype.isAuthorized = function (datatypes, onSuccess, onError) { + exec(onSuccess, onError, "health", "isAuthorized", datatypes); }; - Health.prototype.query = function (opts, onSuccess, onError) { //calories.active is done by asking all calories and subtracting the basal if(opts.dataType =='calories.active'){ From a83b095b1dc9860e480dd3423a080aef73a32c31 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 7 Mar 2017 10:47:47 +0000 Subject: [PATCH 049/157] release 0.8.5 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e77a70be..261f7bd3 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.8.4", + "version": "0.8.5", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 793692e9..5ba14305 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.8.5"> Cordova Health From 14493d868849f56f1a2f2524f3ae8f0910105131 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 13 Mar 2017 12:31:06 +0000 Subject: [PATCH 050/157] first attempt to support delete() on iOS --- README.md | 33 +++++++++++++++++++++++++++++---- src/ios/HealthKit.h | 6 ++---- src/ios/HealthKit.m | 43 ++++++++++++++++++++++++++++++++++--------- www/ios/HealthKit.js | 4 +++- www/ios/health.js | 17 +++++++++++++++++ 5 files changed, 85 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 14113f2d..e9d2cf01 100644 --- a/README.md +++ b/README.md @@ -268,10 +268,10 @@ navigator.health.store({ sourceBundleId: 'com.example.my_app' }, successCallback, errorCallback) ``` -- startDate: {type: Date}, start date from which to get data -- endDate: {type: Date}, end data to which to get the data +- startDate: {type: Date}, start date from which the new data starts +- endDate: {type: Date}, end date to which he new data ends - dataType: {type: a String}, the data type -- value: {type: a number or an Object}, depending on the actual data type +- value: {type: a number or an Object}, the value, depending on the actual data type - sourceName: {type: String}, the source that produced this data. In iOS this is ignored and set automatically to the name of your app. - sourceBundleId: {type: String}, the complete package of the source that produced this data. In Android, if not specified, it's assigned to the package of the App. In iOS this is ignored and set automatically to the bunde id of the app. - successCallback: {type: function}, called if all OK @@ -280,13 +280,38 @@ navigator.health.store({ Quirks of store() -- Google Fit doesn't allow you to overwrite data points that overlap with others already stored of the same type (see [here](https://developers.google.com/fit/android/history#manageConflicting)). At the moment there is no support for [update](https://developers.google.com/fit/android/history#updateData) nor delete. +- Google Fit doesn't allow you to overwrite data points that overlap with others already stored of the same type (see [here](https://developers.google.com/fit/android/history#manageConflicting)). At the moment there is no support for [update](https://developers.google.com/fit/android/history#updateData). - In iOS you cannot store the total calories, you need to specify either basal or active. If you use total calories, the active ones will be stored. - In Android you can only store active calories, as the basal are estimated automatically. If you store total calories, these will be treated as active. - In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. - In iOS storing the sleep activities is not supported at the moment. - Storing of nutrients is not supported at the moment. +### delete() + +Deletes a range of data points. + +``` +navigator.health.delete({ + startDate: new Date(new Date().getTime() - 3 * 60 * 1000), // three minutes ago + endDate: new Date(), + dataType: 'steps' + }, successCallback, errorCallback) +``` + +- startDate: {type: Date}, start date from which to delete data +- endDate: {type: Date}, end date to which to delete the data +- dataType: {type: a String}, the data type +- successCallback: {type: function}, called if all OK +- errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem + + +Quirks of delete() + +- Google Fit doesn't allow you to delete data points that were generated by an app different than yours. +- In iOS you cannot delete the total calories, you need to specify either basal or active. If you use total calories, the active ones will be delete. +- In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. + ## Differences between HealthKit and Google Fit * HealthKit includes medical data (eg blood glucose), Google Fit is only related to fitness data. diff --git a/src/ios/HealthKit.h b/src/ios/HealthKit.h index 6bcdfa20..6ae6f52c 100755 --- a/src/ios/HealthKit.h +++ b/src/ios/HealthKit.h @@ -3,7 +3,6 @@ @interface HealthKit :CDVPlugin - /** * Tell delegate whether or not health data is available * @@ -145,11 +144,10 @@ - (void) queryCorrelationType:(CDVInvokedUrlCommand*)command; /** - * Delete a specified object from teh HealthKit store - * @TODO implement me + * Delete matching samples from the HealthKit store * * @param command *CDVInvokedUrlCommand */ -- (void) deleteObject:(CDVInvokedUrlCommand*)command; +- (void) deleteSamples:(CDVInvokedUrlCommand*)command; @end diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index 18f0ee90..1c56ba88 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -1710,18 +1710,43 @@ - (void)saveCorrelation:(CDVInvokedUrlCommand *)command { } /** - * Delete a specified object from teh HealthKit store - * @TODO implement me + * Delete matching samples from the HealthKit store. + * See https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HKHealthStore_Class/#//apple_ref/occ/instm/HKHealthStore/deleteObject:withCompletion: * * @param command *CDVInvokedUrlCommand */ -- (void)deleteObject:(CDVInvokedUrlCommand *)command { - //NSDictionary *args = command.arguments[0]; - - // TODO see the 3 methods at https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HKHealthStore_Class/#//apple_ref/occ/instm/HKHealthStore/deleteObject:withCompletion: - - CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; - [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +- (void)deleteSamples:(CDVInvokedUrlCommand *)command { + NSDictionary *args = command.arguments[0]; + NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:[args[HKPluginKeyStartDate] longValue]]; + NSDate *endDate = [NSDate dateWithTimeIntervalSince1970:[args[HKPluginKeyEndDate] longValue]]; + NSString *sampleTypeString = args[HKPluginKeySampleType]; + + HKSampleType *type = [HealthKit getHKSampleType:sampleTypeString]; + if (type == nil) { + [HealthKit triggerErrorCallbackWithMessage:@"sampleType was invalid" command:command delegate:self.commandDelegate]; + return; + } + + NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate]; + + NSSet *requestTypes = [NSSet setWithObjects:type, nil]; + [[HealthKit sharedHealthStore] requestAuthorizationToShareTypes:nil readTypes:requestTypes completion:^(BOOL success, NSError *error) { + __block HealthKit *bSelf = self; + if (success) { + [[HealthKit sharedHealthStore] deleteObjectsOfType:type predicate:predicate withCompletion:^(BOOL success, NSUInteger deletedObjectCount, NSError * _Nullable deletionError) { + if (deletionError != nil) { + dispatch_sync(dispatch_get_main_queue(), ^{ + [HealthKit triggerErrorCallbackWithMessage:deletionError.localizedDescription command:command delegate:bSelf.commandDelegate]; + }); + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:(int)deletedObjectCount]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }); + } + }]; + } + }]; } @end diff --git a/www/ios/HealthKit.js b/www/ios/HealthKit.js index 921aeedf..11a7f339 100644 --- a/www/ios/HealthKit.js +++ b/www/ios/HealthKit.js @@ -71,7 +71,7 @@ var define = function(methodName, params, fn) { var args = options ? [options] : []; cordova.exec(callback, onError, 'HealthKit', methodName, args); }; - } + }; }; define('available', {noArgs: true}); @@ -112,6 +112,8 @@ define('querySampleType', {required: 'sampleType'}, hasValidDates); define('querySampleTypeAggregated', {required: 'sampleType'}, hasValidDates); +define('deleteSamples', {required: 'sampleType'}, hasValidDates); + define('queryCorrelationType', {required: 'correlationType'}, hasValidDates); define('saveQuantitySample', {required: 'sampleType'}, hasValidDates); diff --git a/www/ios/health.js b/www/ios/health.js index 9940161a..3a6db931 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -398,6 +398,23 @@ Health.prototype.store = function (data, onSuccess, onError) { } }; + +Health.prototype.delete = function (data, onSuccess, onError) { + if (data.dataType === 'gender') { + onError('Gender is not writeable'); + } else if (data.dataType === 'date_of_birth') { + onError('Date of birth is not writeable'); + } else if (dataTypes[ data.dataType ]) { + data.sampleType = dataTypes[ data.dataType ]; + if ((data.dataType === 'distance') && data.cycling) { + data.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; + } + window.plugins.healthkit.deleteSamples(data, onSuccess, onError); + } else { + onError('unknown data type ' + data.dataType); + } +}; + cordova.addConstructor(function () { navigator.health = new Health(); return navigator.health; From 2bc85423cf261f52e946f9844555b4bf34bdd491 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 13 Mar 2017 12:39:04 +0000 Subject: [PATCH 051/157] simplified and corrected delete in iOS --- www/ios/health.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index 3a6db931..36c03e34 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -401,18 +401,20 @@ Health.prototype.store = function (data, onSuccess, onError) { Health.prototype.delete = function (data, onSuccess, onError) { if (data.dataType === 'gender') { - onError('Gender is not writeable'); + onError('Gender is not deletable'); } else if (data.dataType === 'date_of_birth') { - onError('Date of birth is not writeable'); + onError('Date of birth is not deletable'); + } else if ((data.dataType === 'activity') && (data.value.lastIndexOf('sleep', 0) === 0)) { + data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; + } else if ((data.dataType === 'distance') && data.cycling) { + data.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; } else if (dataTypes[ data.dataType ]) { data.sampleType = dataTypes[ data.dataType ]; - if ((data.dataType === 'distance') && data.cycling) { - data.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; - } - window.plugins.healthkit.deleteSamples(data, onSuccess, onError); } else { onError('unknown data type ' + data.dataType); + return; } + window.plugins.healthkit.deleteSamples(data, onSuccess, onError); }; cordova.addConstructor(function () { From 4768c8773427bda8b3c4c2baec598fcbcfb45138 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 14 Mar 2017 12:59:27 +0000 Subject: [PATCH 052/157] fixed a bug in store, when no energy is specified --- src/ios/HealthKit.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index 1c56ba88..06fb81f5 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -688,6 +688,12 @@ - (void)saveWorkout:(CDVInvokedUrlCommand *)command { }); } }]; + } else { + // no samples, all OK then! + dispatch_sync(dispatch_get_main_queue(), ^{ + CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + [bSelf.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }); } } else { dispatch_sync(dispatch_get_main_queue(), ^{ From 7f170da3279c5a0956c28dc2573bf3aaa5d8e4e8 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 14 Mar 2017 13:14:18 +0000 Subject: [PATCH 053/157] delete of workouts --- src/ios/HealthKit.m | 2 +- www/ios/health.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index 06fb81f5..ad91b15e 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -663,7 +663,7 @@ - (void)saveWorkout:(CDVInvokedUrlCommand *)command { [[HealthKit sharedHealthStore] saveObject:workout withCompletion:^(BOOL success_save, NSError *innerError) { if (success_save) { // now store the samples, so it shows up in the health app as well (pass this in as an option?) - if (energy != nil) { + if (energy != nil || distance != nil) { HKQuantitySample *sampleActivity = [HKQuantitySample quantitySampleWithType:[HKQuantityType quantityTypeForIdentifier: quantityType] quantity:nrOfDistanceUnits diff --git a/www/ios/health.js b/www/ios/health.js index 36c03e34..63808b63 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -404,8 +404,10 @@ Health.prototype.delete = function (data, onSuccess, onError) { onError('Gender is not deletable'); } else if (data.dataType === 'date_of_birth') { onError('Date of birth is not deletable'); - } else if ((data.dataType === 'activity') && (data.value.lastIndexOf('sleep', 0) === 0)) { + } else if ((data.dataType === 'activity') && (data.dataType.lastIndexOf('sleep', 0) === 0)) { data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; + } else if(data.dataType === 'activity') { + data.sampleType = 'workoutType'; } else if ((data.dataType === 'distance') && data.cycling) { data.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; } else if (dataTypes[ data.dataType ]) { From 162107c8547c3af67fb8151c9859de88f3124a0d Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 14 Mar 2017 17:41:55 +0000 Subject: [PATCH 054/157] draft of delete() for Android --- README.md | 5 ++- src/android/HealthPlugin.java | 78 +++++++++++++++++++++++++++++++++++ www/android/health.js | 19 +++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e9d2cf01..ed4dca76 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ navigator.health.delete({ - startDate: {type: Date}, start date from which to delete data - endDate: {type: Date}, end date to which to delete the data -- dataType: {type: a String}, the data type +- dataType: {type: a String}, the data type to be deleted - successCallback: {type: function}, called if all OK - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem @@ -309,8 +309,11 @@ navigator.health.delete({ Quirks of delete() - Google Fit doesn't allow you to delete data points that were generated by an app different than yours. +- In Android you can only delete active calories, as the basal are estimated automatically. If you try to delete total calories, these will be treated as active. - In iOS you cannot delete the total calories, you need to specify either basal or active. If you use total calories, the active ones will be delete. - In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. +- In iOS deleting sleep activities is not supported at the moment. + ## Differences between HealthKit and Google Fit diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 6b9e4907..6a8be1f0 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -14,6 +14,7 @@ import com.google.android.gms.common.Scopes; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Scope; import com.google.android.gms.common.api.Status; import com.google.android.gms.fitness.Fitness; @@ -24,6 +25,7 @@ import com.google.android.gms.fitness.data.DataType; import com.google.android.gms.fitness.data.Field; import com.google.android.gms.fitness.data.Value; +import com.google.android.gms.fitness.request.DataDeleteRequest; import com.google.android.gms.fitness.request.DataReadRequest; import com.google.android.gms.fitness.request.DataTypeCreateRequest; import com.google.android.gms.fitness.result.DataReadResult; @@ -340,6 +342,18 @@ public void run() { } }); return true; + } else if("delete".equals(action)){ + cordova.getThreadPool().execute(new Runnable() { + @Override + public void run() { + try { + delete(args, callbackContext); + } catch (Exception ex) { + callbackContext.error(ex.getMessage()); + } + } + }); + return true; } return false; @@ -1241,4 +1255,68 @@ private void store(final JSONArray args, final CallbackContext callbackContext) callbackContext.success(); } } + + private void delete(final JSONArray args, final CallbackContext callbackContext) throws JSONException { + if (!args.getJSONObject(0).has("startDate")) { + callbackContext.error("Missing argument startDate"); + return; + } + final long st = args.getJSONObject(0).getLong("startDate"); + if (!args.getJSONObject(0).has("endDate")) { + callbackContext.error("Missing argument endDate"); + return; + } + final long et = args.getJSONObject(0).getLong("endDate"); + if (!args.getJSONObject(0).has("dataType")) { + callbackContext.error("Missing argument dataType"); + return; + } + final String datatype = args.getJSONObject(0).getString("dataType"); + if (!args.getJSONObject(0).has("value")) { + callbackContext.error("Missing argument value"); + return; + } + + DataType dt = null; + if (bodydatatypes.get(datatype) != null) + dt = bodydatatypes.get(datatype); + if (activitydatatypes.get(datatype) != null) + dt = activitydatatypes.get(datatype); + if (locationdatatypes.get(datatype) != null) + dt = locationdatatypes.get(datatype); + if (nutritiondatatypes.get(datatype) != null) + dt = nutritiondatatypes.get(datatype); + if (customdatatypes.get(datatype) != null) + dt = customdatatypes.get(datatype); + if (dt == null) { + callbackContext.error("Datatype " + datatype + " not supported"); + return; + } + + if ((mClient == null) || (!mClient.isConnected())) { + if (!lightConnect()) { + callbackContext.error("Cannot connect to Google Fit"); + return; + } + } + + DataDeleteRequest request = new DataDeleteRequest.Builder() + .setTimeInterval(st, et, TimeUnit.MILLISECONDS) + .addDataType(dt) + .build(); + + Fitness.HistoryApi.deleteData(mClient, request) + .setResultCallback(new ResultCallback() { + @Override + public void onResult(Status status) { + if (status.isSuccess()) { + callbackContext.success(); + } else { + Log.e(TAG, "Cannot delete samples of " + datatype + ", status code " + + status.getStatusCode() + ", message " + status.getStatusMessage()); + callbackContext.error(status.getStatusMessage()); + } + } + }); + } } diff --git a/www/android/health.js b/www/android/health.js index c2af6f40..f100999d 100644 --- a/www/android/health.js +++ b/www/android/health.js @@ -146,6 +146,25 @@ Health.prototype.store = function (data, onSuccess, onError) { exec(onSuccess, onError, "health", "store", [data]); }; +Health.prototype.delete = function (data, onSuccess, onError) { + if(data.dataType =='calories.basal'){ + onError('basal calories cannot be deleted in Android'); + return; + } + if(data.dataType =='calories.active'){ + //rename active calories to total calories + data.dataType ='calories'; + } + if(data.startDate && (typeof data.startDate == 'object')) + data.startDate = data.startDate.getTime(); + if(data.endDate && (typeof data.endDate == 'object')) + data.endDate = data.endDate.getTime(); + if(data.dataType =='activity'){ + data.value = navigator.health.toFitActivity(data.value); + } + exec(onSuccess, onError, "health", "delete", [data]); +}; + Health.prototype.toFitActivity = function (act) { if (act === 'core_training') return 'strength_training'; if (act === 'flexibility') return 'gymnastics'; From e03e00f52a5a6acd96caf9334d14f6b821dcba0a Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 15 Mar 2017 11:55:23 +0000 Subject: [PATCH 055/157] removed unnecessary code from delete() --- src/android/HealthPlugin.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 6a8be1f0..01308687 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -1272,10 +1272,6 @@ private void delete(final JSONArray args, final CallbackContext callbackContext) return; } final String datatype = args.getJSONObject(0).getString("dataType"); - if (!args.getJSONObject(0).has("value")) { - callbackContext.error("Missing argument value"); - return; - } DataType dt = null; if (bodydatatypes.get(datatype) != null) From 37e403f3b1845c5dc0a803b0fce11397817ba0e0 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 15 Mar 2017 11:58:00 +0000 Subject: [PATCH 056/157] release 0.9.0 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 261f7bd3..37f4df6b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.8.5", + "version": "0.9.0", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 5ba14305..2ad633df 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.9.0"> Cordova Health From f9a5525473cfbab1966e61f44c6df229624bfc12 Mon Sep 17 00:00:00 2001 From: Ankush Aggarwal Date: Sun, 9 Apr 2017 22:00:05 -0700 Subject: [PATCH 057/157] added editorconfig --- .editorconfig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..853359dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file From 6e09bdb9e372017f22ad65bc70f79e0869940b44 Mon Sep 17 00:00:00 2001 From: Ankush Aggarwal Date: Sun, 9 Apr 2017 22:17:42 -0700 Subject: [PATCH 058/157] ios allows read or write only permissions --- www/ios/health.js | 163 +++++++++++++++++++++++++++++----------------- 1 file changed, 103 insertions(+), 60 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index 63808b63..9be71712 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -68,45 +68,88 @@ Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); }; -var prepareDatatype4Auth = function (dts, success, error) { - var HKdatatypes = []; - for (var i = 0; i < dts.length; i++) { - if ((dts[i] !== 'gender') && (dts[i] !== 'date_of_birth')) { // ignore gender and DOB - if(dts[i] === 'nutrition') { +var getHKDataTypes = function (dtArr) { + var HKDataTypes = []; + for (var i = 0; i < dtArr.length; i++) { + if ((dtArr[i] !== 'gender') && (dtArr[i] !== 'date_of_birth')) { // ignore gender and DOB + if (dtArr[i] === 'nutrition') { // add all nutrition stuff - for(var datatype in dataTypes){ - if (datatype.startsWith('nutrition')) HKdatatypes.push(dataTypes[datatype]); + for (var dataType in dataTypes) { + if (dataType.startsWith('nutrition')) HKDataTypes.push(dataTypes[dataType]); } - } else if (dataTypes[dts[i]]) { - HKdatatypes.push(dataTypes[dts[i]]); - if (dts[i] === 'distance') HKdatatypes.push('HKQuantityTypeIdentifierDistanceCycling'); - if (dts[i] === 'activity') HKdatatypes.push('HKCategoryTypeIdentifierSleepAnalysis'); - if (dts[i] === 'calories') HKdatatypes.push('HKQuantityTypeIdentifierBasalEnergyBurned'); + } else if (dataTypes[dtArr[i]]) { + HKDataTypes.push(dataTypes[dtArr[i]]); + if (dtArr[i] === 'distance') HKDataTypes.push('HKQuantityTypeIdentifierDistanceCycling'); + if (dtArr[i] === 'activity') HKDataTypes.push('HKCategoryTypeIdentifierSleepAnalysis'); + if (dtArr[i] === 'calories') HKDataTypes.push('HKQuantityTypeIdentifierBasalEnergyBurned'); + } else { + // return the not found dataType instead of array + return dtArr[i]; + } + } + } + return HKDataTypes; +}; + +var getReadWriteTypes = function (dts, success, error) { + var readTypes = []; + var writeTypes = []; + for (var i = 0; i < dts.length; i++) { + var HKDataTypes = []; + if (typeof dts[i] === 'string') { + HKDataTypes = getHKDataTypes([dts[i]]); + if (Array.isArray(HKDataTypes)) { + readTypes = readTypes.concat(HKDataTypes); + writeTypes = writeTypes.concat(HKDataTypes); } else { - error('unknown data type ' + dts[i]); + error('unknown data type - ' + HKDataTypes); return; } + } else { + if (dts[i]['read']) { + HKDataTypes = getHKDataTypes(dts[i]['read']); + if (Array.isArray(HKDataTypes)) { + readTypes = readTypes.concat(HKDataTypes); + } else { + error('unknown read data type - ' + HKDataTypes); + return; + } + } + if (dts[i]['write']) { + HKDataTypes = getHKDataTypes(dts[i]['write']); + if (Array.isArray(HKDataTypes)) { + writeTypes = writeTypes.concat(HKDataTypes); + } else { + error('unknown write data type - ' + HKDataTypes); + return; + } + } } } - success(HKdatatypes); -} + success(dedupe(readTypes), dedupe(writeTypes)); +}; + +var dedupe = function (arr) { + return arr.filter(function (el, i, arr) { + return arr.indexOf(el) === i; + }); +}; Health.prototype.requestAuthorization = function (dts, onSuccess, onError) { - prepareDatatype4Auth(dts, function (HKdatatypes) { - if (HKdatatypes.length) { - window.plugins.healthkit.requestAuthorization({ - 'readTypes': HKdatatypes, - 'writeTypes': HKdatatypes - }, onSuccess, onError); - } else onSuccess(); + getReadWriteTypes(dts, function (readTypes, writeTypes) { + window.plugins.healthkit.requestAuthorization({ + 'readTypes': readTypes, + 'writeTypes': writeTypes + }, onSuccess, onError); }, onError); }; -Health.prototype.isAuthorized = function(dts, onSuccess, onError) { - prepareDatatype4Auth(dts, function (HKdatatypes) { +Health.prototype.isAuthorized = function (dts, onSuccess, onError) { + getReadWriteTypes(dts, function (readTypes, writeTypes) { + var HKDataTypes = dedupe(readTypes.concat(writeTypes)); var check = function () { - if (HKdatatypes.length > 0) { - var dt = HKdatatypes.shift(); + if (HKDataTypes.length > 0) { + var dt = HKDataTypes.shift(); window.plugins.healthkit.checkAuthStatus({ type: dt }, function (auth) { @@ -114,10 +157,10 @@ Health.prototype.isAuthorized = function(dts, onSuccess, onError) { else onSuccess(false); }, onError); } else onSuccess(true); - } + }; check(); }, onError); -} +}; Health.prototype.query = function (opts, onSuccess, onError) { var startD = opts.startDate; @@ -144,7 +187,7 @@ Health.prototype.query = function (opts, onSuccess, onError) { res[0] = { startDate: opts.startDate, endDate: opts.endDate, - value: { day: date.getDate(), month: date.getMonth() + 1, year: date.getFullYear() }, + value: {day: date.getDate(), month: date.getMonth() + 1, year: date.getFullYear()}, sourceName: 'Health', sourceBundleId: 'com.apple.Health' }; @@ -194,15 +237,15 @@ Health.prototype.query = function (opts, onSuccess, onError) { correlationType: 'HKCorrelationTypeIdentifierFood', units: ['g', 'ml', 'kcal'] }, function (data) { - for(var i=0; i Date: Sun, 9 Apr 2017 22:33:24 -0700 Subject: [PATCH 059/157] [docs] - read or write only permissions --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ed4dca76..1e1c5a1f 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,16 @@ This function must be called before using the query and store functions, even if navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) ``` -- datatypes: {type: Array of String}, a list of data types you want to be granted access to +- datatypes: {type: Mixed array}, a list of data types you want to be granted access to. You can also specify read or write only permissions. +```javascript +[ + 'calories', 'distance', //read and write permissions + { + read : ['steps'], //read only permission + write : ['height', 'weight'] //write only permission + } +] +``` - successCallback: {type: function}, called if all OK - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem From 0f8985eb0aa1c3d885203ecd6cb5e2aa5f62d46c Mon Sep 17 00:00:00 2001 From: Ankush Aggarwal Date: Sun, 9 Apr 2017 23:03:25 -0700 Subject: [PATCH 060/157] plugin accepts variable name for health description - config.xml - or specify while installing cordova plugin add cordova-plugin-health --variable HEALTH_READ_PERMISSION='I want to read your data' --variable HEALTH_WRITE_PERMISSION='let me write also please' --save --- plugin.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin.xml b/plugin.xml index 2ad633df..3204e44c 100755 --- a/plugin.xml +++ b/plugin.xml @@ -47,11 +47,13 @@ + + - This app needs to access read your health data + $HEALTH_READ_PERMISSION - This app needs to access update your health data + $HEALTH_WRITE_PERMISSION From 13f3320ea2cae743a3dea17e8068558443dedb7c Mon Sep 17 00:00:00 2001 From: Ankush Aggarwal Date: Mon, 10 Apr 2017 02:01:15 -0700 Subject: [PATCH 061/157] android allows read or write only permissions --- .editorconfig | 4 +- src/android/HealthPlugin.java | 104 ++++++++++++++++++++++++++-------- 2 files changed, 83 insertions(+), 25 deletions(-) diff --git a/.editorconfig b/.editorconfig index 853359dd..d928df9d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,7 @@ root = true -[*] +[*.js] indent_style = space indent_size = 2 end_of_line = lf @@ -12,4 +12,4 @@ trim_trailing_whitespace = true insert_final_newline = true [*.md] -trim_trailing_whitespace = false \ No newline at end of file +trim_trailing_whitespace = false diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 01308687..88f41a04 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -43,6 +43,7 @@ import java.util.Calendar; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -67,6 +68,8 @@ public class HealthPlugin extends CordovaPlugin { private boolean authAutoresolve = false; private static final LinkedList dynPerms = new LinkedList(); private static final int REQUEST_DYN_PERMS = 2; + private static final int READ_PERMS = 1; + private static final int READ_WRITE_PERMS = 2; //Scope for read/write access to activity-related data types in Google Fit. //These include activity type, calories consumed and expended, step counts, and others. @@ -342,7 +345,7 @@ public void run() { } }); return true; - } else if("delete".equals(action)){ + } else if ("delete".equals(action)) { cordova.getThreadPool().execute(new Runnable() { @Override public void run() { @@ -413,35 +416,90 @@ private void checkAuthorization(final JSONArray args, final CallbackContext call authAutoresolve = autoresolve; //reset scopes - boolean bodyscope = false; - boolean activityscope = false; - boolean locationscope = false; - boolean nutritionscope = false; + int bodyscope = 0; + int activityscope = 0; + int locationscope = 0; + int nutritionscope = 0; + + HashSet readWriteTypes = new HashSet(); + HashSet readTypes = new HashSet(); for (int i = 0; i < args.length(); i++) { - String type = args.getString(i); - if (bodydatatypes.get(type) != null) - bodyscope = true; - if (activitydatatypes.get(type) != null) - activityscope = true; - if (locationdatatypes.get(type) != null) - locationscope = true; - if (nutritiondatatypes.get(type) != null) - nutritionscope = true; + Object object = args.get(i); + if (object instanceof JSONObject) { + JSONObject readWriteObj = (JSONObject) object; + if (readWriteObj.has("read")) { + JSONArray readArray = readWriteObj.getJSONArray("read"); + for (int j = 0; j < readArray.length(); j++) { + readTypes.add(readArray.getString(j)); + } + } + if (readWriteObj.has("write")) { + JSONArray writeArray = readWriteObj.getJSONArray("write"); + for (int j = 0; j < writeArray.length(); j++) { + readWriteTypes.add(writeArray.getString(j)); + } + } + } else if (object instanceof String) { + readWriteTypes.add(String.valueOf(object)); + } + } + + readTypes.removeAll(readWriteTypes); + + for (String readType : readTypes) { + if (bodydatatypes.get(readType) != null) + bodyscope = READ_PERMS; + if (activitydatatypes.get(readType) != null) + activityscope = READ_PERMS; + if (locationdatatypes.get(readType) != null) + locationscope = READ_PERMS; + if (nutritiondatatypes.get(readType) != null) + nutritionscope = READ_PERMS; } + + for (String readWriteType : readWriteTypes) { + if (bodydatatypes.get(readWriteType) != null) + bodyscope = READ_WRITE_PERMS; + if (activitydatatypes.get(readWriteType) != null) + activityscope = READ_WRITE_PERMS; + if (locationdatatypes.get(readWriteType) != null) + locationscope = READ_WRITE_PERMS; + if (nutritiondatatypes.get(readWriteType) != null) + nutritionscope = READ_WRITE_PERMS; + } + dynPerms.clear(); - if (locationscope) dynPerms.add(Manifest.permission.ACCESS_FINE_LOCATION); - if (bodyscope) dynPerms.add(Manifest.permission.BODY_SENSORS); + if (locationscope == READ_PERMS || locationscope == READ_WRITE_PERMS) + dynPerms.add(Manifest.permission.ACCESS_FINE_LOCATION); + if (bodyscope == READ_PERMS || bodyscope == READ_WRITE_PERMS) + dynPerms.add(Manifest.permission.BODY_SENSORS); GoogleApiClient.Builder builder = new GoogleApiClient.Builder(this.cordova.getActivity()); builder.addApi(Fitness.HISTORY_API); builder.addApi(Fitness.CONFIG_API); builder.addApi(Fitness.SESSIONS_API); //scopes: https://developers.google.com/android/reference/com/google/android/gms/common/Scopes.html - if (bodyscope) builder.addScope(new Scope(Scopes.FITNESS_BODY_READ_WRITE)); - if (activityscope) builder.addScope(new Scope(Scopes.FITNESS_ACTIVITY_READ_WRITE)); - if (locationscope) builder.addScope(new Scope(Scopes.FITNESS_LOCATION_READ_WRITE)); - if (nutritionscope) builder.addScope(new Scope(Scopes.FITNESS_NUTRITION_READ_WRITE)); + if (bodyscope == READ_PERMS) { + builder.addScope(new Scope(Scopes.FITNESS_BODY_READ)); + } else if (bodyscope == READ_WRITE_PERMS) { + builder.addScope(new Scope(Scopes.FITNESS_BODY_READ_WRITE)); + } + if (activityscope == READ_PERMS) { + builder.addScope(new Scope(Scopes.FITNESS_ACTIVITY_READ)); + } else if (activityscope == READ_WRITE_PERMS) { + builder.addScope(new Scope(Scopes.FITNESS_ACTIVITY_READ_WRITE)); + } + if (locationscope == READ_PERMS) { + builder.addScope(new Scope(Scopes.FITNESS_LOCATION_READ)); + } else if (locationscope == READ_WRITE_PERMS) { + builder.addScope(new Scope(Scopes.FITNESS_LOCATION_READ_WRITE)); + } + if (nutritionscope == READ_PERMS) { + builder.addScope(new Scope(Scopes.FITNESS_NUTRITION_READ)); + } else if (nutritionscope == READ_WRITE_PERMS) { + builder.addScope(new Scope(Scopes.FITNESS_NUTRITION_READ_WRITE)); + } builder.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { @Override @@ -862,9 +920,9 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("calories.basal")) { builder.aggregate(DataType.TYPE_BASAL_METABOLIC_RATE, DataType.AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY); } else if (datatype.equalsIgnoreCase("activity")) { - if(hasbucket) { + if (hasbucket) { builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); - } else { + } else { builder.aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED); //here we could also get the distance: builder.aggregate(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA); } @@ -948,7 +1006,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac // special case of the activity without time buckets // here the buckets contain activities and the datapoints contain calories - if(datatype.equalsIgnoreCase("activity") && !hasbucket){ + if (datatype.equalsIgnoreCase("activity") && !hasbucket) { String activity = bucket.getActivity(); float calories = 0; int duration = (int) (bucket.getEndTime(TimeUnit.MILLISECONDS) - bucket.getStartTime(TimeUnit.MILLISECONDS)); From 687a9217a9a3308e71871b5ca310547b90232bbc Mon Sep 17 00:00:00 2001 From: Ankush Aggarwal Date: Mon, 10 Apr 2017 11:27:05 -0700 Subject: [PATCH 062/157] [docs] - health description variables --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e1c5a1f..a59596dc 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ See the [official terms](https://developers.google.com/fit/terms). Just execute this line in your project's folder: ``` -cordova plugin add cordova-plugin-health +cordova plugin add cordova-plugin-health --variable HEALTH_READ_PERMISSION='App needs read access' --variable HEALTH_WRITE_PERMISSION='App needs write access' ``` this will install the latest release. From 19acebb02c45351160ddf5f203f304658f7c4ba0 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 11 Apr 2017 12:18:27 +0100 Subject: [PATCH 063/157] [docs] a bit more precise readme and some more comments in the java class --- README.md | 3 +- src/android/HealthPlugin.java | 65 +++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a59596dc..f96a270b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ cordova plugin add cordova-plugin-health --variable HEALTH_READ_PERMISSION='App ``` this will install the latest release. +`HEALTH_READ_PERMISSION` and `HEALTH_WRITE_PERMISSION` are shown when the app tries to grant access to data in HealthKit. ## Requirements for iOS apps @@ -166,7 +167,7 @@ navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) Quirks of requestAuthorization() -- In Android, it will try to get authorization from the Google Fit APIs. It is necessary that the app's package name and the signing key are registered in the Google API console (see [here](https://developers.google.com/fit/android/get-api-key)). +- In Android, it will try to get authorization from the Google fitness APIs. It is necessary that the app's package name and the signing key are registered in the Google API console (see [here](https://developers.google.com/fit/android/get-api-key)). - In Android, be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the authorization will ensure that the app is connected again. - In Android 6 and over, this function will also ask for some dynamic permissions if needed (e.g. in the case of "distance", it will need access to ACCESS_FINE_LOCATION). diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 88f41a04..62d8aa54 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -54,25 +54,31 @@ * MIT licensed. */ public class HealthPlugin extends CordovaPlugin { - //logger tag + // logger tag private static final String TAG = "cordova-plugin-health"; - //calling activity + // calling activity private CordovaInterface cordova; - //actual Google API client + // actual Google API client private GoogleApiClient mClient; - private static final int REQUEST_OAUTH = 1; + // instance of the call back when requesting or checking authorisation private CallbackContext authReqCallbackCtx; + + // if true, when checking authorisation, tries to get it from user private boolean authAutoresolve = false; + + // list of OS level dynamic permissions needed (if any) private static final LinkedList dynPerms = new LinkedList(); + + private static final int REQUEST_OAUTH = 1; private static final int REQUEST_DYN_PERMS = 2; private static final int READ_PERMS = 1; private static final int READ_WRITE_PERMS = 2; - //Scope for read/write access to activity-related data types in Google Fit. - //These include activity type, calories consumed and expended, step counts, and others. + // Scope for read/write access to activity-related data types in Google Fit. + // These include activity type, calories consumed and expended, step counts, and others. public static Map activitydatatypes = new HashMap(); static { activitydatatypes.put("steps", DataType.TYPE_STEP_COUNT_DELTA); @@ -81,7 +87,7 @@ public class HealthPlugin extends CordovaPlugin { activitydatatypes.put("activity", DataType.TYPE_ACTIVITY_SEGMENT); } - //Scope for read/write access to biometric data types in Google Fit. These include heart rate, height, and weight. + // Scope for read/write access to biometric data types in Google Fit. These include heart rate, height, and weight. public static Map bodydatatypes = new HashMap(); static { bodydatatypes.put("height", DataType.TYPE_HEIGHT); @@ -90,12 +96,13 @@ public class HealthPlugin extends CordovaPlugin { bodydatatypes.put("fat_percentage", DataType.TYPE_BODY_FAT_PERCENTAGE); } - //Scope for read/write access to location-related data types in Google Fit. These include location, distance, and speed. + // Scope for read/write access to location-related data types in Google Fit. These include location, distance, and speed. public static Map locationdatatypes = new HashMap(); static { locationdatatypes.put("distance", DataType.TYPE_DISTANCE_DELTA); } + // Helper class used for storing nutrients information (name and unit of measurement) private static class NutrientFieldInfo { public String field; public String unit; @@ -106,7 +113,7 @@ public NutrientFieldInfo(String field, String unit) { } } - //Lookup for nutrition fields and units + // Lookup for nutrition fields and units public static Map nutrientFields = new HashMap(); static { @@ -130,7 +137,7 @@ public NutrientFieldInfo(String field, String unit) { nutrientFields.put("nutrition.iron", new NutrientFieldInfo(Field.NUTRIENT_IRON, "mg")); } - //Scope for read/write access to nutrition data types in Google Fit. + // Scope for read/write access to nutrition data types in Google Fit. public static Map nutritiondatatypes = new HashMap(); static { @@ -147,13 +154,15 @@ public NutrientFieldInfo(String field, String unit) { public HealthPlugin() { } + // general initalisation @Override public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); this.cordova = cordova; } - + // called once authorisation is completed + // creates custom data types private void authReqSuccess() { //Create custom data types cordova.getThreadPool().execute(new Runnable() { @@ -197,6 +206,8 @@ public void run() { }); } + // called once custom data types have been created + // asks for dynamic permissions on Android 6 and more private void requestDynamicPermissions() { if (dynPerms.isEmpty()) { // nothing to be done @@ -214,13 +225,16 @@ private void requestDynamicPermissions() { } else { if (authAutoresolve) { cordova.requestPermissions(this, REQUEST_DYN_PERMS, perms.toArray(new String[perms.size()])); + // the request results will be taken care of by onRequestPermissionResult() } else { + // if should not autoresolve, and there are dynamic permissions needed, send a fail authReqCallbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, false)); } } } } + // called when the dynamic permissions are asked @Override public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException { if (requestCode == REQUEST_DYN_PERMS) { @@ -234,11 +248,12 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, int return; } } - //all accepted! + // all accepted! authReqCallbackCtx.success(); } } + // called when access to Google API is answered @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { if (requestCode == REQUEST_OAUTH) { @@ -247,6 +262,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent intent) { if (!mClient.isConnected() && !mClient.isConnecting()) { Log.d(TAG, "Re-trying connection with Fit"); mClient.connect(); + // the connection success / failure will be taken care of by ConnectionCallbacks in checkAuthorization() } } else if (resultCode == Activity.RESULT_CANCELED) { // The user cancelled the login dialog before selecting any action. @@ -302,7 +318,7 @@ public void run() { @Override public void run() { try { - checkAuthorization(args, callbackContext, false); + checkAuthorization(args, callbackContext, false); // without autoresolve } catch (Exception ex) { callbackContext.error(ex.getMessage()); } @@ -362,6 +378,7 @@ public void run() { return false; } + // detects if a) Google APIs are available, b) Google Fit is actually installed private void isAvailable(final CallbackContext callbackContext) { // first check that the Google APIs are available GoogleApiAvailability gapi = GoogleApiAvailability.getInstance(); @@ -384,6 +401,7 @@ private void isAvailable(final CallbackContext callbackContext) { callbackContext.sendPluginResult(result); } + // prompts to install GooglePlayServices if not available then Google Fit if not available private void promptInstall(final CallbackContext callbackContext) { GoogleApiAvailability gapi = GoogleApiAvailability.getInstance(); int apiresult = gapi.isGooglePlayServicesAvailable(this.cordova.getActivity()); @@ -398,8 +416,8 @@ private void promptInstall(final CallbackContext callbackContext) { try { pm.getPackageInfo("com.google.android.apps.fitness", PackageManager.GET_ACTIVITIES); } catch (PackageManager.NameNotFoundException e) { - //show popup for downloading app - //code from http://stackoverflow.com/questions/11753000/how-to-open-the-google-play-store-directly-from-my-android-application + // show popup for downloading app + // code from http://stackoverflow.com/questions/11753000/how-to-open-the-google-play-store-directly-from-my-android-application try { cordova.getActivity().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.google.android.apps.fitness"))); } catch (android.content.ActivityNotFoundException anfe) { @@ -410,12 +428,15 @@ private void promptInstall(final CallbackContext callbackContext) { callbackContext.success(); } + // check if the app is authorised to use Google fitness APIs + // if autoresolve is set, it will try to get authorisation from the user + // also includes some OS dynamic permissions if needed (eg location) private void checkAuthorization(final JSONArray args, final CallbackContext callbackContext, final boolean autoresolve) throws JSONException { this.cordova.setActivityResultCallback(this); authReqCallbackCtx = callbackContext; authAutoresolve = autoresolve; - //reset scopes + // reset scopes int bodyscope = 0; int activityscope = 0; int locationscope = 0; @@ -479,7 +500,7 @@ private void checkAuthorization(final JSONArray args, final CallbackContext call builder.addApi(Fitness.HISTORY_API); builder.addApi(Fitness.CONFIG_API); builder.addApi(Fitness.SESSIONS_API); - //scopes: https://developers.google.com/android/reference/com/google/android/gms/common/Scopes.html + // scopes: https://developers.google.com/android/reference/com/google/android/gms/common/Scopes.html if (bodyscope == READ_PERMS) { builder.addScope(new Scope(Scopes.FITNESS_BODY_READ)); } else if (bodyscope == READ_WRITE_PERMS) { @@ -558,7 +579,7 @@ public void onConnectionFailed(ConnectionResult result) { mClient.connect(); } - + // helper function, connects to fitness APIs assuming that authorisation was granted private boolean lightConnect() { this.cordova.setActivityResultCallback(this); @@ -577,6 +598,7 @@ private boolean lightConnect() { } } + // queries for datapoints private void query(final JSONArray args, final CallbackContext callbackContext) throws JSONException { if (!args.getJSONObject(0).has("startDate")) { callbackContext.error("Missing argument startDate"); @@ -756,6 +778,7 @@ else if (mealt == Field.MEAL_TYPE_SNACK) } } + // utility function, gets nutrients from a Value and merges the value inside a json object private JSONObject getNutrients(Value nutrientsMap, JSONObject mergewith) throws JSONException { JSONObject nutrients; if (mergewith != null) { @@ -785,6 +808,7 @@ private JSONObject getNutrients(Value nutrientsMap, JSONObject mergewith) throws return nutrients; } + // utility function, merges a nutrient in an json object private void mergeNutrient(String f, Value nutrientsMap, JSONObject nutrients) throws JSONException { if (nutrientsMap.getKeyValue(f) != null) { String n = null; @@ -804,6 +828,7 @@ private void mergeNutrient(String f, Value nutrientsMap, JSONObject nutrients) t } } + // queries and aggregates data private void queryAggregated(final JSONArray args, final CallbackContext callbackContext) throws JSONException { if (!args.getJSONObject(0).has("startDate")) { callbackContext.error("Missing argument startDate"); @@ -1149,6 +1174,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } + // utility function that gets the basal metabolic rate averaged over a week private float getBasalAVG(long _et) throws Exception { float basalAVG = 0; Calendar cal = java.util.Calendar.getInstance(); @@ -1184,7 +1210,7 @@ private float getBasalAVG(long _et) throws Exception { } else throw new Exception(dataReadResult.getStatus().getStatusMessage()); } - + // stores a data point private void store(final JSONArray args, final CallbackContext callbackContext) throws JSONException { if (!args.getJSONObject(0).has("startDate")) { callbackContext.error("Missing argument startDate"); @@ -1314,6 +1340,7 @@ private void store(final JSONArray args, final CallbackContext callbackContext) } } + // deletes data points in a given time window private void delete(final JSONArray args, final CallbackContext callbackContext) throws JSONException { if (!args.getJSONObject(0).has("startDate")) { callbackContext.error("Missing argument startDate"); From 07f2f57ad54b4029b8f78e5da9f46b5e8e4d0504 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 11 Apr 2017 14:41:15 +0100 Subject: [PATCH 064/157] adding support for HK category sample types in save() --- src/ios/HealthKit.h | 4 +-- src/ios/HealthKit.m | 79 ++++++++++++++++++++++++++++++++++---------- www/ios/HealthKit.js | 2 +- www/ios/health.js | 6 ++-- 4 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/ios/HealthKit.h b/src/ios/HealthKit.h index 6ae6f52c..0e1b22ad 100755 --- a/src/ios/HealthKit.h +++ b/src/ios/HealthKit.h @@ -123,11 +123,11 @@ - (void) querySampleTypeAggregated:(CDVInvokedUrlCommand*)command; /** - * Save quantity sample data + * Save sample data * * @param command *CDVInvokedUrlCommand */ -- (void) saveQuantitySample:(CDVInvokedUrlCommand*)command; +- (void) saveSample:(CDVInvokedUrlCommand*)command; /** * Save correlation data diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index ad91b15e..2d3b0a04 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -58,7 +58,7 @@ + (HKQuantityType *)getHKQuantityType:(NSString *)elem; + (HKSampleType *)getHKSampleType:(NSString *)elem; -- (HKQuantitySample *)loadHKQuantitySampleFromInputDictionary:(NSDictionary *)inputDictionary error:(NSError **)error; +- (HKQuantitySample *)loadHKSampleFromInputDictionary:(NSDictionary *)inputDictionary error:(NSError **)error; - (HKCorrelation *)loadHKCorrelationFromInputDictionary:(NSDictionary *)inputDictionary error:(NSError **)error; @@ -236,37 +236,51 @@ + (HKSampleType *)getHKSampleType:(NSString *)elem { } /** - * Parse out a quantity sample from a dictionary and perform error checking + * Parse out a sample from a dictionary and perform error checking * * @param inputDictionary *NSDictionary * @param error **NSError * @return *HKQuantitySample */ -- (HKQuantitySample *)loadHKQuantitySampleFromInputDictionary:(NSDictionary *)inputDictionary error:(NSError **)error { +- (HKSample *)loadHKSampleFromInputDictionary:(NSDictionary *)inputDictionary error:(NSError **)error { //Load quantity sample from args to command - if (![inputDictionary hasAllRequiredKeys:@[HKPluginKeyStartDate, HKPluginKeyEndDate, HKPluginKeySampleType, HKPluginKeyUnit, HKPluginKeyAmount] error:error]) { + if (![inputDictionary hasAllRequiredKeys:@[HKPluginKeyStartDate, HKPluginKeyEndDate, HKPluginKeySampleType] error:error]) { return nil; } NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:[inputDictionary[HKPluginKeyStartDate] longValue]]; NSDate *endDate = [NSDate dateWithTimeIntervalSince1970:[inputDictionary[HKPluginKeyEndDate] longValue]]; NSString *sampleTypeString = inputDictionary[HKPluginKeySampleType]; - NSString *unitString = inputDictionary[HKPluginKeyUnit]; //Load optional metadata key NSDictionary *metadata = inputDictionary[HKPluginKeyMetadata]; if (metadata == nil) { - metadata = @{}; + metadata = @{}; } - return [HealthKit getHKQuantitySampleWithStartDate:startDate - endDate:endDate - sampleTypeString:sampleTypeString - unitTypeString:unitString - value:[inputDictionary[HKPluginKeyAmount] doubleValue] - metadata:metadata error:error]; -} + if ([inputDictionary objectForKey:HKPluginKeyUnit]) { + if (![inputDictionary hasAllRequiredKeys:@[HKPluginKeyUnit] error:error]) return nil; + NSString *unitString = [inputDictionary objectForKey:HKPluginKeyUnit]; + + return [HealthKit getHKQuantitySampleWithStartDate:startDate + endDate:endDate + sampleTypeString:sampleTypeString + unitTypeString:unitString + value:[inputDictionary[HKPluginKeyAmount] doubleValue] + metadata:metadata error:error]; + } else { + if (![inputDictionary hasAllRequiredKeys:@[HKPluginKeyValue] error:error]) return nil; + NSString *categoryString = [inputDictionary objectForKey:HKPluginKeyValue]; + + return [self getHKCategorySampleWithStartDate:startDate + endDate:endDate + sampleTypeString:sampleTypeString + categoryString:categoryString + metadata:metadata + error:error]; + } + } /** * Parse out a correlation from a dictionary and perform error checking @@ -289,7 +303,7 @@ - (HKCorrelation *)loadHKCorrelationFromInputDictionary:(NSDictionary *)inputDic NSMutableSet *objects = [NSMutableSet set]; for (NSDictionary *objectDictionary in objectDictionaries) { - HKQuantitySample *sample = [self loadHKQuantitySampleFromInputDictionary:objectDictionary error:error]; + HKSample *sample = [self loadHKSampleFromInputDictionary:objectDictionary error:error]; if (sample == nil) { return nil; } @@ -366,6 +380,37 @@ + (HKQuantitySample *)getHKQuantitySampleWithStartDate:(NSDate *)startDate return [HKQuantitySample quantitySampleWithType:type quantity:quantity startDate:startDate endDate:endDate metadata:metadata]; } +// Helper to handle the functionality with HealthKit to get a category sample +- (HKCategorySample*) getHKCategorySampleWithStartDate:(NSDate*) startDate endDate:(NSDate*) endDate sampleTypeString:(NSString*) sampleTypeString categoryString:(NSString*) categoryString metadata:(NSDictionary*) metadata error:(NSError**) error { + HKCategoryType *type = [HKCategoryType categoryTypeForIdentifier:sampleTypeString]; + if (type==nil) { + *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey:@"quantity type string was invalid"}]; + return nil; + } + NSNumber* value = [self getCategoryValueByName:categoryString type:type]; + if (value == nil) { + *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"%@,%@,%@",@"category value was not compatible with category",type.identifier,categoryString]}]; + return nil; + } + + return [HKCategorySample categorySampleWithType:type value:[value integerValue] startDate:startDate endDate:endDate]; +} + +- (NSNumber*) getCategoryValueByName:(NSString *) categoryValue type:(HKCategoryType*) type { + NSDictionary * map = @{ + @"HKCategoryTypeIdentifierSleepAnalysis":@{ + @"HKCategoryValueSleepAnalysisInBed":@(HKCategoryValueSleepAnalysisInBed), + @"HKCategoryValueSleepAnalysisAsleep":@(HKCategoryValueSleepAnalysisAsleep) + } + }; + + NSDictionary * valueMap = map[type.identifier]; + if (!valueMap) { + return HKCategoryValueNotApplicable; + } + return valueMap[categoryValue]; +} + /** * Query HealthKit to get correlation data within a specified date range * @@ -1647,16 +1692,16 @@ - (void)queryCorrelationType:(CDVInvokedUrlCommand *)command { } /** - * Save quantity sample data + * Save sample data * * @param command *CDVInvokedUrlCommand */ -- (void)saveQuantitySample:(CDVInvokedUrlCommand *)command { +- (void)saveSample:(CDVInvokedUrlCommand *)command { NSDictionary *args = command.arguments[0]; //Use helper method to create quantity sample NSError *error = nil; - HKQuantitySample *sample = [self loadHKQuantitySampleFromInputDictionary:args error:&error]; + HKSample *sample = [self loadHKSampleFromInputDictionary:args error:&error]; //If error in creation, return plugin result if (error) { diff --git a/www/ios/HealthKit.js b/www/ios/HealthKit.js index 11a7f339..86a53c9a 100644 --- a/www/ios/HealthKit.js +++ b/www/ios/HealthKit.js @@ -115,7 +115,7 @@ define('querySampleTypeAggregated', {required: 'sampleType'}, hasValidDates); define('deleteSamples', {required: 'sampleType'}, hasValidDates); define('queryCorrelationType', {required: 'correlationType'}, hasValidDates); -define('saveQuantitySample', {required: 'sampleType'}, hasValidDates); +define('saveSample', {required: 'sampleType'}, hasValidDates); define('saveCorrelation', {required: ['correlationType', 'samples']}, function(options) { hasValidDates(options); diff --git a/www/ios/health.js b/www/ios/health.js index 9be71712..a79cb251 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -407,11 +407,11 @@ Health.prototype.store = function (data, onSuccess, onError) { (data.value === 'sleep.rem')) { data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; data.amount = 1; // amount or value?? - window.plugins.healthkit.saveQuantitySample(data, onSuccess, onError); + window.plugins.healthkit.saveSample(data, onSuccess, onError); } else if (data.value === 'sleep.awake') { data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; data.amount = 0; // amount or value?? - window.plugins.healthkit.saveQuantitySample(data, onSuccess, onError); + window.plugins.healthkit.saveSample(data, onSuccess, onError); } else { // some other kind of workout data.activityType = data.value; @@ -435,7 +435,7 @@ Health.prototype.store = function (data, onSuccess, onError) { if (units[data.dataType]) { data.unit = units[data.dataType]; } - window.plugins.healthkit.saveQuantitySample(data, onSuccess, onError); + window.plugins.healthkit.saveSample(data, onSuccess, onError); } else { onError('unknown data type ' + data.dataType); } From ca7ac2299b2e68640928856b019537cfae84919a Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 11 Apr 2017 16:09:48 +0100 Subject: [PATCH 065/157] confirmed support for sleep fixes #38 --- README.md | 20 ++++++++++++-------- src/ios/HealthKit.m | 2 +- www/ios/health.js | 4 ++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f96a270b..40550520 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ This function is similar to requestAuthorization() and has similar quirks. navigator.health.isAuthorized(datatypes, successCallback, errorCallback) ``` -- datatypes: {type: Array of String}, a list of data types you want to be granted access to +- datatypes: {type: Mixed array}, a list of data types you want to check access of, same as in requestAuthorization - successCallback: {type: function(authorized)}, if the argument is true, the app is authorized - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem @@ -250,7 +250,7 @@ The following table shows what types are supported and examples of the returned | calories | { startDate: Date, endDate: Date, value: 25698.1, unit: 'kcal' } | | calories.active | { startDate: Date, endDate: Date, value: 3547.4, unit: 'kcal' } | | calories.basal | { startDate: Date, endDate: Date, value: 13146.1, unit: 'kcal' } | -| activity | { startDate: Date, endDate: Date, value: { still: { duration: 520000, calories: 30, distance: 0 }, walking: { duration: 223000, calories: 20, distance: 15 }}, unit: 'activitySummary' } (note: duration is expressed in milliseconds, distance in metres and calories in kcal) | +| activity | { startDate: Date, endDate: Date, value: { still: { duration: 520000, calories: 30, distance: 0 }, walking: { duration: 223000, calories: 20, distance: 15 }}, unit: 'activitySummary' } (note: duration is expressed in milliseconds, distance in meters and calories in kcal) | | nutrition | { startDate: Date, endDate: Date, value: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 }, unit: 'nutrition' } (note: units of measurement for nutrients are fixed according to the table at the beginning of this readme) | | nutrition.X | { startDate: Date, endDate: Date, value: 23, unit: 'mg'} | @@ -281,9 +281,16 @@ navigator.health.store({ - startDate: {type: Date}, start date from which the new data starts - endDate: {type: Date}, end date to which he new data ends - dataType: {type: a String}, the data type -- value: {type: a number or an Object}, the value, depending on the actual data type +- value: {type: a number or an Object}, the value, depending on the actual data type. In the case of activity, the value must be set as the activity name. In iOS calories and distance can also be added. +Example: +```javascript +dataType: 'activity', +value: 'walking', +calories: 20, +distance: 15 +``` - sourceName: {type: String}, the source that produced this data. In iOS this is ignored and set automatically to the name of your app. -- sourceBundleId: {type: String}, the complete package of the source that produced this data. In Android, if not specified, it's assigned to the package of the App. In iOS this is ignored and set automatically to the bunde id of the app. +- sourceBundleId: {type: String}, the complete package of the source that produced this data. In Android, if not specified, it's assigned to the package of the App. In iOS this is ignored and set automatically to the bundle id of the app. - successCallback: {type: function}, called if all OK - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem @@ -294,8 +301,7 @@ Quirks of store() - In iOS you cannot store the total calories, you need to specify either basal or active. If you use total calories, the active ones will be stored. - In Android you can only store active calories, as the basal are estimated automatically. If you store total calories, these will be treated as active. - In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. -- In iOS storing the sleep activities is not supported at the moment. -- Storing of nutrients is not supported at the moment. +- Storing of nutrients is not supported at the moment in Android. ### delete() @@ -345,8 +351,6 @@ Quirks of delete() short term: - add storing of nutrition -- allow deletion of data points -- add support for storing HKCategory samples in HealthKit - add more datatypes - body fat percentage - oxygen saturation diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index 2d3b0a04..d7af5205 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -710,7 +710,7 @@ - (void)saveWorkout:(CDVInvokedUrlCommand *)command { // now store the samples, so it shows up in the health app as well (pass this in as an option?) if (energy != nil || distance != nil) { HKQuantitySample *sampleActivity = [HKQuantitySample quantitySampleWithType:[HKQuantityType quantityTypeForIdentifier: - quantityType] + HKQuantityTypeIdentifierDistanceWalkingRunning] quantity:nrOfDistanceUnits startDate:startDate endDate:endDate]; diff --git a/www/ios/health.js b/www/ios/health.js index a79cb251..edf6c919 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -406,11 +406,11 @@ Health.prototype.store = function (data, onSuccess, onError) { (data.value === 'sleep.deep') || (data.value === 'sleep.rem')) { data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; - data.amount = 1; // amount or value?? + data.value = 'HKCategoryValueSleepAnalysisAsleep'; window.plugins.healthkit.saveSample(data, onSuccess, onError); } else if (data.value === 'sleep.awake') { data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; - data.amount = 0; // amount or value?? + data.value = 'HKCategoryValueSleepAnalysisInBed'; window.plugins.healthkit.saveSample(data, onSuccess, onError); } else { // some other kind of workout From 6f02e797c1174928655ab7d1a2eeb6ba374d3b0c Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 11 Apr 2017 17:11:28 +0100 Subject: [PATCH 066/157] added store of nutrition in iOS --- www/ios/health.js | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index edf6c919..574b9877 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -18,6 +18,7 @@ dataTypes['activity'] = 'HKWorkoutTypeIdentifier'; // and HKCategoryTypeIdentifi dataTypes['nutrition'] = 'HKCorrelationTypeIdentifierFood'; dataTypes['nutrition.calories'] = 'HKQuantityTypeIdentifierDietaryEnergyConsumed'; dataTypes['nutrition.fat.total'] = 'HKQuantityTypeIdentifierDietaryFatTotal'; +dataTypes['nutrition.fat.saturated'] = 'HKQuantityTypeIdentifierDietaryFatSaturated'; dataTypes['nutrition.fat.polyunsaturated'] = 'HKQuantityTypeIdentifierDietaryFatPolyunsaturated'; dataTypes['nutrition.fat.monounsaturated'] = 'HKQuantityTypeIdentifierDietaryFatMonounsaturated'; dataTypes['nutrition.cholesterol'] = 'HKQuantityTypeIdentifierDietaryCholesterol'; @@ -75,7 +76,7 @@ var getHKDataTypes = function (dtArr) { if (dtArr[i] === 'nutrition') { // add all nutrition stuff for (var dataType in dataTypes) { - if (dataType.startsWith('nutrition')) HKDataTypes.push(dataTypes[dataType]); + if (dataType.startsWith('nutrition.')) HKDataTypes.push(dataTypes[dataType]); } } else if (dataTypes[dtArr[i]]) { HKDataTypes.push(dataTypes[dtArr[i]]); @@ -289,9 +290,9 @@ Health.prototype.query = function (opts, onSuccess, onError) { Health.prototype.queryAggregated = function (opts, onSuccess, onError) { if ((opts.dataType !== 'steps') && (opts.dataType !== 'distance') && - (opts.dataType !== 'calories') && (opts.dataType !== 'calories.active') && - (opts.dataType !== 'calories.basal') && (opts.dataType !== 'activity') && - (!opts.dataType.startsWith('nutrition'))) { + (opts.dataType !== 'calories') && (opts.dataType !== 'calories.active') && + (opts.dataType !== 'calories.basal') && (opts.dataType !== 'activity') && + (!opts.dataType.startsWith('nutrition'))) { // unsupported datatype onError('Datatype ' + opts.dataType + ' not supported in queryAggregated'); return; @@ -402,9 +403,9 @@ Health.prototype.store = function (data, onSuccess, onError) { } else if (data.dataType === 'activity') { // sleep activity, needs a different call than workout if ((data.value === 'sleep') || - (data.value === 'sleep.light') || - (data.value === 'sleep.deep') || - (data.value === 'sleep.rem')) { + (data.value === 'sleep.light') || + (data.value === 'sleep.deep') || + (data.value === 'sleep.rem')) { data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; data.value = 'HKCategoryValueSleepAnalysisAsleep'; window.plugins.healthkit.saveSample(data, onSuccess, onError); @@ -425,6 +426,29 @@ Health.prototype.store = function (data, onSuccess, onError) { } window.plugins.healthkit.saveWorkout(data, onSuccess, onError); } + } else if (data.dataType === 'nutrition') { + data.correlationType = 'HKCorrelationTypeIdentifierFood'; + if (!data.metadata) data.metadata = {}; + if (data.value.item) data.metadata.HKFoodType = data.value.item; + if (data.value.meal_type) data.metadata.HKFoodMeal = data.value.meal_type; + data.samples = []; + for (var nutrientName in data.value.nutrients) { + var unit = units[nutrientName]; + var sampletype = dataTypes[nutrientName]; + if(! sampletype) { + onError('Cannot recognise nutrition item ' + nutrientName); + return; + } + var sample = { + 'startDate': data.startDate, + 'endDate': data.endDate, + 'sampleType': sampletype, + 'unit': unit, + 'amount': convertToGrams(unit, data.value.nutrients[nutrientName]) + }; + data.samples.push(sample) + } + window.plugins.healthkit.saveCorrelation(data, onSuccess, onError); } else if (dataTypes[data.dataType]) { // generic case data.sampleType = dataTypes[data.dataType]; @@ -479,6 +503,14 @@ var convertFromGrams = function (toUnit, q) { return q; } +// converts to grams from another unit +var convertToGrams = function (fromUnit, q) { + if (fromUnit === 'mcg') return q / 1000000; + if (fromUnit === 'mg') return q / 1000; + if (fromUnit === 'kg') return q * 1000; + return q; +} + // refactors the result of a query into returned type var prepareResult = function (data, unit) { var res = { From ca76fb21d04c2f9586886bcadda65547fed8e61d Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 11 Apr 2017 17:14:24 +0100 Subject: [PATCH 067/157] release 0.9.1 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 37f4df6b..3f5bf9af 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.9.0", + "version": "0.9.1", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 3204e44c..96438f05 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.9.1"> Cordova Health From 81a308cc961783a66c4fc1da390163cfde33f736 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 11 Apr 2017 17:18:56 +0100 Subject: [PATCH 068/157] added explanation of quirky isAuthorized in HK --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 40550520..7618496e 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,10 @@ navigator.health.isAuthorized(datatypes, successCallback, errorCallback) - successCallback: {type: function(authorized)}, if the argument is true, the app is authorized - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem +Quirks of isAuthorized() + +- In iOS, this funciton will only check authorization status for writeable data. Read-only data will always be considered as not authorized. +This is [an intended behaviour of HealthKit](https://developer.apple.com/reference/healthkit/hkhealthstore/1614154-authorizationstatus). ### query() From c08fb1de23da9436f3bc9d161afe9d68c841c67e Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 11 Apr 2017 17:26:59 +0100 Subject: [PATCH 069/157] [docs] just fixing typos --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 7618496e..24431e92 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,6 @@ Quirks of requestAuthorization() ### isAuthorized() Check if the app has authorization to read/write a set of datatypes. -This function is similar to requestAuthorization() and has similar quirks. ``` navigator.health.isAuthorized(datatypes, successCallback, errorCallback) @@ -186,7 +185,7 @@ navigator.health.isAuthorized(datatypes, successCallback, errorCallback) Quirks of isAuthorized() -- In iOS, this funciton will only check authorization status for writeable data. Read-only data will always be considered as not authorized. +- In iOS, this function will only check authorization status for writeable data. Read-only data will always be considered as not authorized. This is [an intended behaviour of HealthKit](https://developer.apple.com/reference/healthkit/hkhealthstore/1614154-authorizationstatus). ### query() From dbc103158aebf49c6307df4cb60dbb25035955dc Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 12 Apr 2017 10:17:57 +0100 Subject: [PATCH 070/157] bug when storing nutrition: no need to convert to grams! --- www/ios/health.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/ios/health.js b/www/ios/health.js index 574b9877..d83fdc21 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -444,7 +444,7 @@ Health.prototype.store = function (data, onSuccess, onError) { 'endDate': data.endDate, 'sampleType': sampletype, 'unit': unit, - 'amount': convertToGrams(unit, data.value.nutrients[nutrientName]) + 'amount': data.value.nutrients[nutrientName] }; data.samples.push(sample) } From bef74fba516386cd51b1b0555fc6b9f5aaa25682 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 3 May 2017 12:26:22 +0100 Subject: [PATCH 071/157] [docs] nicer readme --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 24431e92..ce77992a 100644 --- a/README.md +++ b/README.md @@ -284,14 +284,7 @@ navigator.health.store({ - startDate: {type: Date}, start date from which the new data starts - endDate: {type: Date}, end date to which he new data ends - dataType: {type: a String}, the data type -- value: {type: a number or an Object}, the value, depending on the actual data type. In the case of activity, the value must be set as the activity name. In iOS calories and distance can also be added. -Example: -```javascript -dataType: 'activity', -value: 'walking', -calories: 20, -distance: 15 -``` +- value: {type: a number or an Object}, the value, depending on the actual data type. In the case of activity, the value must be set as the activity name. - sourceName: {type: String}, the source that produced this data. In iOS this is ignored and set automatically to the name of your app. - sourceBundleId: {type: String}, the complete package of the source that produced this data. In Android, if not specified, it's assigned to the package of the App. In iOS this is ignored and set automatically to the bundle id of the app. - successCallback: {type: function}, called if all OK @@ -305,6 +298,7 @@ Quirks of store() - In Android you can only store active calories, as the basal are estimated automatically. If you store total calories, these will be treated as active. - In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. - Storing of nutrients is not supported at the moment in Android. +- In iOS, when storing an activity, you can also specify calories (active, in kcal) and distance (walked or run, in meters). For example: `dataType: 'activity', value: 'walking', calories: 20, distance: 520`. Be aware, though, that you need permission to write calories and distance first, or the call will fail. ### delete() From f35c14440038c5b88368a7e44ca5518db1e8ecbc Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 3 May 2017 12:31:44 +0100 Subject: [PATCH 072/157] removed calories from aggregated query of activity fixes #47 --- README.md | 2 +- src/android/HealthPlugin.java | 28 +--------------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ce77992a..6df1c20e 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ The following table shows what types are supported and examples of the returned Quirks of queryAggregated() - In Android, to query for steps as filtered by the Google Fit app, the flag `filtered: true` must be added into the query object. -- When querying for activities, calories and distance are provided when available in HealthKit. In Google Fit only calories are provided and only when no bucket is specified. +- When querying for activities, calories and distance are provided, when available, in HealthKit. In Google Fit they are not provided. - In Android, the start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. - When bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00. - Weeks start on Monday. diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 62d8aa54..f74588a9 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -945,12 +945,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("calories.basal")) { builder.aggregate(DataType.TYPE_BASAL_METABOLIC_RATE, DataType.AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY); } else if (datatype.equalsIgnoreCase("activity")) { - if (hasbucket) { - builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); - } else { - builder.aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED); - //here we could also get the distance: builder.aggregate(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA); - } + builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); } else if (datatype.equalsIgnoreCase("nutrition.water")) { builder.aggregate(DataType.TYPE_HYDRATION, DataType.AGGREGATE_HYDRATION); } else if (nutritiondatatypes.get(datatype) != null) { @@ -1029,27 +1024,6 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac for (Bucket bucket : dataReadResult.getBuckets()) { - // special case of the activity without time buckets - // here the buckets contain activities and the datapoints contain calories - if (datatype.equalsIgnoreCase("activity") && !hasbucket) { - String activity = bucket.getActivity(); - float calories = 0; - int duration = (int) (bucket.getEndTime(TimeUnit.MILLISECONDS) - bucket.getStartTime(TimeUnit.MILLISECONDS)); - for (DataSet dataset : bucket.getDataSets()) { - for (DataPoint datapoint : dataset.getDataPoints()) { - calories += datapoint.getValue(Field.FIELD_CALORIES).asFloat(); - } - } - JSONObject actobj = retBucket.getJSONObject("value"); - JSONObject summary = new JSONObject(); - summary.put("duration", duration); - summary.put("calories", calories); - actobj.put(activity, summary); - retBucket.put("value", actobj); - // jump to the next iteration - continue; - } - if (hasbucket) { if (customBuckets) { //find the bucket among customs From 1e230ce714de64db161a2497f753b8fca2ca0016 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 15 May 2017 14:28:20 +0100 Subject: [PATCH 073/157] added support for nutrition in java fixes #24 --- src/android/HealthPlugin.java | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index f74588a9..40bf87a5 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -44,6 +44,7 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -1280,6 +1281,44 @@ private void store(final JSONArray args, final CallbackContext callbackContext) } else if (dt.equals(DataType.TYPE_ACTIVITY_SEGMENT)) { String value = args.getJSONObject(0).getString("value"); datapoint.getValue(Field.FIELD_ACTIVITY).setActivity(value); + } else if (dt.equals(DataType.TYPE_NUTRITION)) { + if(datatype.startsWith("nutrition.")){ + //it's a single nutrient + NutrientFieldInfo nuf = nutrientFields.get(datatype); + float nuv = (float) args.getJSONObject(0).getDouble("value"); + datapoint.getValue(Field.FIELD_NUTRIENTS).setKeyValue(nuf.field, nuv); + } else { + // it's a nutrition object + JSONObject nutrobj = args.getJSONObject(0).getJSONObject("value"); + String mealtype = nutrobj.getString("meal_type"); + if(mealtype!=null && !mealtype.isEmpty()) { + if(mealtype.equalsIgnoreCase("breakfast")) + datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(Field.MEAL_TYPE_BREAKFAST); + else if(mealtype.equalsIgnoreCase("lunch")) + datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(Field.MEAL_TYPE_LUNCH); + else if(mealtype.equalsIgnoreCase("snack")) + datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(Field.MEAL_TYPE_SNACK); + else if(mealtype.equalsIgnoreCase("dinner")) + datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(Field.MEAL_TYPE_DINNER); + else datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(Field.MEAL_TYPE_UNKNOWN); + } + String item = nutrobj.getString("item"); + if(item!=null && !item.isEmpty()) { + datapoint.getValue(Field.FIELD_FOOD_ITEM).setString(item); + } + JSONObject nutrientsobj = nutrobj.getJSONObject("nutrients"); + if(nutrientsobj!=null){ + Iterator nutrients = nutrientsobj.keys(); + while(nutrients.hasNext()) { + String nutrientname = nutrients.next(); + NutrientFieldInfo nuf = nutrientFields.get(nutrientname); + if(nuf != null){ + float nuv = (float) nutrientsobj.getDouble(nutrientname); + datapoint.getValue(Field.FIELD_NUTRIENTS).setKeyValue(nuf.field, nuv); + } + } + } + } } else if (dt.equals(customdatatypes.get("gender"))) { String value = args.getJSONObject(0).getString("value"); for (Field f : customdatatypes.get("gender").getFields()) { From bc9d94d4b817987fb0809f320da1cdb9261da39e Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 15 May 2017 14:30:36 +0100 Subject: [PATCH 074/157] release 0.10.0 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3f5bf9af..f0ec7559 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.9.1", + "version": "0.10.0", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 96438f05..a74f4477 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.10.0"> Cordova Health From ca9474c97f036f2aa11bfbbd4ab24f0a72b6a81f Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Sun, 28 May 2017 14:32:18 +0100 Subject: [PATCH 075/157] Added support for HKFoodBrandName metadata key in iOS --- www/ios/health.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/ios/health.js b/www/ios/health.js index d83fdc21..b628fbd0 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -431,6 +431,7 @@ Health.prototype.store = function (data, onSuccess, onError) { if (!data.metadata) data.metadata = {}; if (data.value.item) data.metadata.HKFoodType = data.value.item; if (data.value.meal_type) data.metadata.HKFoodMeal = data.value.meal_type; + if (data.value.brand_name) data.metadata.HKFoodBrandName = data.value.brand_name; data.samples = []; for (var nutrientName in data.value.nutrients) { var unit = units[nutrientName]; @@ -536,6 +537,7 @@ var prepareNutrition = function (data) { if (data.sourceBundleId) res.sourceBundleId = data.sourceBundleId; if (data.metadata && data.metadata.HKFoodType) res.value.item = data.metadata.HKFoodType; if (data.metadata && data.metadata.HKFoodMeal) res.value.meal_type = data.metadata.HKFoodMeal; + if (data.metadata && data.metadata.HKFoodBrandName) res.value.brand_name = data.metadata.HKFoodBrandName; res.value.nutrients = {}; for (var j = 0; j < data.samples.length; j++) { var sample = data.samples[j]; From 451dafc602769c09ebbd1c2f3c9f5093ec656f4d Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Sun, 28 May 2017 15:21:39 +0100 Subject: [PATCH 076/157] Updated README to include brand_name in nutrition response example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6df1c20e..dd1ad919 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ value can be of different types, see examples below: | fat_percentage | 31.2 | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | -| nutrition | { item: "cheese", meal_type: "lunch", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } } | +| nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } } | | nutrition.X | 12.4 | ## Methods From ad0c11268c38be94a800524b120a0fdcdae96be4 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Mon, 29 May 2017 12:34:13 +0100 Subject: [PATCH 077/157] Clarified meal_type and brand_name platform availability in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dd1ad919..ed805f7a 100644 --- a/README.md +++ b/README.md @@ -98,14 +98,14 @@ value can be of different types, see examples below: | steps | 34 | | distance | 101.2 | | calories | 245.3 | -| activity | "walking" (note: recognized activities and their mapping to Fit / HealthKit equivalents are listed in [this file](activities_map.md)) | +| activity | "walking"
**Note**: recognized activities and their mapping to Fit / HealthKit equivalents are listed in [this file](activities_map.md) | | height | 185.9 | | weight | 83.3 | | heart_rate | 66 | | fat_percentage | 31.2 | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | -| nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } } | +| nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: `meal_type` and `brand_name` properties are only available on iOS | | nutrition.X | 12.4 | ## Methods From d485c14d356ff36201524c06b75f9dcde7a9c4f7 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Mon, 29 May 2017 13:09:43 +0100 Subject: [PATCH 078/157] Fixed README error - nutrition meal_type property is also available on Android --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed805f7a..0eb622bb 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ value can be of different types, see examples below: | fat_percentage | 31.2 | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | -| nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: `meal_type` and `brand_name` properties are only available on iOS | +| nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | | nutrition.X | 12.4 | ## Methods From 5a209ca172ef6bb249969970b8b4be470b92f1d5 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Sat, 3 Jun 2017 20:46:40 +0100 Subject: [PATCH 079/157] Initial iOS support for reading blood glucose --- README.md | 25 ++++++++++++++----------- www/ios/health.js | 1 + 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0eb622bb..ddb57016 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,15 @@ Google Fit is limited to fitness data and, for health, custom data types are def |-----------------|-------|-----------------------------------------------|------------------------------------------| | steps | count | HKQuantityTypeIdentifierStepCount | TYPE_STEP_COUNT_DELTA | | distance | m | HKQuantityTypeIdentifierDistanceWalkingRunning + HKQuantityTypeIdentifierDistanceCycling | TYPE_DISTANCE_DELTA | -| calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | -| calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | -| calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | +| calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | +| calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | +| calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | | activity | | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | | height | m | HKQuantityTypeIdentifierHeight | TYPE_HEIGHT | | weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | -| heart_rate | count/min| HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | +| heart_rate | count/min | HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | | fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | +| blood_glucose | mg/dL or mmol/L | HKQuantityTypeIdentifierBloodGlucose| NA | | gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | | nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | @@ -80,7 +81,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | nutrition.water | ml | HKQuantityTypeIdentifierDietaryWater | TYPE_HYDRATION | | nutrition.caffeine | g | HKQuantityTypeIdentifierDietaryCaffeine | NA | -Note: units of measurements are fixed ! +**Note**: with the exception of `blood_glucose` events, units are fixed! Returned objects contain a set of fixed fields: @@ -103,6 +104,7 @@ value can be of different types, see examples below: | weight | 83.3 | | heart_rate | 66 | | fat_percentage | 31.2 | +| blood_glucose | 132 (mg/dL) or 5.4 (mmol/L) | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | @@ -349,12 +351,13 @@ short term: - add storing of nutrition - add more datatypes - - body fat percentage - - oxygen saturation - - blood pressure - - blood glucose - - temperature - - respiratory rate +- body fat percentage +- oxygen saturation +- blood pressure +- storing of blood glucose on iOS +- blood glucose on Android +- temperature +- respiratory rate long term: diff --git a/www/ios/health.js b/www/ios/health.js index b628fbd0..feb218e4 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -34,6 +34,7 @@ dataTypes['nutrition.calcium'] = 'HKQuantityTypeIdentifierDietaryCalcium'; dataTypes['nutrition.iron'] = 'HKQuantityTypeIdentifierDietaryIron'; dataTypes['nutrition.water'] = 'HKQuantityTypeIdentifierDietaryWater'; dataTypes['nutrition.caffeine'] = 'HKQuantityTypeIdentifierDietaryCaffeine'; +dataTypes['blood_glucose'] = 'HKQuantityTypeIdentifierBloodGlucose'; var units = []; units['steps'] = 'count'; From eb4e52abd22e1a12fc3c53eb01979647431a6d38 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Sat, 3 Jun 2017 20:46:40 +0100 Subject: [PATCH 080/157] Initial iOS support for reading blood glucose --- README.md | 25 ++++++++++++++----------- www/ios/health.js | 1 + 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0eb622bb..ddb57016 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,15 @@ Google Fit is limited to fitness data and, for health, custom data types are def |-----------------|-------|-----------------------------------------------|------------------------------------------| | steps | count | HKQuantityTypeIdentifierStepCount | TYPE_STEP_COUNT_DELTA | | distance | m | HKQuantityTypeIdentifierDistanceWalkingRunning + HKQuantityTypeIdentifierDistanceCycling | TYPE_DISTANCE_DELTA | -| calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | -| calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | -| calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | +| calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | +| calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | +| calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | | activity | | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | | height | m | HKQuantityTypeIdentifierHeight | TYPE_HEIGHT | | weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | -| heart_rate | count/min| HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | +| heart_rate | count/min | HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | | fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | +| blood_glucose | mg/dL or mmol/L | HKQuantityTypeIdentifierBloodGlucose| NA | | gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | | nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | @@ -80,7 +81,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | nutrition.water | ml | HKQuantityTypeIdentifierDietaryWater | TYPE_HYDRATION | | nutrition.caffeine | g | HKQuantityTypeIdentifierDietaryCaffeine | NA | -Note: units of measurements are fixed ! +**Note**: with the exception of `blood_glucose` events, units are fixed! Returned objects contain a set of fixed fields: @@ -103,6 +104,7 @@ value can be of different types, see examples below: | weight | 83.3 | | heart_rate | 66 | | fat_percentage | 31.2 | +| blood_glucose | 132 (mg/dL) or 5.4 (mmol/L) | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | @@ -349,12 +351,13 @@ short term: - add storing of nutrition - add more datatypes - - body fat percentage - - oxygen saturation - - blood pressure - - blood glucose - - temperature - - respiratory rate +- body fat percentage +- oxygen saturation +- blood pressure +- storing of blood glucose on iOS +- blood glucose on Android +- temperature +- respiratory rate long term: diff --git a/www/ios/health.js b/www/ios/health.js index b628fbd0..feb218e4 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -34,6 +34,7 @@ dataTypes['nutrition.calcium'] = 'HKQuantityTypeIdentifierDietaryCalcium'; dataTypes['nutrition.iron'] = 'HKQuantityTypeIdentifierDietaryIron'; dataTypes['nutrition.water'] = 'HKQuantityTypeIdentifierDietaryWater'; dataTypes['nutrition.caffeine'] = 'HKQuantityTypeIdentifierDietaryCaffeine'; +dataTypes['blood_glucose'] = 'HKQuantityTypeIdentifierBloodGlucose'; var units = []; units['steps'] = 'count'; From 92154a0f779199ebda3a0a75f809456f36812d23 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Sat, 3 Jun 2017 21:42:23 +0100 Subject: [PATCH 081/157] Fixing issue with BG unit --- www/ios/health.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/ios/health.js b/www/ios/health.js index feb218e4..4601c681 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -65,6 +65,7 @@ units['nutrition.calcium'] = 'mg'; units['nutrition.iron'] = 'mg'; units['nutrition.water'] = 'ml'; units['nutrition.caffeine'] = 'g'; +units['blood_glucose'] = 'mg/dL'; Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); From e318474d10299c5a1a9b6d5692693431adb5752b Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Sun, 4 Jun 2017 02:16:43 +0100 Subject: [PATCH 082/157] Fixed README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ddb57016..b0ed6254 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | | heart_rate | count/min | HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | | fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | -| blood_glucose | mg/dL or mmol/L | HKQuantityTypeIdentifierBloodGlucose| NA | +| blood_glucose | mg/dL | HKQuantityTypeIdentifierBloodGlucose| NA | | gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | | nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | @@ -81,7 +81,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | nutrition.water | ml | HKQuantityTypeIdentifierDietaryWater | TYPE_HYDRATION | | nutrition.caffeine | g | HKQuantityTypeIdentifierDietaryCaffeine | NA | -**Note**: with the exception of `blood_glucose` events, units are fixed! +**Note**: units of measurement are fixed ! Returned objects contain a set of fixed fields: @@ -104,7 +104,7 @@ value can be of different types, see examples below: | weight | 83.3 | | heart_rate | 66 | | fat_percentage | 31.2 | -| blood_glucose | 132 (mg/dL) or 5.4 (mmol/L) | +| blood_glucose | 132
**Note**: to convert to mmol/L, divide by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose)). | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | From 5b11eebc60c38d87e9f1f317b58a986b0e478091 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Sun, 4 Jun 2017 03:31:02 +0100 Subject: [PATCH 083/157] Prettified README :tada: :bikini: --- README.md | 194 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 114 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index b0ed6254..0b977788 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This work is based on [cordova plugin googlefit](https://github.com/2dvisio/cord For an introduction about Google Fit versus HealthKit see [this very good article](https://yalantis.com/blog/how-can-healthkit-and-googlefit-help-you-develop-healthcare-and-fitness-apps/). -This plugin is kept up to date and requires a recent version of cordova (6 and on) and recent iOS and Android SDKs. +This plugin is kept up to date and requires a recent version of cordova (6 and on) as well as recent iOS and Android SDKs. ## Warning @@ -16,22 +16,39 @@ See the [official terms](https://developers.google.com/fit/terms). ## Installation -Just execute this line in your project's folder: +In Cordova: ``` cordova plugin add cordova-plugin-health --variable HEALTH_READ_PERMISSION='App needs read access' --variable HEALTH_WRITE_PERMISSION='App needs write access' ``` -this will install the latest release. `HEALTH_READ_PERMISSION` and `HEALTH_WRITE_PERMISSION` are shown when the app tries to grant access to data in HealthKit. -## Requirements for iOS apps +Phonegap Build `config.xml`: + +``` + + + + + + + + App needs read access + + + + App needs write access + +``` + +## iOS requirements * Make sure your app id has the 'HealthKit' entitlement when this plugin is installed (see iOS dev center). -* Also, make sure your app and AppStore description complies with these Apple review guidelines: https://developer.apple.com/app-store/review/guidelines/#healthkit +* Also, make sure your app and App Store description comply with the [Apple review guidelines](https://developer.apple.com/app-store/review/guidelines/#healthkit). * There are [two keys](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW48) to be added to the info.plist file: `NSHealthShareUsageDescription` and `NSHealthUpdateUsageDescription`. These are assigned with a default string by the plugin, but you may want to contextualise them for your app. -## Requirements for Android apps +## Android requirements * You need to have the Google Services API downloaded in your SDK. * Be sure to give your app access to the Google Fitness API, see [this](https://developers.google.com/fit/android/get-api-key) and [this](https://github.com/2dvisio/cordova-plugin-googlefit#sdk-requirements-for-compiling-the-plugin). @@ -44,7 +61,7 @@ this will install the latest release. As HealthKit does not allow adding custom data types, only a subset of data types supported by HealthKit has been chosen. Google Fit is limited to fitness data and, for health, custom data types are defined with the suffix of the package name of your project. -| data type | Unit | HealthKit equivalent | Google Fit equivalent | +| Data type | Unit | HealthKit equivalent | Google Fit equivalent | |-----------------|-------|-----------------------------------------------|------------------------------------------| | steps | count | HKQuantityTypeIdentifierStepCount | TYPE_STEP_COUNT_DELTA | | distance | m | HKQuantityTypeIdentifierDistanceWalkingRunning + HKQuantityTypeIdentifierDistanceCycling | TYPE_DISTANCE_DELTA | @@ -92,14 +109,14 @@ Returned objects contain a set of fixed fields: - unit: {type: String} the unit of measurement - value: the actual value -value can be of different types, see examples below: +Example values: -| data type | value | +| Data type | Value | |----------------|-----------------------------------| | steps | 34 | | distance | 101.2 | | calories | 245.3 | -| activity | "walking"
**Note**: recognized activities and their mapping to Fit / HealthKit equivalents are listed in [this file](activities_map.md) | +| activity | "walking"
**Note**: recognized activities and their mappings in Google Fit / HealthKit can be found [here](activities_map.md) | | height | 185.9 | | weight | 83.3 | | heart_rate | 66 | @@ -124,7 +141,7 @@ navigator.health.isAvailable(successCallback, errorCallback) - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem -### promptInstallFit() (Android only) +### promptInstallFit() - Android only Checks if recent Google Play Services and Google Fit are installed. If the play services are not installed, or are obsolete, it will show a pop-up suggesting to download them. @@ -157,20 +174,20 @@ navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) - datatypes: {type: Mixed array}, a list of data types you want to be granted access to. You can also specify read or write only permissions. ```javascript [ - 'calories', 'distance', //read and write permissions + 'calories', 'distance', // Read and write permissions { - read : ['steps'], //read only permission - write : ['height', 'weight'] //write only permission + read : ['steps'], // Read only permission + write : ['height', 'weight'] // Write only permission } ] ``` - successCallback: {type: function}, called if all OK - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem -Quirks of requestAuthorization() +#### Android quirks -- In Android, it will try to get authorization from the Google fitness APIs. It is necessary that the app's package name and the signing key are registered in the Google API console (see [here](https://developers.google.com/fit/android/get-api-key)). -- In Android, be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the authorization will ensure that the app is connected again. +- It will try to get authorization from the Google fitness APIs. It is necessary that the app's package name and the signing key are registered in the Google API console (see [here](https://developers.google.com/fit/android/get-api-key)). +- Be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the authorization will ensure that the app is connected again. - In Android 6 and over, this function will also ask for some dynamic permissions if needed (e.g. in the case of "distance", it will need access to ACCESS_FINE_LOCATION). ### isAuthorized() @@ -185,9 +202,9 @@ navigator.health.isAuthorized(datatypes, successCallback, errorCallback) - successCallback: {type: function(authorized)}, if the argument is true, the app is authorized - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem -Quirks of isAuthorized() +#### iOS quirks -- In iOS, this function will only check authorization status for writeable data. Read-only data will always be considered as not authorized. +- This method will only check authorization status for writeable data. Read-only data will always be considered as not authorized. This is [an intended behaviour of HealthKit](https://developer.apple.com/reference/healthkit/hkhealthstore/1614154-authorizationstatus). ### query() @@ -198,10 +215,10 @@ Warning: if the time span is big, it can generate long arrays! ``` navigator.health.query({ - startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // three days ago - endDate: new Date(), // now - dataType: 'height' - }, successCallback, errorCallback) + startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // three days ago + endDate: new Date(), // now + dataType: 'height' +}, successCallback, errorCallback) ``` - startDate: {type: Date}, start date from which to get data @@ -210,19 +227,25 @@ navigator.health.query({ - successCallback: {type: function(data) }, called if all OK, data contains the result of the query in the form of an array of: { startDate: Date, endDate: Date, value: xxx, unit: 'xxx', sourceName: 'aaaa', sourceBundleId: 'bbbb' } - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem +#### iOS quirks -Quirks of query() +- The amount of datapoints is limited to 1000 by default. You can override this by adding a `limit: xxx` to your query object. +- Datapoints are ordered in an descending fashion (from newer to older). You can revert this behaviour by adding `ascending: true` to your query object. +- HealthKit does not calculate active and basal calories - these must be inputted from an app +- HealthKit does not detect specific activities - these must be inputted from an app +- Activities in HealthKit may include two extra fields: calories (kcal) and distance (m) +- When querying for nutrition, HealthKit only returns those stored as correlation. To be sure to get all stored quantities, it's better to query nutrients individually (e.g. MyFitnessPal doesn't store meals as correlations). +- nutrition.vitamin_a is given in micrograms. Automatic conversion to international units is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). -- In iOS, the amount of datapoints is limited to 1000 by default. You can override this by adding a `limit: xxx` to your query object. -- In iOS, datapoints are ordered in an descending fashion (from newer to older). You can revert this behaviour by adding `ascending: true` to your query object. -- In Android, it is possible to query for "raw" steps or to select those as filtered by the Google Fit app. In the latter case the query object must contain the field `filtered: true`. -- In Google Fit, calories.basal is returned as an average per day, and usually is not available in all days. -- In Google Fit, calories.active is computed by subtracting the basal calories from the total. As basal energy expenditure, an average is computed from the week before endDate. -- While Google Fit calculates basal and active calories automatically, HealthKit needs an explicit input from some app. -- When querying for activities, Google Fit is able to determine some activities automatically (still, walking, running, biking, in vehicle), while HealthKit only relies on the input of the user or of some external app. -- When querying for activities, calories and distance are also provided in HealthKit (units are kcal and meters) and never in Google Fit. -- When querying for nutrition, Google Fit always returns all the nutrition elements it has, while HealthKit returns only those that are stored as correlation. To be sure to get all stored the quantities (regardless of they are stored as correlation or not), it's better to query single nutrients. -- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. Automatic conversion is not trivial and depends on the actual substance (see [this](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). +#### Android quirks + +- It is possible to query for "raw" steps or to select those as filtered by the Google Fit app. In the latter case the query object must contain the field `filtered: true`. +- calories.basal is returned as an average per day, and usually is not available in all days. +- calories.active is computed by subtracting the basal calories from the total. As basal energy expenditure, an average is computed from the week before endDate. +- Active and basal calories can be automatically calculated +- Some activities can be determined automatically (still, walking, running, biking, in vehicle) +- When querying for nutrition, Google Fit always returns all the nutrition elements it has. +- nutrition.vitamin_a is given in international units. Automatic conversion to micrograms is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). ### queryAggregated() @@ -231,11 +254,11 @@ Usually the sum is returned for the given quantity. ``` navigator.health.queryAggregated({ - startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // three days ago - endDate: new Date(), // now - dataType: 'steps', - bucket: 'day' - }, successCallback, errorCallback) + startDate: new Date(new Date().getTime() - 3 * 24 * 60 * 60 * 1000), // three days ago + endDate: new Date(), // now + dataType: 'steps', + bucket: 'day' +}, successCallback, errorCallback) ``` - startDate: {type: Date}, start date from which to get data @@ -248,7 +271,7 @@ navigator.health.queryAggregated({ Not all data types are supported for aggregated queries. The following table shows what types are supported and examples of the returned object: -| data type | example of returned object | +| Data type | Example of returned object | |-----------------|----------------------------| | steps | { startDate: Date, endDate: Date, value: 5780, unit: 'count' } | | distance | { startDate: Date, endDate: Date, value: 12500.0, unit: 'm' } | @@ -257,17 +280,24 @@ The following table shows what types are supported and examples of the returned | calories.basal | { startDate: Date, endDate: Date, value: 13146.1, unit: 'kcal' } | | activity | { startDate: Date, endDate: Date, value: { still: { duration: 520000, calories: 30, distance: 0 }, walking: { duration: 223000, calories: 20, distance: 15 }}, unit: 'activitySummary' } (note: duration is expressed in milliseconds, distance in meters and calories in kcal) | | nutrition | { startDate: Date, endDate: Date, value: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 }, unit: 'nutrition' } (note: units of measurement for nutrients are fixed according to the table at the beginning of this readme) | -| nutrition.X | { startDate: Date, endDate: Date, value: 23, unit: 'mg'} | +| nutrition.x | { startDate: Date, endDate: Date, value: 23, unit: 'mg'} | -Quirks of queryAggregated() +#### Quirks -- In Android, to query for steps as filtered by the Google Fit app, the flag `filtered: true` must be added into the query object. -- When querying for activities, calories and distance are provided, when available, in HealthKit. In Google Fit they are not provided. -- In Android, the start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. +- The start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. - When bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00. - Weeks start on Monday. -- When querying for nutrition, HealthKit returns only those that are stored as correlation. To be sure to get all the stored quantities, it's better to query single nutrients. -- nutrition.vitamin_a is given in micrograms in HealthKit and International Unit in Google Fit. + +#### iOS quirks + +- Activities in HealthKit may include two extra fields: calories (kcal) and distance (m) +- When querying for nutrition, HealthKit only returns those stored as correlation. To be sure to get all stored quantities, it's better to query nutrients individually (e.g. MyFitnessPal doesn't store meals as correlations). +- nutrition.vitamin_a is given in micrograms. Automatic conversion to international units is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). + +#### Android quirks + +- To query for steps as filtered by the Google Fit app, the flag `filtered: true` must be added into the query object. +- nutrition.vitamin_a is given in international units. Automatic conversion to micrograms is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). ### store() @@ -280,7 +310,8 @@ navigator.health.store({ dataType: 'steps', value: 180, sourceName: 'my_app', - sourceBundleId: 'com.example.my_app' }, successCallback, errorCallback) + sourceBundleId: 'com.example.my_app' +}, successCallback, errorCallback) ``` - startDate: {type: Date}, start date from which the new data starts @@ -292,15 +323,17 @@ navigator.health.store({ - successCallback: {type: function}, called if all OK - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem +#### iOS quirks + +- In iOS, when storing an activity, you can also specify calories (active, in kcal) and distance (walked or run, in meters). For example: `dataType: 'activity', value: 'walking', calories: 20, distance: 520`. Be aware, though, that you need permission to write calories and distance first, or the call will fail. +- In iOS you cannot store the total calories, you need to specify either basal or active. If you use total calories, the active ones will be stored. +- In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field `cycling: true`. -Quirks of store() +#### Android quirks - Google Fit doesn't allow you to overwrite data points that overlap with others already stored of the same type (see [here](https://developers.google.com/fit/android/history#manageConflicting)). At the moment there is no support for [update](https://developers.google.com/fit/android/history#updateData). -- In iOS you cannot store the total calories, you need to specify either basal or active. If you use total calories, the active ones will be stored. -- In Android you can only store active calories, as the basal are estimated automatically. If you store total calories, these will be treated as active. -- In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. - Storing of nutrients is not supported at the moment in Android. -- In iOS, when storing an activity, you can also specify calories (active, in kcal) and distance (walked or run, in meters). For example: `dataType: 'activity', value: 'walking', calories: 20, distance: 520`. Be aware, though, that you need permission to write calories and distance first, or the call will fail. +- In Android you can only store active calories, as the basal are estimated automatically. If you store total calories, these will be treated as active. ### delete() @@ -311,7 +344,7 @@ navigator.health.delete({ startDate: new Date(new Date().getTime() - 3 * 60 * 1000), // three minutes ago endDate: new Date(), dataType: 'steps' - }, successCallback, errorCallback) +}, successCallback, errorCallback) ``` - startDate: {type: Date}, start date from which to delete data @@ -320,25 +353,26 @@ navigator.health.delete({ - successCallback: {type: function}, called if all OK - errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem +#### iOS quirks -Quirks of delete() +- You cannot delete the total calories, you need to specify either basal or active. If you use total calories, the active ones will be delete. +- Distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field `cycling: true`. +- Deleting sleep activities is not supported at the moment. -- Google Fit doesn't allow you to delete data points that were generated by an app different than yours. -- In Android you can only delete active calories, as the basal are estimated automatically. If you try to delete total calories, these will be treated as active. -- In iOS you cannot delete the total calories, you need to specify either basal or active. If you use total calories, the active ones will be delete. -- In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field ` cycling: true `. -- In iOS deleting sleep activities is not supported at the moment. +#### Android quirks +- Google Fit doesn't allow you to delete data points that were generated by other apps +- You can only delete active calories, as the basal are estimated automatically. If you try to delete total calories, these will be treated as active. ## Differences between HealthKit and Google Fit -* HealthKit includes medical data (eg blood glucose), Google Fit is only related to fitness data. -* HealthKit provides a data model that is not extensible, Google Fit allows defining custom data types. -* HealthKit allows to insert data with the unit of measurement of your choice, and automatically translates units when queried, Google Fit uses fixed units of measurement. -* HealthKit automatically counts steps and distance when you carry your phone with you and if your phone has the CoreMotion chip, Google Fit also detects the kind of activity (sedentary, running, walking, cycling, in vehicle). -* HealthKit automatically computes the distance only for running/walking activities, Google Fit includes bicycle also. +* HealthKit includes medical data (e.g. blood glucose), whereas Google Fit is only meant for fitness data (although now supports some medical data). +* HealthKit provides a data model that is not extensible, whereas Google Fit allows defining custom data types. +* HealthKit allows to insert data with the unit of measurement of your choice, and automatically translates units when queried, whereas Google Fit uses fixed units of measurement. +* HealthKit automatically counts steps and distance when you carry your phone with you and if your phone has the CoreMotion chip, whereas Google Fit also detects the kind of activity (sedentary, running, walking, cycling, in vehicle). +* HealthKit can only compute distance for running/walking activities, whereas Google Fit can also do so for bicycle events. -## External Resources +## External resources * The official Apple documentation for HealthKit [can be found here](https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Framework/index.html#//apple_ref/doc/uid/TP40014707). * For functions that require the `unit` attribute, you can find the comprehensive list of possible units from the [Apple Developers documentation](https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HKUnit_Class/index.html#//apple_ref/doc/uid/TP40014727-CH1-SW2). @@ -347,28 +381,28 @@ Quirks of delete() ## Roadmap -short term: +Short term: -- add storing of nutrition -- add more datatypes -- body fat percentage -- oxygen saturation -- blood pressure -- storing of blood glucose on iOS -- blood glucose on Android -- temperature -- respiratory rate +- Add storing of nutrition +- Add more datatypes +- Body fat percentage +- Oxygen saturation +- Blood pressure +- Storing of blood glucose on iOS +- Blood glucose on Android +- Temperature +- Respiratory rate -long term: +Long term: -- add registration to updates (in Fit: HistoryApi#registerDataUpdateListener()). -- add also Samsung Health as a health record for Android. +- Add registration to updates (e.g. in Google Fit: HistoryApi#registerDataUpdateListener()). +- Add support for Samsung Health as an alternate health record for Android. ## Contributions Any help is more than welcome! -I don't know Objectve C and I am not interested into learning it now, so I would particularly appreciate someone who can give me a hand with the iOS part. +I don't know Objective C and I am not interested in learning it now, so I would particularly appreciate someone who could give me a hand with the iOS part. Also, I would love to know from you if the plugin is currently used in any app actually available online. Just send me an email to my_username at gmail.com. For donations, I have a PayPal account at the same email address. From 2b1b3cea1bcfc3eb63ad518de6fa2113ea843548 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Sun, 4 Jun 2017 21:56:32 +0100 Subject: [PATCH 084/157] Changed fixed blood glucose unit to mmol/L --- README.md | 10 +++++----- www/ios/health.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0b977788..06d55b9b 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | | heart_rate | count/min | HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | | fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | -| blood_glucose | mg/dL | HKQuantityTypeIdentifierBloodGlucose| NA | +| blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose| NA | | gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | | nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | @@ -121,7 +121,7 @@ Example values: | weight | 83.3 | | heart_rate | 66 | | fat_percentage | 31.2 | -| blood_glucose | 132
**Note**: to convert to mmol/L, divide by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose)). | +| blood_glucose | 5.5
**Note**: to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose)). | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | @@ -165,7 +165,7 @@ navigator.health.promptInstallFit(successCallback, errorCallback) Requests read and write access to a set of data types. It is recommendable to always explain why the app needs access to the data before asking the user to authorize it. -This function must be called before using the query and store functions, even if the authorization has already been given at some point in the past. +**Important:** this method must be called before using the query and store methods, even if the authorization has already been given at some point in the past. Failure to do so may cause your app to crash, or in the case of Android, Google Fit may not be ready. ``` navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) @@ -278,8 +278,8 @@ The following table shows what types are supported and examples of the returned | calories | { startDate: Date, endDate: Date, value: 25698.1, unit: 'kcal' } | | calories.active | { startDate: Date, endDate: Date, value: 3547.4, unit: 'kcal' } | | calories.basal | { startDate: Date, endDate: Date, value: 13146.1, unit: 'kcal' } | -| activity | { startDate: Date, endDate: Date, value: { still: { duration: 520000, calories: 30, distance: 0 }, walking: { duration: 223000, calories: 20, distance: 15 }}, unit: 'activitySummary' } (note: duration is expressed in milliseconds, distance in meters and calories in kcal) | -| nutrition | { startDate: Date, endDate: Date, value: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 }, unit: 'nutrition' } (note: units of measurement for nutrients are fixed according to the table at the beginning of this readme) | +| activity | { startDate: Date, endDate: Date, value: { still: { duration: 520000, calories: 30, distance: 0 }, walking: { duration: 223000, calories: 20, distance: 15 }}, unit: 'activitySummary' }
**Note:** duration is expressed in milliseconds, distance in meters and calories in kcal | +| nutrition | { startDate: Date, endDate: Date, value: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 }, unit: 'nutrition' }
**Note:** units of measurement for nutrients are fixed according to the table at the beginning of this README | | nutrition.x | { startDate: Date, endDate: Date, value: 23, unit: 'mg'} | #### Quirks diff --git a/www/ios/health.js b/www/ios/health.js index 4601c681..8051df5f 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -65,7 +65,7 @@ units['nutrition.calcium'] = 'mg'; units['nutrition.iron'] = 'mg'; units['nutrition.water'] = 'ml'; units['nutrition.caffeine'] = 'g'; -units['blood_glucose'] = 'mg/dL'; +units['blood_glucose'] = 'mmol/L'; Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); From 3bf326e1ca51e61cbc58637c9c2b7676708b4b7a Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Sun, 4 Jun 2017 22:58:49 +0100 Subject: [PATCH 085/157] Fix for mmol/L blood glucose support --- README.md | 42 +++++++++++++++++++++--------------------- src/ios/HealthKit.m | 15 ++++++++++----- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 06d55b9b..3403944d 100644 --- a/README.md +++ b/README.md @@ -65,40 +65,40 @@ Google Fit is limited to fitness data and, for health, custom data types are def |-----------------|-------|-----------------------------------------------|------------------------------------------| | steps | count | HKQuantityTypeIdentifierStepCount | TYPE_STEP_COUNT_DELTA | | distance | m | HKQuantityTypeIdentifierDistanceWalkingRunning + HKQuantityTypeIdentifierDistanceCycling | TYPE_DISTANCE_DELTA | -| calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | +| calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | | calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | | calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | | activity | | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | -| height | m | HKQuantityTypeIdentifierHeight | TYPE_HEIGHT | -| weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | +| height | m | HKQuantityTypeIdentifierHeight | TYPE_HEIGHT | +| weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | | heart_rate | count/min | HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | | fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | -| blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose| NA | +| blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose | Not yet supported | | gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | | nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | -| nutrition.calories | kcal | HKQuantityTypeIdentifierDietaryEnergyConsumed | TYPE_NUTRITION, NUTRIENT_CALORIES | -| nutrition.fat.total | g | HKQuantityTypeIdentifierDietaryFatTotal | TYPE_NUTRITION, NUTRIENT_TOTAL_FAT | +| nutrition.calories | kcal | HKQuantityTypeIdentifierDietaryEnergyConsumed | TYPE_NUTRITION, NUTRIENT_CALORIES | +| nutrition.fat.total | g | HKQuantityTypeIdentifierDietaryFatTotal | TYPE_NUTRITION, NUTRIENT_TOTAL_FAT | | nutrition.fat.saturated | g | HKQuantityTypeIdentifierDietaryFatSaturated | TYPE_NUTRITION, NUTRIENT_SATURATED_FAT | -| nutrition.fat.unsaturated | g | NA | TYPE_NUTRITION, NUTRIENT_UNSATURATED_FAT | +| nutrition.fat.unsaturated | g | NA | TYPE_NUTRITION, NUTRIENT_UNSATURATED_FAT | | nutrition.fat.polyunsaturated | g | HKQuantityTypeIdentifierDietaryFatPolyunsaturated | TYPE_NUTRITION, NUTRIENT_POLYUNSATURATED_FAT | | nutrition.fat.monounsaturated | g | HKQuantityTypeIdentifierDietaryFatMonounsaturated | TYPE_NUTRITION, NUTRIENT_MONOUNSATURATED_FAT | -| nutrition.fat.trans | g | NA | TYPE_NUTRITION, NUTRIENT_TRANS_FAT (g) | -| nutrition.cholesterol | mg | HKQuantityTypeIdentifierDietaryCholesterol | TYPE_NUTRITION, NUTRIENT_CHOLESTEROL | -| nutrition.sodium | mg | HKQuantityTypeIdentifierDietarySodium | TYPE_NUTRITION, NUTRIENT_SODIUM | -| nutrition.potassium | mg | HKQuantityTypeIdentifierDietaryPotassium | TYPE_NUTRITION, NUTRIENT_POTASSIUM | -| nutrition.carbs.total | g | HKQuantityTypeIdentifierDietaryCarbohydrates | TYPE_NUTRITION, NUTRIENT_TOTAL_CARBS | -| nutrition.dietary_fiber | g | HKQuantityTypeIdentifierDietaryFiber | TYPE_NUTRITION, NUTRIENT_DIETARY_FIBER | -| nutrition.sugar | g | HKQuantityTypeIdentifierDietarySugar | TYPE_NUTRITION, NUTRIENT_SUGAR | -| nutrition.protein | g | HKQuantityTypeIdentifierDietaryProtein | TYPE_NUTRITION, NUTRIENT_PROTEIN | +| nutrition.fat.trans | g | NA | TYPE_NUTRITION, NUTRIENT_TRANS_FAT (g) | +| nutrition.cholesterol | mg | HKQuantityTypeIdentifierDietaryCholesterol | TYPE_NUTRITION, NUTRIENT_CHOLESTEROL | +| nutrition.sodium | mg | HKQuantityTypeIdentifierDietarySodium | TYPE_NUTRITION, NUTRIENT_SODIUM | +| nutrition.potassium | mg | HKQuantityTypeIdentifierDietaryPotassium | TYPE_NUTRITION, NUTRIENT_POTASSIUM | +| nutrition.carbs.total | g | HKQuantityTypeIdentifierDietaryCarbohydrates | TYPE_NUTRITION, NUTRIENT_TOTAL_CARBS | +| nutrition.dietary_fiber | g | HKQuantityTypeIdentifierDietaryFiber | TYPE_NUTRITION, NUTRIENT_DIETARY_FIBER | +| nutrition.sugar | g | HKQuantityTypeIdentifierDietarySugar | TYPE_NUTRITION, NUTRIENT_SUGAR | +| nutrition.protein | g | HKQuantityTypeIdentifierDietaryProtein | TYPE_NUTRITION, NUTRIENT_PROTEIN | | nutrition.vitamin_a | mcg (HK), IU (GF) | HKQuantityTypeIdentifierDietaryVitaminA | TYPE_NUTRITION, NUTRIENT_VITAMIN_A | -| nutrition.vitamin_c | mg | HKQuantityTypeIdentifierDietaryVitaminC | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | -| nutrition.calcium | mg | HKQuantityTypeIdentifierDietaryCalcium | TYPE_NUTRITION, NUTRIENT_CALCIUM | -| nutrition.iron | mg | HKQuantityTypeIdentifierDietaryIron | TYPE_NUTRITION, NUTRIENT_IRON | -| nutrition.water | ml | HKQuantityTypeIdentifierDietaryWater | TYPE_HYDRATION | -| nutrition.caffeine | g | HKQuantityTypeIdentifierDietaryCaffeine | NA | +| nutrition.vitamin_c | mg | HKQuantityTypeIdentifierDietaryVitaminC | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | +| nutrition.calcium | mg | HKQuantityTypeIdentifierDietaryCalcium | TYPE_NUTRITION, NUTRIENT_CALCIUM | +| nutrition.iron | mg | HKQuantityTypeIdentifierDietaryIron | TYPE_NUTRITION, NUTRIENT_IRON | +| nutrition.water | ml | HKQuantityTypeIdentifierDietaryWater | TYPE_HYDRATION | +| nutrition.caffeine | g | HKQuantityTypeIdentifierDietaryCaffeine | NA | -**Note**: units of measurement are fixed ! +**Note**: units of measurement are fixed! Returned objects contain a set of fixed fields: diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index d7af5205..b3c6e209 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -1327,12 +1327,17 @@ - (void)querySampleType:(CDVInvokedUrlCommand *)command { } HKUnit *unit = nil; if (unitString != nil) { - // issue 51 - // @see https://github.com/Telerik-Verified-Plugins/HealthKit/issues/51 - if ([unitString isEqualToString:@"percent"]) { - unitString = @"%"; + if ([unitString isEqualToString:@"mmol/L"]) { + // @see https://stackoverflow.com/a/30196642/1214598 + unit = [[HKUnit moleUnitWithMetricPrefix:HKMetricPrefixMilli molarMass:HKUnitMolarMassBloodGlucose] unitDividedByUnit:[HKUnit literUnit]]; + } else { + // issue 51 + // @see https://github.com/Telerik-Verified-Plugins/HealthKit/issues/51 + if ([unitString isEqualToString:@"percent"]) { + unitString = @"%"; + } + unit = [HKUnit unitFromString:unitString]; } - unit = [HKUnit unitFromString:unitString]; } // TODO check that unit is compatible with sampleType if sample type of HKQuantityType NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate]; From a0355799064ca813af3ea2480551a331b45746ed Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Sun, 4 Jun 2017 22:58:49 +0100 Subject: [PATCH 086/157] Fix for mmol/L blood glucose support --- README.md | 42 +++++++++++++++++++++--------------------- src/ios/HealthKit.m | 15 ++++++++++----- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 06d55b9b..3403944d 100644 --- a/README.md +++ b/README.md @@ -65,40 +65,40 @@ Google Fit is limited to fitness data and, for health, custom data types are def |-----------------|-------|-----------------------------------------------|------------------------------------------| | steps | count | HKQuantityTypeIdentifierStepCount | TYPE_STEP_COUNT_DELTA | | distance | m | HKQuantityTypeIdentifierDistanceWalkingRunning + HKQuantityTypeIdentifierDistanceCycling | TYPE_DISTANCE_DELTA | -| calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | +| calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | | calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | | calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | | activity | | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | -| height | m | HKQuantityTypeIdentifierHeight | TYPE_HEIGHT | -| weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | +| height | m | HKQuantityTypeIdentifierHeight | TYPE_HEIGHT | +| weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | | heart_rate | count/min | HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | | fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | -| blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose| NA | +| blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose | Not yet supported | | gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | | nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | -| nutrition.calories | kcal | HKQuantityTypeIdentifierDietaryEnergyConsumed | TYPE_NUTRITION, NUTRIENT_CALORIES | -| nutrition.fat.total | g | HKQuantityTypeIdentifierDietaryFatTotal | TYPE_NUTRITION, NUTRIENT_TOTAL_FAT | +| nutrition.calories | kcal | HKQuantityTypeIdentifierDietaryEnergyConsumed | TYPE_NUTRITION, NUTRIENT_CALORIES | +| nutrition.fat.total | g | HKQuantityTypeIdentifierDietaryFatTotal | TYPE_NUTRITION, NUTRIENT_TOTAL_FAT | | nutrition.fat.saturated | g | HKQuantityTypeIdentifierDietaryFatSaturated | TYPE_NUTRITION, NUTRIENT_SATURATED_FAT | -| nutrition.fat.unsaturated | g | NA | TYPE_NUTRITION, NUTRIENT_UNSATURATED_FAT | +| nutrition.fat.unsaturated | g | NA | TYPE_NUTRITION, NUTRIENT_UNSATURATED_FAT | | nutrition.fat.polyunsaturated | g | HKQuantityTypeIdentifierDietaryFatPolyunsaturated | TYPE_NUTRITION, NUTRIENT_POLYUNSATURATED_FAT | | nutrition.fat.monounsaturated | g | HKQuantityTypeIdentifierDietaryFatMonounsaturated | TYPE_NUTRITION, NUTRIENT_MONOUNSATURATED_FAT | -| nutrition.fat.trans | g | NA | TYPE_NUTRITION, NUTRIENT_TRANS_FAT (g) | -| nutrition.cholesterol | mg | HKQuantityTypeIdentifierDietaryCholesterol | TYPE_NUTRITION, NUTRIENT_CHOLESTEROL | -| nutrition.sodium | mg | HKQuantityTypeIdentifierDietarySodium | TYPE_NUTRITION, NUTRIENT_SODIUM | -| nutrition.potassium | mg | HKQuantityTypeIdentifierDietaryPotassium | TYPE_NUTRITION, NUTRIENT_POTASSIUM | -| nutrition.carbs.total | g | HKQuantityTypeIdentifierDietaryCarbohydrates | TYPE_NUTRITION, NUTRIENT_TOTAL_CARBS | -| nutrition.dietary_fiber | g | HKQuantityTypeIdentifierDietaryFiber | TYPE_NUTRITION, NUTRIENT_DIETARY_FIBER | -| nutrition.sugar | g | HKQuantityTypeIdentifierDietarySugar | TYPE_NUTRITION, NUTRIENT_SUGAR | -| nutrition.protein | g | HKQuantityTypeIdentifierDietaryProtein | TYPE_NUTRITION, NUTRIENT_PROTEIN | +| nutrition.fat.trans | g | NA | TYPE_NUTRITION, NUTRIENT_TRANS_FAT (g) | +| nutrition.cholesterol | mg | HKQuantityTypeIdentifierDietaryCholesterol | TYPE_NUTRITION, NUTRIENT_CHOLESTEROL | +| nutrition.sodium | mg | HKQuantityTypeIdentifierDietarySodium | TYPE_NUTRITION, NUTRIENT_SODIUM | +| nutrition.potassium | mg | HKQuantityTypeIdentifierDietaryPotassium | TYPE_NUTRITION, NUTRIENT_POTASSIUM | +| nutrition.carbs.total | g | HKQuantityTypeIdentifierDietaryCarbohydrates | TYPE_NUTRITION, NUTRIENT_TOTAL_CARBS | +| nutrition.dietary_fiber | g | HKQuantityTypeIdentifierDietaryFiber | TYPE_NUTRITION, NUTRIENT_DIETARY_FIBER | +| nutrition.sugar | g | HKQuantityTypeIdentifierDietarySugar | TYPE_NUTRITION, NUTRIENT_SUGAR | +| nutrition.protein | g | HKQuantityTypeIdentifierDietaryProtein | TYPE_NUTRITION, NUTRIENT_PROTEIN | | nutrition.vitamin_a | mcg (HK), IU (GF) | HKQuantityTypeIdentifierDietaryVitaminA | TYPE_NUTRITION, NUTRIENT_VITAMIN_A | -| nutrition.vitamin_c | mg | HKQuantityTypeIdentifierDietaryVitaminC | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | -| nutrition.calcium | mg | HKQuantityTypeIdentifierDietaryCalcium | TYPE_NUTRITION, NUTRIENT_CALCIUM | -| nutrition.iron | mg | HKQuantityTypeIdentifierDietaryIron | TYPE_NUTRITION, NUTRIENT_IRON | -| nutrition.water | ml | HKQuantityTypeIdentifierDietaryWater | TYPE_HYDRATION | -| nutrition.caffeine | g | HKQuantityTypeIdentifierDietaryCaffeine | NA | +| nutrition.vitamin_c | mg | HKQuantityTypeIdentifierDietaryVitaminC | TYPE_NUTRITION, NUTRIENT_VITAMIN_C | +| nutrition.calcium | mg | HKQuantityTypeIdentifierDietaryCalcium | TYPE_NUTRITION, NUTRIENT_CALCIUM | +| nutrition.iron | mg | HKQuantityTypeIdentifierDietaryIron | TYPE_NUTRITION, NUTRIENT_IRON | +| nutrition.water | ml | HKQuantityTypeIdentifierDietaryWater | TYPE_HYDRATION | +| nutrition.caffeine | g | HKQuantityTypeIdentifierDietaryCaffeine | NA | -**Note**: units of measurement are fixed ! +**Note**: units of measurement are fixed! Returned objects contain a set of fixed fields: diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index d7af5205..b3c6e209 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -1327,12 +1327,17 @@ - (void)querySampleType:(CDVInvokedUrlCommand *)command { } HKUnit *unit = nil; if (unitString != nil) { - // issue 51 - // @see https://github.com/Telerik-Verified-Plugins/HealthKit/issues/51 - if ([unitString isEqualToString:@"percent"]) { - unitString = @"%"; + if ([unitString isEqualToString:@"mmol/L"]) { + // @see https://stackoverflow.com/a/30196642/1214598 + unit = [[HKUnit moleUnitWithMetricPrefix:HKMetricPrefixMilli molarMass:HKUnitMolarMassBloodGlucose] unitDividedByUnit:[HKUnit literUnit]]; + } else { + // issue 51 + // @see https://github.com/Telerik-Verified-Plugins/HealthKit/issues/51 + if ([unitString isEqualToString:@"percent"]) { + unitString = @"%"; + } + unit = [HKUnit unitFromString:unitString]; } - unit = [HKUnit unitFromString:unitString]; } // TODO check that unit is compatible with sampleType if sample type of HKQuantityType NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate]; From 75ede9686b84ebaa1dcbdcfba05219046a92d0be Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 5 Jun 2017 09:14:55 +0100 Subject: [PATCH 087/157] [docs] added a long term objective --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 06d55b9b..688678dc 100644 --- a/README.md +++ b/README.md @@ -385,16 +385,17 @@ Short term: - Add storing of nutrition - Add more datatypes -- Body fat percentage -- Oxygen saturation -- Blood pressure -- Storing of blood glucose on iOS -- Blood glucose on Android -- Temperature -- Respiratory rate + Body fat percentage + Oxygen saturation + Blood pressure + Storing of blood glucose on iOS + Blood glucose on Android + Temperature + Respiratory rate Long term: +- Include [Core Motion Activity API](https://developer.apple.com/reference/coremotion/cmmotionactivitymanager) - Add registration to updates (e.g. in Google Fit: HistoryApi#registerDataUpdateListener()). - Add support for Samsung Health as an alternate health record for Android. From 212797c94181b8e9873c6cc91d8de6e8a9b6799d Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Tue, 6 Jun 2017 14:41:48 +0100 Subject: [PATCH 088/157] Attempting to fix UIRequiredDeviceCapabilities issue with iOS --- plugin.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.xml b/plugin.xml index a74f4477..44966281 100755 --- a/plugin.xml +++ b/plugin.xml @@ -40,11 +40,11 @@ - + From 6f93d42f38db15ec23fdd9bd44e157240b1e7157 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Tue, 6 Jun 2017 18:05:13 +0100 Subject: [PATCH 089/157] Removed HealthKit device support requirement from plugin --- plugin.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin.xml b/plugin.xml index 44966281..f4a26a5f 100755 --- a/plugin.xml +++ b/plugin.xml @@ -40,13 +40,15 @@ + + - + From 9fdcb791b8871b9a930f94732c3c7381751ec211 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Tue, 6 Jun 2017 18:21:51 +0100 Subject: [PATCH 090/157] Added clarification on iOS/Android quirks when it comes to querying activities --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c00af598..d5850c1e 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ This is [an intended behaviour of HealthKit](https://developer.apple.com/referen Gets all the data points of a certain data type within a certain time window. -Warning: if the time span is big, it can generate long arrays! +**Warning:** if the time span is big, it can generate long arrays! ``` navigator.health.query({ @@ -236,6 +236,7 @@ navigator.health.query({ - Activities in HealthKit may include two extra fields: calories (kcal) and distance (m) - When querying for nutrition, HealthKit only returns those stored as correlation. To be sure to get all stored quantities, it's better to query nutrients individually (e.g. MyFitnessPal doesn't store meals as correlations). - nutrition.vitamin_a is given in micrograms. Automatic conversion to international units is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). +- When querying for activities, only events whose startDate and endDate are **both** in the query range will be returned. #### Android quirks @@ -246,6 +247,7 @@ navigator.health.query({ - Some activities can be determined automatically (still, walking, running, biking, in vehicle) - When querying for nutrition, Google Fit always returns all the nutrition elements it has. - nutrition.vitamin_a is given in international units. Automatic conversion to micrograms is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). +- When querying for activities, if an event's startDate is out of the query range but its endDate is within, Google Fit will truncate the startDate to match that of the query. ### queryAggregated() From 07f360ddb7177a18f7991169537e35f8e64ae964 Mon Sep 17 00:00:00 2001 From: Vipul Date: Wed, 14 Jun 2017 19:32:43 +0530 Subject: [PATCH 091/157] - Added disconnect method --- src/android/HealthPlugin.java | 36 +++++++++++++++++++++++------------ www/android/health.js | 4 ++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 40bf87a5..b26af917 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -81,6 +81,7 @@ public class HealthPlugin extends CordovaPlugin { // Scope for read/write access to activity-related data types in Google Fit. // These include activity type, calories consumed and expended, step counts, and others. public static Map activitydatatypes = new HashMap(); + static { activitydatatypes.put("steps", DataType.TYPE_STEP_COUNT_DELTA); activitydatatypes.put("calories", DataType.TYPE_CALORIES_EXPENDED); @@ -90,6 +91,7 @@ public class HealthPlugin extends CordovaPlugin { // Scope for read/write access to biometric data types in Google Fit. These include heart rate, height, and weight. public static Map bodydatatypes = new HashMap(); + static { bodydatatypes.put("height", DataType.TYPE_HEIGHT); bodydatatypes.put("weight", DataType.TYPE_WEIGHT); @@ -99,6 +101,7 @@ public class HealthPlugin extends CordovaPlugin { // Scope for read/write access to location-related data types in Google Fit. These include location, distance, and speed. public static Map locationdatatypes = new HashMap(); + static { locationdatatypes.put("distance", DataType.TYPE_DISTANCE_DELTA); } @@ -262,7 +265,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent intent) { Log.i(TAG, "Got authorisation from Google Fit"); if (!mClient.isConnected() && !mClient.isConnecting()) { Log.d(TAG, "Re-trying connection with Fit"); - mClient.connect(); + //mClient.connect(); + mClient.clearDefaultAccountAndReconnect(); // the connection success / failure will be taken care of by ConnectionCallbacks in checkAuthorization() } } else if (resultCode == Activity.RESULT_CANCELED) { @@ -287,6 +291,9 @@ public boolean execute(String action, final JSONArray args, final CallbackContex if ("isAvailable".equals(action)) { isAvailable(callbackContext); return true; + } else if ("disconnect".equals(action)) { + disconnect(); + return true; } else if ("promptInstallFit".equals(action)) { promptInstall(callbackContext); return true; @@ -402,6 +409,10 @@ private void isAvailable(final CallbackContext callbackContext) { callbackContext.sendPluginResult(result); } + private void disconnect() { + mClient.disconnect(); + } + // prompts to install GooglePlayServices if not available then Google Fit if not available private void promptInstall(final CallbackContext callbackContext) { GoogleApiAvailability gapi = GoogleApiAvailability.getInstance(); @@ -577,7 +588,8 @@ public void onConnectionFailed(ConnectionResult result) { } ); mClient = builder.build(); - mClient.connect(); + mClient.clearDefaultAccountAndReconnect(); + //mClient.connect(); } // helper function, connects to fitness APIs assuming that authorisation was granted @@ -1282,7 +1294,7 @@ private void store(final JSONArray args, final CallbackContext callbackContext) String value = args.getJSONObject(0).getString("value"); datapoint.getValue(Field.FIELD_ACTIVITY).setActivity(value); } else if (dt.equals(DataType.TYPE_NUTRITION)) { - if(datatype.startsWith("nutrition.")){ + if (datatype.startsWith("nutrition.")) { //it's a single nutrient NutrientFieldInfo nuf = nutrientFields.get(datatype); float nuv = (float) args.getJSONObject(0).getDouble("value"); @@ -1291,28 +1303,28 @@ private void store(final JSONArray args, final CallbackContext callbackContext) // it's a nutrition object JSONObject nutrobj = args.getJSONObject(0).getJSONObject("value"); String mealtype = nutrobj.getString("meal_type"); - if(mealtype!=null && !mealtype.isEmpty()) { - if(mealtype.equalsIgnoreCase("breakfast")) + if (mealtype != null && !mealtype.isEmpty()) { + if (mealtype.equalsIgnoreCase("breakfast")) datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(Field.MEAL_TYPE_BREAKFAST); - else if(mealtype.equalsIgnoreCase("lunch")) + else if (mealtype.equalsIgnoreCase("lunch")) datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(Field.MEAL_TYPE_LUNCH); - else if(mealtype.equalsIgnoreCase("snack")) + else if (mealtype.equalsIgnoreCase("snack")) datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(Field.MEAL_TYPE_SNACK); - else if(mealtype.equalsIgnoreCase("dinner")) + else if (mealtype.equalsIgnoreCase("dinner")) datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(Field.MEAL_TYPE_DINNER); else datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(Field.MEAL_TYPE_UNKNOWN); } String item = nutrobj.getString("item"); - if(item!=null && !item.isEmpty()) { + if (item != null && !item.isEmpty()) { datapoint.getValue(Field.FIELD_FOOD_ITEM).setString(item); } JSONObject nutrientsobj = nutrobj.getJSONObject("nutrients"); - if(nutrientsobj!=null){ + if (nutrientsobj != null) { Iterator nutrients = nutrientsobj.keys(); - while(nutrients.hasNext()) { + while (nutrients.hasNext()) { String nutrientname = nutrients.next(); NutrientFieldInfo nuf = nutrientFields.get(nutrientname); - if(nuf != null){ + if (nuf != null) { float nuv = (float) nutrientsobj.getDouble(nutrientname); datapoint.getValue(Field.FIELD_NUTRIENTS).setKeyValue(nuf.field, nuv); } diff --git a/www/android/health.js b/www/android/health.js index f100999d..354a5329 100644 --- a/www/android/health.js +++ b/www/android/health.js @@ -8,6 +8,10 @@ Health.prototype.isAvailable = function (onSuccess, onError) { exec(onSuccess, onError, "health", "isAvailable", []); }; +Health.prototype.disconnect = function () { + exec(null, null, "health", "disconnect", []); +}; + Health.prototype.promptInstallFit = function (onSuccess, onError) { exec(onSuccess, onError, "health", "promptInstallFit", []); }; From 72c6b6f4aaa3c07ce31e7bf53746b0b9bd7579a0 Mon Sep 17 00:00:00 2001 From: Vipul Date: Thu, 15 Jun 2017 11:57:38 +0530 Subject: [PATCH 092/157] - Changed disconnect method - Removed clearDefaultAccountAndReconnect() --- src/android/HealthPlugin.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index b26af917..6bd9069e 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -265,8 +265,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent intent) { Log.i(TAG, "Got authorisation from Google Fit"); if (!mClient.isConnected() && !mClient.isConnecting()) { Log.d(TAG, "Re-trying connection with Fit"); - //mClient.connect(); - mClient.clearDefaultAccountAndReconnect(); + mClient.connect(); // the connection success / failure will be taken care of by ConnectionCallbacks in checkAuthorization() } } else if (resultCode == Activity.RESULT_CANCELED) { @@ -410,7 +409,10 @@ private void isAvailable(final CallbackContext callbackContext) { } private void disconnect() { - mClient.disconnect(); + if (mClient.isConnected()) { + mClient.clearDefaultAccountAndReconnect(); + mClient.disconnect(); + } } // prompts to install GooglePlayServices if not available then Google Fit if not available @@ -588,8 +590,7 @@ public void onConnectionFailed(ConnectionResult result) { } ); mClient = builder.build(); - mClient.clearDefaultAccountAndReconnect(); - //mClient.connect(); + mClient.connect(); } // helper function, connects to fitness APIs assuming that authorisation was granted From e379d4b4df128381e20d52a0011e4b9c355a9d1d Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 19 Jun 2017 17:20:18 +0100 Subject: [PATCH 093/157] release 0.10.1 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f0ec7559..db515dcb 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.10.0", + "version": "0.10.1", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index f4a26a5f..7005da1b 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.10.1"> Cordova Health From 9496a49945a5cc8ca8a9e16702bd47889fa0cf68 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 20 Jun 2017 17:12:03 +0100 Subject: [PATCH 094/157] better disconnect on Android and now documented --- README.md | 14 ++++++++++++++ src/android/HealthPlugin.java | 20 +++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d5850c1e..bcc86736 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,20 @@ navigator.health.isAuthorized(datatypes, successCallback, errorCallback) - This method will only check authorization status for writeable data. Read-only data will always be considered as not authorized. This is [an intended behaviour of HealthKit](https://developer.apple.com/reference/healthkit/hkhealthstore/1614154-authorizationstatus). + +### disconnect() - Android only + +Check if the app has authorization to read/write a set of datatypes. +Works only on Android. + +``` +navigator.health.disconnect(successCallback, errorCallback) +``` + +- successCallback: {type: function(disconnected)}, if the argument is true, the app has been disconnected from Google Fit +- errorCallback: {type: function(err)}, called if something went wrong, err contains a textual description of the problem + + ### query() Gets all the data points of a certain data type within a certain time window. diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 6bd9069e..b88442a6 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -290,9 +290,6 @@ public boolean execute(String action, final JSONArray args, final CallbackContex if ("isAvailable".equals(action)) { isAvailable(callbackContext); return true; - } else if ("disconnect".equals(action)) { - disconnect(); - return true; } else if ("promptInstallFit".equals(action)) { promptInstall(callbackContext); return true; @@ -332,6 +329,9 @@ public void run() { } }); return true; + } else if ("disconnect".equals(action)) { + disconnect(callbackContext); + return true; } else if ("query".equals(action)) { cordova.getThreadPool().execute(new Runnable() { @Override @@ -408,10 +408,20 @@ private void isAvailable(final CallbackContext callbackContext) { callbackContext.sendPluginResult(result); } - private void disconnect() { - if (mClient.isConnected()) { + /** + * Disconnects the client from the Google APIs + * @param callbackContext + */ + private void disconnect(final CallbackContext callbackContext) { + PluginResult result; + if (mClient != null && mClient.isConnected()) { mClient.clearDefaultAccountAndReconnect(); mClient.disconnect(); + result = new PluginResult(PluginResult.Status.OK, true); + callbackContext.sendPluginResult(result); + return; + } else{ + callbackContext.error("cannot disconnect, client not connected"); } } From 98871e382fbc697c54efc84830011d0880452326 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 20 Jun 2017 17:19:19 +0100 Subject: [PATCH 095/157] fixes #60 --- plugin.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugin.xml b/plugin.xml index 7005da1b..3630b151 100755 --- a/plugin.xml +++ b/plugin.xml @@ -58,6 +58,14 @@ $HEALTH_WRITE_PERMISSION + + + + + + + + From 1233844bb63cf675c3b8ddf14ea298121d1130eb Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 21 Jun 2017 10:44:39 +0100 Subject: [PATCH 096/157] better and complete disconnection from Fit --- README.md | 7 ++++--- src/android/HealthPlugin.java | 18 ++++++++++++------ www/android/health.js | 4 ++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bcc86736..2242a2d0 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,8 @@ navigator.health.promptInstallFit(successCallback, errorCallback) Requests read and write access to a set of data types. It is recommendable to always explain why the app needs access to the data before asking the user to authorize it. -**Important:** this method must be called before using the query and store methods, even if the authorization has already been given at some point in the past. Failure to do so may cause your app to crash, or in the case of Android, Google Fit may not be ready. +**Important:** this method must be called before using the query and store methods, even if the authorization has already been given at some point in the past. +Failure to do so may cause your app to crash, or in the case of Android, Google Fit may not be ready. ``` navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) @@ -190,6 +191,7 @@ navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) - Be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the authorization will ensure that the app is connected again. - In Android 6 and over, this function will also ask for some dynamic permissions if needed (e.g. in the case of "distance", it will need access to ACCESS_FINE_LOCATION). + ### isAuthorized() Check if the app has authorization to read/write a set of datatypes. @@ -210,8 +212,7 @@ This is [an intended behaviour of HealthKit](https://developer.apple.com/referen ### disconnect() - Android only -Check if the app has authorization to read/write a set of datatypes. -Works only on Android. +Removes authorization from the app. Works only on Android. ``` navigator.health.disconnect(successCallback, errorCallback) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index b88442a6..773a201e 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -7,6 +7,7 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; import android.util.Log; import com.google.android.gms.common.ConnectionResult; @@ -413,13 +414,18 @@ private void isAvailable(final CallbackContext callbackContext) { * @param callbackContext */ private void disconnect(final CallbackContext callbackContext) { - PluginResult result; if (mClient != null && mClient.isConnected()) { - mClient.clearDefaultAccountAndReconnect(); - mClient.disconnect(); - result = new PluginResult(PluginResult.Status.OK, true); - callbackContext.sendPluginResult(result); - return; + Fitness.ConfigApi.disableFit(mClient).setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull Status status) { + if(status.isSuccess()){ + mClient.disconnect(); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, true)); + } else { + callbackContext.error("cannot disconnect," + status.getStatusMessage()); + } + } + }); } else{ callbackContext.error("cannot disconnect, client not connected"); } diff --git a/www/android/health.js b/www/android/health.js index 354a5329..c4660681 100644 --- a/www/android/health.js +++ b/www/android/health.js @@ -8,8 +8,8 @@ Health.prototype.isAvailable = function (onSuccess, onError) { exec(onSuccess, onError, "health", "isAvailable", []); }; -Health.prototype.disconnect = function () { - exec(null, null, "health", "disconnect", []); +Health.prototype.disconnect = function (onSuccess, onError) { + exec(onSuccess, onError, "health", "disconnect", []); }; Health.prototype.promptInstallFit = function (onSuccess, onError) { From 372f08848bbd17b31e50b287be29ca3fa41f8e0b Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 21 Jun 2017 10:51:39 +0100 Subject: [PATCH 097/157] release 0.10.2 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index db515dcb..f2e78e90 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.10.1", + "version": "0.10.2", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 3630b151..926f6f1d 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="0.10.2"> Cordova Health From 6965218540dba3b6309ea9b9ed5f9567c4b8a1f8 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 21 Jun 2017 11:39:00 +0100 Subject: [PATCH 098/157] starting to add blood glucose for Fit --- README.md | 2 +- src/android/HealthPlugin.java | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2242a2d0..42ec506f 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | | heart_rate | count/min | HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | | fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | -| blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose | Not yet supported | +| blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose | TYPE_BLOOD_GLUCOSE | | gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | | nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 773a201e..8a08f814 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -25,6 +25,8 @@ import com.google.android.gms.fitness.data.DataSource; import com.google.android.gms.fitness.data.DataType; import com.google.android.gms.fitness.data.Field; +import com.google.android.gms.fitness.data.HealthDataTypes; +import com.google.android.gms.fitness.data.HealthFields; import com.google.android.gms.fitness.data.Value; import com.google.android.gms.fitness.request.DataDeleteRequest; import com.google.android.gms.fitness.request.DataReadRequest; @@ -155,6 +157,11 @@ public NutrientFieldInfo(String field, String unit) { public static Map customdatatypes = new HashMap(); + public static Map healthdatatypes = new HashMap(); + + static { + healthdatatypes.put("blood_glucose", HealthDataTypes.TYPE_BLOOD_GLUCOSE); + } public HealthPlugin() { } @@ -657,6 +664,8 @@ private void query(final JSONArray args, final CallbackContext callbackContext) dt = nutritiondatatypes.get(datatype); if (customdatatypes.get(datatype) != null) dt = customdatatypes.get(datatype); + if(healthdatatypes.get(datatype) != null) + dt = healthdatatypes.get(datatype); if (dt == null) { callbackContext.error("Datatype " + datatype + " not supported"); return; @@ -797,6 +806,10 @@ else if (mealt == Field.MEAL_TYPE_SNACK) dob.put(f.getName(), fieldvalue); } obj.put("value", dob); + } else if (DT.equals(HealthDataTypes.TYPE_BLOOD_GLUCOSE)) { + float glucose = datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL).asFloat(); + obj.put("value", glucose); + obj.put("unit", "mmol/L"); } resultset.put(obj); @@ -1257,6 +1270,8 @@ private void store(final JSONArray args, final CallbackContext callbackContext) dt = nutritiondatatypes.get(datatype); if (customdatatypes.get(datatype) != null) dt = customdatatypes.get(datatype); + if (healthdatatypes.get(datatype) != null) + dt = healthdatatypes.get(datatype); if (dt == null) { callbackContext.error("Datatype " + datatype + " not supported"); return; @@ -1368,6 +1383,10 @@ else if (mealtype.equalsIgnoreCase("dinner")) if (f.getName().equalsIgnoreCase("year")) datapoint.getValue(f).setInt(year); } + } else if (dt.equals(HealthDataTypes.TYPE_BLOOD_GLUCOSE)) { + String value = args.getJSONObject(0).getString("value"); + float glucose = Float.parseFloat(value); + datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL).setFloat(glucose); } dataSet.add(datapoint); @@ -1411,6 +1430,8 @@ private void delete(final JSONArray args, final CallbackContext callbackContext) dt = nutritiondatatypes.get(datatype); if (customdatatypes.get(datatype) != null) dt = customdatatypes.get(datatype); + if (healthdatatypes.get(datatype) != null) + dt = healthdatatypes.get(datatype); if (dt == null) { callbackContext.error("Datatype " + datatype + " not supported"); return; From 10f049b2203c77cb2bd0a649a517a575ff916890 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 27 Jun 2017 10:21:21 +0100 Subject: [PATCH 099/157] added gitter channel --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 42ec506f..a060223d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ For an introduction about Google Fit versus HealthKit see [this very good articl This plugin is kept up to date and requires a recent version of cordova (6 and on) as well as recent iOS and Android SDKs. +If you have any question or small issue, please use the [gitter channel](https://gitter.im/cordova-plugin-health/Lobby). + ## Warning Google discourages from using Google Fit for medical apps. From 1675099dabde559dad7a585d82deb920eadf3383 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 9 Aug 2017 12:57:09 +0100 Subject: [PATCH 100/157] added health data types to request authorization --- src/android/HealthPlugin.java | 72 +++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 8a08f814..2c555fec 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -80,6 +80,9 @@ public class HealthPlugin extends CordovaPlugin { private static final int REQUEST_DYN_PERMS = 2; private static final int READ_PERMS = 1; private static final int READ_WRITE_PERMS = 2; + private static final int REQUEST_OATH_HEALTH = 55; + + private LinkedList healthDatatypesToAuth = new LinkedList(); // Scope for read/write access to activity-related data types in Google Fit. // These include activity type, calories consumed and expended, step counts, and others. @@ -280,6 +283,17 @@ public void onActivityResult(int requestCode, int resultCode, Intent intent) { // The user cancelled the login dialog before selecting any action. authReqCallbackCtx.error("User cancelled the dialog"); } else authReqCallbackCtx.error("Authorisation failed, result code " + resultCode); + } else if (requestCode == REQUEST_OATH_HEALTH) { + if (resultCode == Activity.RESULT_OK) { + if (healthDatatypesToAuth.isEmpty()) { // check if there are still other data types to be authorised, otherwise OK + authReqSuccess(); + } else { + queryHealthDataForAuth(); + } + } else if (resultCode == Activity.RESULT_CANCELED) { + // The user cancelled the login dialog before selecting any action. + authReqCallbackCtx.error("User cancelled the dialog"); + } else authReqCallbackCtx.error("Authorisation failed, result code " + resultCode); } } @@ -418,6 +432,7 @@ private void isAvailable(final CallbackContext callbackContext) { /** * Disconnects the client from the Google APIs + * * @param callbackContext */ private void disconnect(final CallbackContext callbackContext) { @@ -425,7 +440,7 @@ private void disconnect(final CallbackContext callbackContext) { Fitness.ConfigApi.disableFit(mClient).setResultCallback(new ResultCallback() { @Override public void onResult(@NonNull Status status) { - if(status.isSuccess()){ + if (status.isSuccess()) { mClient.disconnect(); callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, true)); } else { @@ -433,7 +448,7 @@ public void onResult(@NonNull Status status) { } } }); - } else{ + } else { callbackContext.error("cannot disconnect, client not connected"); } } @@ -514,6 +529,8 @@ private void checkAuthorization(final JSONArray args, final CallbackContext call locationscope = READ_PERMS; if (nutritiondatatypes.get(readType) != null) nutritionscope = READ_PERMS; + if (healthdatatypes.get(readType) != null) + healthDatatypesToAuth.add(healthdatatypes.get(readType)); } for (String readWriteType : readWriteTypes) { @@ -525,6 +542,8 @@ private void checkAuthorization(final JSONArray args, final CallbackContext call locationscope = READ_WRITE_PERMS; if (nutritiondatatypes.get(readWriteType) != null) nutritionscope = READ_WRITE_PERMS; + if (healthdatatypes.get(readWriteType) != null) + healthDatatypesToAuth.add(healthdatatypes.get(readWriteType)); } dynPerms.clear(); @@ -560,11 +579,15 @@ private void checkAuthorization(final JSONArray args, final CallbackContext call } builder.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { + @Override public void onConnected(Bundle bundle) { mClient.unregisterConnectionCallbacks(this); Log.i(TAG, "Google Fit connected"); - authReqSuccess(); + if (!healthDatatypesToAuth.isEmpty()) { + // need to query for all health data types + queryHealthDataForAuth(); + } else authReqSuccess(); } @Override @@ -635,6 +658,47 @@ private boolean lightConnect() { } } + // starts a small query for granting access to the health data types + private void queryHealthDataForAuth() { + final DataType dt = healthDatatypesToAuth.pop(); + final long ts = new Date().getTime(); + + cordova.getThreadPool().execute(new Runnable() { + @Override + public void run() { + DataReadRequest readRequest = new DataReadRequest.Builder() + .setTimeRange(ts - 60000, ts, TimeUnit.MILLISECONDS) + .read(dt) + .build(); + DataReadResult dataReadResult = Fitness.HistoryApi.readData(mClient, readRequest).await(); + + if (dataReadResult.getStatus().isSuccess()) { + // already authorised, go on + onActivityResult(REQUEST_OATH_HEALTH, Activity.RESULT_OK, null); + } else { + if (authAutoresolve) { + if (dataReadResult.getStatus().hasResolution()) { + try { + dataReadResult.getStatus().startResolutionForResult(cordova.getActivity(), REQUEST_OATH_HEALTH); + } catch (IntentSender.SendIntentException e) { + authReqCallbackCtx.error("Cannot authorise health data type " + dt.getName() + ", cannot send intent: " + e.getMessage()); + return; + } + } else { + authReqCallbackCtx.error("Cannot authorise health data type " + dt.getName() + ", " + dataReadResult.getStatus().getStatusMessage()); + return; + } + } else { + // probably not authorized, send false + Log.d(TAG, "Connection to Fit failed, probably because of authorization, giving up now"); + authReqCallbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, false)); + return; + } + } + } + }); + } + // queries for datapoints private void query(final JSONArray args, final CallbackContext callbackContext) throws JSONException { if (!args.getJSONObject(0).has("startDate")) { @@ -664,7 +728,7 @@ private void query(final JSONArray args, final CallbackContext callbackContext) dt = nutritiondatatypes.get(datatype); if (customdatatypes.get(datatype) != null) dt = customdatatypes.get(datatype); - if(healthdatatypes.get(datatype) != null) + if (healthdatatypes.get(datatype) != null) dt = healthdatatypes.get(datatype); if (dt == null) { callbackContext.error("Datatype " + datatype + " not supported"); From 5ca02a082b44208cc0b7532ecd2f847af0f810df Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 9 Aug 2017 12:57:22 +0100 Subject: [PATCH 101/157] adding blood glucose to query --- README.md | 5 ++- src/android/HealthPlugin.java | 79 ++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a060223d..5338bd65 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,10 @@ Example values: | weight | 83.3 | | heart_rate | 66 | | fat_percentage | 31.2 | -| blood_glucose | 5.5
**Note**: to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose)). | +| blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Note**: to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose)) +
meal can be: 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack' +
sleep can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep' +
source can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 2c555fec..c51282cd 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -871,11 +871,86 @@ else if (mealt == Field.MEAL_TYPE_SNACK) } obj.put("value", dob); } else if (DT.equals(HealthDataTypes.TYPE_BLOOD_GLUCOSE)) { + JSONObject glucob = new JSONObject(); float glucose = datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL).asFloat(); - obj.put("value", glucose); + if(datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL).isSet() && + datapoint.getValue(Field.FIELD_MEAL_TYPE).isSet()){ + int temp_to_meal = datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL).asInt(); + String meal = ""; + if(temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_AFTER_MEAL){ + meal = "after_"; + } else if(temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_BEFORE_MEAL) { + meal = "before_"; + } else if(temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_FASTING) { + meal = "fasting"; + } else if(temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_GENERAL) { + meal = ""; + } + if(temp_to_meal != HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_FASTING) { + switch (datapoint.getValue(Field.FIELD_MEAL_TYPE).asInt()){ + case Field.MEAL_TYPE_BREAKFAST: + meal += "breakfast"; + break; + case Field.MEAL_TYPE_DINNER: + meal += "dinner"; + break; + case Field.MEAL_TYPE_LUNCH: + meal += "lunch"; + break; + case Field.MEAL_TYPE_SNACK: + meal += "snack"; + break; + default: + meal = "unknown"; + } + } + glucob.put("meal", meal); + } + if(datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_SLEEP).isSet()) { + String sleep = ""; + switch (datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_SLEEP).asInt()){ + case HealthFields.TEMPORAL_RELATION_TO_SLEEP_BEFORE_SLEEP: + sleep = "before_sleep"; + break; + case HealthFields.TEMPORAL_RELATION_TO_SLEEP_DURING_SLEEP: + sleep = "during_sleep"; + break; + case HealthFields.TEMPORAL_RELATION_TO_SLEEP_FULLY_AWAKE: + sleep = "fully_awake"; + break; + case HealthFields.TEMPORAL_RELATION_TO_SLEEP_ON_WAKING: + sleep = "on_waking"; + break; + } + glucob.put("meal", sleep); + } + if(datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_SPECIMEN_SOURCE).isSet()) { + String source = ""; + switch (datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_SPECIMEN_SOURCE).asInt()){ + case HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_CAPILLARY_BLOOD: + source = "capillary_blood"; + break; + case HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_INTERSTITIAL_FLUID: + source = "interstitial_fluid"; + break; + case HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_PLASMA: + source = "plasma"; + break; + case HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_SERUM: + source = "serum"; + break; + case HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_TEARS: + source = "tears"; + break; + case HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_WHOLE_BLOOD: + source = "whole_blood"; + break; + } + glucob.put("source", source); + } + obj.put("value", glucob); obj.put("unit", "mmol/L"); } - resultset.put(obj); } } From 60684d517fa42bb3984e9093c6fdc787173f684c Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 9 Aug 2017 15:24:52 +0100 Subject: [PATCH 102/157] added auth for write data and completed glucose on android fixes #50 --- src/android/HealthPlugin.java | 179 +++++++++++++++++++++++++++++----- 1 file changed, 154 insertions(+), 25 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index c51282cd..6db2588f 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -82,7 +82,8 @@ public class HealthPlugin extends CordovaPlugin { private static final int READ_WRITE_PERMS = 2; private static final int REQUEST_OATH_HEALTH = 55; - private LinkedList healthDatatypesToAuth = new LinkedList(); + private LinkedList healthDatatypesToAuthRead = new LinkedList(); + private LinkedList healthDatatypesToAuthWrite = new LinkedList(); // Scope for read/write access to activity-related data types in Google Fit. // These include activity type, calories consumed and expended, step counts, and others. @@ -285,10 +286,18 @@ public void onActivityResult(int requestCode, int resultCode, Intent intent) { } else authReqCallbackCtx.error("Authorisation failed, result code " + resultCode); } else if (requestCode == REQUEST_OATH_HEALTH) { if (resultCode == Activity.RESULT_OK) { - if (healthDatatypesToAuth.isEmpty()) { // check if there are still other data types to be authorised, otherwise OK - authReqSuccess(); - } else { + if (!healthDatatypesToAuthRead.isEmpty()) { + // do the read health data first queryHealthDataForAuth(); + } else { + // if we are finished with the read health data types, do the write ones + if (!healthDatatypesToAuthWrite.isEmpty()) { + // do the write health data + storeHealthDataForAuth(); + } else { + // done + authReqSuccess(); + } } } else if (resultCode == Activity.RESULT_CANCELED) { // The user cancelled the login dialog before selecting any action. @@ -530,7 +539,7 @@ private void checkAuthorization(final JSONArray args, final CallbackContext call if (nutritiondatatypes.get(readType) != null) nutritionscope = READ_PERMS; if (healthdatatypes.get(readType) != null) - healthDatatypesToAuth.add(healthdatatypes.get(readType)); + healthDatatypesToAuthRead.add(healthdatatypes.get(readType)); } for (String readWriteType : readWriteTypes) { @@ -542,8 +551,9 @@ private void checkAuthorization(final JSONArray args, final CallbackContext call locationscope = READ_WRITE_PERMS; if (nutritiondatatypes.get(readWriteType) != null) nutritionscope = READ_WRITE_PERMS; - if (healthdatatypes.get(readWriteType) != null) - healthDatatypesToAuth.add(healthdatatypes.get(readWriteType)); + if (healthdatatypes.get(readWriteType) != null) { + healthDatatypesToAuthWrite.add(healthdatatypes.get(readWriteType)); + } } dynPerms.clear(); @@ -584,9 +594,12 @@ private void checkAuthorization(final JSONArray args, final CallbackContext call public void onConnected(Bundle bundle) { mClient.unregisterConnectionCallbacks(this); Log.i(TAG, "Google Fit connected"); - if (!healthDatatypesToAuth.isEmpty()) { + if (!healthDatatypesToAuthRead.isEmpty()) { // need to query for all health data types queryHealthDataForAuth(); + } else if (!healthDatatypesToAuthWrite.isEmpty()) { + // need to authorise write health data types + storeHealthDataForAuth(); } else authReqSuccess(); } @@ -660,7 +673,7 @@ private boolean lightConnect() { // starts a small query for granting access to the health data types private void queryHealthDataForAuth() { - final DataType dt = healthDatatypesToAuth.pop(); + final DataType dt = healthDatatypesToAuthRead.pop(); final long ts = new Date().getTime(); cordova.getThreadPool().execute(new Runnable() { @@ -699,6 +712,62 @@ public void run() { }); } + // store a bogus sample for granting access to the health data types + private void storeHealthDataForAuth() { + final DataType dt = healthDatatypesToAuthWrite.pop(); + Calendar c = Calendar.getInstance(); + c.add(Calendar.YEAR, -5); // five years ago + final long ts = c.getTimeInMillis(); + + cordova.getThreadPool().execute(new Runnable() { + @Override + public void run() { + DataSource datasrc = new DataSource.Builder() + .setDataType(dt) + .setAppPackageName(cordova.getActivity()) + .setName("BOGUS") + .setType(DataSource.TYPE_RAW) + .build(); + + DataSet dataSet = DataSet.create(datasrc); + DataPoint datapoint = DataPoint.create(datasrc); + datapoint.setTimeInterval(ts, ts, TimeUnit.MILLISECONDS); + if (dt == HealthDataTypes.TYPE_BLOOD_GLUCOSE) { + datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL).setFloat(1); + } + dataSet.add(datapoint); + + Status insertStatus = Fitness.HistoryApi.insertData(mClient, dataSet) + .await(1, TimeUnit.MINUTES); + + if (insertStatus.isSuccess()) { + // already authorised, go on + onActivityResult(REQUEST_OATH_HEALTH, Activity.RESULT_OK, null); + } else { + if (authAutoresolve) { + if (insertStatus.hasResolution()) { + try { + insertStatus.startResolutionForResult(cordova.getActivity(), REQUEST_OATH_HEALTH); + } catch (IntentSender.SendIntentException e) { + authReqCallbackCtx.error("Cannot authorise health data type " + dt.getName() + ", cannot send intent: " + e.getMessage()); + return; + } + } else { + authReqCallbackCtx.error("Cannot authorise health data type " + dt.getName() + ", " + insertStatus.getStatusMessage()); + return; + } + } else { + // probably not authorized, send false + Log.d(TAG, "Connection to Fit failed, probably because of authorization, giving up now"); + authReqCallbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, false)); + return; + } + } + } + }); + } + + // queries for datapoints private void query(final JSONArray args, final CallbackContext callbackContext) throws JSONException { if (!args.getJSONObject(0).has("startDate")) { @@ -873,21 +942,22 @@ else if (mealt == Field.MEAL_TYPE_SNACK) } else if (DT.equals(HealthDataTypes.TYPE_BLOOD_GLUCOSE)) { JSONObject glucob = new JSONObject(); float glucose = datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL).asFloat(); - if(datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL).isSet() && - datapoint.getValue(Field.FIELD_MEAL_TYPE).isSet()){ + glucob.put("glucose", glucose); + if (datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL).isSet() && + datapoint.getValue(Field.FIELD_MEAL_TYPE).isSet()) { int temp_to_meal = datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL).asInt(); String meal = ""; - if(temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_AFTER_MEAL){ + if (temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_AFTER_MEAL) { meal = "after_"; - } else if(temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_BEFORE_MEAL) { + } else if (temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_BEFORE_MEAL) { meal = "before_"; - } else if(temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_FASTING) { + } else if (temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_FASTING) { meal = "fasting"; - } else if(temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_GENERAL) { + } else if (temp_to_meal == HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_GENERAL) { meal = ""; } - if(temp_to_meal != HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_FASTING) { - switch (datapoint.getValue(Field.FIELD_MEAL_TYPE).asInt()){ + if (temp_to_meal != HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_FASTING) { + switch (datapoint.getValue(Field.FIELD_MEAL_TYPE).asInt()) { case Field.MEAL_TYPE_BREAKFAST: meal += "breakfast"; break; @@ -906,9 +976,9 @@ else if (mealt == Field.MEAL_TYPE_SNACK) } glucob.put("meal", meal); } - if(datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_SLEEP).isSet()) { + if (datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_SLEEP).isSet()) { String sleep = ""; - switch (datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_SLEEP).asInt()){ + switch (datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_SLEEP).asInt()) { case HealthFields.TEMPORAL_RELATION_TO_SLEEP_BEFORE_SLEEP: sleep = "before_sleep"; break; @@ -922,11 +992,11 @@ else if (mealt == Field.MEAL_TYPE_SNACK) sleep = "on_waking"; break; } - glucob.put("meal", sleep); + glucob.put("sleep", sleep); } - if(datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_SPECIMEN_SOURCE).isSet()) { + if (datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_SPECIMEN_SOURCE).isSet()) { String source = ""; - switch (datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_SPECIMEN_SOURCE).asInt()){ + switch (datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_SPECIMEN_SOURCE).asInt()) { case HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_CAPILLARY_BLOOD: source = "capillary_blood"; break; @@ -1523,13 +1593,72 @@ else if (mealtype.equalsIgnoreCase("dinner")) datapoint.getValue(f).setInt(year); } } else if (dt.equals(HealthDataTypes.TYPE_BLOOD_GLUCOSE)) { - String value = args.getJSONObject(0).getString("value"); - float glucose = Float.parseFloat(value); + JSONObject glucoseobj = args.getJSONObject(0).getJSONObject("value"); + float glucose = (float) glucoseobj.getDouble("glucose"); datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL).setFloat(glucose); + + if (glucoseobj.has("meal")) { + String meal = glucoseobj.getString("meal"); + int mealType = Field.MEAL_TYPE_UNKNOWN; + int relationToMeal = HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_GENERAL; + if (meal.equalsIgnoreCase("fasting")) { + mealType = Field.MEAL_TYPE_UNKNOWN; + relationToMeal = HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_FASTING; + } else { + if (meal.startsWith("before_")) { + relationToMeal = HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_BEFORE_MEAL; + meal = meal.substring("before_".length()); + } else if (meal.startsWith("after_")) { + relationToMeal = HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL_AFTER_MEAL; + meal = meal.substring("after_".length()); + } + if (meal.equalsIgnoreCase("dinner")) { + mealType = Field.MEAL_TYPE_DINNER; + } else if (meal.equalsIgnoreCase("lunch")) { + mealType = Field.MEAL_TYPE_LUNCH; + } else if (meal.equalsIgnoreCase("snack")) { + mealType = Field.MEAL_TYPE_SNACK; + } else if (meal.equalsIgnoreCase("breakfast")) { + mealType = Field.MEAL_TYPE_BREAKFAST; + } + } + datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_MEAL).setInt(relationToMeal); + datapoint.getValue(Field.FIELD_MEAL_TYPE).setInt(mealType); + } + + if (glucoseobj.has("sleep")) { + String sleep = glucoseobj.getString("sleep"); + int relationToSleep = HealthFields.TEMPORAL_RELATION_TO_SLEEP_FULLY_AWAKE; + if (sleep.equalsIgnoreCase("before_sleep")) { + relationToSleep = HealthFields.TEMPORAL_RELATION_TO_SLEEP_BEFORE_SLEEP; + } else if (sleep.equalsIgnoreCase("on_waking")) { + relationToSleep = HealthFields.TEMPORAL_RELATION_TO_SLEEP_ON_WAKING; + } else if (sleep.equalsIgnoreCase("during_sleep")) { + relationToSleep = HealthFields.TEMPORAL_RELATION_TO_SLEEP_DURING_SLEEP; + } + datapoint.getValue(HealthFields.FIELD_TEMPORAL_RELATION_TO_SLEEP).setInt(relationToSleep); + } + + if (glucoseobj.has("source")) { + String source = glucoseobj.getString("source"); + int specimenSource = HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_CAPILLARY_BLOOD; + if (source.equalsIgnoreCase("interstitial_fluid")) { + specimenSource = HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_INTERSTITIAL_FLUID; + } else if (source.equalsIgnoreCase("plasma")) { + specimenSource = HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_PLASMA; + } else if (source.equalsIgnoreCase("serum")) { + specimenSource = HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_SERUM; + } else if (source.equalsIgnoreCase("tears")) { + specimenSource = HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_TEARS; + } else if (source.equalsIgnoreCase("whole_blood")) { + specimenSource = HealthFields.BLOOD_GLUCOSE_SPECIMEN_SOURCE_WHOLE_BLOOD; + } + datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_SPECIMEN_SOURCE).setInt(specimenSource); + } + } dataSet.add(datapoint); - Status insertStatus = Fitness.HistoryApi.insertData(mClient, dataSet) .await(1, TimeUnit.MINUTES); From 042c819a1eb504cd1cbca64b5a9c93bff31e75b0 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 9 Aug 2017 15:29:50 +0100 Subject: [PATCH 103/157] updated roadmap --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 5338bd65..0fd7a114 100644 --- a/README.md +++ b/README.md @@ -405,15 +405,7 @@ navigator.health.delete({ Short term: -- Add storing of nutrition -- Add more datatypes - Body fat percentage - Oxygen saturation - Blood pressure - Storing of blood glucose on iOS - Blood glucose on Android - Temperature - Respiratory rate +- Add more datatypes (body fat percentage, oxygen saturation, blood pressure, temperature, respiratory rate) Long term: From c4d0c2b132ee5d4ac0c32ab06141ac8111dfdd6d Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 9 Aug 2017 17:00:50 +0100 Subject: [PATCH 104/157] as done in https://github.com/Telerik-Verified-Plugins/HealthKit/commit/502b951e1fd6bb82cf0420f2d9f79fd21b015978 --- src/ios/HealthKit.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index b3c6e209..ad66273e 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -1652,7 +1652,7 @@ - (void)queryCorrelationType:(CDVInvokedUrlCommand *)command { HKPluginKeySampleType: quantitySample.sampleType.identifier, HKPluginKeyValue: @([quantitySample.quantity doubleValueForUnit:unit]), HKPluginKeyUnit: unitS, - HKPluginKeyMetadata: ((quantitySample.metadata != nil) ? quantitySample.metadata : @{}), + HKPluginKeyMetadata: (quantitySample.metadata == nil || ![NSJSONSerialization isValidJSONObject:quantitySample.metadata]) ? @{} : quantitySample.metadata, HKPluginKeyUUID: quantitySample.UUID.UUIDString } ]; From 29488e7bd4b0cf7d864f7290092a70fdc3efa5c3 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 9 Aug 2017 17:18:22 +0100 Subject: [PATCH 105/157] refactoring blood glucose in iOS to use metadata too --- www/ios/health.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index 8051df5f..89a4a067 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -257,7 +257,16 @@ Health.prototype.query = function (opts, onSuccess, onError) { var res = {}; res.startDate = new Date(samples[i].startDate); res.endDate = new Date(samples[i].endDate); - res.value = samples[i].quantity; + if (opts.dataType === 'blood_glucose') { + res.value = { + glucose: samples[i].quantity + } + if (data.metadata && data.metadata.HKMetadataKeyBloodGlucoseMealTime) res.value.meal = data.metadata.HKMetadataKeyBloodGlucoseMealTime; + if (data.metadata && data.metadata.HKMetadataKeyBloodGlucoseSleepTime) res.value.sleep = data.metadata.HKMetadataKeyBloodGlucoseSleepTime; + if (data.metadata && data.metadata.HKMetadataKeyBloodGlucoseSource) res.value.source = data.metadata.HKMetadataKeyBloodGlucoseSource; + } else { + res.value = samples[i].quantity; + } if (data[i].unit) res.unit = samples[i].unit; else if (opts.unit) res.unit = opts.unit; res.sourceName = samples[i].sourceName; @@ -458,7 +467,14 @@ Health.prototype.store = function (data, onSuccess, onError) { if ((data.dataType === 'distance') && data.cycling) { data.sampleType = 'HKQuantityTypeIdentifierDistanceCycling'; } - data.amount = data.value; + if (data.dataType === 'blood_glucose') { + data.amount = data.value.glucose; + if (data.value.meal) data.metadata.HKMetadataKeyBloodGlucoseMealTime = data.value.meal; + if (data.value.sleep) data.metadata.HKMetadataKeyBloodGlucoseSleepTime = data.value.sleep; + if (data.value.source) data.metadata.HKMetadataKeyBloodGlucoseSource = data.value.source; + } else { + data.amount = data.value; + } if (units[data.dataType]) { data.unit = units[data.dataType]; } From ea4d6e459e605f32dcb5b041d06311895460690f Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 9 Aug 2017 17:30:52 +0100 Subject: [PATCH 106/157] fixes #65 --- www/ios/health.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/ios/health.js b/www/ios/health.js index 89a4a067..dbbe09fc 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -469,6 +469,7 @@ Health.prototype.store = function (data, onSuccess, onError) { } if (data.dataType === 'blood_glucose') { data.amount = data.value.glucose; + if (!data.metadata) data.metadata = {}; if (data.value.meal) data.metadata.HKMetadataKeyBloodGlucoseMealTime = data.value.meal; if (data.value.sleep) data.metadata.HKMetadataKeyBloodGlucoseSleepTime = data.value.sleep; if (data.value.source) data.metadata.HKMetadataKeyBloodGlucoseSource = data.value.source; From b0e20509b3ab56186604837ba8a4bdc9a3941f1b Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 10 Aug 2017 09:52:55 +0100 Subject: [PATCH 107/157] fixed README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0fd7a114..a1e883d1 100644 --- a/README.md +++ b/README.md @@ -123,10 +123,10 @@ Example values: | weight | 83.3 | | heart_rate | 66 | | fat_percentage | 31.2 | -| blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Note**: to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose)) -
meal can be: 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack' -
sleep can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep' -
source can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | +| blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Note**: to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose)). +"meal" can be: 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack' +"sleep" can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep' +"source" can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | From 14ace21479d3e82a56af3d052f716db0efb09aa5 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 10 Aug 2017 09:53:53 +0100 Subject: [PATCH 108/157] always parses correct unit --- src/ios/HealthKit.m | 61 ++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index ad66273e..38fc1075 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -261,8 +261,8 @@ - (HKSample *)loadHKSampleFromInputDictionary:(NSDictionary *)inputDictionary er if ([inputDictionary objectForKey:HKPluginKeyUnit]) { if (![inputDictionary hasAllRequiredKeys:@[HKPluginKeyUnit] error:error]) return nil; - NSString *unitString = [inputDictionary objectForKey:HKPluginKeyUnit]; - + NSString *unitString = [inputDictionary objectForKey:HKPluginKeyUnit]; + return [HealthKit getHKQuantitySampleWithStartDate:startDate endDate:endDate sampleTypeString:sampleTypeString @@ -352,26 +352,37 @@ + (HKQuantitySample *)getHKQuantitySampleWithStartDate:(NSDate *)startDate HKUnit *unit = nil; @try { - unit = ((unitTypeString != nil) ? [HKUnit unitFromString:unitTypeString] : nil); - if (unit == nil) { + + ///// ADD THIS CODE + if (unitTypeString != nil) { + if ([unitTypeString isEqualToString:@"mmol/L"]) { + // @see https://stackoverflow.com/a/30196642/1214598 + unit = [[HKUnit moleUnitWithMetricPrefix:HKMetricPrefixMilli molarMass:HKUnitMolarMassBloodGlucose] unitDividedByUnit:[HKUnit literUnit]]; + } else { + // issue 51 + // @see https://github.com/Telerik-Verified-Plugins/HealthKit/issues/51 + if ([unitTypeString isEqualToString:@"percent"]) { + unitTypeString = @"%"; + } + unit = [HKUnit unitFromString:unitTypeString]; + } + } else { if (error != nil) { - *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey: @"unit was invalid"}]; + *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey: @"unit is invalid"}]; } - return nil; } } @catch (NSException *e) { if (error != nil) { - *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey: @"unit was invalid"}]; + *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey: @"unit is invalid"}]; } - return nil; } HKQuantity *quantity = [HKQuantity quantityWithUnit:unit doubleValue:value]; if (![quantity isCompatibleWithUnit:unit]) { if (error != nil) { - *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey: @"unit was not compatible with quantity"}]; + *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey: @"unit is not compatible with quantity"}]; } return nil; @@ -384,12 +395,12 @@ + (HKQuantitySample *)getHKQuantitySampleWithStartDate:(NSDate *)startDate - (HKCategorySample*) getHKCategorySampleWithStartDate:(NSDate*) startDate endDate:(NSDate*) endDate sampleTypeString:(NSString*) sampleTypeString categoryString:(NSString*) categoryString metadata:(NSDictionary*) metadata error:(NSError**) error { HKCategoryType *type = [HKCategoryType categoryTypeForIdentifier:sampleTypeString]; if (type==nil) { - *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey:@"quantity type string was invalid"}]; + *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey:@"quantity type string is invalid"}]; return nil; } NSNumber* value = [self getCategoryValueByName:categoryString type:type]; if (value == nil) { - *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"%@,%@,%@",@"category value was not compatible with category",type.identifier,categoryString]}]; + *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"%@,%@,%@",@"category value is not compatible with category",type.identifier,categoryString]}]; return nil; } @@ -435,7 +446,7 @@ - (HKCorrelation *)getHKCorrelationWithStartDate:(NSDate *)startDate HKCorrelationType *correlationType = [HKCorrelationType correlationTypeForIdentifier:correlationTypeString]; if (correlationType == nil) { if (error != nil) { - *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey: @"correlation type string was invalid"}]; + *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey: @"correlation type string is invalid"}]; } return nil; @@ -487,7 +498,7 @@ - (BOOL)hasAllRequiredKeys:(NSArray *)keys error:(NSError **)error { } if (error != nil) { - NSString *errMsg = [NSString stringWithFormat:@"required value(s) -%@- was missing from dictionary %@", [missing componentsJoinedByString:@", "], [self description]]; + NSString *errMsg = [NSString stringWithFormat:@"required value(s) -%@- is missing from dictionary %@", [missing componentsJoinedByString:@", "], [self description]]; *error = [NSError errorWithDomain:HKPluginError code:0 userInfo:@{NSLocalizedDescriptionKey: errMsg}]; } @@ -651,7 +662,7 @@ - (void)saveWorkout:(CDVInvokedUrlCommand *)command { if (energy != nil && energy != (id) [NSNull null]) { // better safe than sorry HKUnit *preferredEnergyUnit = [HealthKit getUnit:energyUnit expected:@"HKEnergyUnit"]; if (preferredEnergyUnit == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"invalid energyUnit was passed" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"invalid energyUnit is passed" command:command delegate:self.commandDelegate]; return; } nrOfEnergyUnits = [HKQuantity quantityWithUnit:preferredEnergyUnit doubleValue:energy.doubleValue]; @@ -664,7 +675,7 @@ - (void)saveWorkout:(CDVInvokedUrlCommand *)command { if (distance != nil && distance != (id) [NSNull null]) { // better safe than sorry HKUnit *preferredDistanceUnit = [HealthKit getUnit:distanceUnit expected:@"HKLengthUnit"]; if (preferredDistanceUnit == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"invalid distanceUnit was passed" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"invalid distanceUnit is passed" command:command delegate:self.commandDelegate]; return; } nrOfDistanceUnits = [HKQuantity quantityWithUnit:preferredDistanceUnit doubleValue:distance.doubleValue]; @@ -681,7 +692,7 @@ - (void)saveWorkout:(CDVInvokedUrlCommand *)command { } else if (args[HKPluginKeyEndDate] != nil) { endDate = [NSDate dateWithTimeIntervalSince1970:[args[HKPluginKeyEndDate] doubleValue]]; } else { - [HealthKit triggerErrorCallbackWithMessage:@"no duration or endDate was set" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"no duration or endDate is set" command:command delegate:self.commandDelegate]; return; } @@ -849,7 +860,7 @@ - (void)saveWeight:(CDVInvokedUrlCommand *)command { HKUnit *preferredUnit = [HealthKit getUnit:unit expected:@"HKMassUnit"]; if (preferredUnit == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"invalid unit was passed" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"invalid unit is passed" command:command delegate:self.commandDelegate]; return; } @@ -892,7 +903,7 @@ - (void)readWeight:(CDVInvokedUrlCommand *)command { HKUnit *preferredUnit = [HealthKit getUnit:unit expected:@"HKMassUnit"]; if (preferredUnit == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"invalid unit was passed" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"invalid unit is passed" command:command delegate:self.commandDelegate]; return; } @@ -948,13 +959,13 @@ - (void)saveHeight:(CDVInvokedUrlCommand *)command { BOOL requestReadPermission = (args[@"requestReadPermission"] == nil || [args[@"requestReadPermission"] boolValue]); if (amount == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"no amount was set" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"no amount is set" command:command delegate:self.commandDelegate]; return; } HKUnit *preferredUnit = [HealthKit getUnit:unit expected:@"HKLengthUnit"]; if (preferredUnit == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"invalid unit was passed" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"invalid unit is passed" command:command delegate:self.commandDelegate]; return; } @@ -997,7 +1008,7 @@ - (void)readHeight:(CDVInvokedUrlCommand *)command { HKUnit *preferredUnit = [HealthKit getUnit:unit expected:@"HKLengthUnit"]; if (preferredUnit == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"invalid unit was passed" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"invalid unit is passed" command:command delegate:self.commandDelegate]; return; } @@ -1467,7 +1478,7 @@ - (void)querySampleTypeAggregated:(CDVInvokedUrlCommand *)command { HKStatisticsOptions statOpt = HKStatisticsOptionNone; if (quantityType == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"sampleType was invalid" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"sampleType is invalid" command:command delegate:self.commandDelegate]; return; } else if ([sampleTypeString isEqualToString:@"HKQuantityTypeIdentifierHeartRate"]) { statOpt = HKStatisticsOptionDiscreteAverage; @@ -1488,7 +1499,7 @@ - (void)querySampleTypeAggregated:(CDVInvokedUrlCommand *)command { HKSampleType *type = [HealthKit getHKSampleType:sampleTypeString]; if (type == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"sampleType was invalid" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"sampleType is invalid" command:command delegate:self.commandDelegate]; return; } @@ -1589,7 +1600,7 @@ - (void)queryCorrelationType:(CDVInvokedUrlCommand *)command { HKCorrelationType *type = (HKCorrelationType *) [HealthKit getHKSampleType:correlationTypeString]; if (type == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"sampleType was invalid" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"sampleType is invalid" command:command delegate:self.commandDelegate]; return; } NSMutableArray *units = [[NSMutableArray alloc] init]; @@ -1779,7 +1790,7 @@ - (void)deleteSamples:(CDVInvokedUrlCommand *)command { HKSampleType *type = [HealthKit getHKSampleType:sampleTypeString]; if (type == nil) { - [HealthKit triggerErrorCallbackWithMessage:@"sampleType was invalid" command:command delegate:self.commandDelegate]; + [HealthKit triggerErrorCallbackWithMessage:@"sampleType is invalid" command:command delegate:self.commandDelegate]; return; } From f14a58800270dc9634c67a29a576f72843e73468 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 10 Aug 2017 10:11:26 +0100 Subject: [PATCH 109/157] fixed issue when returning glucose --- src/ios/HealthKit.m | 2 -- www/ios/health.js | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index 38fc1075..c3f5f93f 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -352,8 +352,6 @@ + (HKQuantitySample *)getHKQuantitySampleWithStartDate:(NSDate *)startDate HKUnit *unit = nil; @try { - - ///// ADD THIS CODE if (unitTypeString != nil) { if ([unitTypeString isEqualToString:@"mmol/L"]) { // @see https://stackoverflow.com/a/30196642/1214598 diff --git a/www/ios/health.js b/www/ios/health.js index dbbe09fc..01823672 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -261,9 +261,9 @@ Health.prototype.query = function (opts, onSuccess, onError) { res.value = { glucose: samples[i].quantity } - if (data.metadata && data.metadata.HKMetadataKeyBloodGlucoseMealTime) res.value.meal = data.metadata.HKMetadataKeyBloodGlucoseMealTime; - if (data.metadata && data.metadata.HKMetadataKeyBloodGlucoseSleepTime) res.value.sleep = data.metadata.HKMetadataKeyBloodGlucoseSleepTime; - if (data.metadata && data.metadata.HKMetadataKeyBloodGlucoseSource) res.value.source = data.metadata.HKMetadataKeyBloodGlucoseSource; + if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime) res.value.meal = samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime; + if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime) res.value.sleep = samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime; + if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSource) res.value.source = samples[i].metadata.HKMetadataKeyBloodGlucoseSource; } else { res.value = samples[i].quantity; } From 1b5724270122a974fa2ebfe0830691422a3a3285 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 10 Aug 2017 10:13:01 +0100 Subject: [PATCH 110/157] [docs] fixing README (again) --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index a1e883d1..e3e3b30e 100644 --- a/README.md +++ b/README.md @@ -123,10 +123,7 @@ Example values: | weight | 83.3 | | heart_rate | 66 | | fat_percentage | 31.2 | -| blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Note**: to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose)). -"meal" can be: 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack' -"sleep" can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep' -"source" can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | +| blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Note**: to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose)).
"meal" can be: 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack'
"sleep" can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep'
"source" can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | From fa0cb47b0b2ca2557c2acf88095e54b930c1f444 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 10 Aug 2017 10:16:10 +0100 Subject: [PATCH 111/157] [docs] fixing README (again) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3e3b30e..d8c68bdc 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Example values: | weight | 83.3 | | heart_rate | 66 | | fat_percentage | 31.2 | -| blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Note**: to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose)).
"meal" can be: 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack'
"sleep" can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep'
"source" can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | +| blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Notes**:
to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose))
`meal` can be: 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack'
`sleep` can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep'
`source` can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | From bf26687207f31468ed199b0e700a403f688f69d5 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 10 Aug 2017 10:25:04 +0100 Subject: [PATCH 112/157] [docs] added comments about health data types permission in Google Fit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d8c68bdc..2c6a85b0 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Phonegap Build `config.xml`: * Be sure to give your app access to the Google Fitness API, see [this](https://developers.google.com/fit/android/get-api-key) and [this](https://github.com/2dvisio/cordova-plugin-googlefit#sdk-requirements-for-compiling-the-plugin). * If you are wondering what key your compiled app is using, you can type `keytool -list -printcert -jarfile yourapp.apk`. * You can use the Google Fitness API even if the user doesn't have Google Fit installed, but there has to be some other fitness app putting data into the Fitness API otherwise your queries will always be empty. See the [the original documentation](https://developers.google.com/fit/overview). +* If you are planning to use [health data types](https://developers.google.com/android/reference/com/google/android/gms/fitness/data/HealthDataTypes) in Google Fit, be aware that you are always able to read them, but if you want write access [you need to ask permission to Google](https://developers.google.com/fit/android/data-types#restricted_data_types) * This plugin is set to use the latest version of the Google Play Services API (``). This is done to likely guarantee the compatibility with other plugins using Google Play Services, but bear in mind that a) the plugin was tested until version 9.8.0 of the APIs and b) other plugins may be using a different version of the API. If you run into an issue, check the generated gradle file (build.gradle) under `dependencies` between `// SUB-PROJECT DEPENDENCIES START` and `// SUB-PROJECT DEPENDENCIES END` and make sure that all versions of the `com.google.android.gms:play-services-xxxx` are the same. ## Supported data types From 3d17e368d11a9ca90be1aafb9a65aeb3e0bfd681 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 10 Aug 2017 10:25:23 +0100 Subject: [PATCH 113/157] release 1.0.0 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f2e78e90..bde4f7b4 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.10.2", + "version": "1.0.0", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 926f6f1d..28677fde 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.0.0"> Cordova Health From 2d2703825d028411f4a2117f78d2e31c70ded585 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 10 Aug 2017 11:27:38 +0100 Subject: [PATCH 114/157] [docs] some notes about health data --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c6a85b0..6c44a3c6 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ navigator.health.delete({ ## Differences between HealthKit and Google Fit -* HealthKit includes medical data (e.g. blood glucose), whereas Google Fit is only meant for fitness data (although now supports some medical data). +* HealthKit includes medical data (e.g. blood glucose), whereas Google Fit is mainly meant for fitness data (although [now supports some medical data too](https://developers.google.com/android/reference/com/google/android/gms/fitness/data/HealthDataTypes)). * HealthKit provides a data model that is not extensible, whereas Google Fit allows defining custom data types. * HealthKit allows to insert data with the unit of measurement of your choice, and automatically translates units when queried, whereas Google Fit uses fixed units of measurement. * HealthKit automatically counts steps and distance when you carry your phone with you and if your phone has the CoreMotion chip, whereas Google Fit also detects the kind of activity (sedentary, running, walking, cycling, in vehicle). From 03e9cc919a7f620400171afdbad0c6cc60f0f31d Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 18 Aug 2017 15:27:41 +0100 Subject: [PATCH 115/157] added workout only identifier fixes #73 --- www/ios/health.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/ios/health.js b/www/ios/health.js index 01823672..a9cd5402 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -15,6 +15,7 @@ dataTypes['weight'] = 'HKQuantityTypeIdentifierBodyMass'; dataTypes['heart_rate'] = 'HKQuantityTypeIdentifierHeartRate'; dataTypes['fat_percentage'] = 'HKQuantityTypeIdentifierBodyFatPercentage'; dataTypes['activity'] = 'HKWorkoutTypeIdentifier'; // and HKCategoryTypeIdentifierSleepAnalysis +dataTypes['workouts'] = 'HKWorkoutTypeIdentifier'; dataTypes['nutrition'] = 'HKCorrelationTypeIdentifierFood'; dataTypes['nutrition.calories'] = 'HKQuantityTypeIdentifierDietaryEnergyConsumed'; dataTypes['nutrition.fat.total'] = 'HKQuantityTypeIdentifierDietaryFatTotal'; From 7f4d61b1a83b21a54e6016bd334561018c6d74d0 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 18 Aug 2017 15:33:13 +0100 Subject: [PATCH 116/157] [docs] added documentation about use of workouts instead of activity in iOS --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 6c44a3c6..db3f355b 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,10 @@ navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) - Be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the authorization will ensure that the app is connected again. - In Android 6 and over, this function will also ask for some dynamic permissions if needed (e.g. in the case of "distance", it will need access to ACCESS_FINE_LOCATION). +#### iOS quirks + +- The datatype `activity` also includes sleep. If you want to get authorization only for workouts, you can specify `workouts` as datatype, but be aware that this is only availabe in iOS. + ### isAuthorized() @@ -255,6 +259,7 @@ navigator.health.query({ - When querying for nutrition, HealthKit only returns those stored as correlation. To be sure to get all stored quantities, it's better to query nutrients individually (e.g. MyFitnessPal doesn't store meals as correlations). - nutrition.vitamin_a is given in micrograms. Automatic conversion to international units is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). - When querying for activities, only events whose startDate and endDate are **both** in the query range will be returned. +- If you want to query for activity but only want workouts, you can specify the `workouts` datatype, but be aware that this will only be availabe in iOS. #### Android quirks From 1ed7d8eb42f258fb9493ae7c27a351cc6029ec13 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 21 Aug 2017 09:49:21 +0100 Subject: [PATCH 117/157] adding workouts in query too --- www/ios/health.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index a9cd5402..ebbbdc94 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -15,7 +15,7 @@ dataTypes['weight'] = 'HKQuantityTypeIdentifierBodyMass'; dataTypes['heart_rate'] = 'HKQuantityTypeIdentifierHeartRate'; dataTypes['fat_percentage'] = 'HKQuantityTypeIdentifierBodyFatPercentage'; dataTypes['activity'] = 'HKWorkoutTypeIdentifier'; // and HKCategoryTypeIdentifierSleepAnalysis -dataTypes['workouts'] = 'HKWorkoutTypeIdentifier'; +dataTypes['workouts'] = 'HKWorkoutTypeIdentifier'; dataTypes['nutrition'] = 'HKCorrelationTypeIdentifierFood'; dataTypes['nutrition.calories'] = 'HKQuantityTypeIdentifierDietaryEnergyConsumed'; dataTypes['nutrition.fat.total'] = 'HKQuantityTypeIdentifierDietaryFatTotal'; @@ -197,7 +197,7 @@ Health.prototype.query = function (opts, onSuccess, onError) { }; onSuccess(res); }, onError); - } else if (opts.dataType === 'activity') { + } else if (opts.dataType === 'activity' || opts.dataType === 'workouts') { // opts is not really used, the plugin just returns ALL workouts window.plugins.healthkit.findWorkouts(opts, function (data) { var result = []; @@ -216,22 +216,24 @@ Health.prototype.query = function (opts, onSuccess, onError) { result.push(res); } } - // get sleep analysis also - opts.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; - window.plugins.healthkit.querySampleType(opts, function (data) { - for (var i = 0; i < data.length; i++) { - var res = {}; - res.startDate = new Date(data[i].startDate); - res.endDate = new Date(data[i].endDate); - if (data[i].value == 0) res.value = 'sleep.awake'; - else res.value = 'sleep'; - res.unit = 'activityType'; - res.sourceName = data[i].sourceName; - res.sourceBundleId = data[i].sourceBundleId; - result.push(res); - } - onSuccess(result); - }, onError); + if(opts.dataType === 'activity') { + // get sleep analysis also + opts.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; + window.plugins.healthkit.querySampleType(opts, function (data) { + for (var i = 0; i < data.length; i++) { + var res = {}; + res.startDate = new Date(data[i].startDate); + res.endDate = new Date(data[i].endDate); + if (data[i].value == 0) res.value = 'sleep.awake'; + else res.value = 'sleep'; + res.unit = 'activityType'; + res.sourceName = data[i].sourceName; + res.sourceBundleId = data[i].sourceBundleId; + result.push(res); + } + onSuccess(result); + }, onError); + } else onSuccess(result); }, onError); } else if (opts.dataType === 'nutrition') { var result = []; From 41b5cc7cf0b36d7d777e8b51ff29f3349ae5a383 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 3 Oct 2017 12:01:13 +0100 Subject: [PATCH 118/157] fixes #75 --- activities_map.md | 3 ++- src/ios/HealthKit.m | 5 +++-- www/android/health.js | 1 + www/ios/health.js | 11 ++++++++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/activities_map.md b/activities_map.md index 01825cd6..a6c72fe1 100644 --- a/activities_map.md +++ b/activities_map.md @@ -99,7 +99,8 @@ This means that, for example, if you try to save "aerobics" in an iOS applicatio | sleep.light | SLEEP_LIGHT | HKCategoryValueSleepAnalysisAsleep | | sleep.deep | SLEEP_DEEP | HKCategoryValueSleepAnalysisAsleep | | sleep.rem | SLEEP_REM | HKCategoryValueSleepAnalysisAsleep | -| sleep.awake | SLEEP_AWAKE | HKCategoryValueSleepAnalysisInBed | +| sleep.inBed | SLEEP_AWAKE | HKCategoryValueSleepAnalysisInBed | +| sleep.awake | SLEEP_AWAKE | HKCategoryValueSleepAnalysisAwake | | snowboarding | SNOWBOARDING | HKWorkoutActivityTypeSnowboarding | | snowmobile | SNOWMOBILE | HKWorkoutActivityTypeSnowSports | | snowshoeing | SNOWSHOEING | HKWorkoutActivityTypeSnowSports | diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index c3f5f93f..89cbee3a 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -262,7 +262,7 @@ - (HKSample *)loadHKSampleFromInputDictionary:(NSDictionary *)inputDictionary er if ([inputDictionary objectForKey:HKPluginKeyUnit]) { if (![inputDictionary hasAllRequiredKeys:@[HKPluginKeyUnit] error:error]) return nil; NSString *unitString = [inputDictionary objectForKey:HKPluginKeyUnit]; - + return [HealthKit getHKQuantitySampleWithStartDate:startDate endDate:endDate sampleTypeString:sampleTypeString @@ -409,7 +409,8 @@ - (NSNumber*) getCategoryValueByName:(NSString *) categoryValue type:(HKCategory NSDictionary * map = @{ @"HKCategoryTypeIdentifierSleepAnalysis":@{ @"HKCategoryValueSleepAnalysisInBed":@(HKCategoryValueSleepAnalysisInBed), - @"HKCategoryValueSleepAnalysisAsleep":@(HKCategoryValueSleepAnalysisAsleep) + @"HKCategoryValueSleepAnalysisAsleep":@(HKCategoryValueSleepAnalysisAsleep), + @"HKCategoryValueSleepAnalysisAwake":@(HKCategoryValueSleepAnalysisAwake) } }; diff --git a/www/android/health.js b/www/android/health.js index c4660681..a28d2fc8 100644 --- a/www/android/health.js +++ b/www/android/health.js @@ -175,6 +175,7 @@ Health.prototype.toFitActivity = function (act) { if (act === 'stairs') return 'stair_climbing'; if (act === 'wheelchair.walkpace') return 'wheelchair'; if (act === 'wheelchair.runpace') return 'wheelchair'; + if (act === 'sleep.inBed') return 'sleep.awake'; // unsupported activities are mapped to 'other' if ((act === 'archery') || (act === 'barre') || diff --git a/www/ios/health.js b/www/ios/health.js index ebbbdc94..3618f94c 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -224,8 +224,9 @@ Health.prototype.query = function (opts, onSuccess, onError) { var res = {}; res.startDate = new Date(data[i].startDate); res.endDate = new Date(data[i].endDate); - if (data[i].value == 0) res.value = 'sleep.awake'; - else res.value = 'sleep'; + if (data[i].value == 0) res.value = 'sleep.inBed'; + else if (data[i].value == 1) res.value = 'sleep'; + else res.value = 'sleep.awake'; res.unit = 'activityType'; res.sourceName = data[i].sourceName; res.sourceBundleId = data[i].sourceBundleId; @@ -423,10 +424,14 @@ Health.prototype.store = function (data, onSuccess, onError) { data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; data.value = 'HKCategoryValueSleepAnalysisAsleep'; window.plugins.healthkit.saveSample(data, onSuccess, onError); - } else if (data.value === 'sleep.awake') { + } else if (data.value === 'sleep.inBed') { data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; data.value = 'HKCategoryValueSleepAnalysisInBed'; window.plugins.healthkit.saveSample(data, onSuccess, onError); + } else if (data.value === 'sleep.awake') { + data.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; + data.value = 'HKCategoryValueSleepAnalysisAwake'; + window.plugins.healthkit.saveSample(data, onSuccess, onError); } else { // some other kind of workout data.activityType = data.value; From bd74a7a6060fd2e570343d4b1f11cab50e2550b6 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 6 Oct 2017 15:10:12 +0100 Subject: [PATCH 119/157] adding support for HKBloodGlucoseMealTime in iOS --- README.md | 7 ++++--- www/ios/health.js | 12 ++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index db3f355b..5d3c48c1 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Example values: | weight | 83.3 | | heart_rate | 66 | | fat_percentage | 31.2 | -| blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Notes**:
to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose))
`meal` can be: 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack'
`sleep` can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep'
`source` can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | +| blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Notes**:
to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose))
`meal` can be: 'before_meal' (iOS only), 'after_meal' (iOS only), 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack'
`sleep` can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep'
`source` can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | @@ -198,7 +198,6 @@ navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) - The datatype `activity` also includes sleep. If you want to get authorization only for workouts, you can specify `workouts` as datatype, but be aware that this is only availabe in iOS. - ### isAuthorized() Check if the app has authorization to read/write a set of datatypes. @@ -260,6 +259,7 @@ navigator.health.query({ - nutrition.vitamin_a is given in micrograms. Automatic conversion to international units is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). - When querying for activities, only events whose startDate and endDate are **both** in the query range will be returned. - If you want to query for activity but only want workouts, you can specify the `workouts` datatype, but be aware that this will only be availabe in iOS. +- The blood glucose meal information is stored by the Health App as preprandial (before a meal) or postprandial (after a meal), which are mapped to 'before_meal' and 'after_meal'. These two specific values are only used in iOS and can't be used in Android apps. #### Android quirks @@ -350,9 +350,10 @@ navigator.health.store({ #### iOS quirks -- In iOS, when storing an activity, you can also specify calories (active, in kcal) and distance (walked or run, in meters). For example: `dataType: 'activity', value: 'walking', calories: 20, distance: 520`. Be aware, though, that you need permission to write calories and distance first, or the call will fail. +- When storing an activity, you can also specify calories (active, in kcal) and distance (walked or run, in meters). For example: `dataType: 'activity', value: 'walking', calories: 20, distance: 520`. Be aware, though, that you need permission to write calories and distance first, or the call will fail. - In iOS you cannot store the total calories, you need to specify either basal or active. If you use total calories, the active ones will be stored. - In iOS distance is assumed to be of type WalkingRunning, if you want to explicitly set it to Cycling you need to add the field `cycling: true`. +- The blood glucose meal information can be stored as 'before_meal' and 'after_meal', but these two can't be used in Android apps. #### Android quirks diff --git a/www/ios/health.js b/www/ios/health.js index 3618f94c..a486f129 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -265,7 +265,11 @@ Health.prototype.query = function (opts, onSuccess, onError) { res.value = { glucose: samples[i].quantity } - if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime) res.value.meal = samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime; + if (samples[i].metadata && samples[i].metadata.HKBloodGlucoseMealTime) { + if(samples[i].metadata.HKBloodGlucoseMealTime == 1) res.value.meal = 'before_meal' + else res.value.meal = 'after_meal' + } + if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime) res.value.meal = samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime; // overwrite HKBloodGlucoseMealTime if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime) res.value.sleep = samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime; if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSource) res.value.source = samples[i].metadata.HKMetadataKeyBloodGlucoseSource; } else { @@ -478,7 +482,11 @@ Health.prototype.store = function (data, onSuccess, onError) { if (data.dataType === 'blood_glucose') { data.amount = data.value.glucose; if (!data.metadata) data.metadata = {}; - if (data.value.meal) data.metadata.HKMetadataKeyBloodGlucoseMealTime = data.value.meal; + if (data.value.meal) { + data.metadata.HKMetadataKeyBloodGlucoseMealTime = data.value.meal; + if (data.value.meal.startsWith('before_')) data.metadata.HKBloodGlucoseMealTime = 1; + else if (data.value.meal.startsWith('after_')) data.metadata.HKBloodGlucoseMealTime = 0; + } if (data.value.sleep) data.metadata.HKMetadataKeyBloodGlucoseSleepTime = data.value.sleep; if (data.value.source) data.metadata.HKMetadataKeyBloodGlucoseSource = data.value.source; } else { From 838c0c569fd49378abb64f98992cee0f5e1a1100 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 9 Oct 2017 15:31:19 +0100 Subject: [PATCH 120/157] release 1.1.0 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bde4f7b4..f558ceca 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.0.0", + "version": "1.0.1", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 28677fde..3d12de51 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.0.1"> Cordova Health From c308e8bb27d32f43c13a204550239dd8aaa1a4cd Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 9 Oct 2017 15:31:46 +0100 Subject: [PATCH 121/157] Revert "release 1.1.0" This reverts commit 838c0c569fd49378abb64f98992cee0f5e1a1100. --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f558ceca..bde4f7b4 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.0.1", + "version": "1.0.0", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 3d12de51..28677fde 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.0.0"> Cordova Health From 4898a1b07d13a18095d888a5565d2556246533dd Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 9 Oct 2017 15:32:24 +0100 Subject: [PATCH 122/157] Revert "Revert "release 1.1.0"" This reverts commit c308e8bb27d32f43c13a204550239dd8aaa1a4cd. --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bde4f7b4..f558ceca 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.0.0", + "version": "1.0.1", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 28677fde..3d12de51 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.0.1"> Cordova Health From aeee275a24f746577ccfa8af064946319d9e8ef5 Mon Sep 17 00:00:00 2001 From: Deena Wang Date: Wed, 11 Oct 2017 11:51:48 -0400 Subject: [PATCH 123/157] Pass bucket in when querying calories.active --- www/android/health.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/android/health.js b/www/android/health.js index a28d2fc8..1cfefa56 100644 --- a/www/android/health.js +++ b/www/android/health.js @@ -31,7 +31,8 @@ Health.prototype.query = function (opts, onSuccess, onError) { navigator.health.queryAggregated({ dataType:'calories.basal', endDate: opts.endDate, - startDate: opts.startDate + startDate: opts.startDate, + bucket: opts.bucket }, function(data){ var basal_ms = data.value / (opts.endDate - opts.startDate); //now get the total From 03c1872f152043af9040e297a9d9d7140a0c276d Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 16 Oct 2017 16:52:48 +0100 Subject: [PATCH 124/157] fixes #52 --- src/android/HealthPlugin.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 6db2588f..00e79dd3 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -893,8 +893,12 @@ else if (mealt == Field.MEAL_TYPE_SNACK) Value nutrients = datapoint.getValue(Field.FIELD_NUTRIENTS); NutrientFieldInfo fieldInfo = nutrientFields.get(datatype); if (fieldInfo != null) { - obj.put("value", (float) nutrients.getKeyValue(fieldInfo.field)); - obj.put("unit", fieldInfo.unit); + if (nutrients.getKeyValue(fieldInfo.field) != null) { + obj.put("value", (float) nutrients.getKeyValue(fieldInfo.field)); + } else { + obj.put("value", 0f); + } + obj.put("unit", fieldInfo.unit); } } } else if (DT.equals(DataType.TYPE_CALORIES_EXPENDED)) { From 50179eb1c08e8ad1287186c56128835791483726 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 20 Oct 2017 14:46:52 +0100 Subject: [PATCH 125/157] fixes #83 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5d3c48c1..fc6f8c3c 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Phonegap Build `config.xml`: * You need to have the Google Services API downloaded in your SDK. * Be sure to give your app access to the Google Fitness API, see [this](https://developers.google.com/fit/android/get-api-key) and [this](https://github.com/2dvisio/cordova-plugin-googlefit#sdk-requirements-for-compiling-the-plugin). * If you are wondering what key your compiled app is using, you can type `keytool -list -printcert -jarfile yourapp.apk`. +* If you haven't configured the APIs correctly, particularly the OAuth requirements, you are likely to get 'User cancelled the dialog' as an error message. * You can use the Google Fitness API even if the user doesn't have Google Fit installed, but there has to be some other fitness app putting data into the Fitness API otherwise your queries will always be empty. See the [the original documentation](https://developers.google.com/fit/overview). * If you are planning to use [health data types](https://developers.google.com/android/reference/com/google/android/gms/fitness/data/HealthDataTypes) in Google Fit, be aware that you are always able to read them, but if you want write access [you need to ask permission to Google](https://developers.google.com/fit/android/data-types#restricted_data_types) * This plugin is set to use the latest version of the Google Play Services API (``). This is done to likely guarantee the compatibility with other plugins using Google Play Services, but bear in mind that a) the plugin was tested until version 9.8.0 of the APIs and b) other plugins may be using a different version of the API. If you run into an issue, check the generated gradle file (build.gradle) under `dependencies` between `// SUB-PROJECT DEPENDENCIES START` and `// SUB-PROJECT DEPENDENCIES END` and make sure that all versions of the `com.google.android.gms:play-services-xxxx` are the same. From ffd32f346c50d661b5294cbc54649b9d1bc111c0 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 25 Oct 2017 11:54:39 +0100 Subject: [PATCH 126/157] added activityType as unit of activity --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc6f8c3c..41e98920 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | | calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | | calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | -| activity | | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | +| activity | activityType | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | | height | m | HKQuantityTypeIdentifierHeight | TYPE_HEIGHT | | weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | | heart_rate | count/min | HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | From 24a030d3c96348608ef9054ed46452eb796d7578 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 25 Oct 2017 12:25:50 +0100 Subject: [PATCH 127/157] added calories and distance in activity in readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 41e98920..6d3f8bb7 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,8 @@ Example values: | steps | 34 | | distance | 101.2 | | calories | 245.3 | -| activity | "walking"
**Note**: recognized activities and their mappings in Google Fit / HealthKit can be found [here](activities_map.md) | +| activity | "walking"
**Notes**: recognized activities and their mappings in Google Fit / HealthKit can be found [here]
in iOS the query also returns calories (kcal) and distance (m) +(activities_map.md) | | height | 185.9 | | weight | 83.3 | | heart_rate | 66 | From d156bdad6e8ef9d6319fee09fb56b385c3cedb9d Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 25 Oct 2017 14:57:53 +0100 Subject: [PATCH 128/157] fixes #85 --- src/ios/HealthKit.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index 89cbee3a..3fdd3824 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -808,8 +808,8 @@ - (void)findWorkouts:(CDVInvokedUrlCommand *)command { NSEnergyFormatter *energyFormatter = [NSEnergyFormatter new]; energyFormatter.forFoodEnergyUse = NO; - double joules = [workout.totalEnergyBurned doubleValueForUnit:[HKUnit kilocalorieUnit]]; - NSString *calories = [energyFormatter stringFromJoules:joules]; + double cals = [workout.totalEnergyBurned doubleValueForUnit:[HKUnit kilocalorieUnit]]; + NSString *calories = [energyFormatter stringFromValue:cals unit:[HKUnit kilocalorieUnit]]; NSMutableDictionary *entry = [ @{ From c42bc90eb40da35a189a16faf3b79b0e3a25314f Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 27 Oct 2017 09:47:07 +0100 Subject: [PATCH 129/157] release 1.0.2 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f558ceca..1ec5bd74 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.0.1", + "version": "1.0.2", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 3d12de51..2d936fe0 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.0.2"> Cordova Health From cd854d437e848c6f5f6820e84709787b86d50c08 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 8 Nov 2017 15:52:51 +0000 Subject: [PATCH 130/157] fixes #95 fixes #86 --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 6d3f8bb7..2082299d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,23 @@ Phonegap Build `config.xml`: ``` +If, for some reason, the Info.plist loses the HEALTH_READ_PERMISSION and HEALTH_WRITE_PERMISSION, you probably need to add the following to your project's package.json: + +``` +{ + "cordova": { + "plugins": { + "cordova-plugin-health": { + "HEALTH_READ_PERMISSION": "App needs read access", + "HEALTH_WRITE_PERMISSION": "App needs write access" + }, + }, + } +} +``` + +This is known to happen when using the Ionic Package cloud service. + ## iOS requirements * Make sure your app id has the 'HealthKit' entitlement when this plugin is installed (see iOS dev center). From 01b0598a72f1b00e2e4f10758ca4a3bb91ec863a Mon Sep 17 00:00:00 2001 From: Peter McWilliams Date: Mon, 20 Nov 2017 10:36:52 +0000 Subject: [PATCH 131/157] added workouts data type to queryAggregated --- www/ios/health.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index a486f129..db87e4bd 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -311,7 +311,7 @@ Health.prototype.queryAggregated = function (opts, onSuccess, onError) { if ((opts.dataType !== 'steps') && (opts.dataType !== 'distance') && (opts.dataType !== 'calories') && (opts.dataType !== 'calories.active') && (opts.dataType !== 'calories.basal') && (opts.dataType !== 'activity') && - (!opts.dataType.startsWith('nutrition'))) { + (opts.dataType !== 'workouts') && (!opts.dataType.startsWith('nutrition'))) { // unsupported datatype onError('Datatype ' + opts.dataType + ' not supported in queryAggregated'); return; @@ -323,7 +323,7 @@ Health.prototype.queryAggregated = function (opts, onSuccess, onError) { if (opts.bucket) { // ----- with buckets opts.aggregation = opts.bucket; - if (opts.dataType === 'activity') { + if (opts.dataType === 'activity' || opts.dataType === 'workouts') { // query and manually aggregate navigator.health.query(opts, function (data) { onSuccess(bucketize(data, opts.bucket, startD, endD, 'activitySummary', mergeActivitySamples)); @@ -361,7 +361,7 @@ Health.prototype.queryAggregated = function (opts, onSuccess, onError) { } } else { // ---- no bucketing, just sum - if (opts.dataType === 'activity') { + if (opts.dataType === 'activity' || opts.dataType === 'workouts') { navigator.health.query(opts, function (data) { // manually aggregate by activity onSuccess(aggregateIntoResult(data, 'activitySummary', mergeActivitySamples)); From 03af089f37a0e24e8ea81cccf7c4bb9a598335ed Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 14 Dec 2017 09:50:38 +0000 Subject: [PATCH 132/157] fixes #88 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2082299d..366e1d0e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ Phonegap Build `config.xml`: ``` + + + From 71be8f15728838a85ca593d36dce09baf0284ce0 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Wed, 10 Jan 2018 11:10:04 +0000 Subject: [PATCH 133/157] Testing insulin --- www/ios/health.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index db87e4bd..8f3657c1 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -36,6 +36,7 @@ dataTypes['nutrition.iron'] = 'HKQuantityTypeIdentifierDietaryIron'; dataTypes['nutrition.water'] = 'HKQuantityTypeIdentifierDietaryWater'; dataTypes['nutrition.caffeine'] = 'HKQuantityTypeIdentifierDietaryCaffeine'; dataTypes['blood_glucose'] = 'HKQuantityTypeIdentifierBloodGlucose'; +dataTypes['insulin'] = 'HKQuantityTypeIdentifierInsulinDelivery'; var units = []; units['steps'] = 'count'; @@ -266,12 +267,14 @@ Health.prototype.query = function (opts, onSuccess, onError) { glucose: samples[i].quantity } if (samples[i].metadata && samples[i].metadata.HKBloodGlucoseMealTime) { - if(samples[i].metadata.HKBloodGlucoseMealTime == 1) res.value.meal = 'before_meal' - else res.value.meal = 'after_meal' - } - if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime) res.value.meal = samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime; // overwrite HKBloodGlucoseMealTime + if(samples[i].metadata.HKBloodGlucoseMealTime == 1) res.value.meal = 'before_meal' + else res.value.meal = 'after_meal' + } + if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime) res.value.meal = samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime; // overwrite HKBloodGlucoseMealTime if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime) res.value.sleep = samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime; if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSource) res.value.source = samples[i].metadata.HKMetadataKeyBloodGlucoseSource; + } else if (opts.dataType === 'insulin') { +res.value = JSON.stringify(samples[i]); } else { res.value = samples[i].quantity; } From a9e574d49e454ce9bdafb13f0eb2bd53dc332d0b Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Wed, 10 Jan 2018 11:24:44 +0000 Subject: [PATCH 134/157] Added insulin unit --- www/ios/health.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/ios/health.js b/www/ios/health.js index 8f3657c1..2955afa8 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -68,6 +68,7 @@ units['nutrition.iron'] = 'mg'; units['nutrition.water'] = 'ml'; units['nutrition.caffeine'] = 'g'; units['blood_glucose'] = 'mmol/L'; +units['insulin'] = 'IU'; Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); From ead2077616a0281cdd9f326fc5f4707ce390be17 Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Wed, 10 Jan 2018 11:49:47 +0000 Subject: [PATCH 135/157] Added insulin write --- www/ios/health.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index 2955afa8..df82c0dc 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -275,7 +275,14 @@ Health.prototype.query = function (opts, onSuccess, onError) { if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime) res.value.sleep = samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime; if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSource) res.value.source = samples[i].metadata.HKMetadataKeyBloodGlucoseSource; } else if (opts.dataType === 'insulin') { -res.value = JSON.stringify(samples[i]); + res.value = { + insulin: samples[i].quantity + } + if (samples[i].metadata && samples[i].metadata.HKInsulinDeliveryReason) { + if(samples[i].metadata.HKInsulinDeliveryReason == 1) res.value.reason = 'basal' + else res.value.reason = 'bolus' + } + if (samples[i].metadata && samples[i].metadata.HKMetadataKeyInsulinDeliveryReason) res.value.reason = samples[i].metadata.HKMetadataKeyInsulinDeliveryReason; // overwrite HKInsulinDeliveryReason } else { res.value = samples[i].quantity; } @@ -487,12 +494,20 @@ Health.prototype.store = function (data, onSuccess, onError) { data.amount = data.value.glucose; if (!data.metadata) data.metadata = {}; if (data.value.meal) { - data.metadata.HKMetadataKeyBloodGlucoseMealTime = data.value.meal; - if (data.value.meal.startsWith('before_')) data.metadata.HKBloodGlucoseMealTime = 1; - else if (data.value.meal.startsWith('after_')) data.metadata.HKBloodGlucoseMealTime = 0; - } + data.metadata.HKMetadataKeyBloodGlucoseMealTime = data.value.meal; + if (data.value.meal.startsWith('before_')) data.metadata.HKBloodGlucoseMealTime = 1; + else if (data.value.meal.startsWith('after_')) data.metadata.HKBloodGlucoseMealTime = 0; + } if (data.value.sleep) data.metadata.HKMetadataKeyBloodGlucoseSleepTime = data.value.sleep; if (data.value.source) data.metadata.HKMetadataKeyBloodGlucoseSource = data.value.source; + } else if (data.dataType === 'insulin') { + data.amount = data.value.insulin; + if (!data.metadata) data.metadata = {}; + if (data.value.reason) { + data.metadata.HKMetadataKeyInsulinDeliveryReason = data.value.reason; + if (data.value.reason.toLowerCase() === 'basal') data.metadata.HKInsulinDeliveryReason = 1; + else if (data.value.reason.toLowerCase() === 'bolus') data.metadata.HKInsulinDeliveryReason = 2; + } } else { data.amount = data.value; } From bb89ac300c6823236fa161b8366268aaeee6419f Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Wed, 10 Jan 2018 13:31:26 +0000 Subject: [PATCH 136/157] Updated README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 366e1d0e..aac1e901 100644 --- a/README.md +++ b/README.md @@ -92,12 +92,13 @@ Google Fit is limited to fitness data and, for health, custom data types are def | calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | | calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | | calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | -| activity | activityType | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | +| activity | activityType | HKWorkoutTypeIdentifier + HKCategoryTypeIdentifierSleepAnalysis | TYPE_ACTIVITY_SEGMENT | | height | m | HKQuantityTypeIdentifierHeight | TYPE_HEIGHT | | weight | kg | HKQuantityTypeIdentifierBodyMass | TYPE_WEIGHT | | heart_rate | count/min | HKQuantityTypeIdentifierHeartRate | TYPE_HEART_RATE_BPM | | fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | -| blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose | TYPE_BLOOD_GLUCOSE | +| blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose | TYPE_BLOOD_GLUCOSE | +| insulin | IU | HKQuantityTypeIdentifierInsulinDelivery | NA | | gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | | nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | @@ -147,6 +148,7 @@ Example values: | heart_rate | 66 | | fat_percentage | 31.2 | | blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Notes**:
to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose))
`meal` can be: 'before_meal' (iOS only), 'after_meal' (iOS only), 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack'
`sleep` can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep'
`source` can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | +| insulin | { insulin: 2.3, reason: 'bolus' }
**Notes**: Insulin is currently only available on iOS
`reason` can be 'bolus' or 'basal' | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | From 0451b92b7324d56cb206cc1c0921839887ec7567 Mon Sep 17 00:00:00 2001 From: Peter McWilliams Date: Wed, 10 Jan 2018 13:31:53 +0000 Subject: [PATCH 137/157] added new type activitydistcal to support distance and calories for android devices --- README.md | 7 ++- src/android/HealthPlugin.java | 113 ++++++++++++++++++++++++++++++++-- 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 366e1d0e..6bb30300 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Example values: | calories | 245.3 | | activity | "walking"
**Notes**: recognized activities and their mappings in Google Fit / HealthKit can be found [here]
in iOS the query also returns calories (kcal) and distance (m) (activities_map.md) | +| activitydistcal | type only supported for google fit
it returns distance (m) and calories (kcal) per activity (use activity for the same result on iOS) | | height | 185.9 | | weight | 83.3 | | heart_rate | 66 | @@ -215,6 +216,7 @@ navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) - It will try to get authorization from the Google fitness APIs. It is necessary that the app's package name and the signing key are registered in the Google API console (see [here](https://developers.google.com/fit/android/get-api-key)). - Be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the authorization will ensure that the app is connected again. - In Android 6 and over, this function will also ask for some dynamic permissions if needed (e.g. in the case of "distance", it will need access to ACCESS_FINE_LOCATION). +- If using activitydistcal then you need to request permission for distance. #### iOS quirks @@ -326,6 +328,7 @@ The following table shows what types are supported and examples of the returned | calories.active | { startDate: Date, endDate: Date, value: 3547.4, unit: 'kcal' } | | calories.basal | { startDate: Date, endDate: Date, value: 13146.1, unit: 'kcal' } | | activity | { startDate: Date, endDate: Date, value: { still: { duration: 520000, calories: 30, distance: 0 }, walking: { duration: 223000, calories: 20, distance: 15 }}, unit: 'activitySummary' }
**Note:** duration is expressed in milliseconds, distance in meters and calories in kcal | +| activitydistcal | as activity but includes calories and distance on android | | nutrition | { startDate: Date, endDate: Date, value: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 }, unit: 'nutrition' }
**Note:** units of measurement for nutrients are fixed according to the table at the beginning of this README | | nutrition.x | { startDate: Date, endDate: Date, value: 23, unit: 'mg'} | @@ -338,11 +341,13 @@ The following table shows what types are supported and examples of the returned #### iOS quirks - Activities in HealthKit may include two extra fields: calories (kcal) and distance (m) +- activitydistcal is not supported on iOS - When querying for nutrition, HealthKit only returns those stored as correlation. To be sure to get all stored quantities, it's better to query nutrients individually (e.g. MyFitnessPal doesn't store meals as correlations). - nutrition.vitamin_a is given in micrograms. Automatic conversion to international units is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). #### Android quirks - +- activity - android does not include distance or calories, use activitydistcal for this +- activitydistcal includes the two extra fields: calories (kcal) and distance (m). - To query for steps as filtered by the Google Fit app, the flag `filtered: true` must be added into the query object. - nutrition.vitamin_a is given in international units. Automatic conversion to micrograms is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 00e79dd3..ad87e278 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -799,6 +799,9 @@ private void query(final JSONArray args, final CallbackContext callbackContext) dt = customdatatypes.get(datatype); if (healthdatatypes.get(datatype) != null) dt = healthdatatypes.get(datatype); + if (datatype.equalsIgnoreCase("activitydistcal")) { + dt = activitydatatypes.get("activity"); + } if (dt == null) { callbackContext.error("Datatype " + datatype + " not supported"); return; @@ -927,8 +930,59 @@ else if (mealt == Field.MEAL_TYPE_SNACK) obj.put("unit", "percent"); } else if (DT.equals(DataType.TYPE_ACTIVITY_SEGMENT)) { String activity = datapoint.getValue(Field.FIELD_ACTIVITY).asActivity(); + long duration = datapoint.getEndTime(TimeUnit.MILLISECONDS) - datapoint.getStartTime(TimeUnit.MILLISECONDS); obj.put("value", activity); obj.put("unit", "activityType"); + obj.put("duration", duration); + + if (datatype.equalsIgnoreCase("activitydistcal")) { + DataReadRequest readActivityRequest = new DataReadRequest.Builder() + .setTimeRange(datapoint.getStartTime(TimeUnit.MILLISECONDS), datapoint.getEndTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS) + .read(DataType.TYPE_DISTANCE_DELTA) + .read(DataType.TYPE_CALORIES_EXPENDED) + .build(); + + DataReadResult dataReadActivityResult = Fitness.HistoryApi.readData(mClient, readActivityRequest).await(); + + if (dataReadActivityResult.getStatus().isSuccess()) { + JSONArray distanceDataPoints = new JSONArray(); + JSONArray calorieDataPoints = new JSONArray(); + + List dataActivitySets = dataReadActivityResult.getDataSets(); + for (DataSet dataActivitySet : dataActivitySets) { + for (DataPoint dataActivityPoint : dataActivitySet.getDataPoints()) { + + JSONObject activityObj = new JSONObject(); + + activityObj.put("startDate", dataActivityPoint.getStartTime(TimeUnit.MILLISECONDS)); + activityObj.put("endDate", dataActivityPoint.getEndTime(TimeUnit.MILLISECONDS)); + DataSource dataActivitySource = dataActivityPoint.getOriginalDataSource(); + if (dataSource != null) { + String sourceName = dataActivitySource.getName(); + if (sourceName != null) activityObj.put("sourceName", sourceName); + String sourceBundleId = dataSource.getAppPackageName(); + if (sourceBundleId != null) activityObj.put("sourceBundleId", sourceBundleId); + } + + if (dataActivitySet.getDataType().equals(DataType.TYPE_DISTANCE_DELTA)) + { + float distance = dataActivityPoint.getValue(Field.FIELD_DISTANCE).asFloat(); + activityObj.put("value", distance); + activityObj.put("unit", "m"); + distanceDataPoints.put(activityObj); + } else { + // calories + float calories = dataActivityPoint.getValue(Field.FIELD_CALORIES).asFloat(); + activityObj.put("value", calories); + activityObj.put("unit", "kcal"); + calorieDataPoints.put(activityObj); + } + } + } + obj.put("distance", distanceDataPoints); + obj.put("calorie", calorieDataPoints); + } + } } else if (DT.equals(customdatatypes.get("gender"))) { for (Field f : customdatatypes.get("gender").getFields()) { //there should be only one field named gender @@ -1200,7 +1254,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac builder.aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED); } else if (datatype.equalsIgnoreCase("calories.basal")) { builder.aggregate(DataType.TYPE_BASAL_METABOLIC_RATE, DataType.AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY); - } else if (datatype.equalsIgnoreCase("activity")) { + } else if (datatype.equalsIgnoreCase("activity") || datatype.equalsIgnoreCase("activitydistcal")) { builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); } else if (datatype.equalsIgnoreCase("nutrition.water")) { builder.aggregate(DataType.TYPE_HYDRATION, DataType.AGGREGATE_HYDRATION); @@ -1221,7 +1275,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac builder.bucketByTime(1, TimeUnit.DAYS); } } else { - if (datatype.equalsIgnoreCase("activity")) { + if (datatype.equalsIgnoreCase("activity") || datatype.equalsIgnoreCase("activitydistcal")) { builder.bucketByActivityType(1, TimeUnit.MILLISECONDS); } else { builder.bucketByTime(allms, TimeUnit.MILLISECONDS); @@ -1265,7 +1319,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac retBucket.put("unit", "m"); } else if (datatype.equalsIgnoreCase("calories")) { retBucket.put("unit", "kcal"); - } else if (datatype.equalsIgnoreCase("activity")) { + } else if (datatype.equalsIgnoreCase("activity") || datatype.equalsIgnoreCase("activitydistcal")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); } else if (datatype.equalsIgnoreCase("nutrition.water")) { @@ -1307,7 +1361,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac retBucket.put("unit", "m"); } else if (datatype.equalsIgnoreCase("calories")) { retBucket.put("unit", "kcal"); - } else if (datatype.equalsIgnoreCase("activity")) { + } else if (datatype.equalsIgnoreCase("activity") || datatype.equalsIgnoreCase("activitydistcal")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); } else if (datatype.equalsIgnoreCase("nutrition.water")) { @@ -1322,6 +1376,55 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } } + + // if type was activitydistcal then run subsequent query per bucket time to get distance per activity + if (datatype.equalsIgnoreCase("activitydistcal")) { + + DataReadRequest readActivityDistCalRequest = new DataReadRequest.Builder() + .aggregate(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA) + .aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED) + .bucketByActivityType(1, TimeUnit.SECONDS) + .setTimeRange(bucket.getStartTime(TimeUnit.MILLISECONDS), bucket.getEndTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS) + .build(); + + DataReadResult dataActivityDistCalReadResult = Fitness.HistoryApi.readData(mClient, readActivityDistCalRequest).await(); + + if (dataActivityDistCalReadResult.getStatus().isSuccess()) { + for (Bucket activityBucket : dataActivityDistCalReadResult.getBuckets()) { + //each bucket is an activity + float distance = 0; + float calories = 0; + String activity = activityBucket.getActivity(); + + DataSet distanceDataSet = activityBucket.getDataSet(DataType.AGGREGATE_DISTANCE_DELTA); + for (DataPoint datapoint : distanceDataSet.getDataPoints()) { + distance += datapoint.getValue(Field.FIELD_DISTANCE).asFloat(); + } + + DataSet caloriesDataSet = activityBucket.getDataSet(DataType.AGGREGATE_CALORIES_EXPENDED); + for (DataPoint datapoint : caloriesDataSet.getDataPoints()) { + calories += datapoint.getValue(Field.FIELD_CALORIES).asFloat(); + } + + JSONObject actobj = retBucket.getJSONObject("value"); + JSONObject summary; + if (actobj.has(activity)) { + summary = actobj.getJSONObject(activity); + double existingdistance = summary.getDouble("distance"); + summary.put("distance", distance + existingdistance); + double existingcalories = summary.getDouble("calories"); + summary.put("calories", calories + existingcalories); + } else { + summary = new JSONObject(); + summary.put("duration", 0); // sum onto this whilst aggregating over bucket below. + summary.put("distance", distance); + summary.put("calories", calories); + } + actobj.put(activity, summary); + retBucket.put("value", actobj); + } + } + } } // aggregate data points over the bucket @@ -1363,7 +1466,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac double total = retBucket.getDouble("value"); retBucket.put("value", total + value); } - } else if (datatype.equalsIgnoreCase("activity")) { + } else if (datatype.equalsIgnoreCase("activity") || datatype.equalsIgnoreCase("activitydistcal")) { String activity = datapoint.getValue(Field.FIELD_ACTIVITY).asActivity(); int ndur = datapoint.getValue(Field.FIELD_DURATION).asInt(); JSONObject actobj = retBucket.getJSONObject("value"); From 96488ae1bfba3820322fd7717b7e63b495ee0eae Mon Sep 17 00:00:00 2001 From: Julian Laval Date: Wed, 10 Jan 2018 15:37:56 +0000 Subject: [PATCH 138/157] Fixed meal type enum issue --- www/ios/health.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/ios/health.js b/www/ios/health.js index df82c0dc..0b7023f8 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -496,7 +496,7 @@ Health.prototype.store = function (data, onSuccess, onError) { if (data.value.meal) { data.metadata.HKMetadataKeyBloodGlucoseMealTime = data.value.meal; if (data.value.meal.startsWith('before_')) data.metadata.HKBloodGlucoseMealTime = 1; - else if (data.value.meal.startsWith('after_')) data.metadata.HKBloodGlucoseMealTime = 0; + else if (data.value.meal.startsWith('after_')) data.metadata.HKBloodGlucoseMealTime = 2; } if (data.value.sleep) data.metadata.HKMetadataKeyBloodGlucoseSleepTime = data.value.sleep; if (data.value.source) data.metadata.HKMetadataKeyBloodGlucoseSource = data.value.source; From 6b37b599adb12c0e98c4fa87ce09e161462b194d Mon Sep 17 00:00:00 2001 From: Peter McWilliams Date: Thu, 11 Jan 2018 11:32:46 +0000 Subject: [PATCH 139/157] removed the added new type, use activity to return distance and calories automatically, if activity permission is asked for then grant location access too --- README.md | 11 +--- src/android/HealthPlugin.java | 118 ++++++++++++++++------------------ 2 files changed, 60 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 6bb30300..3712fe2b 100644 --- a/README.md +++ b/README.md @@ -140,9 +140,8 @@ Example values: | steps | 34 | | distance | 101.2 | | calories | 245.3 | -| activity | "walking"
**Notes**: recognized activities and their mappings in Google Fit / HealthKit can be found [here]
in iOS the query also returns calories (kcal) and distance (m) +| activity | "walking"
**Notes**: recognized activities and their mappings in Google Fit / HealthKit can be found [here]
the query also returns calories (kcal) and distance (m) (activities_map.md) | -| activitydistcal | type only supported for google fit
it returns distance (m) and calories (kcal) per activity (use activity for the same result on iOS) | | height | 185.9 | | weight | 83.3 | | heart_rate | 66 | @@ -215,8 +214,7 @@ navigator.health.requestAuthorization(datatypes, successCallback, errorCallback) - It will try to get authorization from the Google fitness APIs. It is necessary that the app's package name and the signing key are registered in the Google API console (see [here](https://developers.google.com/fit/android/get-api-key)). - Be aware that if the activity is destroyed (e.g. after a rotation) or is put in background, the connection to Google Fit may be lost without any callback. Going through the authorization will ensure that the app is connected again. -- In Android 6 and over, this function will also ask for some dynamic permissions if needed (e.g. in the case of "distance", it will need access to ACCESS_FINE_LOCATION). -- If using activitydistcal then you need to request permission for distance. +- In Android 6 and over, this function will also ask for some dynamic permissions if needed (e.g. in the case of "distance" or "activity", it will need access to ACCESS_FINE_LOCATION). #### iOS quirks @@ -328,7 +326,6 @@ The following table shows what types are supported and examples of the returned | calories.active | { startDate: Date, endDate: Date, value: 3547.4, unit: 'kcal' } | | calories.basal | { startDate: Date, endDate: Date, value: 13146.1, unit: 'kcal' } | | activity | { startDate: Date, endDate: Date, value: { still: { duration: 520000, calories: 30, distance: 0 }, walking: { duration: 223000, calories: 20, distance: 15 }}, unit: 'activitySummary' }
**Note:** duration is expressed in milliseconds, distance in meters and calories in kcal | -| activitydistcal | as activity but includes calories and distance on android | | nutrition | { startDate: Date, endDate: Date, value: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 }, unit: 'nutrition' }
**Note:** units of measurement for nutrients are fixed according to the table at the beginning of this README | | nutrition.x | { startDate: Date, endDate: Date, value: 23, unit: 'mg'} | @@ -341,13 +338,11 @@ The following table shows what types are supported and examples of the returned #### iOS quirks - Activities in HealthKit may include two extra fields: calories (kcal) and distance (m) -- activitydistcal is not supported on iOS - When querying for nutrition, HealthKit only returns those stored as correlation. To be sure to get all stored quantities, it's better to query nutrients individually (e.g. MyFitnessPal doesn't store meals as correlations). - nutrition.vitamin_a is given in micrograms. Automatic conversion to international units is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). #### Android quirks -- activity - android does not include distance or calories, use activitydistcal for this -- activitydistcal includes the two extra fields: calories (kcal) and distance (m). +- Activities will include two extra fields: calories (kcal) and distance (m) and requires the user to grant access to location - To query for steps as filtered by the Google Fit app, the flag `filtered: true` must be added into the query object. - nutrition.vitamin_a is given in international units. Automatic conversion to micrograms is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index ad87e278..b985a5d1 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -557,7 +557,7 @@ private void checkAuthorization(final JSONArray args, final CallbackContext call } dynPerms.clear(); - if (locationscope == READ_PERMS || locationscope == READ_WRITE_PERMS) + if (locationscope == READ_PERMS || locationscope == READ_WRITE_PERMS || activityscope == READ_PERMS || activityscope == READ_WRITE_PERMS) //activity requires access to location to report distace dynPerms.add(Manifest.permission.ACCESS_FINE_LOCATION); if (bodyscope == READ_PERMS || bodyscope == READ_WRITE_PERMS) dynPerms.add(Manifest.permission.BODY_SENSORS); @@ -577,10 +577,10 @@ private void checkAuthorization(final JSONArray args, final CallbackContext call } else if (activityscope == READ_WRITE_PERMS) { builder.addScope(new Scope(Scopes.FITNESS_ACTIVITY_READ_WRITE)); } - if (locationscope == READ_PERMS) { - builder.addScope(new Scope(Scopes.FITNESS_LOCATION_READ)); - } else if (locationscope == READ_WRITE_PERMS) { + if (locationscope == READ_WRITE_PERMS) { //specifially request read write permission for location. builder.addScope(new Scope(Scopes.FITNESS_LOCATION_READ_WRITE)); + } else if (locationscope == READ_PERMS || activityscope == READ_PERMS || activityscope == READ_WRITE_PERMS) { // if read permission request for location or any read/write permissions for activities were requested then give read location + builder.addScope(new Scope(Scopes.FITNESS_LOCATION_READ)); } if (nutritionscope == READ_PERMS) { builder.addScope(new Scope(Scopes.FITNESS_NUTRITION_READ)); @@ -799,9 +799,6 @@ private void query(final JSONArray args, final CallbackContext callbackContext) dt = customdatatypes.get(datatype); if (healthdatatypes.get(datatype) != null) dt = healthdatatypes.get(datatype); - if (datatype.equalsIgnoreCase("activitydistcal")) { - dt = activitydatatypes.get("activity"); - } if (dt == null) { callbackContext.error("Datatype " + datatype + " not supported"); return; @@ -930,59 +927,56 @@ else if (mealt == Field.MEAL_TYPE_SNACK) obj.put("unit", "percent"); } else if (DT.equals(DataType.TYPE_ACTIVITY_SEGMENT)) { String activity = datapoint.getValue(Field.FIELD_ACTIVITY).asActivity(); - long duration = datapoint.getEndTime(TimeUnit.MILLISECONDS) - datapoint.getStartTime(TimeUnit.MILLISECONDS); obj.put("value", activity); obj.put("unit", "activityType"); - obj.put("duration", duration); - if (datatype.equalsIgnoreCase("activitydistcal")) { - DataReadRequest readActivityRequest = new DataReadRequest.Builder() - .setTimeRange(datapoint.getStartTime(TimeUnit.MILLISECONDS), datapoint.getEndTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS) - .read(DataType.TYPE_DISTANCE_DELTA) - .read(DataType.TYPE_CALORIES_EXPENDED) - .build(); - - DataReadResult dataReadActivityResult = Fitness.HistoryApi.readData(mClient, readActivityRequest).await(); + //extra queries to get calorie and distance records related to the activity times + DataReadRequest readActivityRequest = new DataReadRequest.Builder() + .setTimeRange(datapoint.getStartTime(TimeUnit.MILLISECONDS), datapoint.getEndTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS) + .read(DataType.TYPE_DISTANCE_DELTA) + .read(DataType.TYPE_CALORIES_EXPENDED) + .build(); + + DataReadResult dataReadActivityResult = Fitness.HistoryApi.readData(mClient, readActivityRequest).await(); + + if (dataReadActivityResult.getStatus().isSuccess()) { + JSONArray distanceDataPoints = new JSONArray(); + JSONArray calorieDataPoints = new JSONArray(); + + List dataActivitySets = dataReadActivityResult.getDataSets(); + for (DataSet dataActivitySet : dataActivitySets) { + for (DataPoint dataActivityPoint : dataActivitySet.getDataPoints()) { + + JSONObject activityObj = new JSONObject(); + + activityObj.put("startDate", dataActivityPoint.getStartTime(TimeUnit.MILLISECONDS)); + activityObj.put("endDate", dataActivityPoint.getEndTime(TimeUnit.MILLISECONDS)); + DataSource dataActivitySource = dataActivityPoint.getOriginalDataSource(); + if (dataSource != null) { + String sourceName = dataActivitySource.getName(); + if (sourceName != null) activityObj.put("sourceName", sourceName); + String sourceBundleId = dataSource.getAppPackageName(); + if (sourceBundleId != null) activityObj.put("sourceBundleId", sourceBundleId); + } - if (dataReadActivityResult.getStatus().isSuccess()) { - JSONArray distanceDataPoints = new JSONArray(); - JSONArray calorieDataPoints = new JSONArray(); - - List dataActivitySets = dataReadActivityResult.getDataSets(); - for (DataSet dataActivitySet : dataActivitySets) { - for (DataPoint dataActivityPoint : dataActivitySet.getDataPoints()) { - - JSONObject activityObj = new JSONObject(); - - activityObj.put("startDate", dataActivityPoint.getStartTime(TimeUnit.MILLISECONDS)); - activityObj.put("endDate", dataActivityPoint.getEndTime(TimeUnit.MILLISECONDS)); - DataSource dataActivitySource = dataActivityPoint.getOriginalDataSource(); - if (dataSource != null) { - String sourceName = dataActivitySource.getName(); - if (sourceName != null) activityObj.put("sourceName", sourceName); - String sourceBundleId = dataSource.getAppPackageName(); - if (sourceBundleId != null) activityObj.put("sourceBundleId", sourceBundleId); - } - - if (dataActivitySet.getDataType().equals(DataType.TYPE_DISTANCE_DELTA)) - { - float distance = dataActivityPoint.getValue(Field.FIELD_DISTANCE).asFloat(); - activityObj.put("value", distance); - activityObj.put("unit", "m"); - distanceDataPoints.put(activityObj); - } else { - // calories - float calories = dataActivityPoint.getValue(Field.FIELD_CALORIES).asFloat(); - activityObj.put("value", calories); - activityObj.put("unit", "kcal"); - calorieDataPoints.put(activityObj); - } + if (dataActivitySet.getDataType().equals(DataType.TYPE_DISTANCE_DELTA)) + { + float distance = dataActivityPoint.getValue(Field.FIELD_DISTANCE).asFloat(); + activityObj.put("value", distance); + activityObj.put("unit", "m"); + distanceDataPoints.put(activityObj); + } else { + // calories + float calories = dataActivityPoint.getValue(Field.FIELD_CALORIES).asFloat(); + activityObj.put("value", calories); + activityObj.put("unit", "kcal"); + calorieDataPoints.put(activityObj); } } - obj.put("distance", distanceDataPoints); - obj.put("calorie", calorieDataPoints); - } - } + } + obj.put("distance", distanceDataPoints); + obj.put("calories", calorieDataPoints); + } } else if (DT.equals(customdatatypes.get("gender"))) { for (Field f : customdatatypes.get("gender").getFields()) { //there should be only one field named gender @@ -1254,7 +1248,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac builder.aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED); } else if (datatype.equalsIgnoreCase("calories.basal")) { builder.aggregate(DataType.TYPE_BASAL_METABOLIC_RATE, DataType.AGGREGATE_BASAL_METABOLIC_RATE_SUMMARY); - } else if (datatype.equalsIgnoreCase("activity") || datatype.equalsIgnoreCase("activitydistcal")) { + } else if (datatype.equalsIgnoreCase("activity")) { builder.aggregate(DataType.TYPE_ACTIVITY_SEGMENT, DataType.AGGREGATE_ACTIVITY_SUMMARY); } else if (datatype.equalsIgnoreCase("nutrition.water")) { builder.aggregate(DataType.TYPE_HYDRATION, DataType.AGGREGATE_HYDRATION); @@ -1275,7 +1269,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac builder.bucketByTime(1, TimeUnit.DAYS); } } else { - if (datatype.equalsIgnoreCase("activity") || datatype.equalsIgnoreCase("activitydistcal")) { + if (datatype.equalsIgnoreCase("activity")) { builder.bucketByActivityType(1, TimeUnit.MILLISECONDS); } else { builder.bucketByTime(allms, TimeUnit.MILLISECONDS); @@ -1319,7 +1313,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac retBucket.put("unit", "m"); } else if (datatype.equalsIgnoreCase("calories")) { retBucket.put("unit", "kcal"); - } else if (datatype.equalsIgnoreCase("activity") || datatype.equalsIgnoreCase("activitydistcal")) { + } else if (datatype.equalsIgnoreCase("activity")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); } else if (datatype.equalsIgnoreCase("nutrition.water")) { @@ -1361,7 +1355,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac retBucket.put("unit", "m"); } else if (datatype.equalsIgnoreCase("calories")) { retBucket.put("unit", "kcal"); - } else if (datatype.equalsIgnoreCase("activity") || datatype.equalsIgnoreCase("activitydistcal")) { + } else if (datatype.equalsIgnoreCase("activity")) { retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); } else if (datatype.equalsIgnoreCase("nutrition.water")) { @@ -1377,8 +1371,8 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } - // if type was activitydistcal then run subsequent query per bucket time to get distance per activity - if (datatype.equalsIgnoreCase("activitydistcal")) { + // query per bucket time to get distance and calories per activity + if (datatype.equalsIgnoreCase("activity")) { DataReadRequest readActivityDistCalRequest = new DataReadRequest.Builder() .aggregate(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA) @@ -1389,6 +1383,8 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac DataReadResult dataActivityDistCalReadResult = Fitness.HistoryApi.readData(mClient, readActivityDistCalRequest).await(); + + if (dataActivityDistCalReadResult.getStatus().isSuccess()) { for (Bucket activityBucket : dataActivityDistCalReadResult.getBuckets()) { //each bucket is an activity @@ -1466,7 +1462,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac double total = retBucket.getDouble("value"); retBucket.put("value", total + value); } - } else if (datatype.equalsIgnoreCase("activity") || datatype.equalsIgnoreCase("activitydistcal")) { + } else if (datatype.equalsIgnoreCase("activity")) { String activity = datapoint.getValue(Field.FIELD_ACTIVITY).asActivity(); int ndur = datapoint.getValue(Field.FIELD_DURATION).asInt(); JSONObject actobj = retBucket.getJSONObject("value"); From 99e67f3acfd059e9a0f8aa3ef3ede7afdc4f45cc Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Thu, 11 Jan 2018 12:10:18 +0000 Subject: [PATCH 140/157] release 1.0.3 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1ec5bd74..428b258c 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.0.2", + "version": "1.0.3", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index 2d936fe0..e15c25dd 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.0.3"> Cordova Health From eed577529f578c359819fd1a1e44a1540be88ec1 Mon Sep 17 00:00:00 2001 From: Peter McWilliams Date: Mon, 15 Jan 2018 11:25:40 +0000 Subject: [PATCH 141/157] aggregate query added distance and calories when no bucket --- src/android/HealthPlugin.java | 109 +++++++++++++++++----------------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index b985a5d1..8d3f6f91 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -1314,8 +1314,10 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("calories")) { retBucket.put("unit", "kcal"); } else if (datatype.equalsIgnoreCase("activity")) { - retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); + // query per bucket time to get distance and calories per activity + JSONObject actobj = getAggregatedActivityDistanceCalories (st, et); + retBucket.put("value", actobj); } else if (datatype.equalsIgnoreCase("nutrition.water")) { retBucket.put("unit", "ml"); } else if (datatype.equalsIgnoreCase("nutrition")) { @@ -1356,8 +1358,10 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } else if (datatype.equalsIgnoreCase("calories")) { retBucket.put("unit", "kcal"); } else if (datatype.equalsIgnoreCase("activity")) { - retBucket.put("value", new JSONObject()); retBucket.put("unit", "activitySummary"); + // query per bucket time to get distance and calories per activity + JSONObject actobj = getAggregatedActivityDistanceCalories (bucket.getStartTime(TimeUnit.MILLISECONDS), bucket.getEndTime(TimeUnit.MILLISECONDS)); + retBucket.put("value", actobj); } else if (datatype.equalsIgnoreCase("nutrition.water")) { retBucket.put("unit", "ml"); } else if (datatype.equalsIgnoreCase("nutrition")) { @@ -1370,57 +1374,6 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } } - - // query per bucket time to get distance and calories per activity - if (datatype.equalsIgnoreCase("activity")) { - - DataReadRequest readActivityDistCalRequest = new DataReadRequest.Builder() - .aggregate(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA) - .aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED) - .bucketByActivityType(1, TimeUnit.SECONDS) - .setTimeRange(bucket.getStartTime(TimeUnit.MILLISECONDS), bucket.getEndTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS) - .build(); - - DataReadResult dataActivityDistCalReadResult = Fitness.HistoryApi.readData(mClient, readActivityDistCalRequest).await(); - - - - if (dataActivityDistCalReadResult.getStatus().isSuccess()) { - for (Bucket activityBucket : dataActivityDistCalReadResult.getBuckets()) { - //each bucket is an activity - float distance = 0; - float calories = 0; - String activity = activityBucket.getActivity(); - - DataSet distanceDataSet = activityBucket.getDataSet(DataType.AGGREGATE_DISTANCE_DELTA); - for (DataPoint datapoint : distanceDataSet.getDataPoints()) { - distance += datapoint.getValue(Field.FIELD_DISTANCE).asFloat(); - } - - DataSet caloriesDataSet = activityBucket.getDataSet(DataType.AGGREGATE_CALORIES_EXPENDED); - for (DataPoint datapoint : caloriesDataSet.getDataPoints()) { - calories += datapoint.getValue(Field.FIELD_CALORIES).asFloat(); - } - - JSONObject actobj = retBucket.getJSONObject("value"); - JSONObject summary; - if (actobj.has(activity)) { - summary = actobj.getJSONObject(activity); - double existingdistance = summary.getDouble("distance"); - summary.put("distance", distance + existingdistance); - double existingcalories = summary.getDouble("calories"); - summary.put("calories", calories + existingcalories); - } else { - summary = new JSONObject(); - summary.put("duration", 0); // sum onto this whilst aggregating over bucket below. - summary.put("distance", distance); - summary.put("calories", calories); - } - actobj.put(activity, summary); - retBucket.put("value", actobj); - } - } - } } // aggregate data points over the bucket @@ -1503,6 +1456,56 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac } } + private JSONObject getAggregatedActivityDistanceCalories (long st, long et) throws JSONException { + JSONObject actobj = new JSONObject(); + + DataReadRequest readActivityDistCalRequest = new DataReadRequest.Builder() + .aggregate(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA) + .aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED) + .bucketByActivityType(1, TimeUnit.SECONDS) + .setTimeRange(st, et, TimeUnit.MILLISECONDS) + .build(); + + DataReadResult dataActivityDistCalReadResult = Fitness.HistoryApi.readData(mClient, readActivityDistCalRequest).await(); + + if (dataActivityDistCalReadResult.getStatus().isSuccess()) { + for (Bucket activityBucket : dataActivityDistCalReadResult.getBuckets()) { + //each bucket is an activity + float distance = 0; + float calories = 0; + String activity = activityBucket.getActivity(); + + DataSet distanceDataSet = activityBucket.getDataSet(DataType.AGGREGATE_DISTANCE_DELTA); + for (DataPoint datapoint : distanceDataSet.getDataPoints()) { + distance += datapoint.getValue(Field.FIELD_DISTANCE).asFloat(); + } + + DataSet caloriesDataSet = activityBucket.getDataSet(DataType.AGGREGATE_CALORIES_EXPENDED); + for (DataPoint datapoint : caloriesDataSet.getDataPoints()) { + calories += datapoint.getValue(Field.FIELD_CALORIES).asFloat(); + } + + JSONObject summary; + if (actobj.has(activity)) { + summary = actobj.getJSONObject(activity); + double existingdistance = summary.getDouble("distance"); + summary.put("distance", distance + existingdistance); + double existingcalories = summary.getDouble("calories"); + summary.put("calories", calories + existingcalories); + } else { + summary = new JSONObject(); + summary.put("duration", 0); // sum onto this whilst aggregating over buckets. + summary.put("distance", distance); + summary.put("calories", calories); + } + + actobj.put(activity, summary); + } + } + return actobj; + } + + // utility function that gets the basal metabolic rate averaged over a week private float getBasalAVG(long _et) throws Exception { float basalAVG = 0; From 83fde8079327c3654045799392637424468831d5 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Fri, 26 Jan 2018 09:08:21 +0000 Subject: [PATCH 142/157] Fixes #100 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6bcd584f..f6c0e5ea 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ This is known to happen when using the Ionic Package cloud service. * You need to have the Google Services API downloaded in your SDK. * Be sure to give your app access to the Google Fitness API, see [this](https://developers.google.com/fit/android/get-api-key) and [this](https://github.com/2dvisio/cordova-plugin-googlefit#sdk-requirements-for-compiling-the-plugin). * If you are wondering what key your compiled app is using, you can type `keytool -list -printcert -jarfile yourapp.apk`. -* If you haven't configured the APIs correctly, particularly the OAuth requirements, you are likely to get 'User cancelled the dialog' as an error message. +* If you haven't configured the APIs correctly, particularly the OAuth requirements, you are likely to get 'User cancelled the dialog' as an error message, particularly this can happen if you mismatch the signing certificate and SHA-1 fingerprint. * You can use the Google Fitness API even if the user doesn't have Google Fit installed, but there has to be some other fitness app putting data into the Fitness API otherwise your queries will always be empty. See the [the original documentation](https://developers.google.com/fit/overview). * If you are planning to use [health data types](https://developers.google.com/android/reference/com/google/android/gms/fitness/data/HealthDataTypes) in Google Fit, be aware that you are always able to read them, but if you want write access [you need to ask permission to Google](https://developers.google.com/fit/android/data-types#restricted_data_types) * This plugin is set to use the latest version of the Google Play Services API (``). This is done to likely guarantee the compatibility with other plugins using Google Play Services, but bear in mind that a) the plugin was tested until version 9.8.0 of the APIs and b) other plugins may be using a different version of the API. If you run into an issue, check the generated gradle file (build.gradle) under `dependencies` between `// SUB-PROJECT DEPENDENCIES START` and `// SUB-PROJECT DEPENDENCIES END` and make sure that all versions of the `com.google.android.gms:play-services-xxxx` are the same. From d551a1c7a662a2f0d2bd380630e0f5caedc22a70 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 31 Jan 2018 15:08:14 +0000 Subject: [PATCH 143/157] fixes #101 --- www/ios/health.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index 0b7023f8..134823f0 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -211,8 +211,8 @@ Health.prototype.query = function (opts, onSuccess, onError) { if ((res.startDate >= opts.startDate) && (res.endDate <= opts.endDate)) { res.value = data[i].activityType; res.unit = 'activityType'; - res.calories = parseInt(data[i].energy.slice(0, -2)); // remove the ending J - res.distance = parseInt(data[i].distance); + if (data[i].energy) res.calories = parseInt(data[i].energy.slice(0, -2)); // remove the ending J + if (data[i].distance) res.distance = parseInt(data[i].distance); res.sourceName = data[i].sourceName; res.sourceBundleId = data[i].sourceBundleId; result.push(res); From 2b3fbd911b4c1bb3d70b1155aace8c8df3fa755c Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 14 Feb 2018 12:08:30 +0000 Subject: [PATCH 144/157] release 1.0.4 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- plugin.xml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..43f40d02 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +Log of changes + +v 1.0.4 + +* updated README with better documentation +* minor bug fixes \ No newline at end of file diff --git a/package.json b/package.json index 428b258c..e6775890 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.0.3", + "version": "1.0.4", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index e15c25dd..c43ae1d9 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.0.4"> Cordova Health From 96e0cc5f8b40115e06b4c47eb4a40fa2e4af28e2 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Mon, 5 Mar 2018 11:11:19 +0000 Subject: [PATCH 145/157] fixes #104 --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f6c0e5ea..e9fe5d1d 100644 --- a/README.md +++ b/README.md @@ -141,8 +141,7 @@ Example values: | steps | 34 | | distance | 101.2 | | calories | 245.3 | -| activity | "walking"
**Notes**: recognized activities and their mappings in Google Fit / HealthKit can be found [here]
the query also returns calories (kcal) and distance (m) -(activities_map.md) | +| activity | "walking"
**Notes**: recognized activities and their mappings in Google Fit / HealthKit can be found [here](activities_map.md)
the query also returns calories (kcal) and distance (m) | | height | 185.9 | | weight | 83.3 | | heart_rate | 66 | @@ -448,6 +447,6 @@ Any help is more than welcome! I don't know Objective C and I am not interested in learning it now, so I would particularly appreciate someone who could give me a hand with the iOS part. Also, I would love to know from you if the plugin is currently used in any app actually available online. Just send me an email to my_username at gmail.com. -For donations, I have a PayPal account at the same email address. +For donations, you can use my [monzo.me](https://monzo.me/dariosalvi) account. Thanks! From 363a3bcbf02cb2c6035d2d2963b350a634ffe100 Mon Sep 17 00:00:00 2001 From: Ankush Aggarwal Date: Mon, 12 Mar 2018 19:40:28 -0700 Subject: [PATCH 146/157] fix #105 optional filtered flag to ignore manual steps, fix #105 --- src/ios/HealthKit.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ios/HealthKit.m b/src/ios/HealthKit.m index 3fdd3824..d531d0a5 100755 --- a/src/ios/HealthKit.m +++ b/src/ios/HealthKit.m @@ -1504,6 +1504,11 @@ - (void)querySampleTypeAggregated:(CDVInvokedUrlCommand *)command { // NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate]; NSPredicate *predicate = nil; + + BOOL filtered = (args[@"filtered"] != nil && [args[@"filtered"] boolValue]); + if (filtered) { + predicate = [NSPredicate predicateWithFormat:@"metadata.%K != YES", HKMetadataKeyWasUserEntered]; + } NSSet *requestTypes = [NSSet setWithObjects:type, nil]; [[HealthKit sharedHealthStore] requestAuthorizationToShareTypes:nil readTypes:requestTypes completion:^(BOOL success, NSError *error) { From 98a00ac1a5615b42bc555f83c5d2d43c52bc464c Mon Sep 17 00:00:00 2001 From: Bryan Swagerty Date: Tue, 13 Mar 2018 10:13:29 -0700 Subject: [PATCH 147/157] Add support for Apple Exercise Time This will reflect the Exercise Time as show in HealthKit and on Apple Watch. --- www/ios/health.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/www/ios/health.js b/www/ios/health.js index 134823f0..11f52716 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -37,6 +37,7 @@ dataTypes['nutrition.water'] = 'HKQuantityTypeIdentifierDietaryWater'; dataTypes['nutrition.caffeine'] = 'HKQuantityTypeIdentifierDietaryCaffeine'; dataTypes['blood_glucose'] = 'HKQuantityTypeIdentifierBloodGlucose'; dataTypes['insulin'] = 'HKQuantityTypeIdentifierInsulinDelivery'; +dataTypes['appleExerciseTime'] = 'HKQuantityTypeIdentifierAppleExerciseTime'; var units = []; units['steps'] = 'count'; @@ -69,6 +70,7 @@ units['nutrition.water'] = 'ml'; units['nutrition.caffeine'] = 'g'; units['blood_glucose'] = 'mmol/L'; units['insulin'] = 'IU'; +units['appleExerciseTime'] = 'min'; Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); @@ -322,7 +324,8 @@ Health.prototype.queryAggregated = function (opts, onSuccess, onError) { if ((opts.dataType !== 'steps') && (opts.dataType !== 'distance') && (opts.dataType !== 'calories') && (opts.dataType !== 'calories.active') && (opts.dataType !== 'calories.basal') && (opts.dataType !== 'activity') && - (opts.dataType !== 'workouts') && (!opts.dataType.startsWith('nutrition'))) { + (opts.dataType !== 'workouts') && (!opts.dataType.startsWith('nutrition')) && + (opts.dataType !== 'appleExerciseTime')) { // unsupported datatype onError('Datatype ' + opts.dataType + ' not supported in queryAggregated'); return; From f52880a7e56f9c5551ea194a6c32b1c2f2361b22 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 13 Mar 2018 17:27:19 +0000 Subject: [PATCH 148/157] added description of appleExerciseTime to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e9fe5d1d..b5063d2b 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def |-----------------|-------|-----------------------------------------------|------------------------------------------| | steps | count | HKQuantityTypeIdentifierStepCount | TYPE_STEP_COUNT_DELTA | | distance | m | HKQuantityTypeIdentifierDistanceWalkingRunning + HKQuantityTypeIdentifierDistanceCycling | TYPE_DISTANCE_DELTA | +| appleExerciseTime | min | HKQuantityTypeIdentifierAppleExerciseTime | NA | | calories | kcal | HKQuantityTypeIdentifierActiveEnergyBurned + HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_CALORIES_EXPENDED | | calories.active | kcal | HKQuantityTypeIdentifierActiveEnergyBurned | TYPE_CALORIES_EXPENDED - (TYPE_BASAL_METABOLIC_RATE * time window) | | calories.basal | kcal | HKQuantityTypeIdentifierBasalEnergyBurned | TYPE_BASAL_METABOLIC_RATE * time window | @@ -140,6 +141,7 @@ Example values: |----------------|-----------------------------------| | steps | 34 | | distance | 101.2 | +| appleExerciseTime | 24 | | calories | 245.3 | | activity | "walking"
**Notes**: recognized activities and their mappings in Google Fit / HealthKit can be found [here](activities_map.md)
the query also returns calories (kcal) and distance (m) | | height | 185.9 | From 514726215a805ba9aae0bd7842e3408cac40b045 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 13 Mar 2018 17:30:00 +0000 Subject: [PATCH 149/157] added filtered steps description to query aggregated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5063d2b..ce19bd8e 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,7 @@ The following table shows what types are supported and examples of the returned - The start and end dates returned are the date of the first and the last available samples. If no samples are found, start and end may not be set. - When bucketing, buckets will include the whole hour / day / month / week / year where start and end times fall into. For example, if your start time is 2016-10-21 10:53:34, the first daily bucket will start at 2016-10-21 00:00:00. - Weeks start on Monday. +- You can query for "filtered steps" adding the flag `filtered: true` to the query object. This returns the steps as filtered out by Google Fit, or the non-manual ones from HealthKit. #### iOS quirks @@ -346,7 +347,6 @@ The following table shows what types are supported and examples of the returned #### Android quirks - Activities will include two extra fields: calories (kcal) and distance (m) and requires the user to grant access to location -- To query for steps as filtered by the Google Fit app, the flag `filtered: true` must be added into the query object. - nutrition.vitamin_a is given in international units. Automatic conversion to micrograms is not trivial and depends on the actual substance (see [here](https://dietarysupplementdatabase.usda.nih.gov/ingredient_calculator/help.php#q9)). ### store() From 0339379681bd409f669d8898bd6d69f7acd13f81 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 21 Mar 2018 16:04:10 +0000 Subject: [PATCH 150/157] scaffolding BP support, only query --- README.md | 1 + src/android/HealthPlugin.java | 12 +++++++++++- www/ios/health.js | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ce19bd8e..66534dfd 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | | blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose | TYPE_BLOOD_GLUCOSE | | insulin | IU | HKQuantityTypeIdentifierInsulinDelivery | NA | +| blood_pressure | mmHg | HKCorrelationTypeIdentifierBloodPressure | TYPE_BLOOD_PRESSURE | | gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | | nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 8d3f6f91..f03fe511 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -165,6 +165,7 @@ public NutrientFieldInfo(String field, String unit) { static { healthdatatypes.put("blood_glucose", HealthDataTypes.TYPE_BLOOD_GLUCOSE); + healthdatatypes.put("blood_pressure", HealthDataTypes.TYPE_BLOOD_GLUCOSE); } public HealthPlugin() { @@ -1072,7 +1073,16 @@ else if (mealt == Field.MEAL_TYPE_SNACK) } obj.put("value", glucob); obj.put("unit", "mmol/L"); - } + } else if (DT.equals(HealthDataTypes.TYPE_BLOOD_GLUCOSE)) { + JSONObject bpobj = new JSONObject(); + float systolic = datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC).asFloat(); + bpobj.put("systolic", systolic); + float diastolic = datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC).asFloat(); + bpobj.put("diastolic", diastolic); + // TODO: we can also add FIELD_BODY_POSITION and FIELD_BLOOD_PRESSURE_MEASUREMENT_LOCATION + obj.put("value", bpobj); + obj.put("unit", "mmHg"); + } resultset.put(obj); } } diff --git a/www/ios/health.js b/www/ios/health.js index 11f52716..dac70926 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -38,6 +38,8 @@ dataTypes['nutrition.caffeine'] = 'HKQuantityTypeIdentifierDietaryCaffeine'; dataTypes['blood_glucose'] = 'HKQuantityTypeIdentifierBloodGlucose'; dataTypes['insulin'] = 'HKQuantityTypeIdentifierInsulinDelivery'; dataTypes['appleExerciseTime'] = 'HKQuantityTypeIdentifierAppleExerciseTime'; +dataTypes['blood_pressure'] = 'HKCorrelationTypeIdentifierBloodPressure'; + var units = []; units['steps'] = 'count'; @@ -71,6 +73,8 @@ units['nutrition.caffeine'] = 'g'; units['blood_glucose'] = 'mmol/L'; units['insulin'] = 'IU'; units['appleExerciseTime'] = 'min'; +dataTypes['blood_pressure'] = 'mmHg'; + Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); @@ -253,6 +257,19 @@ Health.prototype.query = function (opts, onSuccess, onError) { } onSuccess(result); }, onError); + } else if (opts.dataType === 'blood_pressure') { + var result = []; + window.plugins.healthkit.queryCorrelationType({ + startDate: opts.startDate, + endDate: opts.endDate, + correlationType: 'HKCorrelationTypeIdentifierBloodPressure', + units: ['mmHg'] + }, function (data) { + for (var i = 0; i < data.length; i++) { + result.push(prepareBloodPressure(data[i])); + } + onSuccess(result); + }, onError); } else if (dataTypes[opts.dataType]) { opts.sampleType = dataTypes[opts.dataType]; if (units[opts.dataType]) { @@ -608,6 +625,21 @@ var prepareNutrition = function (data) { return res; }; +var prepareBloodPressure = function (data) { + var res = { + startDate: new Date(data.startDate), + endDate: new Date(data.endDate), + value: {}, + unit: 'mmHg' + }; + if (data.sourceName) res.sourceName = data.sourceName; + if (data.sourceBundleId) res.sourceBundleId = data.sourceBundleId; + res.value.systolic = null; + res.value.diastolic = null; + // TODO: put the correct values, depending on how they are formatted by Telerik's plugin + return res; +}; + // merges activity (workout) samples // fromObj is formatted as returned by query var mergeActivitySamples = function (fromObj, intoObj) { From 36510ec9b9f986491cf0f7d9be000c9e6b7c6f48 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 21 Mar 2018 16:10:27 +0000 Subject: [PATCH 151/157] added example to readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 66534dfd..4d440c9d 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Google Fit is limited to fitness data and, for health, custom data types are def | fat_percentage | % | HKQuantityTypeIdentifierBodyFatPercentage | TYPE_BODY_FAT_PERCENTAGE | | blood_glucose | mmol/L | HKQuantityTypeIdentifierBloodGlucose | TYPE_BLOOD_GLUCOSE | | insulin | IU | HKQuantityTypeIdentifierInsulinDelivery | NA | -| blood_pressure | mmHg | HKCorrelationTypeIdentifierBloodPressure | TYPE_BLOOD_PRESSURE | +| blood_pressure | mmHg | HKCorrelationTypeIdentifierBloodPressure | TYPE_BLOOD_PRESSURE | | gender | | HKCharacteristicTypeIdentifierBiologicalSex | custom (YOUR_PACKAGE_NAME.gender) | | date_of_birth | | HKCharacteristicTypeIdentifierDateOfBirth | custom (YOUR_PACKAGE_NAME.date_of_birth) | | nutrition | | HKCorrelationTypeIdentifierFood | TYPE_NUTRITION | @@ -151,6 +151,7 @@ Example values: | fat_percentage | 31.2 | | blood_glucose | { glucose: 5.5, meal: 'breakfast', sleep: 'fully_awake', source: 'capillary_blood' }
**Notes**:
to convert to mg/dL, multiply by `18.01559` ([The molar mass of glucose is 180.1559](http://www.convertunits.com/molarmass/Glucose))
`meal` can be: 'before_meal' (iOS only), 'after_meal' (iOS only), 'fasting', 'breakfast', 'dinner', 'lunch', 'snack', 'unknown', 'before_breakfast', 'before_dinner', 'before_lunch', 'before_snack', 'after_breakfast', 'after_dinner', 'after_lunch', 'after_snack'
`sleep` can be: 'fully_awake', 'before_sleep', 'on_waking', 'during_sleep'
`source` can be: 'capillary_blood' ,'interstitial_fluid', 'plasma', 'serum', 'tears', whole_blood' | | insulin | { insulin: 2.3, reason: 'bolus' }
**Notes**: Insulin is currently only available on iOS
`reason` can be 'bolus' or 'basal' | +| blood_pressure | { systolic: 110, diastolic: 70 } | | gender | "male" | | date_of_birth | { day: 3, month: 12, year: 1978 } | | nutrition | { item: "cheese", meal_type: "lunch", brand_name: "McDonald's", nutrients: { nutrition.fat.saturated: 11.5, nutrition.calories: 233.1 } }
**Note**: the `brand_name` property is only available on iOS | From c5ceeff388e0b9537b7cf96e042bf549e7d036e0 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Tue, 27 Mar 2018 15:28:26 +0100 Subject: [PATCH 152/157] added store support for android --- src/android/HealthPlugin.java | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index f03fe511..797128e1 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -165,7 +165,6 @@ public NutrientFieldInfo(String field, String unit) { static { healthdatatypes.put("blood_glucose", HealthDataTypes.TYPE_BLOOD_GLUCOSE); - healthdatatypes.put("blood_pressure", HealthDataTypes.TYPE_BLOOD_GLUCOSE); } public HealthPlugin() { @@ -717,7 +716,7 @@ public void run() { private void storeHealthDataForAuth() { final DataType dt = healthDatatypesToAuthWrite.pop(); Calendar c = Calendar.getInstance(); - c.add(Calendar.YEAR, -5); // five years ago + c.add(Calendar.YEAR, -10); // ten years ago final long ts = c.getTimeInMillis(); cordova.getThreadPool().execute(new Runnable() { @@ -735,6 +734,9 @@ public void run() { datapoint.setTimeInterval(ts, ts, TimeUnit.MILLISECONDS); if (dt == HealthDataTypes.TYPE_BLOOD_GLUCOSE) { datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL).setFloat(1); + } else if (dt == HealthDataTypes.TYPE_BLOOD_PRESSURE) { + datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC).setFloat(70); + datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC).setFloat(100); } dataSet.add(datapoint); @@ -1073,16 +1075,7 @@ else if (mealt == Field.MEAL_TYPE_SNACK) } obj.put("value", glucob); obj.put("unit", "mmol/L"); - } else if (DT.equals(HealthDataTypes.TYPE_BLOOD_GLUCOSE)) { - JSONObject bpobj = new JSONObject(); - float systolic = datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC).asFloat(); - bpobj.put("systolic", systolic); - float diastolic = datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC).asFloat(); - bpobj.put("diastolic", diastolic); - // TODO: we can also add FIELD_BODY_POSITION and FIELD_BLOOD_PRESSURE_MEASUREMENT_LOCATION - obj.put("value", bpobj); - obj.put("unit", "mmHg"); - } + } resultset.put(obj); } } @@ -1771,7 +1764,16 @@ else if (mealtype.equalsIgnoreCase("dinner")) } datapoint.getValue(HealthFields.FIELD_BLOOD_GLUCOSE_SPECIMEN_SOURCE).setInt(specimenSource); } - + } else if (dt == HealthDataTypes.TYPE_BLOOD_PRESSURE) { + JSONObject bpobj = args.getJSONObject(0).getJSONObject("value"); + if (bpobj.has("systolic")) { + float systolic = (float) bpobj.getDouble("systolic"); + datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC).setFloat(systolic); + } + if (bpobj.has("diastolic")) { + float diastolic = (float) bpobj.getDouble("diastolic"); + datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC).setFloat(diastolic); + } } dataSet.add(datapoint); From 705de9b8f0acc602c7796d05a1eb5cee293cd76f Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 28 Mar 2018 10:45:59 +0100 Subject: [PATCH 153/157] support for querying bp on android --- src/android/HealthPlugin.java | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index 797128e1..c44c6dd3 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -165,6 +165,7 @@ public NutrientFieldInfo(String field, String unit) { static { healthdatatypes.put("blood_glucose", HealthDataTypes.TYPE_BLOOD_GLUCOSE); + healthdatatypes.put("blood_pressure", HealthDataTypes.TYPE_BLOOD_PRESSURE); } public HealthPlugin() { @@ -945,13 +946,13 @@ else if (mealt == Field.MEAL_TYPE_SNACK) if (dataReadActivityResult.getStatus().isSuccess()) { JSONArray distanceDataPoints = new JSONArray(); JSONArray calorieDataPoints = new JSONArray(); - + List dataActivitySets = dataReadActivityResult.getDataSets(); for (DataSet dataActivitySet : dataActivitySets) { for (DataPoint dataActivityPoint : dataActivitySet.getDataPoints()) { JSONObject activityObj = new JSONObject(); - + activityObj.put("startDate", dataActivityPoint.getStartTime(TimeUnit.MILLISECONDS)); activityObj.put("endDate", dataActivityPoint.getEndTime(TimeUnit.MILLISECONDS)); DataSource dataActivitySource = dataActivityPoint.getOriginalDataSource(); @@ -979,7 +980,7 @@ else if (mealt == Field.MEAL_TYPE_SNACK) } obj.put("distance", distanceDataPoints); obj.put("calories", calorieDataPoints); - } + } } else if (DT.equals(customdatatypes.get("gender"))) { for (Field f : customdatatypes.get("gender").getFields()) { //there should be only one field named gender @@ -1075,6 +1076,18 @@ else if (mealt == Field.MEAL_TYPE_SNACK) } obj.put("value", glucob); obj.put("unit", "mmol/L"); + } else if (DT.equals(HealthDataTypes.TYPE_BLOOD_GLUCOSE)) { + JSONObject bpobj = new JSONObject(); + if (datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC).isSet()){ + float systolic = datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC).asFloat(); + bpobj.put("systolic", systolic); + } + if (datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC).isSet()){ + float diastolic = datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC).asFloat(); + bpobj.put("diastolic", diastolic); + } + obj.put("value", bpobj); + obj.put("unit", "mmHg"); } resultset.put(obj); } @@ -1320,7 +1333,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac retBucket.put("unit", "activitySummary"); // query per bucket time to get distance and calories per activity JSONObject actobj = getAggregatedActivityDistanceCalories (st, et); - retBucket.put("value", actobj); + retBucket.put("value", actobj); } else if (datatype.equalsIgnoreCase("nutrition.water")) { retBucket.put("unit", "ml"); } else if (datatype.equalsIgnoreCase("nutrition")) { @@ -1364,7 +1377,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac retBucket.put("unit", "activitySummary"); // query per bucket time to get distance and calories per activity JSONObject actobj = getAggregatedActivityDistanceCalories (bucket.getStartTime(TimeUnit.MILLISECONDS), bucket.getEndTime(TimeUnit.MILLISECONDS)); - retBucket.put("value", actobj); + retBucket.put("value", actobj); } else if (datatype.equalsIgnoreCase("nutrition.water")) { retBucket.put("unit", "ml"); } else if (datatype.equalsIgnoreCase("nutrition")) { @@ -1461,7 +1474,7 @@ private void queryAggregated(final JSONArray args, final CallbackContext callbac private JSONObject getAggregatedActivityDistanceCalories (long st, long et) throws JSONException { JSONObject actobj = new JSONObject(); - + DataReadRequest readActivityDistCalRequest = new DataReadRequest.Builder() .aggregate(DataType.TYPE_DISTANCE_DELTA, DataType.AGGREGATE_DISTANCE_DELTA) .aggregate(DataType.TYPE_CALORIES_EXPENDED, DataType.AGGREGATE_CALORIES_EXPENDED) @@ -1482,12 +1495,12 @@ private JSONObject getAggregatedActivityDistanceCalories (long st, long et) thr for (DataPoint datapoint : distanceDataSet.getDataPoints()) { distance += datapoint.getValue(Field.FIELD_DISTANCE).asFloat(); } - + DataSet caloriesDataSet = activityBucket.getDataSet(DataType.AGGREGATE_CALORIES_EXPENDED); for (DataPoint datapoint : caloriesDataSet.getDataPoints()) { calories += datapoint.getValue(Field.FIELD_CALORIES).asFloat(); } - + JSONObject summary; if (actobj.has(activity)) { summary = actobj.getJSONObject(activity); @@ -1504,7 +1517,7 @@ private JSONObject getAggregatedActivityDistanceCalories (long st, long et) thr actobj.put(activity, summary); } - } + } return actobj; } From a35f77a29ef020413b49a51a86f2083070cf6e93 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 28 Mar 2018 10:57:46 +0100 Subject: [PATCH 154/157] bug fixes, tested and works on Android --- src/android/HealthPlugin.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/android/HealthPlugin.java b/src/android/HealthPlugin.java index c44c6dd3..ec9c8c96 100644 --- a/src/android/HealthPlugin.java +++ b/src/android/HealthPlugin.java @@ -726,7 +726,6 @@ public void run() { DataSource datasrc = new DataSource.Builder() .setDataType(dt) .setAppPackageName(cordova.getActivity()) - .setName("BOGUS") .setType(DataSource.TYPE_RAW) .build(); @@ -1076,7 +1075,7 @@ else if (mealt == Field.MEAL_TYPE_SNACK) } obj.put("value", glucob); obj.put("unit", "mmol/L"); - } else if (DT.equals(HealthDataTypes.TYPE_BLOOD_GLUCOSE)) { + } else if (DT.equals(HealthDataTypes.TYPE_BLOOD_PRESSURE)) { JSONObject bpobj = new JSONObject(); if (datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC).isSet()){ float systolic = datapoint.getValue(HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC).asFloat(); From e5762075ca9340c0ab6704626d429657a15c889b Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 28 Mar 2018 16:20:02 +0100 Subject: [PATCH 155/157] BP store and query tested on iOS --- www/ios/health.js | 151 ++++++++++++++++++++++++---------------------- 1 file changed, 80 insertions(+), 71 deletions(-) diff --git a/www/ios/health.js b/www/ios/health.js index dac70926..93bfa3fc 100644 --- a/www/ios/health.js +++ b/www/ios/health.js @@ -38,7 +38,7 @@ dataTypes['nutrition.caffeine'] = 'HKQuantityTypeIdentifierDietaryCaffeine'; dataTypes['blood_glucose'] = 'HKQuantityTypeIdentifierBloodGlucose'; dataTypes['insulin'] = 'HKQuantityTypeIdentifierInsulinDelivery'; dataTypes['appleExerciseTime'] = 'HKQuantityTypeIdentifierAppleExerciseTime'; -dataTypes['blood_pressure'] = 'HKCorrelationTypeIdentifierBloodPressure'; +dataTypes['blood_pressure'] = 'HKCorrelationTypeIdentifierBloodPressure'; // when requesting auth it's HKQuantityTypeIdentifierBloodPressureSystolic and HKQuantityTypeIdentifierBloodPressureDiastolic var units = []; @@ -51,7 +51,7 @@ units['height'] = 'm'; units['weight'] = 'kg'; units['heart_rate'] = 'count/min'; units['fat_percentage'] = '%'; -units['nutrition'] = 'nutrition'; +units['nutrition'] = ['g', 'ml', 'kcal']; units['nutrition.calories'] = 'kcal'; units['nutrition.fat.total'] = 'g'; units['nutrition.fat.saturated'] = 'g'; @@ -73,13 +73,14 @@ units['nutrition.caffeine'] = 'g'; units['blood_glucose'] = 'mmol/L'; units['insulin'] = 'IU'; units['appleExerciseTime'] = 'min'; -dataTypes['blood_pressure'] = 'mmHg'; - +units['blood_pressure'] = 'mmHg'; +// just a wrapper for querying Telerik's if HK is available Health.prototype.isAvailable = function (success, error) { window.plugins.healthkit.available(success, error); }; +// returns the equivalent native HealthKit data type from the custom one var getHKDataTypes = function (dtArr) { var HKDataTypes = []; for (var i = 0; i < dtArr.length; i++) { @@ -89,6 +90,9 @@ var getHKDataTypes = function (dtArr) { for (var dataType in dataTypes) { if (dataType.startsWith('nutrition.')) HKDataTypes.push(dataTypes[dataType]); } + } else if (dtArr[i] === 'blood_pressure') { + HKDataTypes.push('HKQuantityTypeIdentifierBloodPressureSystolic'); + HKDataTypes.push('HKQuantityTypeIdentifierBloodPressureDiastolic'); } else if (dataTypes[dtArr[i]]) { HKDataTypes.push(dataTypes[dtArr[i]]); if (dtArr[i] === 'distance') HKDataTypes.push('HKQuantityTypeIdentifierDistanceCycling'); @@ -141,12 +145,7 @@ var getReadWriteTypes = function (dts, success, error) { success(dedupe(readTypes), dedupe(writeTypes)); }; -var dedupe = function (arr) { - return arr.filter(function (el, i, arr) { - return arr.indexOf(el) === i; - }); -}; - +// requests authorization to HK, a wrapper on top of Telerik's plugin Health.prototype.requestAuthorization = function (dts, onSuccess, onError) { getReadWriteTypes(dts, function (readTypes, writeTypes) { window.plugins.healthkit.requestAuthorization({ @@ -156,6 +155,7 @@ Health.prototype.requestAuthorization = function (dts, onSuccess, onError) { }, onError); }; +// checks if a datatype has been authorized Health.prototype.isAuthorized = function (dts, onSuccess, onError) { getReadWriteTypes(dts, function (readTypes, writeTypes) { var HKDataTypes = dedupe(readTypes.concat(writeTypes)); @@ -174,6 +174,7 @@ Health.prototype.isAuthorized = function (dts, onSuccess, onError) { }, onError); }; +// queries for a datatype Health.prototype.query = function (opts, onSuccess, onError) { var startD = opts.startDate; var endD = opts.endDate; @@ -206,7 +207,7 @@ Health.prototype.query = function (opts, onSuccess, onError) { onSuccess(res); }, onError); } else if (opts.dataType === 'activity' || opts.dataType === 'workouts') { - // opts is not really used, the plugin just returns ALL workouts + // opts is not really used, Telerik's plugin just returns ALL workouts window.plugins.healthkit.findWorkouts(opts, function (data) { var result = []; for (var i = 0; i < data.length; i++) { @@ -217,14 +218,14 @@ Health.prototype.query = function (opts, onSuccess, onError) { if ((res.startDate >= opts.startDate) && (res.endDate <= opts.endDate)) { res.value = data[i].activityType; res.unit = 'activityType'; - if (data[i].energy) res.calories = parseInt(data[i].energy.slice(0, -2)); // remove the ending J + if (data[i].energy) res.calories = parseInt(data[i].energy.slice(0, -2)); // remove the ending J if (data[i].distance) res.distance = parseInt(data[i].distance); res.sourceName = data[i].sourceName; res.sourceBundleId = data[i].sourceBundleId; result.push(res); } } - if(opts.dataType === 'activity') { + if (opts.dataType === 'activity') { // get sleep analysis also opts.sampleType = 'HKCategoryTypeIdentifierSleepAnalysis'; window.plugins.healthkit.querySampleType(opts, function (data) { @@ -244,29 +245,20 @@ Health.prototype.query = function (opts, onSuccess, onError) { }, onError); } else onSuccess(result); }, onError); - } else if (opts.dataType === 'nutrition') { + } else if (opts.dataType === 'nutrition' || opts.dataType === 'blood_pressure') { + // do the correlation queries var result = []; - window.plugins.healthkit.queryCorrelationType({ + var qops = { // query-specific options startDate: opts.startDate, endDate: opts.endDate, - correlationType: 'HKCorrelationTypeIdentifierFood', - units: ['g', 'ml', 'kcal'] - }, function (data) { - for (var i = 0; i < data.length; i++) { - result.push(prepareNutrition(data[i])); - } - onSuccess(result); - }, onError); - } else if (opts.dataType === 'blood_pressure') { - var result = []; - window.plugins.healthkit.queryCorrelationType({ - startDate: opts.startDate, - endDate: opts.endDate, - correlationType: 'HKCorrelationTypeIdentifierBloodPressure', - units: ['mmHg'] - }, function (data) { + correlationType: dataTypes[opts.dataType] + } + if (units[opts.dataType].constructor.name == "Array") qops.units = units[opts.dataType]; + else qops.units = [ units[opts.dataType] ]; + + window.plugins.healthkit.queryCorrelationType(qops, function (data) { for (var i = 0; i < data.length; i++) { - result.push(prepareBloodPressure(data[i])); + result.push(prepareCorrelation(data[i], opts.dataType)); } onSuccess(result); }, onError); @@ -287,10 +279,10 @@ Health.prototype.query = function (opts, onSuccess, onError) { glucose: samples[i].quantity } if (samples[i].metadata && samples[i].metadata.HKBloodGlucoseMealTime) { - if(samples[i].metadata.HKBloodGlucoseMealTime == 1) res.value.meal = 'before_meal' - else res.value.meal = 'after_meal' - } - if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime) res.value.meal = samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime; // overwrite HKBloodGlucoseMealTime + if(samples[i].metadata.HKBloodGlucoseMealTime == 1) res.value.meal = 'before_meal' + else res.value.meal = 'after_meal' + } + if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime) res.value.meal = samples[i].metadata.HKMetadataKeyBloodGlucoseMealTime; // overwrite HKBloodGlucoseMealTime if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime) res.value.sleep = samples[i].metadata.HKMetadataKeyBloodGlucoseSleepTime; if (samples[i].metadata && samples[i].metadata.HKMetadataKeyBloodGlucoseSource) res.value.source = samples[i].metadata.HKMetadataKeyBloodGlucoseSource; } else if (opts.dataType === 'insulin') { @@ -341,7 +333,7 @@ Health.prototype.queryAggregated = function (opts, onSuccess, onError) { if ((opts.dataType !== 'steps') && (opts.dataType !== 'distance') && (opts.dataType !== 'calories') && (opts.dataType !== 'calories.active') && (opts.dataType !== 'calories.basal') && (opts.dataType !== 'activity') && - (opts.dataType !== 'workouts') && (!opts.dataType.startsWith('nutrition')) && + (opts.dataType !== 'workouts') && (!opts.dataType.startsWith('nutrition')) && (opts.dataType !== 'appleExerciseTime')) { // unsupported datatype onError('Datatype ' + opts.dataType + ' not supported in queryAggregated'); @@ -504,6 +496,22 @@ Health.prototype.store = function (data, onSuccess, onError) { data.samples.push(sample) } window.plugins.healthkit.saveCorrelation(data, onSuccess, onError); + } else if (data.dataType === 'blood_pressure') { + data.correlationType = 'HKCorrelationTypeIdentifierBloodPressure'; + data.samples = [{ + 'startDate': data.startDate, + 'endDate': data.endDate, + 'sampleType': 'HKQuantityTypeIdentifierBloodPressureSystolic', + 'unit': 'mmHg', + 'amount': data.value.systolic + }, { + 'startDate': data.startDate, + 'endDate': data.endDate, + 'sampleType': 'HKQuantityTypeIdentifierBloodPressureDiastolic', + 'unit': 'mmHg', + 'amount': data.value.diastolic + }]; + window.plugins.healthkit.saveCorrelation(data, onSuccess, onError); } else if (dataTypes[data.dataType]) { // generic case data.sampleType = dataTypes[data.dataType]; @@ -514,10 +522,10 @@ Health.prototype.store = function (data, onSuccess, onError) { data.amount = data.value.glucose; if (!data.metadata) data.metadata = {}; if (data.value.meal) { - data.metadata.HKMetadataKeyBloodGlucoseMealTime = data.value.meal; - if (data.value.meal.startsWith('before_')) data.metadata.HKBloodGlucoseMealTime = 1; - else if (data.value.meal.startsWith('after_')) data.metadata.HKBloodGlucoseMealTime = 2; - } + data.metadata.HKMetadataKeyBloodGlucoseMealTime = data.value.meal; + if (data.value.meal.startsWith('before_')) data.metadata.HKBloodGlucoseMealTime = 1; + else if (data.value.meal.startsWith('after_')) data.metadata.HKBloodGlucoseMealTime = 2; + } if (data.value.sleep) data.metadata.HKMetadataKeyBloodGlucoseSleepTime = data.value.sleep; if (data.value.source) data.metadata.HKMetadataKeyBloodGlucoseSource = data.value.source; } else if (data.dataType === 'insulin') { @@ -569,6 +577,13 @@ cordova.addConstructor(function () { // UTILITY functions +// shallow removal of duplicates in an array +var dedupe = function (arr) { + return arr.filter(function (el, i, arr) { + return arr.indexOf(el) === i; + }); +}; + // converts from grams into another unit // if the unit is not specified or is not weight, then the original quantity is returned var convertFromGrams = function (toUnit, q) { @@ -586,7 +601,7 @@ var convertToGrams = function (fromUnit, q) { return q; } -// refactors the result of a query into returned type +// refactors the result of a quantity type query into returned type var prepareResult = function (data, unit) { var res = { startDate: new Date(data.startDate), @@ -599,47 +614,41 @@ var prepareResult = function (data, unit) { return res; }; -// refactors the result of a nutrition query into returned type -var prepareNutrition = function (data) { +// refactors the result of a correlation query into returned type +var prepareCorrelation = function (data, dataType) { var res = { startDate: new Date(data.startDate), endDate: new Date(data.endDate), - value: {}, - unit: 'nutrition' + value: {} }; if (data.sourceName) res.sourceName = data.sourceName; if (data.sourceBundleId) res.sourceBundleId = data.sourceBundleId; - if (data.metadata && data.metadata.HKFoodType) res.value.item = data.metadata.HKFoodType; - if (data.metadata && data.metadata.HKFoodMeal) res.value.meal_type = data.metadata.HKFoodMeal; - if (data.metadata && data.metadata.HKFoodBrandName) res.value.brand_name = data.metadata.HKFoodBrandName; - res.value.nutrients = {}; - for (var j = 0; j < data.samples.length; j++) { - var sample = data.samples[j]; - for (var dataname in dataTypes) { - if (dataTypes[dataname] === sample.sampleType) { - res.value.nutrients[dataname] = convertFromGrams(units[dataname], sample.value); - break; + if (dataType === 'nutrition') { + res.unit = 'nutrition' + if (data.metadata && data.metadata.HKFoodType) res.value.item = data.metadata.HKFoodType; + if (data.metadata && data.metadata.HKFoodMeal) res.value.meal_type = data.metadata.HKFoodMeal; + if (data.metadata && data.metadata.HKFoodBrandName) res.value.brand_name = data.metadata.HKFoodBrandName; + res.value.nutrients = {}; + for (var j = 0; j < data.samples.length; j++) { + var sample = data.samples[j]; + for (var dataname in dataTypes) { + if (dataTypes[dataname] === sample.sampleType) { + res.value.nutrients[dataname] = convertFromGrams(units[dataname], sample.value); + break; + } } } + } else if (dataType === 'blood_pressure') { + res.unit = 'mmHG' + for (var j = 0; j < data.samples.length; j++) { + var sample = data.samples[j]; + if (sample.sampleType === 'HKQuantityTypeIdentifierBloodPressureSystolic') res.value.systolic = sample.value; + if (sample.sampleType === 'HKQuantityTypeIdentifierBloodPressureDiastolic') res.value.diastolic = sample.value; + } } return res; }; -var prepareBloodPressure = function (data) { - var res = { - startDate: new Date(data.startDate), - endDate: new Date(data.endDate), - value: {}, - unit: 'mmHg' - }; - if (data.sourceName) res.sourceName = data.sourceName; - if (data.sourceBundleId) res.sourceBundleId = data.sourceBundleId; - res.value.systolic = null; - res.value.diastolic = null; - // TODO: put the correct values, depending on how they are formatted by Telerik's plugin - return res; -}; - // merges activity (workout) samples // fromObj is formatted as returned by query var mergeActivitySamples = function (fromObj, intoObj) { From cdddeca080528ef294142cd0496d596cff535f87 Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 28 Mar 2018 16:31:22 +0100 Subject: [PATCH 156/157] updated readme --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4d440c9d..0efbbb3b 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ If, for some reason, the Info.plist loses the HEALTH_READ_PERMISSION and HEALTH_ } ``` -This is known to happen when using the Ionic Package cloud service. +This is known to happen when using the Ionic Package cloud service. ## iOS requirements @@ -78,12 +78,11 @@ This is known to happen when using the Ionic Package cloud service. * If you haven't configured the APIs correctly, particularly the OAuth requirements, you are likely to get 'User cancelled the dialog' as an error message, particularly this can happen if you mismatch the signing certificate and SHA-1 fingerprint. * You can use the Google Fitness API even if the user doesn't have Google Fit installed, but there has to be some other fitness app putting data into the Fitness API otherwise your queries will always be empty. See the [the original documentation](https://developers.google.com/fit/overview). * If you are planning to use [health data types](https://developers.google.com/android/reference/com/google/android/gms/fitness/data/HealthDataTypes) in Google Fit, be aware that you are always able to read them, but if you want write access [you need to ask permission to Google](https://developers.google.com/fit/android/data-types#restricted_data_types) -* This plugin is set to use the latest version of the Google Play Services API (``). This is done to likely guarantee the compatibility with other plugins using Google Play Services, but bear in mind that a) the plugin was tested until version 9.8.0 of the APIs and b) other plugins may be using a different version of the API. If you run into an issue, check the generated gradle file (build.gradle) under `dependencies` between `// SUB-PROJECT DEPENDENCIES START` and `// SUB-PROJECT DEPENDENCIES END` and make sure that all versions of the `com.google.android.gms:play-services-xxxx` are the same. +* This plugin is set to use the latest version of the Google Play Services API (``). This is done to likely guarantee the compatibility with other plugins using Google Play Services, but bear in mind that other plugins may be using a different version of the API. If you run into an issue, check the generated gradle file (build.gradle) under `dependencies` between `// SUB-PROJECT DEPENDENCIES START` and `// SUB-PROJECT DEPENDENCIES END` and make sure that all versions of the `com.google.android.gms:play-services-xxxx` are the same. ## Supported data types As HealthKit does not allow adding custom data types, only a subset of data types supported by HealthKit has been chosen. -Google Fit is limited to fitness data and, for health, custom data types are defined with the suffix of the package name of your project. | Data type | Unit | HealthKit equivalent | Google Fit equivalent | |-----------------|-------|-----------------------------------------------|------------------------------------------| @@ -436,7 +435,7 @@ navigator.health.delete({ Short term: -- Add more datatypes (body fat percentage, oxygen saturation, blood pressure, temperature, respiratory rate) +- Add more datatypes (body fat percentage, oxygen saturation, temperature, respiratory rate) Long term: From f2090c98ace366ea517b4b9fb965fc68e7ea6bda Mon Sep 17 00:00:00 2001 From: Dario Salvi Date: Wed, 28 Mar 2018 16:43:56 +0100 Subject: [PATCH 157/157] release 1.0.5 --- CHANGELOG.md | 13 +++++++++++-- package.json | 2 +- plugin.xml | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f40d02..a294733a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ Log of changes +============== -v 1.0.4 +## v 1.0.4 * updated README with better documentation -* minor bug fixes \ No newline at end of file +* minor bug fixes + + +## v 1.0.5 + +* updated README +* added `filtered` flag to steps also for iOS (but only in aggregatedQuery) +* added `appleExerciseTime` in datatypes (store and query) only on iOS +* added support for blood pressure store and query on both Android and iOS diff --git a/package.json b/package.json index e6775890..5a9ddc93 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.0.4", + "version": "1.0.5", "name": "cordova-plugin-health", "cordova_name": "Health", "description": "A plugin that abstracts fitness and health repositories like Apple HealthKit or Google Fit", diff --git a/plugin.xml b/plugin.xml index c43ae1d9..e9101423 100755 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.0.5"> Cordova Health