Skip to content

Commit a70b633

Browse files
authored
Add HealthService for managing health data and caching mechanisms (#79)
This pull request includes significant changes to the health data handling and device information retrieval in the repository. The most important changes involve refactoring the `BaseLocalHealthImpl` class, updating the `DeviceInfoRepositoryImpl` class, and modifying the `StreakRepositoryImpl` class to use a new health service. Refactoring and improvements to health data handling: * [`lib/data/repositories/base_local_health_impl.dart`](diffhunk://#diff-d65e3fe018d4f55ec02d268560a3058f224079bd55bcebd461a01bbc171e4abbR1-R171): Introduced the `BaseLocalHealthRepoImpl` class with improved methods for fetching health data, handling overlaps, and writing tracking data to health. This includes methods like `getHealthDataInInterval`, `getDistanceOfWorkoutsInInterval`, and `writeTrackingToHealth`. * [`lib/data/repositories/local_health_impl.dart`](diffhunk://#diff-99442f8cf53d535dc947e240c47d72949d679badbe5c1f253d9d99974446d774L1-L300): Removed the old `LocalHealthRepoImpl` class and its methods, which have been replaced by the new `BaseLocalHealthRepoImpl` class. Updates to device information retrieval: * [`lib/data/repositories/device_info_repository_impl.dart`](diffhunk://#diff-43201840425b81fa4da9d0288f18d570d606c925360655e6a514648e699d9696L15-R15): Changed the logic for setting the installation date and getting the last opened date to use the current date instead of a date 30 days ago. [[1]](diffhunk://#diff-43201840425b81fa4da9d0288f18d570d606c925360655e6a514648e699d9696L15-R15) [[2]](diffhunk://#diff-43201840425b81fa4da9d0288f18d570d606c925360655e6a514648e699d9696L33-R33) Modifications to streak repository: * [`lib/data/repositories/streak_repository_impl.dart`](diffhunk://#diff-a37166ebc82c76ca34e48751e335bc8caf6c49f725d4e09b45f8bbed14f169c1L5-L7): Replaced the `LocalHealthRepository` dependency with `HealthServiceImpl` and updated the logic to use this new service for fetching steps and other health data. [[1]](diffhunk://#diff-a37166ebc82c76ca34e48751e335bc8caf6c49f725d4e09b45f8bbed14f169c1L5-L7) [[2]](diffhunk://#diff-a37166ebc82c76ca34e48751e335bc8caf6c49f725d4e09b45f8bbed14f169c1L21-R22) [[3]](diffhunk://#diff-a37166ebc82c76ca34e48751e335bc8caf6c49f725d4e09b45f8bbed14f169c1L138-R136) [[4]](diffhunk://#diff-a37166ebc82c76ca34e48751e335bc8caf6c49f725d4e09b45f8bbed14f169c1L163-R162) [[5]](diffhunk://#diff-a37166ebc82c76ca34e48751e335bc8caf6c49f725d4e09b45f8bbed14f169c1L255-R254) - Replace current local_health_impl.dart with a new caching approach - Add healthService, that either requests flutter health data from local_health or returns the cached data
2 parents 43ca1cf + f78906c commit a70b633

16 files changed

Lines changed: 615 additions & 366 deletions
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import 'package:activity_tracking/model/activity.dart' as tracking_activity;
2+
import 'package:activity_tracking/model/activity_type.dart';
3+
import 'package:health/health.dart';
4+
import 'package:logging/logging.dart';
5+
import 'package:movetopia/data/model/activity.dart';
6+
import 'package:movetopia/domain/repositories/local_health.dart';
7+
import 'package:package_info_plus/package_info_plus.dart';
8+
import 'package:riverpod/riverpod.dart';
9+
10+
final localHealthRepositoryProvider =
11+
Provider<BaseLocalHealthRepository>((ref) => BaseLocalHealthRepoImpl());
12+
13+
interface class BaseLocalHealthRepoImpl extends BaseLocalHealthRepository {
14+
final log = Logger('LocalHealthRepoImpl');
15+
16+
@override
17+
Future<List<HealthDataPoint>?> getHealthDataInInterval(
18+
DateTime start, DateTime end, List<HealthDataType> types) async {
19+
try {
20+
_compareDate(start, end);
21+
22+
List<HealthDataPoint> dataPoints = await Health()
23+
.getHealthDataFromTypes(types: types, startTime: start, endTime: end);
24+
25+
// Remove duplicates from the data points - with proper overlap handling
26+
List<HealthDataPoint> uniqueDataPoints = [];
27+
Map<String, List<HealthDataPoint>> dataByType = {};
28+
29+
// Group data points by type only (not by time)
30+
for (var point in dataPoints) {
31+
final key = point.typeString;
32+
33+
if (!dataByType.containsKey(key)) {
34+
dataByType[key] = [];
35+
}
36+
dataByType[key]!.add(point);
37+
}
38+
39+
// Process each type group to handle overlaps
40+
for (var typePoints in dataByType.values) {
41+
// Sort by start time to make overlap detection easier
42+
typePoints.sort((a, b) => a.dateFrom.compareTo(b.dateFrom));
43+
44+
List<HealthDataPoint> nonOverlapping = [];
45+
46+
for (var point in typePoints) {
47+
// Check if this point overlaps with any already accepted point
48+
bool hasOverlap = false;
49+
int overlapIndex = -1;
50+
51+
for (int i = 0; i < nonOverlapping.length; i++) {
52+
var existing = nonOverlapping[i];
53+
54+
// Check for time overlap
55+
if ((point.dateFrom.isBefore(existing.dateTo) ||
56+
point.dateFrom.isAtSameMomentAs(existing.dateTo)) &&
57+
(point.dateTo.isAfter(existing.dateFrom) ||
58+
point.dateTo.isAtSameMomentAs(existing.dateFrom))) {
59+
hasOverlap = true;
60+
overlapIndex = i;
61+
62+
// If this is from movetopia, prioritize it
63+
if (point.sourceName.startsWith('de.movetopia') &&
64+
point.type == HealthDataType.WORKOUT) {
65+
nonOverlapping[i] = point;
66+
break;
67+
}
68+
69+
// Otherwise, take the one with longer duration
70+
if (point.dateTo.difference(point.dateFrom) >
71+
existing.dateTo.difference(existing.dateFrom)) {
72+
nonOverlapping[i] = point;
73+
}
74+
75+
break;
76+
}
77+
}
78+
79+
if (!hasOverlap) {
80+
nonOverlapping.add(point);
81+
}
82+
}
83+
84+
uniqueDataPoints.addAll(nonOverlapping);
85+
}
86+
87+
dataPoints = uniqueDataPoints;
88+
89+
return dataPoints;
90+
} catch (e) {
91+
log.info(e);
92+
}
93+
return null;
94+
}
95+
96+
bool _compareDate(DateTime start, DateTime end) {
97+
if (start.compareTo(end) < 0) {
98+
return true;
99+
} else {
100+
throw Exception("Start date is greater than end date");
101+
}
102+
}
103+
104+
Future<double> getDistanceOfWorkoutsInInterval(DateTime start, DateTime end,
105+
List<HealthWorkoutActivityType> workoutTypes) async {
106+
try {
107+
var workouts = await getHealthDataInInterval(start, end, [
108+
HealthDataType.WORKOUT,
109+
]);
110+
if (workouts == null || workouts.isEmpty) {
111+
return 0;
112+
} else {
113+
workouts = workouts
114+
.where((element) => workoutTypes.contains(
115+
(element.value as WorkoutHealthValue).workoutActivityType))
116+
.toList();
117+
double distance = 0;
118+
for (var workout in workouts) {
119+
distance += (workout.value as WorkoutHealthValue).totalDistance ?? 0;
120+
}
121+
return distance;
122+
}
123+
} catch (e) {
124+
log.info(e);
125+
}
126+
return 0;
127+
}
128+
129+
@override
130+
Future<ActivityPreview?> writeTrackingToHealth(
131+
tracking_activity.Activity preview) async {
132+
final startTime =
133+
DateTime.fromMillisecondsSinceEpoch(preview.startDateTime ?? 0);
134+
final endTime =
135+
DateTime.fromMillisecondsSinceEpoch(preview.endDateTime ?? 0);
136+
var success = true;
137+
try {
138+
if (preview.steps! > 0) {
139+
success = await Health().writeHealthData(
140+
value: (preview.steps ?? 0).toDouble(),
141+
type: HealthDataType.STEPS,
142+
startTime: startTime,
143+
endTime: endTime);
144+
} else if (preview.activityType == ActivityType.walking ||
145+
preview.activityType == ActivityType.running) {
146+
return null;
147+
}
148+
success = await Health().writeWorkoutData(
149+
activityType: HealthWorkoutActivityType.values.firstWhere(
150+
(element) => element.name == preview.activityType?.name),
151+
start: startTime,
152+
end: endTime,
153+
totalDistance: (preview.distance ?? 0).toInt() * 1000,
154+
totalDistanceUnit: HealthDataUnit.METER);
155+
} catch (e) {
156+
log.info(e);
157+
}
158+
if (success) {
159+
var appPackage = await PackageInfo.fromPlatform();
160+
return ActivityPreview(
161+
activityType: HealthWorkoutActivityType.values.firstWhere(
162+
(element) => element.name == preview.activityType?.name),
163+
start: startTime,
164+
end: endTime,
165+
distance: preview.distance ?? 0,
166+
sourceId: appPackage.packageName,
167+
caloriesBurnt: 0);
168+
}
169+
return null;
170+
}
171+
}

0 commit comments

Comments
 (0)