feat(watch): clôture lot #91 - fréquence cardiaque live, notifications de séance et finitions montre
Consolide le lot applicatif watch companion validé : - télémétrie fréquence cardiaque live remontée montre -> téléphone (collecteur watch, adapter Wear Data Layer, persistance Drift, propagation aux écrans historique/programme/profil/exécution) - notifications de séance en arrière-plan côté téléphone (service foreground de statut + passerelle applicative) - finitions montre : chrono d'étape, score d'étape, retrait du bouton "lancer une séance", thème, icônes et polices watch_app Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:watch_bridge_contract/watch_bridge_contract.dart';
|
||||
|
||||
import '../../application/use_cases.dart';
|
||||
import '../../application/watch_companion_use_cases.dart';
|
||||
import 'native_watch_bridge_channel.dart';
|
||||
|
||||
@ -10,20 +11,26 @@ final class WatchWearDataLayerAdapter implements WatchProjectionPublisher {
|
||||
required WatchBridgeNativeChannel nativeChannel,
|
||||
required WatchCommandIngress commandIngress,
|
||||
required WatchProjectionSource projectionSource,
|
||||
Duration heartbeatInterval = const Duration(seconds: 5),
|
||||
WorkoutHistoryUseCases? workoutHistoryUseCases,
|
||||
ActiveWorkoutSensorUseCases? activeWorkoutSensorUseCases,
|
||||
Duration projectionRefreshInterval = const Duration(seconds: 2),
|
||||
}) : _nativeChannel = nativeChannel,
|
||||
_commandIngress = commandIngress,
|
||||
_projectionSource = projectionSource,
|
||||
_heartbeatInterval = heartbeatInterval;
|
||||
_workoutHistoryUseCases = workoutHistoryUseCases,
|
||||
_activeWorkoutSensorUseCases = activeWorkoutSensorUseCases,
|
||||
_projectionRefreshInterval = projectionRefreshInterval;
|
||||
|
||||
final WatchBridgeNativeChannel _nativeChannel;
|
||||
final WatchCommandIngress _commandIngress;
|
||||
final WatchProjectionSource _projectionSource;
|
||||
final Duration _heartbeatInterval;
|
||||
final WorkoutHistoryUseCases? _workoutHistoryUseCases;
|
||||
final ActiveWorkoutSensorUseCases? _activeWorkoutSensorUseCases;
|
||||
final Duration _projectionRefreshInterval;
|
||||
final _commandAcks = <_WatchAdapterCommandKey, WatchCommandAck>{};
|
||||
final _subscriptions = <StreamSubscription<dynamic>>[];
|
||||
Future<void> _commandTail = Future<void>.value();
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _projectionRefreshTimer;
|
||||
WatchSessionProjection? _latestProjection;
|
||||
bool _started = false;
|
||||
bool _foregroundActive = false;
|
||||
@ -33,6 +40,7 @@ final class WatchWearDataLayerAdapter implements WatchProjectionPublisher {
|
||||
return;
|
||||
}
|
||||
_started = true;
|
||||
_ensureProjectionRefreshLoop();
|
||||
_subscriptions.add(
|
||||
_projectionSource.projections.listen((projection) {
|
||||
unawaited(publish(projection));
|
||||
@ -43,6 +51,22 @@ final class WatchWearDataLayerAdapter implements WatchProjectionPublisher {
|
||||
unawaited(_enqueueCommand(command));
|
||||
}),
|
||||
);
|
||||
final workoutHistoryUseCases = _workoutHistoryUseCases;
|
||||
if (workoutHistoryUseCases != null) {
|
||||
_subscriptions.add(
|
||||
_nativeChannel.sensorSummaries.listen((summary) {
|
||||
unawaited(workoutHistoryUseCases.updateHeartRateSummary(summary));
|
||||
}),
|
||||
);
|
||||
}
|
||||
final activeWorkoutSensorUseCases = _activeWorkoutSensorUseCases;
|
||||
if (activeWorkoutSensorUseCases != null) {
|
||||
_subscriptions.add(
|
||||
_nativeChannel.sensorSamples.listen((sample) {
|
||||
activeWorkoutSensorUseCases.recordTelemetrySample(sample);
|
||||
}),
|
||||
);
|
||||
}
|
||||
_subscriptions.add(
|
||||
_nativeChannel.connectionEvents.listen((event) {
|
||||
if (event.isReachable || event.requestsResync) {
|
||||
@ -55,8 +79,8 @@ final class WatchWearDataLayerAdapter implements WatchProjectionPublisher {
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
_projectionRefreshTimer?.cancel();
|
||||
_projectionRefreshTimer = null;
|
||||
for (final subscription in _subscriptions) {
|
||||
await subscription.cancel();
|
||||
}
|
||||
@ -66,10 +90,16 @@ final class WatchWearDataLayerAdapter implements WatchProjectionPublisher {
|
||||
|
||||
@override
|
||||
Future<void> publish(WatchSessionProjection projection) async {
|
||||
final previousProjection = _latestProjection;
|
||||
_latestProjection = projection;
|
||||
if (projection.phase == WatchSessionPhase.noActiveSession) {
|
||||
final previousSessionId = previousProjection?.deviceSessionId;
|
||||
if (previousSessionId != null && previousSessionId.isNotEmpty) {
|
||||
_activeWorkoutSensorUseCases?.clear(previousSessionId);
|
||||
}
|
||||
}
|
||||
await _nativeChannel.publishProjection(projection);
|
||||
await _syncForegroundService(projection);
|
||||
_syncHeartbeat(projection);
|
||||
}
|
||||
|
||||
Future<void> _enqueueCommand(WatchCommandEnvelope command) {
|
||||
@ -136,26 +166,13 @@ final class WatchWearDataLayerAdapter implements WatchProjectionPublisher {
|
||||
}
|
||||
}
|
||||
|
||||
void _syncHeartbeat(WatchSessionProjection projection) {
|
||||
if (!_hasRunningTimer(projection)) {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
return;
|
||||
}
|
||||
_heartbeatTimer ??= Timer.periodic(_heartbeatInterval, (_) {
|
||||
void _ensureProjectionRefreshLoop() {
|
||||
_projectionRefreshTimer ??= Timer.periodic(_projectionRefreshInterval, (_) {
|
||||
unawaited(_projectionSource.emitCurrentProjection());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasRunningTimer(WatchSessionProjection projection) {
|
||||
final timers = [
|
||||
if (projection.dominantTimer != null) projection.dominantTimer!,
|
||||
...projection.secondaryTimers,
|
||||
];
|
||||
return timers.any((timer) => timer.runState == WatchTimerRunState.running);
|
||||
}
|
||||
|
||||
final class _WatchAdapterCommandKey {
|
||||
_WatchAdapterCommandKey(WatchCommandEnvelope command)
|
||||
: sessionId = command.sessionId,
|
||||
|
||||
Reference in New Issue
Block a user