From 13f2898a8bf30514103dd757f387fd4d3bf3e658 Mon Sep 17 00:00:00 2001 From: Haroon Amjad <> Date: Tue, 30 Jun 2026 20:47:33 +0300 Subject: [PATCH] Fixes for Apple Watch integration --- .../health_data_transformations.dart | 151 ++++++++++-------- .../health_service.dart | 16 +- .../model/vitals_data_model.dart | 14 +- .../health_connect_helper.dart | 49 +++++- .../smartwatches/activity_detail.dart | 3 +- 5 files changed, 148 insertions(+), 85 deletions(-) diff --git a/lib/features/smartwatch_health_data/health_data_transformations.dart b/lib/features/smartwatch_health_data/health_data_transformations.dart index 6f777353..1aeff65b 100644 --- a/lib/features/smartwatch_health_data/health_data_transformations.dart +++ b/lib/features/smartwatch_health_data/health_data_transformations.dart @@ -13,80 +13,90 @@ enum Durations { yearly("yearly"); final String value; + const Durations(this.value); } class HealthDataTransformation { - Map> transformVitalsToDataPoints(VitalsWRTType vitals, String filterType, String selectedSection,) { - final Map> dataPointMap = {}; + Map> transformVitalsToDataPoints( + VitalsWRTType vitals, + String filterType, + String selectedSection, + ) { + final Map> dataPointMap = {}; Map> data = vitals.getVitals(); // Group data based on the filter type Map> groupedData = {}; // List > items = data.values.toList(); List keys = data.keys.toList(); var count = 0; - List item = data[selectedSection] ?? []; + List item = data[selectedSection] ?? []; // for(var item in items) { - List dataPoints = []; - - for (var vital in item) { - String key = ""; - if (vital.value == "" || vital.timestamp == "") continue; - var parseDate = DateTime.parse(vital.timestamp); - var currentDate = normalizeToStartOfDay(DateTime.now()); - if (filterType == Durations.daily.value) { - if(isBetweenInclusive(parseDate, currentDate, DateTime.now())) { - key = DateFormat('yyyy-MM-dd HH').format(DateTime.parse(vital.timestamp)); - groupedData.putIfAbsent(key, () => []).add(vital); - }// Group by hour - } else if (filterType == Durations.weekly.value) { - if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 7)), DateTime.now())) { - key = DateFormat('yyyy-MM-dd').format(DateTime.parse(vital.timestamp)); - groupedData.putIfAbsent(key, () => []).add(vital); - - } // Group by day - } else if (filterType == Durations.monthly.value) { - if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 30)), DateTime.now())) { - print("the value for the monthly filter is ${vital.value} with the timestamp ${vital.timestamp} and the current date is $currentDate and the parse date is $parseDate"); - key = DateFormat('yyyy-MM-dd').format(DateTime.parse(vital.timestamp)); - groupedData.putIfAbsent(key, () => []).add(vital); - - } // Group by day - } else if (filterType == Durations.halfYearly.value || filterType == Durations.yearly.value) { - if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: filterType == Durations.halfYearly.value?180: 365)), DateTime.now())) { - key = DateFormat('yyyy-MM').format(DateTime.parse(vital.timestamp)); - groupedData.putIfAbsent(key, () => []).add(vital); - - } // Group by month - } else { - throw ArgumentError('Invalid filter type'); - } + List dataPoints = []; + + for (var vital in item) { + String key = ""; + if (vital.value == "" || vital.timestamp == "") continue; + + DateTime parseDate; + try { + parseDate = DateTime.parse(vital.timestamp); + } catch (e) { + print('Error parsing timestamp "${vital.timestamp}": $e'); + continue; // Skip this vital if timestamp is invalid + } + + var currentDate = normalizeToStartOfDay(DateTime.now()); + if (filterType == Durations.daily.value) { + if (isBetweenInclusive(parseDate, currentDate, DateTime.now())) { + key = DateFormat('yyyy-MM-dd HH').format(parseDate); + groupedData.putIfAbsent(key, () => []).add(vital); + } // Group by hour + } else if (filterType == Durations.weekly.value) { + if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 7)), DateTime.now())) { + key = DateFormat('yyyy-MM-dd').format(parseDate); + groupedData.putIfAbsent(key, () => []).add(vital); + } // Group by day + } else if (filterType == Durations.monthly.value) { + if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 30)), DateTime.now())) { + print("the value for the monthly filter is ${vital.value} with the timestamp ${vital.timestamp} and the current date is $currentDate and the parse date is $parseDate"); + key = DateFormat('yyyy-MM-dd').format(parseDate); + groupedData.putIfAbsent(key, () => []).add(vital); + } // Group by day + } else if (filterType == Durations.halfYearly.value || filterType == Durations.yearly.value) { + if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: filterType == Durations.halfYearly.value ? 180 : 365)), DateTime.now())) { + key = DateFormat('yyyy-MM').format(parseDate); + groupedData.putIfAbsent(key, () => []).add(vital); + } // Group by month + } else { + throw ArgumentError('Invalid filter type'); } - print("the size of groupData is ${groupedData.values.length}"); - - // Process grouped data - groupedData.forEach((key, values) { - double sum = values.fold(0, (acc, v) => acc + num.parse(v.value)); - double mean = sum / values.length; - if(selectedSection == "bodyOxygen" || selectedSection == "bodyTemperature") { - mean = sum / values.length; - }else { - mean = sum; - } - - double finalValue = mean; - print("the final value is $finalValue for the key $key with the original values ${values.map((v) => v.value).toList()} and uom is ${values.first.unitOfMeasure}"); - dataPoints.add(DataPoint( - value: smartScale(finalValue), - label: key, - actualValue: finalValue.toStringAsFixed(2), - displayTime: key, - unitOfMeasurement:values.first.unitOfMeasure , - time: DateTime.parse(values.first.timestamp), - )); - }); - - dataPointMap[filterType] = dataPoints; + } + print("the size of groupData is ${groupedData.values.length}"); + + // Process grouped data + groupedData.forEach((key, values) { + double sum = values.fold(0, (acc, v) => acc + num.parse(v.value)); + double mean = sum / values.length; + if (selectedSection == "bodyOxygen" || selectedSection == "bodyTemperature") { + mean = sum / values.length; + } else { + mean = sum; + } + + double finalValue = mean; + print("the final value is $finalValue for the key $key with the original values ${values.map((v) => v.value).toList()} and uom is ${values.first.unitOfMeasure}"); + dataPoints.add(DataPoint( + value: smartScale(finalValue), + label: key, + actualValue: finalValue.toStringAsFixed(2), + displayTime: key, + unitOfMeasurement: values.first.unitOfMeasure, + time: DateTime.parse(values.first.timestamp), + )); + }); + + dataPointMap[filterType] = dataPoints; // } return dataPointMap; } @@ -109,10 +119,10 @@ class HealthDataTransformation { final random = Random(); // Smooth compression scaling - double baseScaled = number <20 ? number:100 * (number / (number + 100)); + double baseScaled = number < 20 ? number : 100 * (number / (number + 100)); // Random factor between 0.9 and 1.1 (±10%) - double randomFactor = number <20 ? random.nextDouble() * 1.5: 0.9 + random.nextDouble() * 0.2; + double randomFactor = number < 20 ? random.nextDouble() * 1.5 : 0.9 + random.nextDouble() * 0.2; double result = baseScaled * randomFactor; @@ -122,13 +132,12 @@ class HealthDataTransformation { DateTime normalizeToStartOfDay(DateTime date) { return DateTime(date.year, date.month, date.day); } + bool isBetweenInclusive( - DateTime target, - DateTime start, - DateTime end, - ) { + DateTime target, + DateTime start, + DateTime end, + ) { return !normalizeToStartOfDay(target).isBefore(start) && !normalizeToStartOfDay(target).isAfter(end); } - - -} \ No newline at end of file +} diff --git a/lib/features/smartwatch_health_data/health_service.dart b/lib/features/smartwatch_health_data/health_service.dart index 78411bfb..9e6c91b2 100644 --- a/lib/features/smartwatch_health_data/health_service.dart +++ b/lib/features/smartwatch_health_data/health_service.dart @@ -172,12 +172,12 @@ class HealthService { } } - void addWatchHelper(SmartWatchTypes watchType){ - watchHelper = CreateWatchHelper.getWatchName(watchType) ; + void addWatchHelper(SmartWatchTypes watchType) { + watchHelper = CreateWatchHelper.getWatchName(watchType); } Future> initDevice() async { - if(watchHelper == null){ + if (watchHelper == null) { return Result.error('No watch helper found'); } return await watchHelper!.initDevice(); @@ -197,14 +197,14 @@ class HealthService { await watchHelper!.getDistance(); await watchHelper!.getBloodOxygen(); Result data = await watchHelper!.retrieveData(); - if(data.isError) { + if (data.isError) { print('Unable to get the data'); } - var response = jsonDecode(data.asValue?.value?.toString()?.trim().replaceAll("\n", "")??""); - VitalsWRTType vitals = VitalsWRTType.fromMap(response); - log("the data is ${vitals}"); + var response = jsonDecode(data.asValue?.value?.toString()?.trim().replaceAll("\n", "") ?? ""); + VitalsWRTType vitals = VitalsWRTType.fromMap(response); + // log("the data is ${vitals}"); return vitals; - }catch(e){ + } catch (e) { print('Error getting heart rate: $e'); } return null; diff --git a/lib/features/smartwatch_health_data/model/vitals_data_model.dart b/lib/features/smartwatch_health_data/model/vitals_data_model.dart index ba1ac9f1..83cd4ea6 100644 --- a/lib/features/smartwatch_health_data/model/vitals_data_model.dart +++ b/lib/features/smartwatch_health_data/model/vitals_data_model.dart @@ -9,10 +9,22 @@ class Vitals { this.unitOfMeasure = "", }); + Map toJson() { + return { + 'value': value, + 'timestamp': timestamp, + 'uom': unitOfMeasure, + }; + } + factory Vitals.fromMap(Map map) { + String timestamp = map['timestamp'] ?? map['timeStamp'] ?? ""; + if (timestamp.isEmpty) { + timestamp = DateTime.now().toIso8601String(); + } return Vitals( value: map['value'] ?? "", - timestamp: map['timeStamp'] ?? "", + timestamp: timestamp, unitOfMeasure: map['uom'] ?? "", ); } diff --git a/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart b/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart index b77b5430..aeca1e87 100644 --- a/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart +++ b/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:async/src/result/result.dart'; import 'package:flutter/material.dart'; @@ -80,7 +81,10 @@ class HealthConnectHelper extends WatchHelper { @override Future retrieveData() async { + // String retData; + // retData = Result.value(getMappedData()) as String; return Result.value(getMappedData()); + // return retData; } @override @@ -151,9 +155,22 @@ class HealthConnectHelper extends WatchHelper { for (var point in data) { if (point.value is NumericHealthValue) { final numericValue = (point.value as NumericHealthValue).numericValue; + String timestamp = DateTime.now().toIso8601String(); + + try { + if (point.dateFrom != null) { + String tempTimestamp = point.dateFrom.toIso8601String(); + if (tempTimestamp.isNotEmpty) { + timestamp = tempTimestamp; + } + } + } catch (e) { + debugPrint('Error converting dateFrom to ISO8601 for $s: $e'); + } + Vitals vitals = Vitals( - value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2), - timestamp: point.dateFrom.toString() + value: numericValue.toStringAsFixed(2), + timestamp: timestamp ); mappedData[s]?.add(vitals); } @@ -165,17 +182,41 @@ class HealthConnectHelper extends WatchHelper { for (var point in data) { if (point.value is NumericHealthValue) { final numericValue = (point.value as NumericHealthValue).numericValue; + String timestamp = DateTime.now().toIso8601String(); + + try { + if (point.dateFrom != null) { + String tempTimestamp = point.dateFrom.toIso8601String(); + if (tempTimestamp.isNotEmpty) { + timestamp = tempTimestamp; + } + } + } catch (e) { + debugPrint('Error converting dateFrom to ISO8601 for $s: $e'); + } + point.value = NumericHealthValue( numericValue: numericValue * 100, ); - Vitals vitals = Vitals(value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2), timestamp: point.dateFrom.toString()); + Vitals vitals = Vitals( + value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2), + timestamp: timestamp + ); mappedData[s]?.add(vitals); } } } getMappedData() { - return " { \"heartRate\": ${mappedData["heartRate"] ?? []}, \"sleep\": ${mappedData["sleep"] ?? []}, \"steps\": ${mappedData["steps"] ?? []}, \"activity\": ${mappedData["activity"] ?? []}, \"bloodOxygen\": ${mappedData["bloodOxygen"] ?? []}, \"bodyTemperature\": ${mappedData["bodyTemperature"] ?? []}, \"distance\": ${mappedData["distance"] ?? []} }"; + return jsonEncode({ + "heartRate": (mappedData["heartRate"] ?? []).map((v) => v.toJson()).toList(), + "sleep": (mappedData["sleep"] ?? []).map((v) => v.toJson()).toList(), + "steps": (mappedData["steps"] ?? []).map((v) => v.toJson()).toList(), + "activity": (mappedData["activity"] ?? []).map((v) => v.toJson()).toList(), + "bloodOxygen": (mappedData["bloodOxygen"] ?? []).map((v) => v.toJson()).toList(), + "bodyTemperature": (mappedData["bodyTemperature"] ?? []).map((v) => v.toJson()).toList(), + "distance": (mappedData["distance"] ?? []).map((v) => v.toJson()).toList(), + }); } getHeartData(DateTime startDate, DateTime endDate, HealthDataType types) async { diff --git a/lib/presentation/smartwatches/activity_detail.dart b/lib/presentation/smartwatches/activity_detail.dart index b023e04f..7911baf6 100644 --- a/lib/presentation/smartwatches/activity_detail.dart +++ b/lib/presentation/smartwatches/activity_detail.dart @@ -240,7 +240,8 @@ class _ActivityDetailsState extends State { return Selector>?>( selector: (_, model) => model.selectedData, builder: (_, data, __) { - if (context.read().selectedData.values.toList().first?.isEmpty == true) return SizedBox(); + // if (context.read().selectedData.values.toList().first.isEmpty == true) return SizedBox(); + if (context.read().selectedData.values.toList().isEmpty == true) return SizedBox(); return CustomBarChart( dataPoints: context.read().selectedData.values.toList().first,