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:
2026-07-30 08:08:45 +02:00
parent 639231e3fd
commit 7130635177
15 changed files with 1023 additions and 119 deletions

View File

@ -18,36 +18,89 @@ void main() {
final source = _FakeProjectionSource(_projection(revision: 0));
final adapter = _adapter(native: native, source: source);
await adapter.start();
await Future<void>.delayed(Duration.zero);
native.published.clear();
source.emit(_projection(revision: 1));
source.emit(_projection(revision: 2));
source.emit(_projection(revision: 3));
await Future<void>.delayed(Duration.zero);
expect(native.published.map((projection) => projection.revision), [1, 2]);
expect(native.published.map((projection) => projection.revision), [2, 3]);
await adapter.stop();
});
test('heartbeats while a timer is running', () async {
test('republishes volatile heartbeats as non urgent keepalives', () async {
final native = _FakeWatchBridgeNativeChannel();
final source = _FakeProjectionSource(_runningProjection(revision: 1));
final source = _FakeProjectionSource(
_runningProjection(revision: 1),
incrementsRevisionOnEmit: false,
);
final adapter = _adapter(
native: native,
source: source,
heartbeatInterval: const Duration(milliseconds: 10),
);
await adapter.start();
await Future<void>.delayed(Duration.zero);
native.published.clear();
native.urgentFlags.clear();
source.emitCount = 0;
await adapter.publish(_runningProjection(revision: 1));
await Future<void>.delayed(const Duration(milliseconds: 35));
expect(source.emitCount, greaterThanOrEqualTo(1));
expect(native.published.length, greaterThanOrEqualTo(2));
expect(native.published.length, greaterThanOrEqualTo(1));
expect(native.urgentFlags, everyElement(false));
await adapter.stop();
});
test('marks only projection revision changes as urgent', () async {
final native = _FakeWatchBridgeNativeChannel();
final source = _FakeProjectionSource(_runningProjection(revision: 0));
final adapter = _adapter(native: native, source: source);
await adapter.start();
await Future<void>.delayed(Duration.zero);
native.published.clear();
native.urgentFlags.clear();
await adapter.publish(_runningProjection(revision: 2));
await adapter.publish(_runningProjection(revision: 2));
expect(native.published.map((projection) => projection.revision), [2]);
expect(native.urgentFlags, [true]);
await adapter.stop();
});
test(
'forces an urgent projection resync even when state is unchanged',
() async {
final native = _FakeWatchBridgeNativeChannel();
final source = _FakeProjectionSource(
_runningProjection(revision: 1),
incrementsRevisionOnEmit: false,
);
final adapter = _adapter(native: native, source: source);
await adapter.start();
await Future<void>.delayed(Duration.zero);
native.published.clear();
native.urgentFlags.clear();
source.emitCount = 0;
native.emitConnection(
const WatchBridgeConnectionEvent(
isReachable: true,
requestsResync: true,
),
);
await Future<void>.delayed(Duration.zero);
expect(source.emitCount, 1);
expect(native.published.map((projection) => projection.revision), [1]);
expect(native.urgentFlags, [true]);
await adapter.stop();
},
);
test('dispatches watch command and sends ack back to native layer', () async {
final native = _FakeWatchBridgeNativeChannel();
final ingress = _FakeCommandIngress();
@ -89,6 +142,7 @@ void main() {
final source = _FakeProjectionSource(_projection(revision: 3));
final adapter = _adapter(native: native, source: source);
await adapter.start();
await Future<void>.delayed(Duration.zero);
native.published.clear();
source.emitCount = 0;
@ -372,9 +426,10 @@ final class _FakeIds implements IdGenerator {
}
final class _FakeProjectionSource implements WatchProjectionSource {
_FakeProjectionSource(this.current);
_FakeProjectionSource(this.current, {this.incrementsRevisionOnEmit = true});
WatchSessionProjection current;
final bool incrementsRevisionOnEmit;
var emitCount = 0;
final _controller = StreamController<WatchSessionProjection>.broadcast();
@ -392,19 +447,44 @@ final class _FakeProjectionSource implements WatchProjectionSource {
@override
Future<WatchSessionProjection> emitCurrentProjection() async {
emitCount += 1;
final nextRevision = incrementsRevisionOnEmit
? current.revision + 1
: current.revision;
current = WatchSessionProjection(
deviceSessionId: current.deviceSessionId,
revision: current.revision + 1,
projectedAtEpochMs: current.projectedAtEpochMs,
revision: nextRevision,
projectedAtEpochMs: current.projectedAtEpochMs + 1000,
expiresAtEpochMs: current.expiresAtEpochMs == 0
? 0
: current.expiresAtEpochMs + 1000,
phase: current.phase,
phoneReachable: current.phoneReachable,
seriesIndex: current.seriesIndex,
seriesTotal: current.seriesTotal,
exerciseName: current.exerciseName,
programIndex: current.programIndex,
exerciseIndex: current.exerciseIndex,
setIndex: current.setIndex,
passageIndex: current.passageIndex,
passageTotal: current.passageTotal,
stepIndex: current.stepIndex,
stepTotal: current.stepTotal,
stepName: current.stepName,
stepType: current.stepType,
stepTargetValue: current.stepTargetValue,
dominantTimer: current.dominantTimer,
secondaryTimers: current.secondaryTimers,
primaryAction: current.primaryAction,
secondaryActions: current.secondaryActions,
nextExerciseName: current.nextExerciseName,
statusLabel: current.statusLabel,
hasManualScore: current.hasManualScore,
currentManualScoreValue: current.currentManualScoreValue,
canDecrementScore: current.canDecrementScore,
manualScoreTargetValue: current.manualScoreTargetValue,
manualScoreTargetLabel: current.manualScoreTargetLabel,
manualScoreRepsTargetValue: current.manualScoreRepsTargetValue,
manualScoreScope: current.manualScoreScope,
);
_controller.add(current);
return current;
@ -479,9 +559,15 @@ final class _FakeWatchBridgeNativeChannel implements WatchBridgeNativeChannel {
_connections.add(event);
}
final urgentFlags = <bool>[];
@override
Future<void> publishProjection(WatchSessionProjection projection) async {
Future<void> publishProjection(
WatchSessionProjection projection, {
bool urgent = true,
}) async {
published.add(projection);
urgentFlags.add(urgent);
}
@override