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'; final class WatchWearDataLayerAdapter implements WatchProjectionPublisher, WatchAlertPublisher { WatchWearDataLayerAdapter({ required WatchBridgeNativeChannel nativeChannel, required WatchCommandIngress commandIngress, required WatchProjectionSource projectionSource, WorkoutHistoryUseCases? workoutHistoryUseCases, ActiveWorkoutSensorUseCases? activeWorkoutSensorUseCases, WorkoutTelemetryUseCases? workoutTelemetryUseCases, Duration projectionRefreshInterval = const Duration(seconds: 5), }) : _nativeChannel = nativeChannel, _commandIngress = commandIngress, _projectionSource = projectionSource, _workoutHistoryUseCases = workoutHistoryUseCases, _activeWorkoutSensorUseCases = activeWorkoutSensorUseCases, _workoutTelemetryUseCases = workoutTelemetryUseCases, _projectionRefreshInterval = projectionRefreshInterval; final WatchBridgeNativeChannel _nativeChannel; final WatchCommandIngress _commandIngress; final WatchProjectionSource _projectionSource; final WorkoutHistoryUseCases? _workoutHistoryUseCases; final ActiveWorkoutSensorUseCases? _activeWorkoutSensorUseCases; final WorkoutTelemetryUseCases? _workoutTelemetryUseCases; final Duration _projectionRefreshInterval; final _commandAcks = <_WatchAdapterCommandKey, WatchCommandAck>{}; final _subscriptions = >[]; Future _commandTail = Future.value(); Timer? _projectionRefreshTimer; WatchSessionProjection? _latestProjection; WatchSessionProjection? _lastPublishedProjection; bool _skipNextProjectionEmissionForForcedResync = false; int? _lastPublishedProjectionRevision; bool _started = false; bool _foregroundActive = false; Future start() async { if (_started) { return; } _started = true; _ensureProjectionRefreshLoop(); _subscriptions.add( _projectionSource.projections.listen((projection) { if (_skipNextProjectionEmissionForForcedResync) { _skipNextProjectionEmissionForForcedResync = false; return; } unawaited(publish(projection)); }), ); _subscriptions.add( _nativeChannel.commands.listen((command) { unawaited(_enqueueCommand(command)); }), ); final workoutHistoryUseCases = _workoutHistoryUseCases; if (workoutHistoryUseCases != null) { _subscriptions.add( _nativeChannel.sensorSummaries.listen((summary) { unawaited(workoutHistoryUseCases.updateHeartRateSummary(summary)); }), ); } final activeWorkoutSensorUseCases = _activeWorkoutSensorUseCases; final workoutTelemetryUseCases = _workoutTelemetryUseCases; if (activeWorkoutSensorUseCases != null || workoutTelemetryUseCases != null) { _subscriptions.add( _nativeChannel.sensorSamples.listen((sample) { activeWorkoutSensorUseCases?.recordTelemetrySample(sample); unawaited(workoutTelemetryUseCases?.recordTelemetrySample(sample)); }), ); } _subscriptions.add( _nativeChannel.connectionEvents.listen((event) { if (event.isReachable || event.requestsResync) { unawaited(_forceProjectionResync()); } }), ); await _projectionSource.emitCurrentProjection(); await _nativeChannel.requestCapabilityRefresh(); } Future stop() async { _projectionRefreshTimer?.cancel(); _projectionRefreshTimer = null; for (final subscription in _subscriptions) { await subscription.cancel(); } _subscriptions.clear(); _started = false; } @override Future publish( WatchSessionProjection projection, { bool urgent = true, }) async { await _publishProjection(projection, urgent: urgent, force: false); } Future _publishProjection( WatchSessionProjection projection, { required bool urgent, required bool force, }) async { final previousProjection = _latestProjection; _latestProjection = projection; if (projection.phase == WatchSessionPhase.noActiveSession) { final previousSessionId = previousProjection?.deviceSessionId; if (previousSessionId != null && previousSessionId.isNotEmpty) { _activeWorkoutSensorUseCases?.clear(previousSessionId); } } 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); } @override Future publishAlert(WatchAlertEnvelope alert) { return _nativeChannel.publishAlert(alert); } Future _enqueueCommand(WatchCommandEnvelope command) { final run = _commandTail.then( (_) => _handleCommand(command), onError: (_) => _handleCommand(command), ); _commandTail = run.then((_) {}, onError: (_) {}); return run; } Future _handleCommand(WatchCommandEnvelope command) async { final key = _WatchAdapterCommandKey(command); final cachedAck = _commandAcks[key]; if (cachedAck != null) { await _sendAck(command, WatchCommandAck.acceptedNoOp); return; } final ack = await _commandIngress.dispatch(command); if (ack == WatchCommandAck.accepted || ack == WatchCommandAck.acceptedNoOp) { _rememberAck(key, ack); } await _sendAck(command, ack); } Future _sendAck( WatchCommandEnvelope command, WatchCommandAck ack, ) async { int? revisionAtAck; try { revisionAtAck = (await _projectionSource.currentProjection()).revision; } on Exception { revisionAtAck = _latestProjection?.revision; } await _nativeChannel.sendCommandAck( command, ack, revisionAtAck: revisionAtAck, ); } void _rememberAck(_WatchAdapterCommandKey key, WatchCommandAck ack) { _commandAcks[key] = ack; if (_commandAcks.length <= 128) { return; } _commandAcks.remove(_commandAcks.keys.first); } Future _syncForegroundService(WatchSessionProjection projection) async { final shouldRun = projection.phase != WatchSessionPhase.noActiveSession && projection.deviceSessionId.isNotEmpty; if (shouldRun == _foregroundActive) { return; } _foregroundActive = shouldRun; if (shouldRun) { await _nativeChannel.startForegroundService(); } else { await _nativeChannel.stopForegroundService(); } } void _ensureProjectionRefreshLoop() { _projectionRefreshTimer ??= Timer.periodic(_projectionRefreshInterval, (_) { unawaited(_publishHeartbeat()); }); } Future _publishHeartbeat() async { final projection = await _emitCurrentProjectionSkippingSourceEcho(); await _publishProjection(projection, urgent: false, force: true); } Future _forceProjectionResync() async { final projection = await _emitCurrentProjectionSkippingSourceEcho(); await _publishProjection(projection, urgent: true, force: true); } Future _emitCurrentProjectionSkippingSourceEcho() async { _skipNextProjectionEmissionForForcedResync = true; try { return await _projectionSource.emitCurrentProjection(); } finally { unawaited( Future.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 left, List 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(List left, List 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 { _WatchAdapterCommandKey(WatchCommandEnvelope command) : sessionId = command.sessionId, expectedRevision = command.expectedRevision, commandId = command.commandId, type = command.type; final String sessionId; final int expectedRevision; final String commandId; final WatchCommandType type; @override bool operator ==(Object other) { return identical(this, other) || other is _WatchAdapterCommandKey && sessionId == other.sessionId && expectedRevision == other.expectedRevision && commandId == other.commandId && type == other.type; } @override int get hashCode => Object.hash(sessionId, expectedRevision, commandId, type); }