fix(watch): stabilise stats live, foreground et resync apres perte de connexion (#179-#189)
Reduit le cout radio des samples live et la cadence des projections telephone -> montre (#179-#183). Restaure les statistiques live FC/distance/calories et le maintien foreground/ongoing activity (#184-#185). Fiabilise le demarrage de seance et l'orchestration des permissions montre (#187). Renforce la resynchronisation des statistiques live et du score apres perte puis retour de connexion (#188-#189). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -5,7 +5,7 @@ abstract interface class WatchCommandIngress {
|
||||
}
|
||||
|
||||
abstract interface class WatchProjectionPublisher {
|
||||
Future<void> publish(WatchSessionProjection projection);
|
||||
Future<void> publish(WatchSessionProjection projection, {bool urgent = true});
|
||||
}
|
||||
|
||||
abstract interface class WatchAlertPublisher {
|
||||
|
||||
@ -22,7 +22,10 @@ abstract interface class WatchBridgeNativeChannel {
|
||||
|
||||
Stream<WatchBridgeConnectionEvent> get connectionEvents;
|
||||
|
||||
Future<void> publishProjection(WatchSessionProjection projection);
|
||||
Future<void> publishProjection(
|
||||
WatchSessionProjection projection, {
|
||||
bool urgent = true,
|
||||
});
|
||||
|
||||
Future<void> publishAlert(WatchAlertEnvelope alert);
|
||||
|
||||
@ -124,11 +127,14 @@ final class MethodChannelWatchBridgeNativeChannel
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> publishProjection(WatchSessionProjection projection) {
|
||||
return _invokeIgnoringMissingPlugin(
|
||||
'publishProjection',
|
||||
projection.toJson(),
|
||||
);
|
||||
Future<void> publishProjection(
|
||||
WatchSessionProjection projection, {
|
||||
bool urgent = true,
|
||||
}) {
|
||||
return _invokeIgnoringMissingPlugin('publishProjection', {
|
||||
...projection.toJson(),
|
||||
'urgent': urgent,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -15,7 +15,7 @@ final class WatchWearDataLayerAdapter
|
||||
WorkoutHistoryUseCases? workoutHistoryUseCases,
|
||||
ActiveWorkoutSensorUseCases? activeWorkoutSensorUseCases,
|
||||
WorkoutTelemetryUseCases? workoutTelemetryUseCases,
|
||||
Duration projectionRefreshInterval = const Duration(seconds: 2),
|
||||
Duration projectionRefreshInterval = const Duration(seconds: 5),
|
||||
}) : _nativeChannel = nativeChannel,
|
||||
_commandIngress = commandIngress,
|
||||
_projectionSource = projectionSource,
|
||||
@ -36,6 +36,9 @@ final class WatchWearDataLayerAdapter
|
||||
Future<void> _commandTail = Future<void>.value();
|
||||
Timer? _projectionRefreshTimer;
|
||||
WatchSessionProjection? _latestProjection;
|
||||
WatchSessionProjection? _lastPublishedProjection;
|
||||
bool _skipNextProjectionEmissionForForcedResync = false;
|
||||
int? _lastPublishedProjectionRevision;
|
||||
bool _started = false;
|
||||
bool _foregroundActive = false;
|
||||
|
||||
@ -47,6 +50,10 @@ final class WatchWearDataLayerAdapter
|
||||
_ensureProjectionRefreshLoop();
|
||||
_subscriptions.add(
|
||||
_projectionSource.projections.listen((projection) {
|
||||
if (_skipNextProjectionEmissionForForcedResync) {
|
||||
_skipNextProjectionEmissionForForcedResync = false;
|
||||
return;
|
||||
}
|
||||
unawaited(publish(projection));
|
||||
}),
|
||||
);
|
||||
@ -77,7 +84,7 @@ final class WatchWearDataLayerAdapter
|
||||
_subscriptions.add(
|
||||
_nativeChannel.connectionEvents.listen((event) {
|
||||
if (event.isReachable || event.requestsResync) {
|
||||
unawaited(_projectionSource.emitCurrentProjection());
|
||||
unawaited(_forceProjectionResync());
|
||||
}
|
||||
}),
|
||||
);
|
||||
@ -96,7 +103,18 @@ final class WatchWearDataLayerAdapter
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> publish(WatchSessionProjection projection) async {
|
||||
Future<void> publish(
|
||||
WatchSessionProjection projection, {
|
||||
bool urgent = true,
|
||||
}) async {
|
||||
await _publishProjection(projection, urgent: urgent, force: false);
|
||||
}
|
||||
|
||||
Future<void> _publishProjection(
|
||||
WatchSessionProjection projection, {
|
||||
required bool urgent,
|
||||
required bool force,
|
||||
}) async {
|
||||
final previousProjection = _latestProjection;
|
||||
_latestProjection = projection;
|
||||
if (projection.phase == WatchSessionPhase.noActiveSession) {
|
||||
@ -105,7 +123,23 @@ final class WatchWearDataLayerAdapter
|
||||
_activeWorkoutSensorUseCases?.clear(previousSessionId);
|
||||
}
|
||||
}
|
||||
await _nativeChannel.publishProjection(projection);
|
||||
if (!force &&
|
||||
_lastPublishedProjection != null &&
|
||||
_hasSameSignificantProjectionState(
|
||||
_lastPublishedProjection!,
|
||||
projection,
|
||||
)) {
|
||||
await _syncForegroundService(projection);
|
||||
return;
|
||||
}
|
||||
final revisionChanged =
|
||||
_lastPublishedProjectionRevision != projection.revision;
|
||||
await _nativeChannel.publishProjection(
|
||||
projection,
|
||||
urgent: force ? urgent : urgent && revisionChanged,
|
||||
);
|
||||
_lastPublishedProjection = projection;
|
||||
_lastPublishedProjectionRevision = projection.revision;
|
||||
await _syncForegroundService(projection);
|
||||
}
|
||||
|
||||
@ -180,9 +214,114 @@ final class WatchWearDataLayerAdapter
|
||||
|
||||
void _ensureProjectionRefreshLoop() {
|
||||
_projectionRefreshTimer ??= Timer.periodic(_projectionRefreshInterval, (_) {
|
||||
unawaited(_projectionSource.emitCurrentProjection());
|
||||
unawaited(_publishHeartbeat());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _publishHeartbeat() async {
|
||||
final projection = await _emitCurrentProjectionSkippingSourceEcho();
|
||||
await _publishProjection(projection, urgent: false, force: true);
|
||||
}
|
||||
|
||||
Future<void> _forceProjectionResync() async {
|
||||
final projection = await _emitCurrentProjectionSkippingSourceEcho();
|
||||
await _publishProjection(projection, urgent: true, force: true);
|
||||
}
|
||||
|
||||
Future<WatchSessionProjection>
|
||||
_emitCurrentProjectionSkippingSourceEcho() async {
|
||||
_skipNextProjectionEmissionForForcedResync = true;
|
||||
try {
|
||||
return await _projectionSource.emitCurrentProjection();
|
||||
} finally {
|
||||
unawaited(
|
||||
Future<void>.delayed(Duration.zero, () {
|
||||
_skipNextProjectionEmissionForForcedResync = false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasSameSignificantProjectionState(
|
||||
WatchSessionProjection left,
|
||||
WatchSessionProjection right,
|
||||
) {
|
||||
return left.schemaVersion == right.schemaVersion &&
|
||||
left.deviceSessionId == right.deviceSessionId &&
|
||||
left.revision == right.revision &&
|
||||
left.phase == right.phase &&
|
||||
left.phoneReachable == right.phoneReachable &&
|
||||
left.seriesIndex == right.seriesIndex &&
|
||||
left.seriesTotal == right.seriesTotal &&
|
||||
left.exerciseName == right.exerciseName &&
|
||||
left.programIndex == right.programIndex &&
|
||||
left.exerciseIndex == right.exerciseIndex &&
|
||||
left.setIndex == right.setIndex &&
|
||||
left.passageIndex == right.passageIndex &&
|
||||
left.passageTotal == right.passageTotal &&
|
||||
left.stepIndex == right.stepIndex &&
|
||||
left.stepTotal == right.stepTotal &&
|
||||
left.stepName == right.stepName &&
|
||||
left.stepType == right.stepType &&
|
||||
left.stepTargetValue == right.stepTargetValue &&
|
||||
_hasSameSignificantTimerState(left.dominantTimer, right.dominantTimer) &&
|
||||
_hasSameSignificantTimerListState(
|
||||
left.secondaryTimers,
|
||||
right.secondaryTimers,
|
||||
) &&
|
||||
left.primaryAction == right.primaryAction &&
|
||||
_listEquals(left.secondaryActions, right.secondaryActions) &&
|
||||
left.nextExerciseName == right.nextExerciseName &&
|
||||
left.statusLabel == right.statusLabel &&
|
||||
left.hasManualScore == right.hasManualScore &&
|
||||
left.currentManualScoreValue == right.currentManualScoreValue &&
|
||||
left.canDecrementScore == right.canDecrementScore &&
|
||||
left.manualScoreTargetValue == right.manualScoreTargetValue &&
|
||||
left.manualScoreTargetLabel == right.manualScoreTargetLabel &&
|
||||
left.manualScoreRepsTargetValue == right.manualScoreRepsTargetValue &&
|
||||
left.manualScoreScope == right.manualScoreScope;
|
||||
}
|
||||
|
||||
bool _hasSameSignificantTimerListState(
|
||||
List<WatchTimerProjection> left,
|
||||
List<WatchTimerProjection> right,
|
||||
) {
|
||||
if (left.length != right.length) {
|
||||
return false;
|
||||
}
|
||||
for (var index = 0; index < left.length; index += 1) {
|
||||
if (!_hasSameSignificantTimerState(left[index], right[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _hasSameSignificantTimerState(
|
||||
WatchTimerProjection? left,
|
||||
WatchTimerProjection? right,
|
||||
) {
|
||||
if (left == null || right == null) {
|
||||
return left == right;
|
||||
}
|
||||
return left.kind == right.kind &&
|
||||
left.label == right.label &&
|
||||
left.displayMode == right.displayMode &&
|
||||
left.runState == right.runState &&
|
||||
left.targetMs == right.targetMs;
|
||||
}
|
||||
|
||||
bool _listEquals<T>(List<T> left, List<T> right) {
|
||||
if (left.length != right.length) {
|
||||
return false;
|
||||
}
|
||||
for (var index = 0; index < left.length; index += 1) {
|
||||
if (left[index] != right[index]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
final class _WatchAdapterCommandKey {
|
||||
|
||||
Reference in New Issue
Block a user