Skip to content

Commit

Permalink
Added "aggregate" function to get statistics about metrics.
Browse files Browse the repository at this point in the history
  • Loading branch information
quentinleguennec committed Sep 15, 2023
1 parent d35ff4c commit 3b5229f
Show file tree
Hide file tree
Showing 29 changed files with 589 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## [2.1.0]
* Added `aggregate` function to get statistics
* Updated README.md
## [2.0.0]
* Added missing data types
* Added Models for each data type
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Flutter plugin for Google Health Connect integration. Health Connect gives you a
## How to install

### Android
Add the following line to the end of your "android/gradle.properties" file:
```android.jetifier.ignorelist = jackson-core, jackson-databind, jackson-datatype-jsr310, fastdoubleparser```

To interact with Health Connect within the app, declare the Health Connect package name in your `AndroidManifest.xml` file:
```
<!-- Check whether Health Connect is installed or not -->
Expand Down
159 changes: 159 additions & 0 deletions android/src/main/kotlin/dev/duynp/flutter_health_connect/Consts.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.duynp.flutter_health_connect

import androidx.health.connect.client.aggregate.AggregateMetric
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.*

Expand Down Expand Up @@ -106,5 +107,163 @@ fun mapTypesToPermissions(
return permissions
}

// Used by the "aggregate" function.
// List of all possible records: https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/package-summary
val HealthConnectAggregateMetricTypeMap = hashMapOf<String, AggregateMetric<*>>(
// ActiveCaloriesBurnedRecord
"ActiveCaloriesBurnedRecordActiveCaloriesTotal" to ActiveCaloriesBurnedRecord.ACTIVE_CALORIES_TOTAL,

// BasalBodyTemperatureRecord: No AggregateMetric

// BasalMetabolicRateRecord:
"BasalMetabolicRateRecordBasalCaloriesTotal" to BasalMetabolicRateRecord.BASAL_CALORIES_TOTAL,

// BloodGlucoseRecord: No AggregateMetric

// BloodPressureRecord
"BloodPressureRecordSystolicAvg" to BloodPressureRecord.SYSTOLIC_AVG,
"BloodPressureRecordSystolicMin" to BloodPressureRecord.SYSTOLIC_MIN,
"BloodPressureRecordSystolicMax" to BloodPressureRecord.SYSTOLIC_MAX,
"BloodPressureRecordDiastolicAvg" to BloodPressureRecord.DIASTOLIC_AVG,
"BloodPressureRecordDiastolicMin" to BloodPressureRecord.DIASTOLIC_MIN,
"BloodPressureRecordDiastolicMax" to BloodPressureRecord.DIASTOLIC_MAX,

// BodyFatRecord: No AggregateMetric

// BodyTemperatureRecord: No AggregateMetric

// BodyWaterMassRecord: No AggregateMetric

// BoneMassRecord: No AggregateMetric

// CervicalMucusRecord: No AggregateMetric

// CyclingPedalingCadenceRecord
"CyclingPedalingCadenceRecordRpmAvg" to CyclingPedalingCadenceRecord.RPM_AVG,
"CyclingPedalingCadenceRecordRpmMin" to CyclingPedalingCadenceRecord.RPM_MIN,
"CyclingPedalingCadenceRecordRpmMax" to CyclingPedalingCadenceRecord.RPM_MAX,

// DistanceRecord
"DistanceRecordDistanceTotal" to DistanceRecord.DISTANCE_TOTAL,

// ElevationGainedRecord
"ElevationGainedRecordElevationGainedTotal" to ElevationGainedRecord.ELEVATION_GAINED_TOTAL,

// ExerciseSessionRecord
"ExerciseSessionRecordExerciseDurationTotal" to ExerciseSessionRecord.EXERCISE_DURATION_TOTAL,

// FloorsClimbedRecord
"FloorsClimbedRecordFloorsClimbedTotal" to FloorsClimbedRecord.FLOORS_CLIMBED_TOTAL,

// HeartRateRecord
"HeartRateRecordBpmAvg" to HeartRateRecord.BPM_AVG,
"HeartRateRecordBpmMin" to HeartRateRecord.BPM_MIN,
"HeartRateRecordBpmMax" to HeartRateRecord.BPM_MAX,
"HeartRateRecordMeasurementsCount" to HeartRateRecord.MEASUREMENTS_COUNT,

// HeartRateVariabilityRmssdRecord: No AggregateMetric

"HeightRecordHeightAvg" to HeightRecord.HEIGHT_AVG,
"HeightRecordHeightMin" to HeightRecord.HEIGHT_MIN,
"HeightRecordHeightMax" to HeightRecord.HEIGHT_MAX,
"HydrationRecordVolumeTotal" to HydrationRecord.VOLUME_TOTAL,

// IntermenstrualBleedingRecord: No AggregateMetric

// LeanBodyMassRecord: No AggregateMetric

// MenstruationFlowRecord: No AggregateMetric

// MenstruationPeriodRecord: No AggregateMetric

// NutritionRecord
"NutritionRecordBiotinTotal" to NutritionRecord.BIOTIN_TOTAL,
"NutritionRecordCaffeineTotal" to NutritionRecord.CAFFEINE_TOTAL,
"NutritionRecordCalciumTotal" to NutritionRecord.CALCIUM_TOTAL,
"NutritionRecordEnergyTotal" to NutritionRecord.ENERGY_TOTAL,
"NutritionRecordEnergyFromFatTotal" to NutritionRecord.ENERGY_FROM_FAT_TOTAL,
"NutritionRecordChlorideTotal" to NutritionRecord.CHLORIDE_TOTAL,
"NutritionRecordCholesterolTotal" to NutritionRecord.CHOLESTEROL_TOTAL,
"NutritionRecordChromiumTotal" to NutritionRecord.CHROMIUM_TOTAL,
"NutritionRecordCopperTotal" to NutritionRecord.COPPER_TOTAL,
"NutritionRecordDietaryFiberTotal" to NutritionRecord.DIETARY_FIBER_TOTAL,
"NutritionRecordFolateTotal" to NutritionRecord.FOLATE_TOTAL,
"NutritionRecordFolicAcidTotal" to NutritionRecord.FOLIC_ACID_TOTAL,
"NutritionRecordIodineTotal" to NutritionRecord.IODINE_TOTAL,
"NutritionRecordIronTotal" to NutritionRecord.IRON_TOTAL,
"NutritionRecordMagnesiumTotal" to NutritionRecord.MAGNESIUM_TOTAL,
"NutritionRecordManganeseTotal" to NutritionRecord.MANGANESE_TOTAL,
"NutritionRecordMolybdenumTotal" to NutritionRecord.MOLYBDENUM_TOTAL,
"NutritionRecordMonounsaturatedFatTotal" to NutritionRecord.MONOUNSATURATED_FAT_TOTAL,
"NutritionRecordNiacinTotal" to NutritionRecord.NIACIN_TOTAL,
"NutritionRecordPantothenicAcidTotal" to NutritionRecord.PANTOTHENIC_ACID_TOTAL,
"NutritionRecordPhosphorusTotal" to NutritionRecord.PHOSPHORUS_TOTAL,
"NutritionRecordPolyunsaturatedFatTotal" to NutritionRecord.POLYUNSATURATED_FAT_TOTAL,
"NutritionRecordPotassiumTotal" to NutritionRecord.POTASSIUM_TOTAL,
"NutritionRecordProteinTotal" to NutritionRecord.PROTEIN_TOTAL,
"NutritionRecordRiboflavinTotal" to NutritionRecord.RIBOFLAVIN_TOTAL,
"NutritionRecordSaturatedFatTotal" to NutritionRecord.SATURATED_FAT_TOTAL,
"NutritionRecordSeleniumTotal" to NutritionRecord.SELENIUM_TOTAL,
"NutritionRecordSodiumTotal" to NutritionRecord.SODIUM_TOTAL,
"NutritionRecordSugarTotal" to NutritionRecord.SUGAR_TOTAL,
"NutritionRecordThiaminTotal" to NutritionRecord.THIAMIN_TOTAL,
"NutritionRecordTotalCarbohydrateTotal" to NutritionRecord.TOTAL_CARBOHYDRATE_TOTAL,
"NutritionRecordTotalFatTotal" to NutritionRecord.TOTAL_FAT_TOTAL,
"NutritionRecordTransFatTotal" to NutritionRecord.TRANS_FAT_TOTAL,
"NutritionRecordUnsaturatedFatTotal" to NutritionRecord.UNSATURATED_FAT_TOTAL,
"NutritionRecordVitaminATotal" to NutritionRecord.VITAMIN_A_TOTAL,
"NutritionRecordVitaminB12Total" to NutritionRecord.VITAMIN_B12_TOTAL,
"NutritionRecordVitaminB6Total" to NutritionRecord.VITAMIN_B6_TOTAL,
"NutritionRecordVitaminCTotal" to NutritionRecord.VITAMIN_C_TOTAL,
"NutritionRecordVitaminDTotal" to NutritionRecord.VITAMIN_D_TOTAL,
"NutritionRecordVitaminETotal" to NutritionRecord.VITAMIN_E_TOTAL,
"NutritionRecordVitaminKTotal" to NutritionRecord.VITAMIN_K_TOTAL,
"NutritionRecordZincTotal" to NutritionRecord.ZINC_TOTAL,

// OvulationTestRecord: No AggregateMetric

// OxygenSaturationRecord: No AggregateMetric

// PowerRecord
"PowerRecordPowerAvg" to PowerRecord.POWER_AVG,
"PowerRecordPowerMin" to PowerRecord.POWER_MIN,
"PowerRecordPowerMax" to PowerRecord.POWER_MAX,

// RespiratoryRateRecord: No AggregateMetric

// RestingHeartRate
"RestingHeartRateRecordBpmAvg" to RestingHeartRateRecord.BPM_AVG,
"RestingHeartRateRecordBpmMin" to RestingHeartRateRecord.BPM_MIN,
"RestingHeartRateRecordBpmMax" to RestingHeartRateRecord.BPM_MAX,

// SexualActivityRecord: No AggregateMetric

// SleepSessionRecord
"SleepSessionRecordSleepDurationTotal" to SleepSessionRecord.SLEEP_DURATION_TOTAL,

// SpeedRecord
"SpeedRecordSpeedAvg" to SpeedRecord.SPEED_AVG,
"SpeedRecordSpeedMin" to SpeedRecord.SPEED_MIN,
"SpeedRecordSpeedMax" to SpeedRecord.SPEED_MAX,

// StepsCadenceRecord
"StepsCadenceRecordRateAvg" to StepsCadenceRecord.RATE_AVG,
"StepsCadenceRecordRateMin" to StepsCadenceRecord.RATE_MIN,
"StepsCadenceRecordRateMax" to StepsCadenceRecord.RATE_MAX,

// StepsRecord
"StepsRecordCountTotal" to StepsRecord.COUNT_TOTAL,

// TotalCaloriesBurnedRecord
"TotalCaloriesBurnedRecordEnergyTotal" to TotalCaloriesBurnedRecord.ENERGY_TOTAL,

// Vo2MaxRecord: No AggregateMetric

// WeightRecord
"WeightRecordWeightAvg" to WeightRecord.WEIGHT_AVG,
"WeightRecordWeightMin" to WeightRecord.WEIGHT_MIN,
"WeightRecordWeightMax" to WeightRecord.WEIGHT_MAX,

// WheelchairPushesRecord
"WheelchairPushesRecordCountTotal" to WheelchairPushesRecord.COUNT_TOTAL,
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.health.connect.client.units.*
import androidx.health.connect.client.records.metadata.DataOrigin
import androidx.health.connect.client.records.metadata.Device
import androidx.health.connect.client.records.metadata.Metadata
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.time.TimeRangeFilter
import androidx.health.connect.client.units.Energy
import com.fasterxml.jackson.databind.ObjectMapper
Expand Down Expand Up @@ -318,6 +319,8 @@ class FlutterHealthConnectPlugin(private var channel: MethodChannel? = null) : F
}
}

"aggregate" -> aggregate(call, result)

else -> {
result.notImplemented()
}
Expand Down Expand Up @@ -709,4 +712,49 @@ class FlutterHealthConnectPlugin(private var channel: MethodChannel? = null) : F
}
}
}

private fun aggregate(call: MethodCall, result: Result) {
scope.launch {
try {
val aggregationKeys =
(call.argument<ArrayList<*>>("aggregationKeys")?.filterIsInstance<String>() as ArrayList<String>?)?.toList()

if(aggregationKeys.isNullOrEmpty()) {
result.success(LinkedHashMap<String, Any?>())
} else {
val startTime = call.argument<String>("startTime")
val endTime = call.argument<String>("endTime")
val start = startTime?.let { Instant.parse(it) } ?: Instant.now()
.minus(1, ChronoUnit.DAYS)
val end = endTime?.let { Instant.parse(it) } ?: Instant.now()
val metrics =
aggregationKeys.mapNotNull { HealthConnectAggregateMetricTypeMap[it] }

val response =
client.aggregate(
AggregateRequest(
metrics.toSet(),
timeRangeFilter = TimeRangeFilter.between(start, end)
)
)

val resultData = aggregationKeys.associateBy(
{it},
{
replyMapper.convertValue(
response[HealthConnectAggregateMetricTypeMap[it]!!],
Double::class.java
)
}
)
result.success(resultData)
}
} catch (e: Exception) {
result.error("AGGREGATE_FAIL", e.localizedMessage, e)
}
}

}


}
22 changes: 22 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,28 @@ class _MyAppState extends State<MyApp> {
},
child: const Text('Send Record'),
),
ElevatedButton(
onPressed: () async {
var startTime =
DateTime.now().subtract(const Duration(days: 1));
var endTime = DateTime.now();
try {
var result = await HealthConnectFactory.aggregate(
aggregationKeys: [
StepsRecord.aggregationKeyCountTotal,
ExerciseSessionRecord.aggregationKeyExerciseDurationTotal,
],
startTime: startTime,
endTime: endTime,
);
resultText = '$result';
} catch (e, s) {
resultText = '$e:$s'.toString();
}
_updateResultText();
},
child: const Text('Get aggregated data'),
),
Text(resultText),
],
),
Expand Down
1 change: 0 additions & 1 deletion lib/flutter_health_connect.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
library flutter_health_connect;

import 'dart:async';
import 'dart:convert';

import 'package:flutter/services.dart';
import 'package:flutter_health_connect/src/records.dart';
Expand Down
50 changes: 50 additions & 0 deletions lib/src/flutter_health_connect_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,54 @@ class HealthConnectFactory {
};
return await _channel.invokeMethod('deleteRecordsByTime', args);
}

/// Get statistics by aggregating data.
/// This can, for example, give you the total steps count over the last 7 days, or the average heart rate over the last month.
///
/// You need the corresponding permission to get statistic for a given type. See [requestPermissions].
///
/// [aggregationKeys] is a list of all the metrics you want to get statistics about. These keys can be found in
/// their corresponding records, like [StepsRecord.aggregationKeyCountTotal].
///
/// This function returns a map with the [aggregationKeys] as keys and the associated results as values. All values are
/// doubles, look at the aggregationKey description to read more about the units.
///
/// This function calls the "aggregate" function of the Health Connect SDK on Android. See:
/// https://developer.android.com/health-and-fitness/guides/health-connect/common-workflows/aggregate-data
/// NOTE: This does not support Bucket aggregation, only Basic aggregation.
///
/// Example:
/// var result = await HealthConnectFactory.aggregate(
/// aggregationKeys: [
/// StepsRecord.aggregationKeyCountTotal,
/// ExerciseSessionRecord.aggregationKeyExerciseDurationTotal,
/// ],
/// startTime: DateTime.now().subtract(const Duration(days: 1)),
/// endTime: DateTime.now(),
/// );
///
/// // Statics over the last 24 hours:
/// var stepsCountTotal = result[StepsRecord.aggregationKeyCountTotal];
/// var exerciseDurationTotal = result[ExerciseSessionRecord.aggregationKeyExerciseDurationTotal];
///
static Future<Map<String, double>> aggregate({
required List<String> aggregationKeys,
required DateTime startTime,
required DateTime endTime,
}) async {
if (aggregationKeys.isEmpty) {
return {};
}
final start = startTime.toUtc().toIso8601String();
final end = endTime.toUtc().toIso8601String();
final args = <String, dynamic>{
'aggregationKeys': aggregationKeys,
'startTime': start,
'endTime': end,
};

return await _channel
.invokeMethod('aggregate', args)
.then((value) => Map<String, double>.from(value));
}
}
4 changes: 4 additions & 0 deletions lib/src/records/active_calories_burned_record.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import 'package:flutter_health_connect/src/units/energy.dart';
import 'interval_record.dart';

class ActiveCaloriesBurnedRecord extends IntervalRecord {
/// Unit: kilocalories
static const aggregationKeyActiveCaloriesTotal =
'ActiveCaloriesBurnedRecordActiveCaloriesTotal';

Energy energy;
@override
DateTime endTime;
Expand Down
4 changes: 4 additions & 0 deletions lib/src/records/basal_metabolic_rate_record.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import 'package:flutter_health_connect/src/units/power.dart';
import 'instantaneous_record.dart';

class BasalMetabolicRateRecord extends InstantaneousRecord {
/// Unit: kilocalories
static const String aggregationKeyBasalCaloriesTotal =
'BasalMetabolicRateRecordBasalCaloriesTotal';

final Power basalMetabolicRate;
@override
Metadata metadata;
Expand Down
24 changes: 24 additions & 0 deletions lib/src/records/blood_pressure_record.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@ import 'package:flutter_health_connect/src/records/metadata/metadata.dart';
import 'package:flutter_health_connect/src/units/pressure.dart';

class BloodPressureRecord extends InstantaneousRecord {
/// Unit: millimeters of mercury
static const String aggregationKeySystolicAvg =
'BloodPressureRecordSystolicAvg';

/// Unit: millimeters of mercury
static const String aggregationKeySystolicMin =
'BloodPressureRecordSystolicMin';

/// Unit: millimeters of mercury
static const String aggregationKeySystolicMax =
'BloodPressureRecordSystolicMax';

/// Unit: millimeters of mercury
static const String aggregationKeyDiastolicAvg =
'BloodPressureRecordDiastolicAvg';

/// Unit: millimeters of mercury
static const String aggregationKeyDiastolicMin =
'BloodPressureRecordDiastolicMin';

/// Unit: millimeters of mercury
static const String aggregationKeyDiastolicMax =
'BloodPressureRecordDiastolicMax';

@override
Metadata metadata;
@override
Expand Down
Loading

0 comments on commit 3b5229f

Please sign in to comment.