diff --git a/packages/health/README.md b/packages/health/README.md index fc83a2b94..f3d47d36a 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -10,6 +10,7 @@ The plugin supports: - reading health data using the `getHealthDataFromTypes` method. - writing health data using the `writeHealthData` method. - writing workouts using the `writeWorkout` method. +- writing meals on iOS (Apple Health) & Android (Google Fit) using the `writeMeal` method. - writing audiograms on iOS using the `writeAudiogram` method. - writing blood pressure data using the `writeBloodPressure` method. - accessing total step counts using the `getTotalStepsInInterval` method. @@ -69,6 +70,7 @@ Note that for Android, the target phone **needs** to have [Google Fit](https://w | HEADACHE_UNSPECIFIED | MINUTES | yes | | | | | AUDIOGRAM | DECIBEL_HEARING_LEVEL | yes | | | | | ELECTROCARDIOGRAM | VOLT | yes | | | Requires Apple Watch to write the data | +| NUTRITION | NO_UNIT | yes | yes | yes | | ## Setup diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index e6fe1c119..ce3c5425e 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt @@ -16,6 +16,11 @@ import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.PermissionController import androidx.health.connect.client.permission.HealthPermission import androidx.health.connect.client.records.* +import androidx.health.connect.client.records.MealType.MEAL_TYPE_BREAKFAST +import androidx.health.connect.client.records.MealType.MEAL_TYPE_DINNER +import androidx.health.connect.client.records.MealType.MEAL_TYPE_LUNCH +import androidx.health.connect.client.records.MealType.MEAL_TYPE_SNACK +import androidx.health.connect.client.records.MealType.MEAL_TYPE_UNKNOWN import androidx.health.connect.client.request.AggregateRequest import androidx.health.connect.client.request.ReadRecordsRequest import androidx.health.connect.client.time.TimeRangeFilter @@ -102,6 +107,13 @@ class HealthPlugin(private var channel: MethodChannel? = null) : private var SLEEP_REM = "SLEEP_REM" private var SLEEP_OUT_OF_BED = "SLEEP_OUT_OF_BED" private var WORKOUT = "WORKOUT" + private var NUTRITION = "NUTRITION" + private var BREAKFAST = "BREAKFAST" + private var LUNCH = "LUNCH" + private var DINNER = "DINNER" + private var SNACK = "SNACK" + private var MEAL_UNKNOWN = "UNKNOWN" + val workoutTypeMap = mapOf( "AEROBICS" to FitnessActivities.AEROBICS, @@ -402,25 +414,10 @@ class HealthPlugin(private var channel: MethodChannel? = null) : mResult?.success(false) } } - if (requestCode == HEALTH_CONNECT_RESULT_CODE) { - if (resultCode == Activity.RESULT_OK) { - if (data != null) { - if(data.extras?.containsKey("request_blocked") == true) { - Log.i("FLUTTER_HEALTH", "Access Denied (to Health Connect) due to too many requests!") - mResult?.success(false) - return false - } - } - Log.i("FLUTTER_HEALTH", "Access Granted (to Health Connect)!") - mResult?.success(true) - } else if (resultCode == Activity.RESULT_CANCELED) { - Log.i("FLUTTER_HEALTH", "Access Denied (to Health Connect)!") - mResult?.success(false) - } - } return false } + private fun keyToHealthDataType(type: String): DataType { return when (type) { BODY_FAT_PERCENTAGE -> DataType.TYPE_BODY_FAT_PERCENTAGE @@ -442,6 +439,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : SLEEP_AWAKE -> DataType.TYPE_SLEEP_SEGMENT SLEEP_IN_BED -> DataType.TYPE_SLEEP_SEGMENT WORKOUT -> DataType.TYPE_ACTIVITY_SEGMENT + NUTRITION -> DataType.TYPE_NUTRITION else -> throw IllegalArgumentException("Unsupported dataType: $type") } } @@ -466,6 +464,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : SLEEP_AWAKE -> Field.FIELD_SLEEP_SEGMENT_TYPE SLEEP_IN_BED -> Field.FIELD_SLEEP_SEGMENT_TYPE WORKOUT -> Field.FIELD_ACTIVITY + NUTRITION -> Field.FIELD_NUTRIENTS else -> throw IllegalArgumentException("Unsupported dataType: $type") } } @@ -598,6 +597,143 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } + private fun writeMealHC(call: MethodCall, result: Result) { + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val calories = call.argument("caloriesConsumed") + val carbs = call.argument("carbohydrates") as Double? + val protein = call.argument("protein") as Double? + val fat = call.argument("fatTotal") as Double? + val name = call.argument("name") + val mealType = call.argument("mealType")!! + + scope.launch { + try { + val list = mutableListOf() + list.add( + NutritionRecord( + name = name, + energy = calories?.kilocalories, + totalCarbohydrate = carbs?.grams, + protein = protein?.grams, + totalFat = fat?.grams, + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + mealType = MapMealTypeToTypeHC[mealType] ?: MEAL_TYPE_UNKNOWN, + ), + ) + healthConnectClient.insertRecords( + list, + ) + result.success(true) + Log.i("FLUTTER_HEALTH::SUCCESS", "[Health Connect] Meal was successfully added!") + + } catch (e: Exception) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the meal", + ) + Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) + result.success(false) + } + + } + } + + + private fun writeMeal(call: MethodCall, result: Result) { + if (useHealthConnectIfAvailable && healthConnectAvailable) { + writeMealHC(call, result) + return + } + + if (context == null) { + result.success(false) + return + } + + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val calories = call.argument("caloriesConsumed") + val carbs = call.argument("carbohydrates") as Double? + val protein = call.argument("protein") as Double? + val fat = call.argument("fatTotal") as Double? + val name = call.argument("name") + val mealType = call.argument("mealType")!! + + val dataType = DataType.TYPE_NUTRITION + + val typesBuilder = FitnessOptions.builder() + typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) + + val dataSource = DataSource.Builder() + .setDataType(dataType) + .setType(DataSource.TYPE_RAW) + .setDevice(Device.getLocalDevice(context!!.applicationContext)) + .setAppPackageName(context!!.applicationContext) + .build() + + val nutrients = mutableMapOf( + Field.NUTRIENT_CALORIES to calories?.toFloat() + ) + + if (carbs != null) { + nutrients[Field.NUTRIENT_TOTAL_CARBS] = carbs.toFloat() + } + + if (protein != null) { + nutrients[Field.NUTRIENT_PROTEIN] = protein.toFloat() + } + + if (fat != null) { + nutrients[Field.NUTRIENT_TOTAL_FAT] = fat.toFloat() + } + + val dataBuilder = DataPoint.builder(dataSource) + .setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) + .setField(Field.FIELD_NUTRIENTS, nutrients) + + if (name != null) { + dataBuilder.setField(Field.FIELD_FOOD_ITEM, name as String) + } + + + dataBuilder.setField( + Field.FIELD_MEAL_TYPE, + MapMealTypeToType[mealType] ?: Field.MEAL_TYPE_UNKNOWN + ) + + + val dataPoint = dataBuilder.build() + + val dataSet = DataSet.builder(dataSource) + .add(dataPoint) + .build() + + val fitnessOptions = typesBuilder.build() + try { + val googleSignInAccount = + GoogleSignIn.getAccountForExtension(context!!.applicationContext, fitnessOptions) + Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) + .insertData(dataSet) + .addOnSuccessListener { + Log.i("FLUTTER_HEALTH::SUCCESS", "Meal added successfully!") + result.success(true) + } + .addOnFailureListener( + errHandler( + result, + "There was an error adding the meal data!" + ) + ) + } catch (e3: Exception) { + result.success(false) + } + } + /** * Save a data type in Google Fit */ @@ -926,6 +1062,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), ) } + DataType.TYPE_ACTIVITY_SEGMENT -> { val readRequest: SessionReadRequest val readRequestBuilder = SessionReadRequest.Builder() @@ -955,6 +1092,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), ) } + else -> { Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) .readData( @@ -988,12 +1126,12 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "date_from" to dataPoint.getStartTime(TimeUnit.MILLISECONDS), "date_to" to dataPoint.getEndTime(TimeUnit.MILLISECONDS), "source_name" to ( - dataPoint.originalDataSource.appPackageName - ?: ( - dataPoint.originalDataSource.device?.model - ?: "" - ) - ), + dataPoint.originalDataSource.appPackageName + ?: ( + dataPoint.originalDataSource.device?.model + ?: "" + ) + ), "source_id" to dataPoint.originalDataSource.streamIdentifier, ) } @@ -1047,12 +1185,12 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "date_to" to dataPoint.getEndTime(TimeUnit.MILLISECONDS), "unit" to "MINUTES", "source_name" to ( - dataPoint.originalDataSource.appPackageName - ?: ( - dataPoint.originalDataSource.device?.model - ?: "unknown" - ) - ), + dataPoint.originalDataSource.appPackageName + ?: ( + dataPoint.originalDataSource.device?.model + ?: "unknown" + ) + ), "source_id" to dataPoint.originalDataSource.streamIdentifier, ), ) @@ -1090,12 +1228,12 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "date_to" to dataPoint.getEndTime(TimeUnit.MILLISECONDS), "unit" to "MINUTES", "source_name" to ( - dataPoint.originalDataSource.appPackageName - ?: ( - dataPoint.originalDataSource.device?.model - ?: "unknown" - ) - ), + dataPoint.originalDataSource.appPackageName + ?: ( + dataPoint.originalDataSource.device?.model + ?: "unknown" + ) + ), "source_id" to dataPoint.originalDataSource.streamIdentifier, ), ) @@ -1131,9 +1269,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : healthData.add( hashMapOf( "workoutActivityType" to ( - workoutTypeMap.filterValues { it == session.activity }.keys.firstOrNull() - ?: "OTHER" - ), + workoutTypeMap.filterValues { it == session.activity }.keys.firstOrNull() + ?: "OTHER" + ), "totalEnergyBurned" to if (totalEnergyBurned == 0.0) null else totalEnergyBurned, "totalEnergyBurnedUnit" to "KILOCALORIE", "totalDistance" to if (totalDistance == 0.0) null else totalDistance, @@ -1169,6 +1307,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_READ) typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) } + else -> throw IllegalArgumentException("Unknown access type $access") } if (typeKey == SLEEP_ASLEEP || typeKey == SLEEP_AWAKE || typeKey == SLEEP_IN_BED) { @@ -1180,6 +1319,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_WRITE) } + else -> throw IllegalArgumentException("Unknown access type $access") } } @@ -1191,6 +1331,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : typesBuilder.accessActivitySessions(FitnessOptions.ACCESS_READ) typesBuilder.accessActivitySessions(FitnessOptions.ACCESS_WRITE) } + else -> throw IllegalArgumentException("Unknown access type $access") } } @@ -1402,6 +1543,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "writeWorkoutData" -> writeWorkoutData(call, result) "writeBloodPressure" -> writeBloodPressure(call, result) "writeBloodOxygen" -> writeBloodOxygen(call, result) + "writeMeal" -> writeMeal(call, result) else -> result.notImplemented() } } @@ -1593,9 +1735,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // mapOf( mapOf( "workoutActivityType" to ( - workoutTypeMapHealthConnect.filterValues { it == record.exerciseType }.keys.firstOrNull() - ?: "OTHER" - ), + workoutTypeMapHealthConnect.filterValues { it == record.exerciseType }.keys.firstOrNull() + ?: "OTHER" + ), "totalDistance" to if (totalDistance == 0.0) null else totalDistance, "totalDistanceUnit" to "METER", "totalEnergyBurned" to if (totalEnergyBurned == 0.0) null else totalEnergyBurned, @@ -1608,7 +1750,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), ) } - // Filter sleep stages for requested stage + // Filter sleep stages for requested stage } else if (classType == SleepStageRecord::class) { for (rec in response.records) { if (rec is SleepStageRecord) { @@ -1617,8 +1759,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } } - } - else { + } else { for (rec in response.records) { healthConnectData.addAll(convertRecord(rec, dataType)) } @@ -1641,6 +1782,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is HeightRecord -> return listOf( mapOf( "value" to record.height.inMeters, @@ -1650,6 +1792,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is BodyFatRecord -> return listOf( mapOf( "value" to record.percentage.value, @@ -1659,6 +1802,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is StepsRecord -> return listOf( mapOf( "value" to record.count, @@ -1668,6 +1812,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is ActiveCaloriesBurnedRecord -> return listOf( mapOf( "value" to record.energy.inKilocalories, @@ -1677,6 +1822,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is HeartRateRecord -> return record.samples.map { mapOf( "value" to it.beatsPerMinute, @@ -1686,6 +1832,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ) } + is BodyTemperatureRecord -> return listOf( mapOf( "value" to record.temperature.inCelsius, @@ -1695,6 +1842,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is BloodPressureRecord -> return listOf( mapOf( "value" to if (dataType == BLOOD_PRESSURE_DIASTOLIC) record.diastolic.inMillimetersOfMercury else record.systolic.inMillimetersOfMercury, @@ -1704,6 +1852,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is OxygenSaturationRecord -> return listOf( mapOf( "value" to record.percentage.value, @@ -1713,6 +1862,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is BloodGlucoseRecord -> return listOf( mapOf( "value" to record.level.inMilligramsPerDeciliter, @@ -1722,6 +1872,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is DistanceRecord -> return listOf( mapOf( "value" to record.distance.inMeters, @@ -1731,6 +1882,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is HydrationRecord -> return listOf( mapOf( "value" to record.volume.inLiters, @@ -1740,6 +1892,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is SleepSessionRecord -> return listOf( mapOf( "date_from" to record.startTime.toEpochMilli(), @@ -1749,6 +1902,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is SleepStageRecord -> return listOf( mapOf( "stage" to record.stage, @@ -1759,6 +1913,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ), ) + is RestingHeartRateRecord -> return listOf( mapOf( "value" to record.beatsPerMinute, @@ -1768,6 +1923,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ) ) + is BasalMetabolicRateRecord -> return listOf( mapOf( "value" to record.basalMetabolicRate.inKilocaloriesPerDay, @@ -1777,6 +1933,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ) ) + is FloorsClimbedRecord -> return listOf( mapOf( "value" to record.floors, @@ -1786,6 +1943,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ) ) + is RespiratoryRateRecord -> return listOf( mapOf( "value" to record.rate, @@ -1795,6 +1953,21 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin.packageName, ) ) + + is NutritionRecord -> return listOf( + mapOf( + "calories" to record.energy!!.inKilocalories, + "protein" to record.protein!!.inGrams, + "carbs" to record.totalCarbohydrate!!.inGrams, + "fat" to record.totalFat!!.inGrams, + "name" to record.name!!, + "mealType" to (MapTypeToMealTypeHC[record.mealType] ?: MEAL_TYPE_UNKNOWN), + "date_from" to record.startTime.toEpochMilli(), + "date_to" to record.endTime.toEpochMilli(), + "source_id" to "", + "source_name" to metadata.dataOrigin.packageName, + ) + ) // is ExerciseSessionRecord -> return listOf(mapOf("value" to , // "date_from" to , // "date_to" to , @@ -1815,16 +1988,19 @@ class HealthPlugin(private var channel: MethodChannel? = null) : percentage = Percentage(value), zoneOffset = null, ) + HEIGHT -> HeightRecord( time = Instant.ofEpochMilli(startTime), height = Length.meters(value), zoneOffset = null, ) + WEIGHT -> WeightRecord( time = Instant.ofEpochMilli(startTime), weight = Mass.kilograms(value), zoneOffset = null, ) + STEPS -> StepsRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1832,6 +2008,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : startZoneOffset = null, endZoneOffset = null, ) + ACTIVE_ENERGY_BURNED -> ActiveCaloriesBurnedRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1839,6 +2016,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : startZoneOffset = null, endZoneOffset = null, ) + HEART_RATE -> HeartRateRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1851,21 +2029,25 @@ class HealthPlugin(private var channel: MethodChannel? = null) : startZoneOffset = null, endZoneOffset = null, ) + BODY_TEMPERATURE -> BodyTemperatureRecord( time = Instant.ofEpochMilli(startTime), temperature = Temperature.celsius(value), zoneOffset = null, ) + BLOOD_OXYGEN -> OxygenSaturationRecord( time = Instant.ofEpochMilli(startTime), percentage = Percentage(value), zoneOffset = null, ) + BLOOD_GLUCOSE -> BloodGlucoseRecord( time = Instant.ofEpochMilli(startTime), level = BloodGlucose.milligramsPerDeciliter(value), zoneOffset = null, ) + DISTANCE_DELTA -> DistanceRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1873,6 +2055,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : startZoneOffset = null, endZoneOffset = null, ) + WATER -> HydrationRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1880,6 +2063,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : startZoneOffset = null, endZoneOffset = null, ) + SLEEP_ASLEEP -> SleepStageRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1887,6 +2071,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : endZoneOffset = null, stage = SleepStageRecord.STAGE_TYPE_SLEEPING, ) + SLEEP_LIGHT -> SleepStageRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1894,6 +2079,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : endZoneOffset = null, stage = SleepStageRecord.STAGE_TYPE_LIGHT, ) + SLEEP_DEEP -> SleepStageRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1901,6 +2087,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : endZoneOffset = null, stage = SleepStageRecord.STAGE_TYPE_DEEP, ) + SLEEP_REM -> SleepStageRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1908,6 +2095,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : endZoneOffset = null, stage = SleepStageRecord.STAGE_TYPE_REM, ) + SLEEP_OUT_OF_BED -> SleepStageRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1915,6 +2103,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : endZoneOffset = null, stage = SleepStageRecord.STAGE_TYPE_OUT_OF_BED, ) + SLEEP_AWAKE -> SleepStageRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1930,16 +2119,19 @@ class HealthPlugin(private var channel: MethodChannel? = null) : startZoneOffset = null, endZoneOffset = null, ) + RESTING_HEART_RATE -> RestingHeartRateRecord( time = Instant.ofEpochMilli(startTime), beatsPerMinute = value.toLong(), zoneOffset = null, ) + BASAL_ENERGY_BURNED -> BasalMetabolicRateRecord( time = Instant.ofEpochMilli(startTime), basalMetabolicRate = Power.kilocaloriesPerDay(value), zoneOffset = null, ) + FLIGHTS_CLIMBED -> FloorsClimbedRecord( startTime = Instant.ofEpochMilli(startTime), endTime = Instant.ofEpochMilli(endTime), @@ -1947,6 +2139,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : startZoneOffset = null, endZoneOffset = null, ) + RESPIRATORY_RATE -> RespiratoryRateRecord( time = Instant.ofEpochMilli(startTime), rate = value, @@ -1956,6 +2149,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : BLOOD_PRESSURE_SYSTOLIC -> throw IllegalArgumentException("You must use the [writeBloodPressure] API ") BLOOD_PRESSURE_DIASTOLIC -> throw IllegalArgumentException("You must use the [writeBloodPressure] API ") WORKOUT -> throw IllegalArgumentException("You must use the [writeWorkoutData] API ") + NUTRITION -> throw IllegalArgumentException("You must use the [writeMeal] API ") else -> throw IllegalArgumentException("The type $type was not supported by the Health plugin or you must use another API ") } scope.launch { @@ -2091,6 +2285,30 @@ class HealthPlugin(private var channel: MethodChannel? = null) : 6 to SLEEP_REM, ) + private val MapMealTypeToTypeHC = hashMapOf( + BREAKFAST to MEAL_TYPE_BREAKFAST, + LUNCH to MEAL_TYPE_LUNCH, + DINNER to MEAL_TYPE_DINNER, + SNACK to MEAL_TYPE_SNACK, + MEAL_UNKNOWN to MEAL_TYPE_UNKNOWN, + ) + + private val MapTypeToMealTypeHC = hashMapOf( + MEAL_TYPE_BREAKFAST to BREAKFAST, + MEAL_TYPE_LUNCH to LUNCH, + MEAL_TYPE_DINNER to DINNER, + MEAL_TYPE_SNACK to SNACK, + MEAL_TYPE_UNKNOWN to MEAL_UNKNOWN, + ) + + private val MapMealTypeToType = hashMapOf( + BREAKFAST to Field.MEAL_TYPE_BREAKFAST, + LUNCH to Field.MEAL_TYPE_LUNCH, + DINNER to Field.MEAL_TYPE_DINNER, + SNACK to Field.MEAL_TYPE_SNACK, + MEAL_UNKNOWN to Field.MEAL_TYPE_UNKNOWN, + ) + val MapToHCType = hashMapOf( BODY_FAT_PERCENTAGE to BodyFatRecord::class, HEIGHT to HeightRecord::class, @@ -2114,6 +2332,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : SLEEP_OUT_OF_BED to SleepStageRecord::class, SLEEP_SESSION to SleepSessionRecord::class, WORKOUT to ExerciseSessionRecord::class, + NUTRITION to NutritionRecord::class, RESTING_HEART_RATE to RestingHeartRateRecord::class, BASAL_ENERGY_BURNED to BasalMetabolicRateRecord::class, FLIGHTS_CLIMBED to FloorsClimbedRecord::class, diff --git a/packages/health/example/android/app/src/main/AndroidManifest.xml b/packages/health/example/android/app/src/main/AndroidManifest.xml index b5168e09e..197e94872 100644 --- a/packages/health/example/android/app/src/main/AndroidManifest.xml +++ b/packages/health/example/android/app/src/main/AndroidManifest.xml @@ -57,7 +57,8 @@ - + + @@ -79,8 +80,8 @@ - + diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 7e570a2fd..92467eb57 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -49,7 +49,7 @@ class _HealthAppState extends State { // // HealthDataType.AUDIOGRAM // ]; - // with coresponsing permissions + // with corresponsing permissions // READ only // final permissions = types.map((e) => HealthDataAccess.READ).toList(); // Or READ and WRITE @@ -169,6 +169,8 @@ class _HealthAppState extends State { success &= await health.writeHealthData( 0.0, HealthDataType.SLEEP_DEEP, earlier, now); + success &= await health.writeMeal( + earlier, now, 1000, 50, 25, 50, "Banana", MealType.SNACK); // Store an Audiogram // Uncomment these on iOS - only available on iOS // const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0]; @@ -279,6 +281,15 @@ class _HealthAppState extends State { subtitle: Text('${p.dateFrom} - ${p.dateTo}'), ); } + if (p.value is NutritionHealthValue) { + return ListTile( + title: Text( + "${p.typeString} ${(p.value as NutritionHealthValue).mealType}: ${(p.value as NutritionHealthValue).name}"), + trailing: + Text('${(p.value as NutritionHealthValue).calories} kcal'), + subtitle: Text('${p.dateFrom} - ${p.dateTo}'), + ); + } return ListTile( title: Text("${p.typeString}: ${p.value}"), trailing: Text('${p.unitString}'), diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index e2756e09e..b0dbac150 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -48,6 +48,7 @@ const List dataTypesIOS = [ HealthDataType.HEADACHE_SEVERE, HealthDataType.HEADACHE_UNSPECIFIED, //HealthDataType.ELECTROCARDIOGRAM, + HealthDataType.NUTRITION, ]; /// List of data types available on Android @@ -78,4 +79,5 @@ const List dataTypesAndroid = [ HealthDataType.WORKOUT, HealthDataType.RESTING_HEART_RATE, HealthDataType.FLIGHTS_CLIMBED, + HealthDataType.NUTRITION, ]; diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index a58c1b175..36d560010 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -3,958 +3,1100 @@ import HealthKit import UIKit public class SwiftHealthPlugin: NSObject, FlutterPlugin { - - let healthStore = HKHealthStore() - var healthDataTypes = [HKSampleType]() - var heartRateEventTypes = Set() - var headacheType = Set() - var allDataTypes = Set() - var dataTypesDict: [String: HKSampleType] = [:] - var unitDict: [String: HKUnit] = [:] - var workoutActivityTypeMap: [String: HKWorkoutActivityType] = [:] - - // Health Data Type Keys - let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" - let AUDIOGRAM = "AUDIOGRAM" - let BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" - let BLOOD_GLUCOSE = "BLOOD_GLUCOSE" - let BLOOD_OXYGEN = "BLOOD_OXYGEN" - let BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" - let BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" - let BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" - let BODY_MASS_INDEX = "BODY_MASS_INDEX" - let BODY_TEMPERATURE = "BODY_TEMPERATURE" - let DIETARY_CARBS_CONSUMED = "DIETARY_CARBS_CONSUMED" - let DIETARY_ENERGY_CONSUMED = "DIETARY_ENERGY_CONSUMED" - let DIETARY_FATS_CONSUMED = "DIETARY_FATS_CONSUMED" - let DIETARY_PROTEIN_CONSUMED = "DIETARY_PROTEIN_CONSUMED" - let ELECTRODERMAL_ACTIVITY = "ELECTRODERMAL_ACTIVITY" - let FORCED_EXPIRATORY_VOLUME = "FORCED_EXPIRATORY_VOLUME" - let HEART_RATE = "HEART_RATE" - let HEART_RATE_VARIABILITY_SDNN = "HEART_RATE_VARIABILITY_SDNN" - let HEIGHT = "HEIGHT" - let HIGH_HEART_RATE_EVENT = "HIGH_HEART_RATE_EVENT" - let IRREGULAR_HEART_RATE_EVENT = "IRREGULAR_HEART_RATE_EVENT" - let LOW_HEART_RATE_EVENT = "LOW_HEART_RATE_EVENT" - let RESTING_HEART_RATE = "RESTING_HEART_RATE" - let RESPIRATORY_RATE = "RESPIRATORY_RATE" - let PERIPHERAL_PERFUSION_INDEX = "PERIPHERAL_PERFUSION_INDEX" - let STEPS = "STEPS" - let WAIST_CIRCUMFERENCE = "WAIST_CIRCUMFERENCE" - let WALKING_HEART_RATE = "WALKING_HEART_RATE" - let WEIGHT = "WEIGHT" - let DISTANCE_WALKING_RUNNING = "DISTANCE_WALKING_RUNNING" - let FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED" - let WATER = "WATER" - let MINDFULNESS = "MINDFULNESS" - let SLEEP_IN_BED = "SLEEP_IN_BED" - let SLEEP_ASLEEP = "SLEEP_ASLEEP" - let SLEEP_AWAKE = "SLEEP_AWAKE" - let SLEEP_DEEP = "SLEEP_DEEP" - let SLEEP_REM = "SLEEP_REM" - - let EXERCISE_TIME = "EXERCISE_TIME" - let WORKOUT = "WORKOUT" - let HEADACHE_UNSPECIFIED = "HEADACHE_UNSPECIFIED" - let HEADACHE_NOT_PRESENT = "HEADACHE_NOT_PRESENT" - let HEADACHE_MILD = "HEADACHE_MILD" - let HEADACHE_MODERATE = "HEADACHE_MODERATE" - let HEADACHE_SEVERE = "HEADACHE_SEVERE" - let ELECTROCARDIOGRAM = "ELECTROCARDIOGRAM" - - // Health Unit types - // MOLE_UNIT_WITH_MOLAR_MASS, // requires molar mass input - not supported yet - // MOLE_UNIT_WITH_PREFIX_MOLAR_MASS, // requires molar mass & prefix input - not supported yet - let GRAM = "GRAM" - let KILOGRAM = "KILOGRAM" - let OUNCE = "OUNCE" - let POUND = "POUND" - let STONE = "STONE" - let METER = "METER" - let INCH = "INCH" - let FOOT = "FOOT" - let YARD = "YARD" - let MILE = "MILE" - let LITER = "LITER" - let MILLILITER = "MILLILITER" - let FLUID_OUNCE_US = "FLUID_OUNCE_US" - let FLUID_OUNCE_IMPERIAL = "FLUID_OUNCE_IMPERIAL" - let CUP_US = "CUP_US" - let CUP_IMPERIAL = "CUP_IMPERIAL" - let PINT_US = "PINT_US" - let PINT_IMPERIAL = "PINT_IMPERIAL" - let PASCAL = "PASCAL" - let MILLIMETER_OF_MERCURY = "MILLIMETER_OF_MERCURY" - let INCHES_OF_MERCURY = "INCHES_OF_MERCURY" - let CENTIMETER_OF_WATER = "CENTIMETER_OF_WATER" - let ATMOSPHERE = "ATMOSPHERE" - let DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL = "DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL" - let SECOND = "SECOND" - let MILLISECOND = "MILLISECOND" - let MINUTE = "MINUTE" - let HOUR = "HOUR" - let DAY = "DAY" - let JOULE = "JOULE" - let KILOCALORIE = "KILOCALORIE" - let LARGE_CALORIE = "LARGE_CALORIE" - let SMALL_CALORIE = "SMALL_CALORIE" - let DEGREE_CELSIUS = "DEGREE_CELSIUS" - let DEGREE_FAHRENHEIT = "DEGREE_FAHRENHEIT" - let KELVIN = "KELVIN" - let DECIBEL_HEARING_LEVEL = "DECIBEL_HEARING_LEVEL" - let HERTZ = "HERTZ" - let SIEMEN = "SIEMEN" - let VOLT = "VOLT" - let INTERNATIONAL_UNIT = "INTERNATIONAL_UNIT" - let COUNT = "COUNT" - let PERCENT = "PERCENT" - let BEATS_PER_MINUTE = "BEATS_PER_MINUTE" - let RESPIRATIONS_PER_MINUTE = "RESPIRATIONS_PER_MINUTE" - let MILLIGRAM_PER_DECILITER = "MILLIGRAM_PER_DECILITER" - let UNKNOWN_UNIT = "UNKNOWN_UNIT" - let NO_UNIT = "NO_UNIT" - - struct PluginError: Error { - let message: String - } - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "flutter_health", binaryMessenger: registrar.messenger()) - let instance = SwiftHealthPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - // Set up all data types - initializeTypes() - - /// Handle checkIfHealthDataAvailable - if call.method.elementsEqual("checkIfHealthDataAvailable") { - checkIfHealthDataAvailable(call: call, result: result) - }/// Handle requestAuthorization - else if call.method.elementsEqual("requestAuthorization") { - try! requestAuthorization(call: call, result: result) - } - - /// Handle getData - else if call.method.elementsEqual("getData") { - getData(call: call, result: result) - } - - /// Handle getTotalStepsInInterval - else if call.method.elementsEqual("getTotalStepsInInterval") { - getTotalStepsInInterval(call: call, result: result) - } - - /// Handle writeData - else if call.method.elementsEqual("writeData") { - try! writeData(call: call, result: result) - } - - /// Handle writeAudiogram - else if call.method.elementsEqual("writeAudiogram") { - try! writeAudiogram(call: call, result: result) - } - - /// Handle writeBloodPressure - else if call.method.elementsEqual("writeBloodPressure") { - try! writeBloodPressure(call: call, result: result) - } - - /// Handle writeWorkoutData - else if call.method.elementsEqual("writeWorkoutData") { - try! writeWorkoutData(call: call, result: result) - } - - /// Handle hasPermission - else if call.method.elementsEqual("hasPermissions") { - try! hasPermissions(call: call, result: result) - } - - /// Handle delete data - else if call.method.elementsEqual("delete") { - try! delete(call: call, result: result) - } - - } - - func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) { - result(HKHealthStore.isHealthDataAvailable()) - } - - func hasPermissions(call: FlutterMethodCall, result: @escaping FlutterResult) throws { - let arguments = call.arguments as? NSDictionary - guard let types = arguments?["types"] as? [String], - let permissions = arguments?["permissions"] as? [Int], - types.count == permissions.count - else { - throw PluginError(message: "Invalid Arguments!") - } - - for (index, type) in types.enumerated() { - let sampleType = dataTypeLookUp(key: type) - let success = hasPermission(type: sampleType, access: permissions[index]) - if success == nil || success == false { - result(success) - return - } - } - - result(true) - } - - func hasPermission(type: HKSampleType, access: Int) -> Bool? { - - if #available(iOS 13.0, *) { - let status = healthStore.authorizationStatus(for: type) - switch access { - case 0: // READ - return nil - case 1: // WRITE - return (status == HKAuthorizationStatus.sharingAuthorized) - default: // READ_WRITE - return nil - } - } else { - return nil - } - } - - func requestAuthorization(call: FlutterMethodCall, result: @escaping FlutterResult) throws { - guard let arguments = call.arguments as? NSDictionary, - let types = arguments["types"] as? [String], - let permissions = arguments["permissions"] as? [Int], - permissions.count == types.count - else { - throw PluginError(message: "Invalid Arguments!") - } - - var typesToRead = Set() - var typesToWrite = Set() - for (index, key) in types.enumerated() { - let dataType = dataTypeLookUp(key: key) - let access = permissions[index] - switch access { - case 0: - typesToRead.insert(dataType) - case 1: - typesToWrite.insert(dataType) - default: - typesToRead.insert(dataType) - typesToWrite.insert(dataType) - } - } - - if #available(iOS 13.0, *) { - healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { - (success, error) in - DispatchQueue.main.async { - result(success) - } - } - } else { - result(false) // Handle the error here. - } - } - - func writeData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { - guard let arguments = call.arguments as? NSDictionary, - let value = (arguments["value"] as? Double), - let type = (arguments["dataTypeKey"] as? String), - let unit = (arguments["dataUnitKey"] as? String), - let startTime = (arguments["startTime"] as? NSNumber), - let endTime = (arguments["endTime"] as? NSNumber) - else { - throw PluginError(message: "Invalid Arguments") + + let healthStore = HKHealthStore() + var healthDataTypes = [HKSampleType]() + var heartRateEventTypes = Set() + var headacheType = Set() + var allDataTypes = Set() + var dataTypesDict: [String: HKSampleType] = [:] + var unitDict: [String: HKUnit] = [:] + var workoutActivityTypeMap: [String: HKWorkoutActivityType] = [:] + + // Health Data Type Keys + let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" + let AUDIOGRAM = "AUDIOGRAM" + let BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" + let BLOOD_GLUCOSE = "BLOOD_GLUCOSE" + let BLOOD_OXYGEN = "BLOOD_OXYGEN" + let BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" + let BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" + let BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" + let BODY_MASS_INDEX = "BODY_MASS_INDEX" + let BODY_TEMPERATURE = "BODY_TEMPERATURE" + let DIETARY_CARBS_CONSUMED = "DIETARY_CARBS_CONSUMED" + let DIETARY_ENERGY_CONSUMED = "DIETARY_ENERGY_CONSUMED" + let DIETARY_FATS_CONSUMED = "DIETARY_FATS_CONSUMED" + let DIETARY_PROTEIN_CONSUMED = "DIETARY_PROTEIN_CONSUMED" + let ELECTRODERMAL_ACTIVITY = "ELECTRODERMAL_ACTIVITY" + let FORCED_EXPIRATORY_VOLUME = "FORCED_EXPIRATORY_VOLUME" + let HEART_RATE = "HEART_RATE" + let HEART_RATE_VARIABILITY_SDNN = "HEART_RATE_VARIABILITY_SDNN" + let HEIGHT = "HEIGHT" + let HIGH_HEART_RATE_EVENT = "HIGH_HEART_RATE_EVENT" + let IRREGULAR_HEART_RATE_EVENT = "IRREGULAR_HEART_RATE_EVENT" + let LOW_HEART_RATE_EVENT = "LOW_HEART_RATE_EVENT" + let RESTING_HEART_RATE = "RESTING_HEART_RATE" + let RESPIRATORY_RATE = "RESPIRATORY_RATE" + let PERIPHERAL_PERFUSION_INDEX = "PERIPHERAL_PERFUSION_INDEX" + let STEPS = "STEPS" + let WAIST_CIRCUMFERENCE = "WAIST_CIRCUMFERENCE" + let WALKING_HEART_RATE = "WALKING_HEART_RATE" + let WEIGHT = "WEIGHT" + let DISTANCE_WALKING_RUNNING = "DISTANCE_WALKING_RUNNING" + let FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED" + let WATER = "WATER" + let MINDFULNESS = "MINDFULNESS" + let SLEEP_IN_BED = "SLEEP_IN_BED" + let SLEEP_ASLEEP = "SLEEP_ASLEEP" + let SLEEP_AWAKE = "SLEEP_AWAKE" + let SLEEP_DEEP = "SLEEP_DEEP" + let SLEEP_REM = "SLEEP_REM" + + let EXERCISE_TIME = "EXERCISE_TIME" + let WORKOUT = "WORKOUT" + let HEADACHE_UNSPECIFIED = "HEADACHE_UNSPECIFIED" + let HEADACHE_NOT_PRESENT = "HEADACHE_NOT_PRESENT" + let HEADACHE_MILD = "HEADACHE_MILD" + let HEADACHE_MODERATE = "HEADACHE_MODERATE" + let HEADACHE_SEVERE = "HEADACHE_SEVERE" + let ELECTROCARDIOGRAM = "ELECTROCARDIOGRAM" + let NUTRITION = "NUTRITION" + + // Health Unit types + // MOLE_UNIT_WITH_MOLAR_MASS, // requires molar mass input - not supported yet + // MOLE_UNIT_WITH_PREFIX_MOLAR_MASS, // requires molar mass & prefix input - not supported yet + let GRAM = "GRAM" + let KILOGRAM = "KILOGRAM" + let OUNCE = "OUNCE" + let POUND = "POUND" + let STONE = "STONE" + let METER = "METER" + let INCH = "INCH" + let FOOT = "FOOT" + let YARD = "YARD" + let MILE = "MILE" + let LITER = "LITER" + let MILLILITER = "MILLILITER" + let FLUID_OUNCE_US = "FLUID_OUNCE_US" + let FLUID_OUNCE_IMPERIAL = "FLUID_OUNCE_IMPERIAL" + let CUP_US = "CUP_US" + let CUP_IMPERIAL = "CUP_IMPERIAL" + let PINT_US = "PINT_US" + let PINT_IMPERIAL = "PINT_IMPERIAL" + let PASCAL = "PASCAL" + let MILLIMETER_OF_MERCURY = "MILLIMETER_OF_MERCURY" + let INCHES_OF_MERCURY = "INCHES_OF_MERCURY" + let CENTIMETER_OF_WATER = "CENTIMETER_OF_WATER" + let ATMOSPHERE = "ATMOSPHERE" + let DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL = "DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL" + let SECOND = "SECOND" + let MILLISECOND = "MILLISECOND" + let MINUTE = "MINUTE" + let HOUR = "HOUR" + let DAY = "DAY" + let JOULE = "JOULE" + let KILOCALORIE = "KILOCALORIE" + let LARGE_CALORIE = "LARGE_CALORIE" + let SMALL_CALORIE = "SMALL_CALORIE" + let DEGREE_CELSIUS = "DEGREE_CELSIUS" + let DEGREE_FAHRENHEIT = "DEGREE_FAHRENHEIT" + let KELVIN = "KELVIN" + let DECIBEL_HEARING_LEVEL = "DECIBEL_HEARING_LEVEL" + let HERTZ = "HERTZ" + let SIEMEN = "SIEMEN" + let VOLT = "VOLT" + let INTERNATIONAL_UNIT = "INTERNATIONAL_UNIT" + let COUNT = "COUNT" + let PERCENT = "PERCENT" + let BEATS_PER_MINUTE = "BEATS_PER_MINUTE" + let RESPIRATIONS_PER_MINUTE = "RESPIRATIONS_PER_MINUTE" + let MILLIGRAM_PER_DECILITER = "MILLIGRAM_PER_DECILITER" + let UNKNOWN_UNIT = "UNKNOWN_UNIT" + let NO_UNIT = "NO_UNIT" + + struct PluginError: Error { + let message: String } - - let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) - let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - - let sample: HKObject - - if dataTypeLookUp(key: type).isKind(of: HKCategoryType.self) { - sample = HKCategorySample( - type: dataTypeLookUp(key: type) as! HKCategoryType, value: Int(value), start: dateFrom, - end: dateTo) - } else { - let quantity = HKQuantity(unit: unitDict[unit]!, doubleValue: value) - sample = HKQuantitySample( - type: dataTypeLookUp(key: type) as! HKQuantityType, quantity: quantity, start: dateFrom, - end: dateTo) + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "flutter_health", binaryMessenger: registrar.messenger()) + let instance = SwiftHealthPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) } - - HKHealthStore().save( - sample, - withCompletion: { (success, error) in - if let err = error { - print("Error Saving \(type) Sample: \(err.localizedDescription)") + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + // Set up all data types + initializeTypes() + + /// Handle checkIfHealthDataAvailable + if call.method.elementsEqual("checkIfHealthDataAvailable") { + checkIfHealthDataAvailable(call: call, result: result) + }/// Handle requestAuthorization + else if call.method.elementsEqual("requestAuthorization") { + try! requestAuthorization(call: call, result: result) } - DispatchQueue.main.async { - result(success) + + /// Handle getData + else if call.method.elementsEqual("getData") { + getData(call: call, result: result) } - }) - } - - func writeAudiogram(call: FlutterMethodCall, result: @escaping FlutterResult) throws { - guard let arguments = call.arguments as? NSDictionary, - let frequencies = (arguments["frequencies"] as? [Double]), - let leftEarSensitivities = (arguments["leftEarSensitivities"] as? [Double]), - let rightEarSensitivities = (arguments["rightEarSensitivities"] as? [Double]), - let startTime = (arguments["startTime"] as? NSNumber), - let endTime = (arguments["endTime"] as? NSNumber) - else { - throw PluginError(message: "Invalid Arguments") - } - - let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) - let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - - var sensitivityPoints = [HKAudiogramSensitivityPoint]() - - for index in 0...frequencies.count - 1 { - let frequency = HKQuantity(unit: HKUnit.hertz(), doubleValue: frequencies[index]) - let dbUnit = HKUnit.decibelHearingLevel() - let left = HKQuantity(unit: dbUnit, doubleValue: leftEarSensitivities[index]) - let right = HKQuantity(unit: dbUnit, doubleValue: rightEarSensitivities[index]) - let sensitivityPoint = try HKAudiogramSensitivityPoint( - frequency: frequency, leftEarSensitivity: left, rightEarSensitivity: right) - sensitivityPoints.append(sensitivityPoint) - } - - let audiogram: HKAudiogramSample - let metadataReceived = (arguments["metadata"] as? [String: Any]?) - - if (metadataReceived) != nil { - guard let deviceName = metadataReceived?!["HKDeviceName"] as? String else { return } - guard let externalUUID = metadataReceived?!["HKExternalUUID"] as? String else { return } - - audiogram = HKAudiogramSample( - sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, - metadata: [HKMetadataKeyDeviceName: deviceName, HKMetadataKeyExternalUUID: externalUUID]) - - } else { - audiogram = HKAudiogramSample( - sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, metadata: nil) - } - - HKHealthStore().save( - audiogram, - withCompletion: { (success, error) in - if let err = error { - print("Error Saving Audiogram. Sample: \(err.localizedDescription)") + + /// Handle getTotalStepsInInterval + else if call.method.elementsEqual("getTotalStepsInInterval") { + getTotalStepsInInterval(call: call, result: result) } - DispatchQueue.main.async { - result(success) + + /// Handle writeData + else if call.method.elementsEqual("writeData") { + try! writeData(call: call, result: result) } - }) - } - - func writeBloodPressure(call: FlutterMethodCall, result: @escaping FlutterResult) throws { - guard let arguments = call.arguments as? NSDictionary, - let systolic = (arguments["systolic"] as? Double), - let diastolic = (arguments["diastolic"] as? Double), - let startTime = (arguments["startTime"] as? NSNumber), - let endTime = (arguments["endTime"] as? NSNumber) - else { - throw PluginError(message: "Invalid Arguments") - } - let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) - let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - - let systolic_sample = HKQuantitySample( - type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!, - quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic), - start: dateFrom, end: dateTo) - let diastolic_sample = HKQuantitySample( - type: HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!, - quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolic), - start: dateFrom, end: dateTo) - - HKHealthStore().save( - [systolic_sample, diastolic_sample], - withCompletion: { (success, error) in - if let err = error { - print("Error Saving Blood Pressure Sample: \(err.localizedDescription)") + + /// Handle writeAudiogram + else if call.method.elementsEqual("writeAudiogram") { + try! writeAudiogram(call: call, result: result) } - DispatchQueue.main.async { - result(success) + + /// Handle writeBloodPressure + else if call.method.elementsEqual("writeBloodPressure") { + try! writeBloodPressure(call: call, result: result) } - }) - } - - func writeWorkoutData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { - guard let arguments = call.arguments as? NSDictionary, - let activityType = (arguments["activityType"] as? String), - let startTime = (arguments["startTime"] as? NSNumber), - let endTime = (arguments["endTime"] as? NSNumber), - let ac = workoutActivityTypeMap[activityType] - else { - throw PluginError(message: "Invalid Arguments - activityType, startTime or endTime invalid") - } - - var totalEnergyBurned: HKQuantity? - var totalDistance: HKQuantity? = nil - - // Handle optional arguments - if let teb = (arguments["totalEnergyBurned"] as? Double) { - totalEnergyBurned = HKQuantity( - unit: unitDict[(arguments["totalEnergyBurnedUnit"] as! String)]!, doubleValue: teb) - } - if let td = (arguments["totalDistance"] as? Double) { - totalDistance = HKQuantity( - unit: unitDict[(arguments["totalDistanceUnit"] as! String)]!, doubleValue: td) - } - - let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) - let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - - var workout: HKWorkout - - workout = HKWorkout( - activityType: ac, start: dateFrom, end: dateTo, duration: dateTo.timeIntervalSince(dateFrom), - totalEnergyBurned: totalEnergyBurned ?? nil, - totalDistance: totalDistance ?? nil, metadata: nil) - - HKHealthStore().save( - workout, - withCompletion: { (success, error) in - if let err = error { - print("Error Saving Workout. Sample: \(err.localizedDescription)") + + /// Handle writeMeal + else if (call.method.elementsEqual("writeMeal")){ + try! writeMeal(call: call, result: result) } - DispatchQueue.main.async { - result(success) + + /// Handle writeWorkoutData + else if call.method.elementsEqual("writeWorkoutData") { + try! writeWorkoutData(call: call, result: result) } - }) - } - - func delete(call: FlutterMethodCall, result: @escaping FlutterResult) { - let arguments = call.arguments as? NSDictionary - let dataTypeKey = (arguments?["dataTypeKey"] as? String)! - let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 - let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 - - let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) - let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - - let dataType = dataTypeLookUp(key: dataTypeKey) - - let predicate = HKQuery.predicateForSamples( - withStart: dateFrom, end: dateTo, options: .strictStartDate) - let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) - - let deleteQuery = HKSampleQuery( - sampleType: dataType, predicate: predicate, limit: HKObjectQueryNoLimit, - sortDescriptors: [sortDescriptor] - ) { [self] x, samplesOrNil, error in - - guard let samplesOrNil = samplesOrNil, error == nil else { - // Handle the error if necessary - print("Error deleting \(dataType)") - return - } - - // Delete the retrieved objects from the HealthKit store - HKHealthStore().delete(samplesOrNil) { (success, error) in - if let err = error { - print("Error deleting \(dataType) Sample: \(err.localizedDescription)") + + /// Handle hasPermission + else if call.method.elementsEqual("hasPermissions") { + try! hasPermissions(call: call, result: result) } - DispatchQueue.main.async { - result(success) + + /// Handle delete data + else if call.method.elementsEqual("delete") { + try! delete(call: call, result: result) } - } + } - - HKHealthStore().execute(deleteQuery) - } - - func getData(call: FlutterMethodCall, result: @escaping FlutterResult) { - let arguments = call.arguments as? NSDictionary - let dataTypeKey = (arguments?["dataTypeKey"] as? String)! - let dataUnitKey = (arguments?["dataUnitKey"] as? String) - let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 - let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 - let limit = (arguments?["limit"] as? Int) ?? HKObjectQueryNoLimit - - // Convert dates from milliseconds to Date() - let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) - let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - - let dataType = dataTypeLookUp(key: dataTypeKey) - var unit: HKUnit? - if let dataUnitKey = dataUnitKey { - unit = unitDict[dataUnitKey] + + func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) { + result(HKHealthStore.isHealthDataAvailable()) } - - let predicate = HKQuery.predicateForSamples( - withStart: dateFrom, end: dateTo, options: .strictStartDate) - let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) - - let query = HKSampleQuery( - sampleType: dataType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor] - ) { - [self] - x, samplesOrNil, error in - - switch samplesOrNil { - case let (samples as [HKQuantitySample]) as Any: - let dictionaries = samples.map { sample -> NSDictionary in - return [ - "uuid": "\(sample.uuid)", - "value": sample.quantity.doubleValue(for: unit!), - "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), - "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), - "source_id": sample.sourceRevision.source.bundleIdentifier, - "source_name": sample.sourceRevision.source.name, - ] + + func hasPermissions(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + let arguments = call.arguments as? NSDictionary + guard var types = arguments?["types"] as? [String], + var permissions = arguments?["permissions"] as? [Int], + types.count == permissions.count + else { + throw PluginError(message: "Invalid Arguments!") } - DispatchQueue.main.async { - result(dictionaries) + + if let nutritionIndex = types.firstIndex(of: NUTRITION) { + types.remove(at: nutritionIndex) + let nutritionPermission = permissions[nutritionIndex] + permissions.remove(at: nutritionIndex) + + types.append(DIETARY_ENERGY_CONSUMED) + permissions.append(nutritionPermission) + types.append(DIETARY_CARBS_CONSUMED) + permissions.append(nutritionPermission) + types.append(DIETARY_PROTEIN_CONSUMED) + permissions.append(nutritionPermission) + types.append(DIETARY_FATS_CONSUMED) + permissions.append(nutritionPermission) } - - case var (samplesCategory as [HKCategorySample]) as Any: - - if dataTypeKey == self.SLEEP_IN_BED { - samplesCategory = samplesCategory.filter { $0.value == 0 } + + for (index, type) in types.enumerated() { + let sampleType = dataTypeLookUp(key: type) + let success = hasPermission(type: sampleType, access: permissions[index]) + if success == nil || success == false { + result(success) + return + } } - if dataTypeKey == self.SLEEP_AWAKE { - samplesCategory = samplesCategory.filter { $0.value == 2 } + + result(true) + } + + func hasPermission(type: HKSampleType, access: Int) -> Bool? { + + if #available(iOS 13.0, *) { + let status = healthStore.authorizationStatus(for: type) + switch access { + case 0: // READ + return nil + case 1: // WRITE + return (status == HKAuthorizationStatus.sharingAuthorized) + default: // READ_WRITE + return nil + } + } else { + return nil } - if dataTypeKey == self.SLEEP_ASLEEP { - samplesCategory = samplesCategory.filter { $0.value == 3 } + } + + func requestAuthorization(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let types = arguments["types"] as? [String], + let permissions = arguments["permissions"] as? [Int], + permissions.count == types.count + else { + throw PluginError(message: "Invalid Arguments!") } - if dataTypeKey == self.SLEEP_DEEP { - samplesCategory = samplesCategory.filter { $0.value == 4 } + + var typesToRead = Set() + var typesToWrite = Set() + for (index, key) in types.enumerated() { + if (key == NUTRITION) { + let caloriesType = dataTypeLookUp(key: DIETARY_ENERGY_CONSUMED) + let carbsType = dataTypeLookUp(key: DIETARY_CARBS_CONSUMED) + let proteinType = dataTypeLookUp(key: DIETARY_PROTEIN_CONSUMED) + let fatType = dataTypeLookUp(key: DIETARY_FATS_CONSUMED) + + typesToWrite.insert(caloriesType); + typesToWrite.insert(carbsType); + typesToWrite.insert(proteinType); + typesToWrite.insert(fatType); + } else { + let dataType = dataTypeLookUp(key: key) + let access = permissions[index] + switch access { + case 0: + typesToRead.insert(dataType) + case 1: + typesToWrite.insert(dataType) + default: + typesToRead.insert(dataType) + typesToWrite.insert(dataType) + } + } } - if dataTypeKey == self.SLEEP_REM { - samplesCategory = samplesCategory.filter { $0.value == 5 } + + if #available(iOS 13.0, *) { + healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { + (success, error) in + DispatchQueue.main.async { + result(success) + } + } + } else { + result(false) // Handle the error here. } - if dataTypeKey == self.HEADACHE_UNSPECIFIED { - samplesCategory = samplesCategory.filter { $0.value == 0 } + } + + func writeData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let value = (arguments["value"] as? Double), + let type = (arguments["dataTypeKey"] as? String), + let unit = (arguments["dataUnitKey"] as? String), + let startTime = (arguments["startTime"] as? NSNumber), + let endTime = (arguments["endTime"] as? NSNumber) + else { + throw PluginError(message: "Invalid Arguments") } - if dataTypeKey == self.HEADACHE_NOT_PRESENT { - samplesCategory = samplesCategory.filter { $0.value == 1 } + + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let sample: HKObject + + if dataTypeLookUp(key: type).isKind(of: HKCategoryType.self) { + sample = HKCategorySample( + type: dataTypeLookUp(key: type) as! HKCategoryType, value: Int(value), start: dateFrom, + end: dateTo) + } else { + let quantity = HKQuantity(unit: unitDict[unit]!, doubleValue: value) + sample = HKQuantitySample( + type: dataTypeLookUp(key: type) as! HKQuantityType, quantity: quantity, start: dateFrom, + end: dateTo) } - if dataTypeKey == self.HEADACHE_MILD { - samplesCategory = samplesCategory.filter { $0.value == 2 } + + HKHealthStore().save( + sample, + withCompletion: { (success, error) in + if let err = error { + print("Error Saving \(type) Sample: \(err.localizedDescription)") + } + DispatchQueue.main.async { + result(success) + } + }) + } + + func writeAudiogram(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let frequencies = (arguments["frequencies"] as? [Double]), + let leftEarSensitivities = (arguments["leftEarSensitivities"] as? [Double]), + let rightEarSensitivities = (arguments["rightEarSensitivities"] as? [Double]), + let startTime = (arguments["startTime"] as? NSNumber), + let endTime = (arguments["endTime"] as? NSNumber) + else { + throw PluginError(message: "Invalid Arguments") } - if dataTypeKey == self.HEADACHE_MODERATE { - samplesCategory = samplesCategory.filter { $0.value == 3 } + + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + var sensitivityPoints = [HKAudiogramSensitivityPoint]() + + for index in 0...frequencies.count - 1 { + let frequency = HKQuantity(unit: HKUnit.hertz(), doubleValue: frequencies[index]) + let dbUnit = HKUnit.decibelHearingLevel() + let left = HKQuantity(unit: dbUnit, doubleValue: leftEarSensitivities[index]) + let right = HKQuantity(unit: dbUnit, doubleValue: rightEarSensitivities[index]) + let sensitivityPoint = try HKAudiogramSensitivityPoint( + frequency: frequency, leftEarSensitivity: left, rightEarSensitivity: right) + sensitivityPoints.append(sensitivityPoint) } - if dataTypeKey == self.HEADACHE_SEVERE { - samplesCategory = samplesCategory.filter { $0.value == 4 } + + let audiogram: HKAudiogramSample + let metadataReceived = (arguments["metadata"] as? [String: Any]?) + + if (metadataReceived) != nil { + guard let deviceName = metadataReceived?!["HKDeviceName"] as? String else { return } + guard let externalUUID = metadataReceived?!["HKExternalUUID"] as? String else { return } + + audiogram = HKAudiogramSample( + sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, + metadata: [HKMetadataKeyDeviceName: deviceName, HKMetadataKeyExternalUUID: externalUUID]) + + } else { + audiogram = HKAudiogramSample( + sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, metadata: nil) } - let categories = samplesCategory.map { sample -> NSDictionary in - return [ - "uuid": "\(sample.uuid)", - "value": sample.value, - "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), - "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), - "source_id": sample.sourceRevision.source.bundleIdentifier, - "source_name": sample.sourceRevision.source.name, - ] + + HKHealthStore().save( + audiogram, + withCompletion: { (success, error) in + if let err = error { + print("Error Saving Audiogram. Sample: \(err.localizedDescription)") + } + DispatchQueue.main.async { + result(success) + } + }) + } + + func writeBloodPressure(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let systolic = (arguments["systolic"] as? Double), + let diastolic = (arguments["diastolic"] as? Double), + let startTime = (arguments["startTime"] as? NSNumber), + let endTime = (arguments["endTime"] as? NSNumber) + else { + throw PluginError(message: "Invalid Arguments") } - DispatchQueue.main.async { - result(categories) + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let systolic_sample = HKQuantitySample( + type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!, + quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic), + start: dateFrom, end: dateTo) + let diastolic_sample = HKQuantitySample( + type: HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!, + quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolic), + start: dateFrom, end: dateTo) + + HKHealthStore().save( + [systolic_sample, diastolic_sample], + withCompletion: { (success, error) in + if let err = error { + print("Error Saving Blood Pressure Sample: \(err.localizedDescription)") + } + DispatchQueue.main.async { + result(success) + } + }) + } + + func writeMeal(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let startTime = (arguments["startTime"] as? NSNumber), + let endTime = (arguments["endTime"] as? NSNumber), + let calories = (arguments["caloriesConsumed"] as? Double?) ?? 0, + let carbs = (arguments["carbohydrates"] as? Double?) ?? 0, + let protein = (arguments["protein"] as? Double?) ?? 0, + let fat = (arguments["fatTotal"] as? Double?) ?? 0, + let name = (arguments["name"] as? String?), + let mealType = (arguments["mealType"] as? String?) + else { + throw PluginError(message: "Invalid Arguments") } - - case let (samplesWorkout as [HKWorkout]) as Any: - - let dictionaries = samplesWorkout.map { sample -> NSDictionary in - return [ - "uuid": "\(sample.uuid)", - "workoutActivityType": workoutActivityTypeMap.first(where: { - $0.value == sample.workoutActivityType - })?.key, - "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()), - "totalEnergyBurnedUnit": "KILOCALORIE", - "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.meter()), - "totalDistanceUnit": "METER", - "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), - "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), - "source_id": sample.sourceRevision.source.bundleIdentifier, - "source_name": sample.sourceRevision.source.name, - ] + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + var mealTypeString = mealType ?? "UNKNOWN" + var metadata = ["HKFoodMeal": "\(mealTypeString)"] + + if(name != nil) { + metadata[HKMetadataKeyFoodType] = "\(name!)" } - - DispatchQueue.main.async { - result(dictionaries) + + var nutrition = Set() + + let caloriesSample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .dietaryEnergyConsumed)!, quantity: HKQuantity(unit: HKUnit.kilocalorie(), doubleValue: calories), start: dateFrom, end: dateTo, metadata: metadata) + nutrition.insert(caloriesSample) + + if(carbs > 0) { + let carbsSample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)!, quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: carbs), start: dateFrom, end: dateTo, metadata: metadata) + nutrition.insert(carbsSample) } - - case let (samplesAudiogram as [HKAudiogramSample]) as Any: - let dictionaries = samplesAudiogram.map { sample -> NSDictionary in - var frequencies = [Double]() - var leftEarSensitivities = [Double]() - var rightEarSensitivities = [Double]() - for samplePoint in sample.sensitivityPoints { - frequencies.append(samplePoint.frequency.doubleValue(for: HKUnit.hertz())) - leftEarSensitivities.append( - samplePoint.leftEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) - rightEarSensitivities.append( - samplePoint.rightEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) - } - return [ - "uuid": "\(sample.uuid)", - "frequencies": frequencies, - "leftEarSensitivities": leftEarSensitivities, - "rightEarSensitivities": rightEarSensitivities, - "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), - "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), - "source_id": sample.sourceRevision.source.bundleIdentifier, - "source_name": sample.sourceRevision.source.name, - ] + + if(protein > 0) { + let proteinSample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .dietaryProtein)!, quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: protein), start: dateFrom, end: dateTo, metadata: metadata) + nutrition.insert(proteinSample) } - DispatchQueue.main.async { - result(dictionaries) + + if(fat > 0) { + let fatSample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .dietaryFatTotal)!, quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: fat), start: dateFrom, end: dateTo, metadata: metadata) + nutrition.insert(fatSample) } - - default: - if #available(iOS 14.0, *), let ecgSamples = samplesOrNil as? [HKElectrocardiogram] { - let dictionaries = ecgSamples.map(fetchEcgMeasurements) - DispatchQueue.main.async { - result(dictionaries) - } + + if #available(iOS 15.0, *){ + let meal = HKCorrelation.init(type: HKCorrelationType.init(HKCorrelationTypeIdentifier.food), start: dateFrom, end: dateTo, objects: nutrition, metadata: metadata) + + HKHealthStore().save(meal, withCompletion: { (success, error) in + if let err = error { + print("Error Saving Meal Sample: \(err.localizedDescription)") + } + DispatchQueue.main.async { + result(success) + } + }) } else { - DispatchQueue.main.async { - print("Error getting ECG - only available on iOS 14.0 and above!") - result(nil) - } + result(false) } - } } - - HKHealthStore().execute(query) - } - - @available(iOS 14.0, *) - private func fetchEcgMeasurements(_ sample: HKElectrocardiogram) -> NSDictionary { - let semaphore = DispatchSemaphore(value: 0) - var voltageValues = [NSDictionary]() - let voltageQuery = HKElectrocardiogramQuery(sample) { query, result in - switch result { - case let .measurement(measurement): - if let voltageQuantity = measurement.quantity(for: .appleWatchSimilarToLeadI) { - let voltage = voltageQuantity.doubleValue(for: HKUnit.volt()) - let timeSinceSampleStart = measurement.timeSinceSampleStart - voltageValues.append(["voltage": voltage, "timeSinceSampleStart": timeSinceSampleStart]) - } - case .done: - semaphore.signal() - case let .error(error): - print(error) - } + + func writeWorkoutData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let activityType = (arguments["activityType"] as? String), + let startTime = (arguments["startTime"] as? NSNumber), + let endTime = (arguments["endTime"] as? NSNumber), + let ac = workoutActivityTypeMap[activityType] + else { + throw PluginError(message: "Invalid Arguments - activityType, startTime or endTime invalid") + } + + var totalEnergyBurned: HKQuantity? + var totalDistance: HKQuantity? = nil + + // Handle optional arguments + if let teb = (arguments["totalEnergyBurned"] as? Double) { + totalEnergyBurned = HKQuantity( + unit: unitDict[(arguments["totalEnergyBurnedUnit"] as! String)]!, doubleValue: teb) + } + if let td = (arguments["totalDistance"] as? Double) { + totalDistance = HKQuantity( + unit: unitDict[(arguments["totalDistanceUnit"] as! String)]!, doubleValue: td) + } + + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + var workout: HKWorkout + + workout = HKWorkout( + activityType: ac, start: dateFrom, end: dateTo, duration: dateTo.timeIntervalSince(dateFrom), + totalEnergyBurned: totalEnergyBurned ?? nil, + totalDistance: totalDistance ?? nil, metadata: nil) + + HKHealthStore().save( + workout, + withCompletion: { (success, error) in + if let err = error { + print("Error Saving Workout. Sample: \(err.localizedDescription)") + } + DispatchQueue.main.async { + result(success) + } + }) } - HKHealthStore().execute(voltageQuery) - semaphore.wait() - return [ - "uuid": "\(sample.uuid)", - "voltageValues": voltageValues, - "averageHeartRate": sample.averageHeartRate?.doubleValue( - for: HKUnit.count().unitDivided(by: HKUnit.minute())), - "samplingFrequency": sample.samplingFrequency?.doubleValue(for: HKUnit.hertz()), - "classification": sample.classification.rawValue, - "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), - "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), - "source_id": sample.sourceRevision.source.bundleIdentifier, - "source_name": sample.sourceRevision.source.name, - ] - } - - func getTotalStepsInInterval(call: FlutterMethodCall, result: @escaping FlutterResult) { - let arguments = call.arguments as? NSDictionary - let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 - let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 - - // Convert dates from milliseconds to Date() - let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) - let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - - let sampleType = HKQuantityType.quantityType(forIdentifier: .stepCount)! - let predicate = HKQuery.predicateForSamples( - withStart: dateFrom, end: dateTo, options: .strictStartDate) - - let query = HKStatisticsQuery( - quantityType: sampleType, - quantitySamplePredicate: predicate, - options: .cumulativeSum - ) { query, queryResult, error in - - guard let queryResult = queryResult else { - let error = error! as NSError - print("Error getting total steps in interval \(error.localizedDescription)") - - DispatchQueue.main.async { - result(nil) + + func delete(call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + let dataTypeKey = (arguments?["dataTypeKey"] as? String)! + let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 + let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 + + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let dataType = dataTypeLookUp(key: dataTypeKey) + + let predicate = HKQuery.predicateForSamples( + withStart: dateFrom, end: dateTo, options: .strictStartDate) + let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) + + let deleteQuery = HKSampleQuery( + sampleType: dataType, predicate: predicate, limit: HKObjectQueryNoLimit, + sortDescriptors: [sortDescriptor] + ) { [self] x, samplesOrNil, error in + + guard let samplesOrNil = samplesOrNil, error == nil else { + // Handle the error if necessary + print("Error deleting \(dataType)") + return + } + + // Delete the retrieved objects from the HealthKit store + HKHealthStore().delete(samplesOrNil) { (success, error) in + if let err = error { + print("Error deleting \(dataType) Sample: \(err.localizedDescription)") + } + DispatchQueue.main.async { + result(success) + } + } } - return - } - - var steps = 0.0 - - if let quantity = queryResult.sumQuantity() { - let unit = HKUnit.count() - steps = quantity.doubleValue(for: unit) - } - - let totalSteps = Int(steps) - DispatchQueue.main.async { - result(totalSteps) - } + + HKHealthStore().execute(deleteQuery) } - - HKHealthStore().execute(query) - } - - func unitLookUp(key: String) -> HKUnit { - guard let unit = unitDict[key] else { - return HKUnit.count() + + func getData(call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + let dataTypeKey = (arguments?["dataTypeKey"] as? String)! + let dataUnitKey = (arguments?["dataUnitKey"] as? String) + let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 + let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 + let limit = (arguments?["limit"] as? Int) ?? HKObjectQueryNoLimit + + // Convert dates from milliseconds to Date() + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let dataType = dataTypeLookUp(key: dataTypeKey) + var unit: HKUnit? + if let dataUnitKey = dataUnitKey { + unit = unitDict[dataUnitKey] + } + + let predicate = HKQuery.predicateForSamples( + withStart: dateFrom, end: dateTo, options: .strictStartDate) + let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) + + let query = HKSampleQuery( + sampleType: dataType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor] + ) { + [self] + x, samplesOrNil, error in + + switch samplesOrNil { + case let (samples as [HKQuantitySample]) as Any: + let dictionaries = samples.map { sample -> NSDictionary in + return [ + "uuid": "\(sample.uuid)", + "value": sample.quantity.doubleValue(for: unit!), + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + ] + } + DispatchQueue.main.async { + result(dictionaries) + } + + case var (samplesCategory as [HKCategorySample]) as Any: + + if dataTypeKey == self.SLEEP_IN_BED { + samplesCategory = samplesCategory.filter { $0.value == 0 } + } + if dataTypeKey == self.SLEEP_AWAKE { + samplesCategory = samplesCategory.filter { $0.value == 2 } + } + if dataTypeKey == self.SLEEP_ASLEEP { + samplesCategory = samplesCategory.filter { $0.value == 3 } + } + if dataTypeKey == self.SLEEP_DEEP { + samplesCategory = samplesCategory.filter { $0.value == 4 } + } + if dataTypeKey == self.SLEEP_REM { + samplesCategory = samplesCategory.filter { $0.value == 5 } + } + if dataTypeKey == self.HEADACHE_UNSPECIFIED { + samplesCategory = samplesCategory.filter { $0.value == 0 } + } + if dataTypeKey == self.HEADACHE_NOT_PRESENT { + samplesCategory = samplesCategory.filter { $0.value == 1 } + } + if dataTypeKey == self.HEADACHE_MILD { + samplesCategory = samplesCategory.filter { $0.value == 2 } + } + if dataTypeKey == self.HEADACHE_MODERATE { + samplesCategory = samplesCategory.filter { $0.value == 3 } + } + if dataTypeKey == self.HEADACHE_SEVERE { + samplesCategory = samplesCategory.filter { $0.value == 4 } + } + let categories = samplesCategory.map { sample -> NSDictionary in + return [ + "uuid": "\(sample.uuid)", + "value": sample.value, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + ] + } + DispatchQueue.main.async { + result(categories) + } + + case let (samplesWorkout as [HKWorkout]) as Any: + + let dictionaries = samplesWorkout.map { sample -> NSDictionary in + return [ + "uuid": "\(sample.uuid)", + "workoutActivityType": workoutActivityTypeMap.first(where: { + $0.value == sample.workoutActivityType + })?.key, + "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()), + "totalEnergyBurnedUnit": "KILOCALORIE", + "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.meter()), + "totalDistanceUnit": "METER", + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + ] + } + + DispatchQueue.main.async { + result(dictionaries) + } + + case let (samplesAudiogram as [HKAudiogramSample]) as Any: + let dictionaries = samplesAudiogram.map { sample -> NSDictionary in + var frequencies = [Double]() + var leftEarSensitivities = [Double]() + var rightEarSensitivities = [Double]() + for samplePoint in sample.sensitivityPoints { + frequencies.append(samplePoint.frequency.doubleValue(for: HKUnit.hertz())) + leftEarSensitivities.append( + samplePoint.leftEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + rightEarSensitivities.append( + samplePoint.rightEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + } + return [ + "uuid": "\(sample.uuid)", + "frequencies": frequencies, + "leftEarSensitivities": leftEarSensitivities, + "rightEarSensitivities": rightEarSensitivities, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + ] + } + DispatchQueue.main.async { + result(dictionaries) + } + + case let (nutritionSample as [HKCorrelation]) as Any: + + //let samples = nutritionSample[0].objects(for: HKObjectType.quantityType(forIdentifier: .dietaryEnergyConsumed)!) + var calories = 0.0 + var fat = 0.0 + var carbs = 0.0 + var protein = 0.0 + + let name = nutritionSample[0].metadata?[HKMetadataKeyFoodType] as! String + let mealType = nutritionSample[0].metadata?["HKFoodMeal"] + let samples = nutritionSample[0].objects + for sample in samples { + if let quantitySample = sample as? HKQuantitySample { + if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: .dietaryEnergyConsumed)){ + calories = quantitySample.quantity.doubleValue(for: HKUnit.kilocalorie()) + } + if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)){ + carbs = quantitySample.quantity.doubleValue(for: HKUnit.gram()) + } + if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: .dietaryProtein)){ + protein = quantitySample.quantity.doubleValue(for: HKUnit.gram()) + } + if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: .dietaryFatTotal)){ + fat = quantitySample.quantity.doubleValue(for: HKUnit.gram()) + } + } + } + + + let dictionaries = nutritionSample.map { sample -> NSDictionary in + return [ + "uuid": "\(sample.uuid)", + "calories": calories, + "carbs": carbs, + "protein": protein, + "fat": fat, + "name": name, + "mealType": mealType, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + ] + } + DispatchQueue.main.async { + result(dictionaries) + } + + default: + if #available(iOS 14.0, *), let ecgSamples = samplesOrNil as? [HKElectrocardiogram] { + let dictionaries = ecgSamples.map(fetchEcgMeasurements) + DispatchQueue.main.async { + result(dictionaries) + } + } else { + DispatchQueue.main.async { + print("Error getting ECG - only available on iOS 14.0 and above!") + result(nil) + } + } + } + } + + HKHealthStore().execute(query) } - return unit - } - - func dataTypeLookUp(key: String) -> HKSampleType { - guard let dataType_ = dataTypesDict[key] else { - return HKSampleType.quantityType(forIdentifier: .bodyMass)! + + @available(iOS 14.0, *) + private func fetchEcgMeasurements(_ sample: HKElectrocardiogram) -> NSDictionary { + let semaphore = DispatchSemaphore(value: 0) + var voltageValues = [NSDictionary]() + let voltageQuery = HKElectrocardiogramQuery(sample) { query, result in + switch result { + case let .measurement(measurement): + if let voltageQuantity = measurement.quantity(for: .appleWatchSimilarToLeadI) { + let voltage = voltageQuantity.doubleValue(for: HKUnit.volt()) + let timeSinceSampleStart = measurement.timeSinceSampleStart + voltageValues.append(["voltage": voltage, "timeSinceSampleStart": timeSinceSampleStart]) + } + case .done: + semaphore.signal() + case let .error(error): + print(error) + } + } + HKHealthStore().execute(voltageQuery) + semaphore.wait() + return [ + "uuid": "\(sample.uuid)", + "voltageValues": voltageValues, + "averageHeartRate": sample.averageHeartRate?.doubleValue( + for: HKUnit.count().unitDivided(by: HKUnit.minute())), + "samplingFrequency": sample.samplingFrequency?.doubleValue(for: HKUnit.hertz()), + "classification": sample.classification.rawValue, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + ] } - return dataType_ - } - - func initializeTypes() { - // Initialize units - unitDict[GRAM] = HKUnit.gram() - unitDict[KILOGRAM] = HKUnit.gramUnit(with: .kilo) - unitDict[OUNCE] = HKUnit.ounce() - unitDict[POUND] = HKUnit.pound() - unitDict[STONE] = HKUnit.stone() - unitDict[METER] = HKUnit.meter() - unitDict[INCH] = HKUnit.inch() - unitDict[FOOT] = HKUnit.foot() - unitDict[YARD] = HKUnit.yard() - unitDict[MILE] = HKUnit.mile() - unitDict[LITER] = HKUnit.liter() - unitDict[MILLILITER] = HKUnit.literUnit(with: .milli) - unitDict[FLUID_OUNCE_US] = HKUnit.fluidOunceUS() - unitDict[FLUID_OUNCE_IMPERIAL] = HKUnit.fluidOunceImperial() - unitDict[CUP_US] = HKUnit.cupUS() - unitDict[CUP_IMPERIAL] = HKUnit.cupImperial() - unitDict[PINT_US] = HKUnit.pintUS() - unitDict[PINT_IMPERIAL] = HKUnit.pintImperial() - unitDict[PASCAL] = HKUnit.pascal() - unitDict[MILLIMETER_OF_MERCURY] = HKUnit.millimeterOfMercury() - unitDict[CENTIMETER_OF_WATER] = HKUnit.centimeterOfWater() - unitDict[ATMOSPHERE] = HKUnit.atmosphere() - unitDict[DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL] = HKUnit.decibelAWeightedSoundPressureLevel() - unitDict[SECOND] = HKUnit.second() - unitDict[MILLISECOND] = HKUnit.secondUnit(with: .milli) - unitDict[MINUTE] = HKUnit.minute() - unitDict[HOUR] = HKUnit.hour() - unitDict[DAY] = HKUnit.day() - unitDict[JOULE] = HKUnit.joule() - unitDict[KILOCALORIE] = HKUnit.kilocalorie() - unitDict[LARGE_CALORIE] = HKUnit.largeCalorie() - unitDict[SMALL_CALORIE] = HKUnit.smallCalorie() - unitDict[DEGREE_CELSIUS] = HKUnit.degreeCelsius() - unitDict[DEGREE_FAHRENHEIT] = HKUnit.degreeFahrenheit() - unitDict[KELVIN] = HKUnit.kelvin() - unitDict[DECIBEL_HEARING_LEVEL] = HKUnit.decibelHearingLevel() - unitDict[HERTZ] = HKUnit.hertz() - unitDict[SIEMEN] = HKUnit.siemen() - unitDict[INTERNATIONAL_UNIT] = HKUnit.internationalUnit() - unitDict[COUNT] = HKUnit.count() - unitDict[PERCENT] = HKUnit.percent() - unitDict[BEATS_PER_MINUTE] = HKUnit.init(from: "count/min") - unitDict[RESPIRATIONS_PER_MINUTE] = HKUnit.init(from: "count/min") - unitDict[MILLIGRAM_PER_DECILITER] = HKUnit.init(from: "mg/dL") - unitDict[UNKNOWN_UNIT] = HKUnit.init(from: "") - unitDict[NO_UNIT] = HKUnit.init(from: "") - - // Initialize workout types - workoutActivityTypeMap["ARCHERY"] = .archery - workoutActivityTypeMap["BOWLING"] = .bowling - workoutActivityTypeMap["FENCING"] = .fencing - workoutActivityTypeMap["GYMNASTICS"] = .gymnastics - workoutActivityTypeMap["TRACK_AND_FIELD"] = .trackAndField - workoutActivityTypeMap["AMERICAN_FOOTBALL"] = .americanFootball - workoutActivityTypeMap["AUSTRALIAN_FOOTBALL"] = .australianFootball - workoutActivityTypeMap["BASEBALL"] = .baseball - workoutActivityTypeMap["BASKETBALL"] = .basketball - workoutActivityTypeMap["CRICKET"] = .cricket - workoutActivityTypeMap["DISC_SPORTS"] = .discSports - workoutActivityTypeMap["HANDBALL"] = .handball - workoutActivityTypeMap["HOCKEY"] = .hockey - workoutActivityTypeMap["LACROSSE"] = .lacrosse - workoutActivityTypeMap["RUGBY"] = .rugby - workoutActivityTypeMap["SOCCER"] = .soccer - workoutActivityTypeMap["SOFTBALL"] = .softball - workoutActivityTypeMap["VOLLEYBALL"] = .volleyball - workoutActivityTypeMap["PREPARATION_AND_RECOVERY"] = .preparationAndRecovery - workoutActivityTypeMap["FLEXIBILITY"] = .flexibility - workoutActivityTypeMap["WALKING"] = .walking - workoutActivityTypeMap["RUNNING"] = .running - workoutActivityTypeMap["RUNNING_JOGGING"] = .running // Supported due to combining with Android naming - workoutActivityTypeMap["RUNNING_SAND"] = .running // Supported due to combining with Android naming - workoutActivityTypeMap["RUNNING_TREADMILL"] = .running // Supported due to combining with Android naming - workoutActivityTypeMap["WHEELCHAIR_WALK_PACE"] = .wheelchairWalkPace - workoutActivityTypeMap["WHEELCHAIR_RUN_PACE"] = .wheelchairRunPace - workoutActivityTypeMap["BIKING"] = .cycling - workoutActivityTypeMap["HAND_CYCLING"] = .handCycling - workoutActivityTypeMap["CORE_TRAINING"] = .coreTraining - workoutActivityTypeMap["ELLIPTICAL"] = .elliptical - workoutActivityTypeMap["FUNCTIONAL_STRENGTH_TRAINING"] = .functionalStrengthTraining - workoutActivityTypeMap["TRADITIONAL_STRENGTH_TRAINING"] = .traditionalStrengthTraining - workoutActivityTypeMap["CROSS_TRAINING"] = .crossTraining - workoutActivityTypeMap["MIXED_CARDIO"] = .mixedCardio - workoutActivityTypeMap["HIGH_INTENSITY_INTERVAL_TRAINING"] = .highIntensityIntervalTraining - workoutActivityTypeMap["JUMP_ROPE"] = .jumpRope - workoutActivityTypeMap["STAIR_CLIMBING"] = .stairClimbing - workoutActivityTypeMap["STAIRS"] = .stairs - workoutActivityTypeMap["STEP_TRAINING"] = .stepTraining - workoutActivityTypeMap["FITNESS_GAMING"] = .fitnessGaming - workoutActivityTypeMap["BARRE"] = .barre - workoutActivityTypeMap["YOGA"] = .yoga - workoutActivityTypeMap["MIND_AND_BODY"] = .mindAndBody - workoutActivityTypeMap["PILATES"] = .pilates - workoutActivityTypeMap["BADMINTON"] = .badminton - workoutActivityTypeMap["RACQUETBALL"] = .racquetball - workoutActivityTypeMap["SQUASH"] = .squash - workoutActivityTypeMap["TABLE_TENNIS"] = .tableTennis - workoutActivityTypeMap["TENNIS"] = .tennis - workoutActivityTypeMap["CLIMBING"] = .climbing - workoutActivityTypeMap["ROCK_CLIMBING"] = .climbing // Supported due to combining with Android naming - workoutActivityTypeMap["EQUESTRIAN_SPORTS"] = .equestrianSports - workoutActivityTypeMap["FISHING"] = .fishing - workoutActivityTypeMap["GOLF"] = .golf - workoutActivityTypeMap["HIKING"] = .hiking - workoutActivityTypeMap["HUNTING"] = .hunting - workoutActivityTypeMap["PLAY"] = .play - workoutActivityTypeMap["CROSS_COUNTRY_SKIING"] = .crossCountrySkiing - workoutActivityTypeMap["CURLING"] = .curling - workoutActivityTypeMap["DOWNHILL_SKIING"] = .downhillSkiing - workoutActivityTypeMap["SNOW_SPORTS"] = .snowSports - workoutActivityTypeMap["SNOWBOARDING"] = .snowboarding - workoutActivityTypeMap["SKATING"] = .skatingSports - workoutActivityTypeMap["SKATING_CROSS,"] = .skatingSports // Supported due to combining with Android naming - workoutActivityTypeMap["SKATING_INDOOR,"] = .skatingSports // Supported due to combining with Android naming - workoutActivityTypeMap["SKATING_INLINE,"] = .skatingSports // Supported due to combining with Android naming - workoutActivityTypeMap["PADDLE_SPORTS"] = .paddleSports - workoutActivityTypeMap["ROWING"] = .rowing - workoutActivityTypeMap["SAILING"] = .sailing - workoutActivityTypeMap["SURFING_SPORTS"] = .surfingSports - workoutActivityTypeMap["SWIMMING"] = .swimming - workoutActivityTypeMap["WATER_FITNESS"] = .waterFitness - workoutActivityTypeMap["WATER_POLO"] = .waterPolo - workoutActivityTypeMap["WATER_SPORTS"] = .waterSports - workoutActivityTypeMap["BOXING"] = .boxing - workoutActivityTypeMap["KICKBOXING"] = .kickboxing - workoutActivityTypeMap["MARTIAL_ARTS"] = .martialArts - workoutActivityTypeMap["TAI_CHI"] = .taiChi - workoutActivityTypeMap["WRESTLING"] = .wrestling - workoutActivityTypeMap["OTHER"] = .other - - // Set up iOS 13 specific types (ordinary health data types) - if #available(iOS 13.0, *) { - dataTypesDict[ACTIVE_ENERGY_BURNED] = HKSampleType.quantityType( - forIdentifier: .activeEnergyBurned)! - dataTypesDict[AUDIOGRAM] = HKSampleType.audiogramSampleType() - dataTypesDict[BASAL_ENERGY_BURNED] = HKSampleType.quantityType( - forIdentifier: .basalEnergyBurned)! - dataTypesDict[BLOOD_GLUCOSE] = HKSampleType.quantityType(forIdentifier: .bloodGlucose)! - dataTypesDict[BLOOD_OXYGEN] = HKSampleType.quantityType(forIdentifier: .oxygenSaturation)! - dataTypesDict[RESPIRATORY_RATE] = HKSampleType.quantityType(forIdentifier: .respiratoryRate)! - dataTypesDict[PERIPHERAL_PERFUSION_INDEX] = HKSampleType.quantityType( - forIdentifier: .peripheralPerfusionIndex)! - - dataTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKSampleType.quantityType( - forIdentifier: .bloodPressureDiastolic)! - dataTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKSampleType.quantityType( - forIdentifier: .bloodPressureSystolic)! - dataTypesDict[BODY_FAT_PERCENTAGE] = HKSampleType.quantityType( - forIdentifier: .bodyFatPercentage)! - dataTypesDict[BODY_MASS_INDEX] = HKSampleType.quantityType(forIdentifier: .bodyMassIndex)! - dataTypesDict[BODY_TEMPERATURE] = HKSampleType.quantityType(forIdentifier: .bodyTemperature)! - dataTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType( - forIdentifier: .dietaryCarbohydrates)! - dataTypesDict[DIETARY_ENERGY_CONSUMED] = HKSampleType.quantityType( - forIdentifier: .dietaryEnergyConsumed)! - dataTypesDict[DIETARY_FATS_CONSUMED] = HKSampleType.quantityType( - forIdentifier: .dietaryFatTotal)! - dataTypesDict[DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType( - forIdentifier: .dietaryProtein)! - dataTypesDict[ELECTRODERMAL_ACTIVITY] = HKSampleType.quantityType( - forIdentifier: .electrodermalActivity)! - dataTypesDict[FORCED_EXPIRATORY_VOLUME] = HKSampleType.quantityType( - forIdentifier: .forcedExpiratoryVolume1)! - dataTypesDict[HEART_RATE] = HKSampleType.quantityType(forIdentifier: .heartRate)! - dataTypesDict[HEART_RATE_VARIABILITY_SDNN] = HKSampleType.quantityType( - forIdentifier: .heartRateVariabilitySDNN)! - dataTypesDict[HEIGHT] = HKSampleType.quantityType(forIdentifier: .height)! - dataTypesDict[RESTING_HEART_RATE] = HKSampleType.quantityType( - forIdentifier: .restingHeartRate)! - dataTypesDict[STEPS] = HKSampleType.quantityType(forIdentifier: .stepCount)! - dataTypesDict[WAIST_CIRCUMFERENCE] = HKSampleType.quantityType( - forIdentifier: .waistCircumference)! - dataTypesDict[WALKING_HEART_RATE] = HKSampleType.quantityType( - forIdentifier: .walkingHeartRateAverage)! - dataTypesDict[WEIGHT] = HKSampleType.quantityType(forIdentifier: .bodyMass)! - dataTypesDict[DISTANCE_WALKING_RUNNING] = HKSampleType.quantityType( - forIdentifier: .distanceWalkingRunning)! - dataTypesDict[FLIGHTS_CLIMBED] = HKSampleType.quantityType(forIdentifier: .flightsClimbed)! - dataTypesDict[WATER] = HKSampleType.quantityType(forIdentifier: .dietaryWater)! - dataTypesDict[MINDFULNESS] = HKSampleType.categoryType(forIdentifier: .mindfulSession)! - dataTypesDict[SLEEP_IN_BED] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - dataTypesDict[SLEEP_ASLEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - dataTypesDict[SLEEP_AWAKE] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - dataTypesDict[SLEEP_DEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - dataTypesDict[SLEEP_REM] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - - dataTypesDict[EXERCISE_TIME] = HKSampleType.quantityType(forIdentifier: .appleExerciseTime)! - dataTypesDict[WORKOUT] = HKSampleType.workoutType() - - healthDataTypes = Array(dataTypesDict.values) + + func getTotalStepsInInterval(call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 + let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 + + // Convert dates from milliseconds to Date() + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let sampleType = HKQuantityType.quantityType(forIdentifier: .stepCount)! + let predicate = HKQuery.predicateForSamples( + withStart: dateFrom, end: dateTo, options: .strictStartDate) + + let query = HKStatisticsQuery( + quantityType: sampleType, + quantitySamplePredicate: predicate, + options: .cumulativeSum + ) { query, queryResult, error in + + guard let queryResult = queryResult else { + let error = error! as NSError + print("Error getting total steps in interval \(error.localizedDescription)") + + DispatchQueue.main.async { + result(nil) + } + return + } + + var steps = 0.0 + + if let quantity = queryResult.sumQuantity() { + let unit = HKUnit.count() + steps = quantity.doubleValue(for: unit) + } + + let totalSteps = Int(steps) + DispatchQueue.main.async { + result(totalSteps) + } + } + + HKHealthStore().execute(query) } - // Set up heart rate data types specific to the apple watch, requires iOS 12 - if #available(iOS 12.2, *) { - dataTypesDict[HIGH_HEART_RATE_EVENT] = HKSampleType.categoryType( - forIdentifier: .highHeartRateEvent)! - dataTypesDict[LOW_HEART_RATE_EVENT] = HKSampleType.categoryType( - forIdentifier: .lowHeartRateEvent)! - dataTypesDict[IRREGULAR_HEART_RATE_EVENT] = HKSampleType.categoryType( - forIdentifier: .irregularHeartRhythmEvent)! - - heartRateEventTypes = Set([ - HKSampleType.categoryType(forIdentifier: .highHeartRateEvent)!, - HKSampleType.categoryType(forIdentifier: .lowHeartRateEvent)!, - HKSampleType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!, - ]) + + func unitLookUp(key: String) -> HKUnit { + guard let unit = unitDict[key] else { + return HKUnit.count() + } + return unit } - - if #available(iOS 13.6, *) { - dataTypesDict[HEADACHE_UNSPECIFIED] = HKSampleType.categoryType(forIdentifier: .headache)! - dataTypesDict[HEADACHE_NOT_PRESENT] = HKSampleType.categoryType(forIdentifier: .headache)! - dataTypesDict[HEADACHE_MILD] = HKSampleType.categoryType(forIdentifier: .headache)! - dataTypesDict[HEADACHE_MODERATE] = HKSampleType.categoryType(forIdentifier: .headache)! - dataTypesDict[HEADACHE_SEVERE] = HKSampleType.categoryType(forIdentifier: .headache)! - - headacheType = Set([ - HKSampleType.categoryType(forIdentifier: .headache)! - ]) + + func dataTypeLookUp(key: String) -> HKSampleType { + guard let dataType_ = dataTypesDict[key] else { + return HKSampleType.quantityType(forIdentifier: .bodyMass)! + } + return dataType_ } - - if #available(iOS 14.0, *) { - dataTypesDict[ELECTROCARDIOGRAM] = HKSampleType.electrocardiogramType() - - unitDict[VOLT] = HKUnit.volt() - unitDict[INCHES_OF_MERCURY] = HKUnit.inchesOfMercury() - - workoutActivityTypeMap["CARDIO_DANCE"] = HKWorkoutActivityType.cardioDance - workoutActivityTypeMap["SOCIAL_DANCE"] = HKWorkoutActivityType.socialDance - workoutActivityTypeMap["PICKLEBALL"] = HKWorkoutActivityType.pickleball - workoutActivityTypeMap["COOLDOWN"] = HKWorkoutActivityType.cooldown + + func initializeTypes() { + // Initialize units + unitDict[GRAM] = HKUnit.gram() + unitDict[KILOGRAM] = HKUnit.gramUnit(with: .kilo) + unitDict[OUNCE] = HKUnit.ounce() + unitDict[POUND] = HKUnit.pound() + unitDict[STONE] = HKUnit.stone() + unitDict[METER] = HKUnit.meter() + unitDict[INCH] = HKUnit.inch() + unitDict[FOOT] = HKUnit.foot() + unitDict[YARD] = HKUnit.yard() + unitDict[MILE] = HKUnit.mile() + unitDict[LITER] = HKUnit.liter() + unitDict[MILLILITER] = HKUnit.literUnit(with: .milli) + unitDict[FLUID_OUNCE_US] = HKUnit.fluidOunceUS() + unitDict[FLUID_OUNCE_IMPERIAL] = HKUnit.fluidOunceImperial() + unitDict[CUP_US] = HKUnit.cupUS() + unitDict[CUP_IMPERIAL] = HKUnit.cupImperial() + unitDict[PINT_US] = HKUnit.pintUS() + unitDict[PINT_IMPERIAL] = HKUnit.pintImperial() + unitDict[PASCAL] = HKUnit.pascal() + unitDict[MILLIMETER_OF_MERCURY] = HKUnit.millimeterOfMercury() + unitDict[CENTIMETER_OF_WATER] = HKUnit.centimeterOfWater() + unitDict[ATMOSPHERE] = HKUnit.atmosphere() + unitDict[DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL] = HKUnit.decibelAWeightedSoundPressureLevel() + unitDict[SECOND] = HKUnit.second() + unitDict[MILLISECOND] = HKUnit.secondUnit(with: .milli) + unitDict[MINUTE] = HKUnit.minute() + unitDict[HOUR] = HKUnit.hour() + unitDict[DAY] = HKUnit.day() + unitDict[JOULE] = HKUnit.joule() + unitDict[KILOCALORIE] = HKUnit.kilocalorie() + unitDict[LARGE_CALORIE] = HKUnit.largeCalorie() + unitDict[SMALL_CALORIE] = HKUnit.smallCalorie() + unitDict[DEGREE_CELSIUS] = HKUnit.degreeCelsius() + unitDict[DEGREE_FAHRENHEIT] = HKUnit.degreeFahrenheit() + unitDict[KELVIN] = HKUnit.kelvin() + unitDict[DECIBEL_HEARING_LEVEL] = HKUnit.decibelHearingLevel() + unitDict[HERTZ] = HKUnit.hertz() + unitDict[SIEMEN] = HKUnit.siemen() + unitDict[INTERNATIONAL_UNIT] = HKUnit.internationalUnit() + unitDict[COUNT] = HKUnit.count() + unitDict[PERCENT] = HKUnit.percent() + unitDict[BEATS_PER_MINUTE] = HKUnit.init(from: "count/min") + unitDict[RESPIRATIONS_PER_MINUTE] = HKUnit.init(from: "count/min") + unitDict[MILLIGRAM_PER_DECILITER] = HKUnit.init(from: "mg/dL") + unitDict[UNKNOWN_UNIT] = HKUnit.init(from: "") + unitDict[NO_UNIT] = HKUnit.init(from: "") + + // Initialize workout types + workoutActivityTypeMap["ARCHERY"] = .archery + workoutActivityTypeMap["BOWLING"] = .bowling + workoutActivityTypeMap["FENCING"] = .fencing + workoutActivityTypeMap["GYMNASTICS"] = .gymnastics + workoutActivityTypeMap["TRACK_AND_FIELD"] = .trackAndField + workoutActivityTypeMap["AMERICAN_FOOTBALL"] = .americanFootball + workoutActivityTypeMap["AUSTRALIAN_FOOTBALL"] = .australianFootball + workoutActivityTypeMap["BASEBALL"] = .baseball + workoutActivityTypeMap["BASKETBALL"] = .basketball + workoutActivityTypeMap["CRICKET"] = .cricket + workoutActivityTypeMap["DISC_SPORTS"] = .discSports + workoutActivityTypeMap["HANDBALL"] = .handball + workoutActivityTypeMap["HOCKEY"] = .hockey + workoutActivityTypeMap["LACROSSE"] = .lacrosse + workoutActivityTypeMap["RUGBY"] = .rugby + workoutActivityTypeMap["SOCCER"] = .soccer + workoutActivityTypeMap["SOFTBALL"] = .softball + workoutActivityTypeMap["VOLLEYBALL"] = .volleyball + workoutActivityTypeMap["PREPARATION_AND_RECOVERY"] = .preparationAndRecovery + workoutActivityTypeMap["FLEXIBILITY"] = .flexibility + workoutActivityTypeMap["WALKING"] = .walking + workoutActivityTypeMap["RUNNING"] = .running + workoutActivityTypeMap["RUNNING_JOGGING"] = .running // Supported due to combining with Android naming + workoutActivityTypeMap["RUNNING_SAND"] = .running // Supported due to combining with Android naming + workoutActivityTypeMap["RUNNING_TREADMILL"] = .running // Supported due to combining with Android naming + workoutActivityTypeMap["WHEELCHAIR_WALK_PACE"] = .wheelchairWalkPace + workoutActivityTypeMap["WHEELCHAIR_RUN_PACE"] = .wheelchairRunPace + workoutActivityTypeMap["BIKING"] = .cycling + workoutActivityTypeMap["HAND_CYCLING"] = .handCycling + workoutActivityTypeMap["CORE_TRAINING"] = .coreTraining + workoutActivityTypeMap["ELLIPTICAL"] = .elliptical + workoutActivityTypeMap["FUNCTIONAL_STRENGTH_TRAINING"] = .functionalStrengthTraining + workoutActivityTypeMap["TRADITIONAL_STRENGTH_TRAINING"] = .traditionalStrengthTraining + workoutActivityTypeMap["CROSS_TRAINING"] = .crossTraining + workoutActivityTypeMap["MIXED_CARDIO"] = .mixedCardio + workoutActivityTypeMap["HIGH_INTENSITY_INTERVAL_TRAINING"] = .highIntensityIntervalTraining + workoutActivityTypeMap["JUMP_ROPE"] = .jumpRope + workoutActivityTypeMap["STAIR_CLIMBING"] = .stairClimbing + workoutActivityTypeMap["STAIRS"] = .stairs + workoutActivityTypeMap["STEP_TRAINING"] = .stepTraining + workoutActivityTypeMap["FITNESS_GAMING"] = .fitnessGaming + workoutActivityTypeMap["BARRE"] = .barre + workoutActivityTypeMap["YOGA"] = .yoga + workoutActivityTypeMap["MIND_AND_BODY"] = .mindAndBody + workoutActivityTypeMap["PILATES"] = .pilates + workoutActivityTypeMap["BADMINTON"] = .badminton + workoutActivityTypeMap["RACQUETBALL"] = .racquetball + workoutActivityTypeMap["SQUASH"] = .squash + workoutActivityTypeMap["TABLE_TENNIS"] = .tableTennis + workoutActivityTypeMap["TENNIS"] = .tennis + workoutActivityTypeMap["CLIMBING"] = .climbing + workoutActivityTypeMap["ROCK_CLIMBING"] = .climbing // Supported due to combining with Android naming + workoutActivityTypeMap["EQUESTRIAN_SPORTS"] = .equestrianSports + workoutActivityTypeMap["FISHING"] = .fishing + workoutActivityTypeMap["GOLF"] = .golf + workoutActivityTypeMap["HIKING"] = .hiking + workoutActivityTypeMap["HUNTING"] = .hunting + workoutActivityTypeMap["PLAY"] = .play + workoutActivityTypeMap["CROSS_COUNTRY_SKIING"] = .crossCountrySkiing + workoutActivityTypeMap["CURLING"] = .curling + workoutActivityTypeMap["DOWNHILL_SKIING"] = .downhillSkiing + workoutActivityTypeMap["SNOW_SPORTS"] = .snowSports + workoutActivityTypeMap["SNOWBOARDING"] = .snowboarding + workoutActivityTypeMap["SKATING"] = .skatingSports + workoutActivityTypeMap["SKATING_CROSS,"] = .skatingSports // Supported due to combining with Android naming + workoutActivityTypeMap["SKATING_INDOOR,"] = .skatingSports // Supported due to combining with Android naming + workoutActivityTypeMap["SKATING_INLINE,"] = .skatingSports // Supported due to combining with Android naming + workoutActivityTypeMap["PADDLE_SPORTS"] = .paddleSports + workoutActivityTypeMap["ROWING"] = .rowing + workoutActivityTypeMap["SAILING"] = .sailing + workoutActivityTypeMap["SURFING_SPORTS"] = .surfingSports + workoutActivityTypeMap["SWIMMING"] = .swimming + workoutActivityTypeMap["WATER_FITNESS"] = .waterFitness + workoutActivityTypeMap["WATER_POLO"] = .waterPolo + workoutActivityTypeMap["WATER_SPORTS"] = .waterSports + workoutActivityTypeMap["BOXING"] = .boxing + workoutActivityTypeMap["KICKBOXING"] = .kickboxing + workoutActivityTypeMap["MARTIAL_ARTS"] = .martialArts + workoutActivityTypeMap["TAI_CHI"] = .taiChi + workoutActivityTypeMap["WRESTLING"] = .wrestling + workoutActivityTypeMap["OTHER"] = .other + + // Set up iOS 13 specific types (ordinary health data types) + if #available(iOS 13.0, *) { + dataTypesDict[ACTIVE_ENERGY_BURNED] = HKSampleType.quantityType( + forIdentifier: .activeEnergyBurned)! + dataTypesDict[AUDIOGRAM] = HKSampleType.audiogramSampleType() + dataTypesDict[BASAL_ENERGY_BURNED] = HKSampleType.quantityType( + forIdentifier: .basalEnergyBurned)! + dataTypesDict[BLOOD_GLUCOSE] = HKSampleType.quantityType(forIdentifier: .bloodGlucose)! + dataTypesDict[BLOOD_OXYGEN] = HKSampleType.quantityType(forIdentifier: .oxygenSaturation)! + dataTypesDict[RESPIRATORY_RATE] = HKSampleType.quantityType(forIdentifier: .respiratoryRate)! + dataTypesDict[PERIPHERAL_PERFUSION_INDEX] = HKSampleType.quantityType( + forIdentifier: .peripheralPerfusionIndex)! + + dataTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKSampleType.quantityType( + forIdentifier: .bloodPressureDiastolic)! + dataTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKSampleType.quantityType( + forIdentifier: .bloodPressureSystolic)! + dataTypesDict[BODY_FAT_PERCENTAGE] = HKSampleType.quantityType( + forIdentifier: .bodyFatPercentage)! + dataTypesDict[BODY_MASS_INDEX] = HKSampleType.quantityType(forIdentifier: .bodyMassIndex)! + dataTypesDict[BODY_TEMPERATURE] = HKSampleType.quantityType(forIdentifier: .bodyTemperature)! + dataTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType( + forIdentifier: .dietaryCarbohydrates)! + dataTypesDict[DIETARY_ENERGY_CONSUMED] = HKSampleType.quantityType( + forIdentifier: .dietaryEnergyConsumed)! + dataTypesDict[DIETARY_FATS_CONSUMED] = HKSampleType.quantityType( + forIdentifier: .dietaryFatTotal)! + dataTypesDict[DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType( + forIdentifier: .dietaryProtein)! + dataTypesDict[ELECTRODERMAL_ACTIVITY] = HKSampleType.quantityType( + forIdentifier: .electrodermalActivity)! + dataTypesDict[FORCED_EXPIRATORY_VOLUME] = HKSampleType.quantityType( + forIdentifier: .forcedExpiratoryVolume1)! + dataTypesDict[HEART_RATE] = HKSampleType.quantityType(forIdentifier: .heartRate)! + dataTypesDict[HEART_RATE_VARIABILITY_SDNN] = HKSampleType.quantityType( + forIdentifier: .heartRateVariabilitySDNN)! + dataTypesDict[HEIGHT] = HKSampleType.quantityType(forIdentifier: .height)! + dataTypesDict[RESTING_HEART_RATE] = HKSampleType.quantityType( + forIdentifier: .restingHeartRate)! + dataTypesDict[STEPS] = HKSampleType.quantityType(forIdentifier: .stepCount)! + dataTypesDict[WAIST_CIRCUMFERENCE] = HKSampleType.quantityType( + forIdentifier: .waistCircumference)! + dataTypesDict[WALKING_HEART_RATE] = HKSampleType.quantityType( + forIdentifier: .walkingHeartRateAverage)! + dataTypesDict[WEIGHT] = HKSampleType.quantityType(forIdentifier: .bodyMass)! + dataTypesDict[DISTANCE_WALKING_RUNNING] = HKSampleType.quantityType( + forIdentifier: .distanceWalkingRunning)! + dataTypesDict[FLIGHTS_CLIMBED] = HKSampleType.quantityType(forIdentifier: .flightsClimbed)! + dataTypesDict[WATER] = HKSampleType.quantityType(forIdentifier: .dietaryWater)! + dataTypesDict[MINDFULNESS] = HKSampleType.categoryType(forIdentifier: .mindfulSession)! + dataTypesDict[SLEEP_IN_BED] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + dataTypesDict[SLEEP_ASLEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + dataTypesDict[SLEEP_AWAKE] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + dataTypesDict[SLEEP_DEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + dataTypesDict[SLEEP_REM] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + + dataTypesDict[EXERCISE_TIME] = HKSampleType.quantityType(forIdentifier: .appleExerciseTime)! + dataTypesDict[WORKOUT] = HKSampleType.workoutType() + dataTypesDict[NUTRITION] = HKSampleType.correlationType( + forIdentifier: .food)! + + healthDataTypes = Array(dataTypesDict.values) + } + // Set up heart rate data types specific to the apple watch, requires iOS 12 + if #available(iOS 12.2, *) { + dataTypesDict[HIGH_HEART_RATE_EVENT] = HKSampleType.categoryType( + forIdentifier: .highHeartRateEvent)! + dataTypesDict[LOW_HEART_RATE_EVENT] = HKSampleType.categoryType( + forIdentifier: .lowHeartRateEvent)! + dataTypesDict[IRREGULAR_HEART_RATE_EVENT] = HKSampleType.categoryType( + forIdentifier: .irregularHeartRhythmEvent)! + + heartRateEventTypes = Set([ + HKSampleType.categoryType(forIdentifier: .highHeartRateEvent)!, + HKSampleType.categoryType(forIdentifier: .lowHeartRateEvent)!, + HKSampleType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!, + ]) + } + + if #available(iOS 13.6, *) { + dataTypesDict[HEADACHE_UNSPECIFIED] = HKSampleType.categoryType(forIdentifier: .headache)! + dataTypesDict[HEADACHE_NOT_PRESENT] = HKSampleType.categoryType(forIdentifier: .headache)! + dataTypesDict[HEADACHE_MILD] = HKSampleType.categoryType(forIdentifier: .headache)! + dataTypesDict[HEADACHE_MODERATE] = HKSampleType.categoryType(forIdentifier: .headache)! + dataTypesDict[HEADACHE_SEVERE] = HKSampleType.categoryType(forIdentifier: .headache)! + + headacheType = Set([ + HKSampleType.categoryType(forIdentifier: .headache)! + ]) + } + + if #available(iOS 14.0, *) { + dataTypesDict[ELECTROCARDIOGRAM] = HKSampleType.electrocardiogramType() + + unitDict[VOLT] = HKUnit.volt() + unitDict[INCHES_OF_MERCURY] = HKUnit.inchesOfMercury() + + workoutActivityTypeMap["CARDIO_DANCE"] = HKWorkoutActivityType.cardioDance + workoutActivityTypeMap["SOCIAL_DANCE"] = HKWorkoutActivityType.socialDance + workoutActivityTypeMap["PICKLEBALL"] = HKWorkoutActivityType.pickleball + workoutActivityTypeMap["COOLDOWN"] = HKWorkoutActivityType.cooldown + } + + // Concatenate heart events, headache and health data types (both may be empty) + allDataTypes = Set(heartRateEventTypes + healthDataTypes) + allDataTypes = allDataTypes.union(headacheType) } - - // Concatenate heart events, headache and health data types (both may be empty) - allDataTypes = Set(heartRateEventTypes + healthDataTypes) - allDataTypes = allDataTypes.union(headacheType) - } } diff --git a/packages/health/lib/src/data_types.dart b/packages/health/lib/src/data_types.dart index 0b7951d93..bca127cd0 100644 --- a/packages/health/lib/src/data_types.dart +++ b/packages/health/lib/src/data_types.dart @@ -48,6 +48,7 @@ enum HealthDataType { HEADACHE_MODERATE, HEADACHE_SEVERE, HEADACHE_UNSPECIFIED, + NUTRITION, // Heart Rate events (specific to Apple Watch) HIGH_HEART_RATE_EVENT, @@ -112,6 +113,7 @@ const List _dataTypeKeysIOS = [ HealthDataType.HEADACHE_SEVERE, HealthDataType.HEADACHE_UNSPECIFIED, HealthDataType.ELECTROCARDIOGRAM, + HealthDataType.NUTRITION, ]; /// List of data types available on Android @@ -143,6 +145,7 @@ const List _dataTypeKeysAndroid = [ HealthDataType.FLIGHTS_CLIMBED, HealthDataType.BASAL_ENERGY_BURNED, HealthDataType.RESPIRATORY_RATE, + HealthDataType.NUTRITION, ]; /// Maps a [HealthDataType] to a [HealthDataUnit]. @@ -203,6 +206,8 @@ const Map _dataTypeToUnit = { HealthDataType.IRREGULAR_HEART_RATE_EVENT: HealthDataUnit.NO_UNIT, HealthDataType.HEART_RATE_VARIABILITY_SDNN: HealthDataUnit.MILLISECOND, HealthDataType.ELECTROCARDIOGRAM: HealthDataUnit.VOLT, + + HealthDataType.NUTRITION: HealthDataUnit.NO_UNIT, }; const PlatformTypeJsonValue = { @@ -450,6 +455,14 @@ enum HealthWorkoutActivityType { OTHER, } +enum MealType { + BREAKFAST, + LUNCH, + DINNER, + SNACK, + UNKNOWN, +} + /// Classifications for ECG readings. enum ElectrocardiogramClassification { NOT_SET, diff --git a/packages/health/lib/src/health_factory.dart b/packages/health/lib/src/health_factory.dart index d0ef1023d..fdeeebbad 100644 --- a/packages/health/lib/src/health_factory.dart +++ b/packages/health/lib/src/health_factory.dart @@ -362,6 +362,46 @@ class HealthFactory { return success ?? false; } + /// Saves meal record into Apple Health or Google Fit. + /// + /// Returns true if successful, false otherwise. + /// + /// Parameters: + /// * [startTime] - the start time when the meal was consumed. + /// + It must be equal to or earlier than [endTime]. + /// * [endTime] - the end time when the meal was consumed. + /// + It must be equal to or later than [startTime]. + /// * [caloriesConsumed] - total calories consumed with this meal. + /// * [carbohydrates] - optional carbohydrates information. + /// * [protein] - optional protein information. + /// * [fatTotal] - optional total fat information. + /// * [name] - optional name information about this meal. + Future writeMeal( + DateTime startTime, + DateTime endTime, + double? caloriesConsumed, + double? carbohydrates, + double? protein, + double? fatTotal, + String? name, + MealType mealType) async { + if (startTime.isAfter(endTime)) + throw ArgumentError("startTime must be equal or earlier than endTime"); + + Map args = { + 'startTime': startTime.millisecondsSinceEpoch, + 'endTime': endTime.millisecondsSinceEpoch, + 'caloriesConsumed': caloriesConsumed, + 'carbohydrates': carbohydrates, + 'protein': protein, + 'fatTotal': fatTotal, + 'name': name, + 'mealType': mealType.name, + }; + bool? success = await _channel.invokeMethod('writeMeal', args); + return success ?? false; + } + /// Saves audiogram into Apple Health. /// /// Returns true if successful, false otherwise. @@ -493,6 +533,8 @@ class HealthFactory { value = WorkoutHealthValue.fromJson(e); } else if (dataType == HealthDataType.ELECTROCARDIOGRAM) { value = ElectrocardiogramHealthValue.fromJson(e); + } else if (dataType == HealthDataType.NUTRITION) { + value = NutritionHealthValue.fromJson(e); } else { value = NumericHealthValue(e['value']); } diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart index a61d5c7d8..ad2adef85 100644 --- a/packages/health/lib/src/health_value_types.dart +++ b/packages/health/lib/src/health_value_types.dart @@ -283,6 +283,89 @@ class ElectrocardiogramVoltageValue extends HealthValue { String toString() => voltage.toString(); } +/// A [HealthValue] object for nutrition +/// Parameters: +/// * [protein] - the amount of protein in grams +/// * [calories] - the amount of calories in kcal +/// * [fat] - the amount of fat in grams +/// * [name] - the name of the food +/// * [carbs] - the amount of carbs in grams +/// * [mealType] - the type of meal +class NutritionHealthValue extends HealthValue { + double? _protein; + double? _calories; + double? _fat; + String? _name; + double? _carbs; + String _mealType; + + NutritionHealthValue(this._protein, this._calories, this._fat, this._name, + this._carbs, this._mealType); + + /// The amount of protein in grams. + double? get protein => _protein; + + /// The amount of calories in kcal. + double? get calories => _calories; + + /// The amount of fat in grams. + double? get fat => _fat; + + /// The name of the food. + String? get name => _name; + + /// The amount of carbs in grams. + double? get carbs => _carbs; + + /// The type of meal. + String get mealType => _mealType; + + factory NutritionHealthValue.fromJson(json) { + return NutritionHealthValue( + json['protein'] != null ? (json['protein'] as num).toDouble() : null, + json['calories'] != null ? (json['calories'] as num).toDouble() : null, + json['fat'] != null ? (json['fat'] as num).toDouble() : null, + json['name'] != null ? (json['name'] as String) : null, + json['carbs'] != null ? (json['carbs'] as num).toDouble() : null, + json['mealType'] as String, + ); + } + + @override + Map toJson() => { + 'protein': _protein, + 'calories': _calories, + 'fat': _fat, + 'name': _name, + 'carbs': _carbs, + 'mealType': _mealType, + }; + + @override + String toString() { + return """protein: ${protein.toString()}, + calories: ${calories.toString()}, + fat: ${fat.toString()}, + name: ${name.toString()}, + carbs: ${carbs.toString()}, + mealType: $mealType"""; + } + + @override + bool operator ==(Object o) { + return o is NutritionHealthValue && + o.protein == this.protein && + o.calories == this.calories && + o.fat == this.fat && + o.name == this.name && + o.carbs == this.carbs && + o.mealType == this.mealType; + } + + @override + int get hashCode => Object.hash(protein, calories, fat, name, carbs); +} + /// An abstract class for health values. abstract class HealthValue { Map toJson();