1616 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1717 */
1818
19- import 'dart:io' ;
20-
2119import 'package:collection/collection.dart' ;
2220import 'package:health/health.dart' ;
2321import 'package:logging/logging.dart' ;
2422import 'package:powersync/powersync.dart' as ps;
2523import 'package:riverpod_annotation/riverpod_annotation.dart' ;
2624import 'package:wger/core/shared_preferences.dart' ;
2725import 'package:wger/features/health/models/health_metric.dart' ;
26+ import 'package:wger/features/health/providers/health_repository.dart' ;
2827import 'package:wger/features/measurements/models/measurement_category.dart' ;
2928import 'package:wger/features/measurements/models/measurement_entry.dart' ;
3029import 'package:wger/features/measurements/providers/measurement_repository.dart' ;
@@ -52,25 +51,22 @@ class HealthSyncState {
5251}
5352
5453/// Imports body metrics from Apple Health / Health Connect into measurement
55- /// categories. Read-only: reads the platform, writes to the local Drift DB, and
56- /// lets PowerSync push the rows up. Re-imports are deduplicated via each
57- /// measurement's [MeasurementEntry.externalId] (the platform record UUID).
54+ /// categories. Read-only: reads the platform (via [HealthRepository] ), writes to
55+ /// the local Drift DB, and lets PowerSync push the rows up. Re-imports are
56+ /// deduplicated via each measurement's [MeasurementEntry.externalId] (the
57+ /// platform record UUID).
5858@Riverpod (keepAlive: true )
5959class HealthSyncNotifier extends _$HealthSyncNotifier {
6060 final _logger = Logger ('HealthSyncNotifier' );
61- late final Health _health;
62- late final MeasurementRepository _repo ;
61+ late final HealthRepository _health;
62+ late final MeasurementRepository _measurements ;
6363
6464 List <HealthDataType > get _types => enabledHealthMetrics.map ((m) => m.dataType).toList ();
6565
66- List <HealthDataAccess > get _readAccess => List .filled (_types.length, HealthDataAccess .READ );
67-
68- String get _sourceName => Platform .isIOS ? 'apple_health' : 'health_connect' ;
69-
7066 @override
7167 HealthSyncState build () {
72- _health = Health ( );
73- _repo = ref.read (measurementRepositoryProvider);
68+ _health = ref. read (healthRepositoryProvider );
69+ _measurements = ref.read (measurementRepositoryProvider);
7470 _loadPersistedState ();
7571 return const HealthSyncState ();
7672 }
@@ -82,33 +78,16 @@ class HealthSyncNotifier extends _$HealthSyncNotifier {
8278 }
8379
8480 /// Whether a health platform is available on this device.
85- Future <bool > isAvailable () async {
86- if (Platform .isAndroid) {
87- await _health.configure ();
88- final status = await _health.getHealthConnectSdkStatus ();
89- return status == HealthConnectSdkStatus .sdkAvailable;
90- }
91- return Platform .isIOS;
92- }
81+ Future <bool > isAvailable () => _health.isAvailable ();
9382
9483 /// Requests permissions, persists the preference, and runs an initial import.
9584 Future <int > enableSync () async {
9685 _logger.info ('Enabling health sync' );
97- await _health.configure ();
98-
99- final authorized = await _health.requestAuthorization (_types, permissions: _readAccess);
100- if (! authorized) {
101- _logger.warning ('Health permissions not granted' );
86+ if (! await _health.ensureAuthorized (_types)) {
10287 return 0 ;
10388 }
104-
105- if (Platform .isAndroid) {
106- await _health.requestHealthDataHistoryAuthorization ();
107- }
108-
10989 await PreferenceHelper .instance.setHealthSyncEnabled (true );
11090 state = state.copyWith (isEnabled: true );
111-
11291 return syncOnAppOpen ();
11392 }
11493
@@ -133,42 +112,32 @@ class HealthSyncNotifier extends _$HealthSyncNotifier {
133112 state = state.copyWith (isEnabled: true , isSyncing: true );
134113
135114 try {
136- await _health.configure ();
137-
138- final hasPerms = await _health.hasPermissions (_types, permissions: _readAccess);
139- if (hasPerms != true ) {
140- final authorized = await _health.requestAuthorization (_types, permissions: _readAccess);
141- if (! authorized) {
142- _logger.warning ('Health permissions not granted during sync' );
143- state = state.copyWith (isSyncing: false );
144- return 0 ;
145- }
115+ if (! await _health.ensureAuthorized (_types)) {
116+ _logger.warning ('Health permissions not granted during sync' );
117+ state = state.copyWith (isSyncing: false );
118+ return 0 ;
146119 }
147120
148121 final lastSyncStr = await prefs.getLastHealthSyncTimestamp ();
149122 final startTime = lastSyncStr != null ? DateTime .parse (lastSyncStr) : DateTime (2000 );
150123 final endTime = DateTime .now ();
151124 _logger.info ('Syncing health data from $startTime to $endTime ' );
152125
153- var points = await _health.getHealthDataFromTypes (
154- types: _types,
155- startTime: startTime,
156- endTime: endTime,
157- );
158- points = _health.removeDuplicates (points);
159- if (points.isEmpty) {
126+ final readings = await _health.read (types: _types, start: startTime, end: endTime);
127+ if (readings.isEmpty) {
160128 _logger.info ('No new health data' );
161129 state = state.copyWith (isSyncing: false , lastSyncCount: 0 );
162130 return 0 ;
163131 }
164132
165- final categories = await _repo.getAllOnce ();
133+ final categories = await _measurements.getAllOnce ();
134+ final source = _health.sourceName;
166135 var synced = 0 ;
167136 DateTime ? latest;
168137
169138 for (final metric in enabledHealthMetrics) {
170- final metricPoints = points .where ((p ) => p .type == metric.dataType);
171- if (metricPoints .isEmpty) {
139+ final metricReadings = readings .where ((r ) => r .type == metric.dataType);
140+ if (metricReadings .isEmpty) {
172141 continue ;
173142 }
174143
@@ -178,33 +147,29 @@ class HealthSyncNotifier extends _$HealthSyncNotifier {
178147 if (e.externalId != null ) e.externalId! ,
179148 };
180149
181- for (final point in metricPoints) {
182- final value = point.value;
183- if (value is ! NumericHealthValue ) {
184- continue ;
185- }
186- final uuid = point.uuid;
187- if (uuid.isNotEmpty && seen.contains (uuid)) {
150+ for (final reading in metricReadings) {
151+ final uuid = reading.externalId;
152+ if (uuid != null && seen.contains (uuid)) {
188153 continue ;
189154 }
190155
191- await _repo .addLocalDrift (
156+ await _measurements .addLocalDrift (
192157 MeasurementEntry (
193158 categoryId: category.id! ,
194- date: point.dateFrom ,
195- value: metric.toCategoryValue (value.numericValue. toDouble () ),
159+ date: reading.date ,
160+ value: metric.toCategoryValue (reading.value ),
196161 notes: '' ,
197- source: _sourceName ,
198- externalId: uuid.isEmpty ? null : uuid ,
162+ source: source ,
163+ externalId: uuid,
199164 ),
200165 );
201166
202- if (uuid.isNotEmpty ) {
167+ if (uuid != null ) {
203168 seen.add (uuid);
204169 }
205170 synced++ ;
206- if (latest == null || point.dateFrom .isAfter (latest)) {
207- latest = point.dateFrom ;
171+ if (latest == null || reading.date .isAfter (latest)) {
172+ latest = reading.date ;
208173 }
209174 }
210175 }
@@ -243,7 +208,7 @@ class HealthSyncNotifier extends _$HealthSyncNotifier {
243208 unit: metric.unit,
244209 metricType: metric.metricType,
245210 );
246- await _repo .addLocalDriftCategory (category);
211+ await _measurements .addLocalDriftCategory (category);
247212 categories.add (category);
248213 return category;
249214 }
0 commit comments