Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,4 @@ app.*.symbols

.sdkmanrc
**/.cxx/
.zed/settings.json
19 changes: 19 additions & 0 deletions .zed/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"lsp": {
"dart": {
"binary": {
"path": "/opt/homebrew/bin/fvm",
"arguments": ["dart", "language-server", "--protocol=lsp"]
}
}
},
"auto_install_extensions": {
"dart": true
},
"debugger": {
"stepping_granularity": "line",
"save_breakpoints": true,
"show_button": true,
"timeout": 3000
}
}
6 changes: 6 additions & 0 deletions packages/health/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 13.1.1

* Fix [#1207](https://github.com/cph-cachet/flutter-plugins/issues/1207) - (**Important**: Some property names might have changed compared to before for `Nutrition`)
* Fix [#1201](https://github.com/cph-cachet/flutter-plugins/issues/1201)
* iOS: Add `APPLE_STAND_TIME`, `APPLE_STAND_HOUR`, and `APPLE_MOVE_TIME` health data types (READ ONLY) [#1190](https://github.com/cph-cachet/flutter-plugins/issues/1190)

## 13.1.0

* Refactored Android native implementation (No Flutter API changes)
Expand Down
3 changes: 3 additions & 0 deletions packages/health/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,9 @@ The plugin supports the following [`HealthDataType`](https://pub.dev/documentati
| UV_INDEX | COUNT | yes | | |
| LEAN_BODY_MASS | KILOGRAMS | yes | yes | |
| WALKING_SPEED | METER_PER_SECOND | yes | (yes) | On Android this will be recorded as `SPEED` with similar unit |
| APPLE_MOVE_TIME | SECOND | yes | | READ Only |
| APPLE_STAND_HOUR | HOUR | yes | | READ Only |
| APPLE_MOVE_TIME | SECOND | yes | | READ Only |

## Workout Types

Expand Down
106 changes: 75 additions & 31 deletions packages/health/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class HealthAppState extends State<HealthApp> {
.map((type) =>
// can only request READ permissions to the following list of types on iOS
[
HealthDataType.APPLE_MOVE_TIME,
HealthDataType.APPLE_STAND_HOUR,
HealthDataType.APPLE_STAND_TIME,
HealthDataType.WALKING_HEART_RATE,
HealthDataType.ELECTROCARDIOGRAM,
HealthDataType.HIGH_HEART_RATE_EVENT,
Expand Down Expand Up @@ -245,19 +248,6 @@ class HealthAppState extends State<HealthApp> {
type: HealthDataType.HEART_RATE,
startTime: earlier,
endTime: now);
if (Platform.isIOS) {
success &= await health.writeHealthData(
value: 30,
type: HealthDataType.HEART_RATE_VARIABILITY_SDNN,
startTime: earlier,
endTime: now);
} else {
success &= await health.writeHealthData(
value: 30,
type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD,
startTime: earlier,
endTime: now);
}
success &= await health.writeHealthData(
value: 37,
type: HealthDataType.BODY_TEMPERATURE,
Expand All @@ -268,6 +258,7 @@ class HealthAppState extends State<HealthApp> {
type: HealthDataType.BLOOD_GLUCOSE,
startTime: earlier,
endTime: now);
success &= await health.writeInsulinDelivery(5, InsulinDeliveryReason.BOLUS, earlier, now);
success &= await health.writeHealthData(
value: 1.8,
type: HealthDataType.WATER,
Expand Down Expand Up @@ -301,22 +292,6 @@ class HealthAppState extends State<HealthApp> {
startTime: earlier,
endTime: now);

if (Platform.isIOS) {
success &= await health.writeHealthData(
value: 1.5, // 1.5 m/s (typical walking speed)
type: HealthDataType.WALKING_SPEED,
startTime: earlier,
endTime: now,
recordingMethod: RecordingMethod.manual);
} else {
success &= await health.writeHealthData(
value: 2.0, // 2.0 m/s (typical jogging speed)
type: HealthDataType.SPEED,
startTime: earlier,
endTime: now,
recordingMethod: RecordingMethod.manual);
}

// specialized write methods
success &= await health.writeBloodOxygen(
saturation: 98,
Expand Down Expand Up @@ -407,7 +382,34 @@ class HealthAppState extends State<HealthApp> {
endTime: now,
);

// Available on iOS 16.0+ only

if (Platform.isIOS) {
success &= await health.writeHealthData(
value: 30,
type: HealthDataType.HEART_RATE_VARIABILITY_SDNN,
startTime: earlier,
endTime: now);
success &= await health.writeHealthData(
value: 1.5, // 1.5 m/s (typical walking speed)
type: HealthDataType.WALKING_SPEED,
startTime: earlier,
endTime: now,
recordingMethod: RecordingMethod.manual);
} else {
success &= await health.writeHealthData(
value: 2.0, // 2.0 m/s (typical jogging speed)
type: HealthDataType.SPEED,
startTime: earlier,
endTime: now,
recordingMethod: RecordingMethod.manual);
success &= await health.writeHealthData(
value: 30,
type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD,
startTime: earlier,
endTime: now);
}

// Available on iOS or iOS 16.0+ only
if (Platform.isIOS) {
success &= await health.writeHealthData(
value: 22,
Expand Down Expand Up @@ -530,6 +532,41 @@ class HealthAppState extends State<HealthApp> {
});
}

Future<void> getIntervalBasedData() async {
final startDate = DateTime.now().subtract(const Duration(days: 7));
final endDate = DateTime.now();

List<HealthDataPoint> healthDataResponse =
await health.getHealthIntervalDataFromTypes(
startDate: startDate,
endDate: endDate,
types: [HealthDataType.BLOOD_OXYGEN, HealthDataType.STEPS],
interval: 86400, // 86400 seconds = 1 day
// recordingMethodsToFilter: recordingMethodsToFilter,
);
debugPrint(
'Total number of interval data points: ${healthDataResponse.length}. '
'${healthDataResponse.length > 100 ? 'Only showing the first 100.' : ''}');

debugPrint("Interval data points: ");
for (var data in healthDataResponse) {
debugPrint(toJsonString(data));
}
healthDataResponse.sort((a, b) => b.dateTo.compareTo(a.dateTo));

_healthDataList.clear();
_healthDataList.addAll(
(healthDataResponse.length < 100) ? healthDataResponse : healthDataResponse.sublist(0, 100));

for (var data in _healthDataList) {
debugPrint(toJsonString(data));
}

setState(() {
_state = _healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY;
});
}

// UI building below

@override
Expand Down Expand Up @@ -607,6 +644,13 @@ class HealthAppState extends State<HealthApp> {
WidgetStatePropertyAll(Colors.blue)),
child: const Text("Revoke Access",
style: TextStyle(color: Colors.white))),
TextButton(
onPressed: getIntervalBasedData,
style: const ButtonStyle(
backgroundColor:
WidgetStatePropertyAll(Colors.blue)),
child: const Text('Get Interval Data (7 days)',
style: TextStyle(color: Colors.white))),
]),
],
),
Expand Down Expand Up @@ -756,7 +800,7 @@ class HealthAppState extends State<HealthApp> {
if (p.value is NutritionHealthValue) {
return ListTile(
title: Text(
"${p.typeString} ${(p.value as NutritionHealthValue).meal_type}: ${(p.value as NutritionHealthValue).name}"),
"${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}\n${p.recordingMethod}'),
Expand Down
4 changes: 4 additions & 0 deletions packages/health/example/lib/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import 'package:health/health.dart';
/// Data types available on iOS via Apple Health.
const List<HealthDataType> dataTypesIOS = [
HealthDataType.ACTIVE_ENERGY_BURNED,
HealthDataType.APPLE_STAND_TIME,
HealthDataType.APPLE_STAND_HOUR,
HealthDataType.APPLE_MOVE_TIME,
HealthDataType.AUDIOGRAM,
HealthDataType.BASAL_ENERGY_BURNED,
HealthDataType.BLOOD_GLUCOSE,
Expand All @@ -22,6 +25,7 @@ const List<HealthDataType> dataTypesIOS = [
HealthDataType.HEART_RATE,
HealthDataType.HEART_RATE_VARIABILITY_SDNN,
HealthDataType.HEIGHT,
HealthDataType.INSULIN_DELIVERY,
HealthDataType.RESPIRATORY_RATE,
HealthDataType.PERIPHERAL_PERFUSION_INDEX,
HealthDataType.STEPS,
Expand Down
3 changes: 3 additions & 0 deletions packages/health/ios/Classes/HealthConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ enum HealthConstants {
static let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED"
static let ATRIAL_FIBRILLATION_BURDEN = "ATRIAL_FIBRILLATION_BURDEN"
static let AUDIOGRAM = "AUDIOGRAM"
static let APPLE_STAND_HOUR = "APPLE_STAND_HOUR"
static let APPLE_MOVE_TIME = "APPLE_MOVE_TIME"
static let APPLE_STAND_TIME = "APPLE_STAND_TIME"
static let BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED"
static let BLOOD_GLUCOSE = "BLOOD_GLUCOSE"
static let BLOOD_OXYGEN = "BLOOD_OXYGEN"
Expand Down
59 changes: 46 additions & 13 deletions packages/health/ios/Classes/HealthDataReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,12 @@ class HealthDataReader {
predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate])
}

let statisticsOptions = statisticsOption(for: dataTypeKey)

let query = HKStatisticsCollectionQuery(
quantityType: quantityType,
quantitySamplePredicate: predicate,
options: [.cumulativeSum, .separateBySource],
options: statisticsOptions,
anchorDate: dateFrom,
intervalComponents: interval
)
Expand Down Expand Up @@ -392,18 +394,30 @@ class HealthDataReader {
var dictionaries = [[String: Any]]()
collection.enumerateStatistics(from: dateFrom, to: dateTo) { [weak self] statisticData, _ in
guard let self = self else { return }

if let quantity = statisticData.sumQuantity(),
let dataUnitKey = dataUnitKey,
let unit = self.unitDict[dataUnitKey] {
let dict = [
"value": quantity.doubleValue(for: unit),
"date_from": Int(statisticData.startDate.timeIntervalSince1970 * 1000),
"date_to": Int(statisticData.endDate.timeIntervalSince1970 * 1000),
"source_id": statisticData.sources?.first?.bundleIdentifier ?? "",
"source_name": statisticData.sources?.first?.name ?? ""
]
dictionaries.append(dict)
if let dataUnitKey = dataUnitKey, let unit = self.unitDict[dataUnitKey] {
var value: Double? = nil
switch statisticsOptions {
case .cumulativeSum:
value = statisticData.sumQuantity()?.doubleValue(for: unit)
case .discreteAverage:
value = statisticData.averageQuantity()?.doubleValue(for: unit)
case .discreteMin:
value = statisticData.minimumQuantity()?.doubleValue(for: unit)
case .discreteMax:
value = statisticData.maximumQuantity()?.doubleValue(for: unit)
default:
value = statisticData.sumQuantity()?.doubleValue(for: unit)
}
if let value = value {
let dict = [
"value": value,
"date_from": Int(statisticData.startDate.timeIntervalSince1970 * 1000),
"date_to": Int(statisticData.endDate.timeIntervalSince1970 * 1000),
"source_id": statisticData.sources?.first?.bundleIdentifier ?? "",
"source_name": statisticData.sources?.first?.name ?? ""
]
dictionaries.append(dict)
}
}
}
DispatchQueue.main.async {
Expand All @@ -412,6 +426,25 @@ class HealthDataReader {
}
healthStore.execute(query)
}

/// Helper to select correct HKStatisticsOptions for a given dataTypeKey
private func statisticsOption(for dataTypeKey: String) -> HKStatisticsOptions {
guard let quantityType = dataQuantityTypesDict[dataTypeKey] else {
// Default to cumulativeSum for backward compatibility
return .cumulativeSum
}
switch quantityType.aggregationStyle {
case .cumulative:
return .cumulativeSum
case .discreteArithmetic:
return .discreteAverage
case .discrete:
// Other options are .discreteAverage, .discreteMin, or .discreteMax
return .discreteAverage
@unknown default:
return .cumulativeSum
}
}

/// Gets total steps in interval
/// - Parameters:
Expand Down
21 changes: 14 additions & 7 deletions packages/health/ios/Classes/SwiftHealthPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
HealthConstants.DIETARY_SELENIUM,
]

// Set up iOS 13 specific types (ordinary health data types)
if #available(iOS 13.0, *) {
initializeIOS13Types()
healthDataTypes = Array(dataTypesDict.values)
characteristicsDataTypes = Array(characteristicsTypesDict.values)
}

// Set up iOS 11 specific types (ordinary health data quantity types)
if #available(iOS 11.0, *) {
initializeIOS11Types()
Expand All @@ -292,6 +285,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
initializeIOS12Types()
}

// Set up iOS 13 specific types (ordinary health data types)
if #available(iOS 13.0, *) {
initializeIOS13Types()
healthDataTypes = Array(dataTypesDict.values)
characteristicsDataTypes = Array(characteristicsTypesDict.values)
}

if #available(iOS 13.6, *) {
initializeIOS13_6Types()
}
Expand All @@ -312,6 +312,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
/// Initialize iOS 11 specific data types
@available(iOS 11.0, *)
private func initializeIOS11Types() {
dataTypesDict[HealthConstants.APPLE_STAND_HOUR] = HKSampleType.categoryType(forIdentifier: .appleStandHour)!

dataQuantityTypesDict[HealthConstants.ACTIVE_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
dataQuantityTypesDict[HealthConstants.BASAL_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .basalEnergyBurned)!
dataQuantityTypesDict[HealthConstants.BLOOD_GLUCOSE] = HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!
Expand Down Expand Up @@ -390,6 +392,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
@available(iOS 13.0, *)
private func initializeIOS13Types() {
dataTypesDict[HealthConstants.ACTIVE_ENERGY_BURNED] = HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!
dataTypesDict[HealthConstants.APPLE_STAND_TIME] = HKSampleType.quantityType(forIdentifier: .appleStandTime)!
dataTypesDict[HealthConstants.AUDIOGRAM] = HKSampleType.audiogramSampleType()
dataTypesDict[HealthConstants.BASAL_ENERGY_BURNED] = HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!
dataTypesDict[HealthConstants.BLOOD_GLUCOSE] = HKSampleType.quantityType(forIdentifier: .bloodGlucose)!
Expand Down Expand Up @@ -525,6 +528,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
workoutActivityTypeMap["SOCIAL_DANCE"] = HKWorkoutActivityType.socialDance
workoutActivityTypeMap["PICKLEBALL"] = HKWorkoutActivityType.pickleball
workoutActivityTypeMap["COOLDOWN"] = HKWorkoutActivityType.cooldown

if #available(iOS 14.5, *) {
dataTypesDict[HealthConstants.APPLE_MOVE_TIME] = HKSampleType.quantityType(forIdentifier: .appleMoveTime)!
}
}

/// Initialize iOS 16 specific data types
Expand Down
2 changes: 1 addition & 1 deletion packages/health/ios/health.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
Pod::Spec.new do |s|
s.name = 'health'
s.version = '13.1.0'
s.version = '13.1.1'
s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.'
s.description = <<-DESC
Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android.
Expand Down
Loading