This commit is contained in:
@ -106,6 +106,7 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
_subscriptions.add(_nativeClient.projections.listen(_handleProjection));
|
||||
_subscriptions.add(_nativeClient.sensorSamples.listen(_handleSensorSample));
|
||||
_subscriptions.add(_nativeClient.acks.listen(_handleAck));
|
||||
_subscriptions.add(_nativeClient.alerts.listen(_handleAlert));
|
||||
_subscriptions.add(
|
||||
_nativeClient.connectionEvents.listen(_handleConnectionEvent),
|
||||
);
|
||||
@ -122,6 +123,7 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
final Duration _staleProjectionThreshold;
|
||||
final Duration _connectionLostThreshold;
|
||||
final _subscriptions = <StreamSubscription<dynamic>>[];
|
||||
final _handledAlertIds = <String>{};
|
||||
|
||||
Timer? _waitingTimer;
|
||||
Timer? _commandTimeoutTimer;
|
||||
@ -379,6 +381,26 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleAlert(WatchAlertEnvelope alert) {
|
||||
final alertId = alert.alertId.trim();
|
||||
final nowMs = DateTime.now().toUtc().millisecondsSinceEpoch;
|
||||
if (alertId.isEmpty ||
|
||||
!_handledAlertIds.add(alertId) ||
|
||||
alert.sessionId != value.projection.deviceSessionId ||
|
||||
(alert.expiresAtEpochMs > 0 && nowMs > alert.expiresAtEpochMs)) {
|
||||
return;
|
||||
}
|
||||
if (_handledAlertIds.length > 64) {
|
||||
_handledAlertIds.remove(_handledAlertIds.first);
|
||||
}
|
||||
switch (alert.pattern) {
|
||||
case WatchAlertPattern.countdownTick:
|
||||
unawaited(HapticFeedback.selectionClick());
|
||||
case WatchAlertPattern.timerFinished:
|
||||
_triggerTimerFinishedHaptic();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleConnectionEvent(WatchBridgeConnectionEvent event) {
|
||||
value = value.copyWith(connectionLost: !event.isReachable);
|
||||
if (event.isReachable || event.requestsResync) {
|
||||
@ -392,15 +414,12 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
return;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final expiresAtEpochMs = value.projection.expiresAtEpochMs;
|
||||
final fallbackExpired =
|
||||
expiresAtEpochMs <= 0 &&
|
||||
now.difference(receivedAt) >= const Duration(seconds: 12);
|
||||
final expiryAge = Duration(
|
||||
milliseconds: _projectionTtlMs(value.projection),
|
||||
);
|
||||
final expired =
|
||||
value.projection.deviceSessionId.isNotEmpty &&
|
||||
(fallbackExpired ||
|
||||
(expiresAtEpochMs > 0 &&
|
||||
now.toUtc().millisecondsSinceEpoch >= expiresAtEpochMs)) &&
|
||||
now.difference(receivedAt) >= expiryAge &&
|
||||
_pendingCommand == null &&
|
||||
_pendingScoreCommandIds.isEmpty;
|
||||
if (expired) {
|
||||
@ -444,22 +463,15 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
if (projection.deviceSessionId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final nowMs = DateTime.now().toUtc().millisecondsSinceEpoch;
|
||||
final expiresAtEpochMs = projection.expiresAtEpochMs > 0
|
||||
? projection.expiresAtEpochMs
|
||||
: nowMs + const Duration(seconds: 12).inMilliseconds;
|
||||
final delayMs = expiresAtEpochMs - nowMs;
|
||||
_projectionExpiryTimer = Timer(
|
||||
Duration(milliseconds: delayMs <= 0 ? 0 : delayMs),
|
||||
() {
|
||||
if (value.projection.deviceSessionId.isEmpty ||
|
||||
_pendingCommand != null ||
|
||||
_pendingScoreCommandIds.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
_invalidateExpiredProjection();
|
||||
},
|
||||
);
|
||||
final delayMs = _projectionTtlMs(projection);
|
||||
_projectionExpiryTimer = Timer(Duration(milliseconds: delayMs), () {
|
||||
if (value.projection.deviceSessionId.isEmpty ||
|
||||
_pendingCommand != null ||
|
||||
_pendingScoreCommandIds.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
_invalidateExpiredProjection();
|
||||
});
|
||||
}
|
||||
|
||||
void _clearCommandTimers() {
|
||||
@ -538,6 +550,29 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _triggerTimerFinishedHaptic() {
|
||||
unawaited(HapticFeedback.heavyImpact());
|
||||
unawaited(
|
||||
Future<void>.delayed(const Duration(milliseconds: 140), () {
|
||||
return HapticFeedback.heavyImpact();
|
||||
}),
|
||||
);
|
||||
unawaited(
|
||||
Future<void>.delayed(const Duration(milliseconds: 320), () {
|
||||
return HapticFeedback.heavyImpact();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int _projectionTtlMs(WatchSessionProjection projection) {
|
||||
final projectedAtEpochMs = projection.projectedAtEpochMs;
|
||||
final expiresAtEpochMs = projection.expiresAtEpochMs;
|
||||
if (projectedAtEpochMs > 0 && expiresAtEpochMs > projectedAtEpochMs) {
|
||||
return expiresAtEpochMs - projectedAtEpochMs;
|
||||
}
|
||||
return const Duration(seconds: 12).inMilliseconds;
|
||||
}
|
||||
|
||||
bool _isRejected(WatchCommandAck ack) {
|
||||
|
||||
Reference in New Issue
Block a user