Files
GameTime/lib/infrastructure/watch_bridge/native_watch_bridge_channel.dart
Blomios 65d43b9768 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>
2026-07-28 05:56:12 +02:00

180 lines
5.1 KiB
Dart

import 'dart:async';
import 'package:flutter/services.dart';
import 'package:watch_bridge_contract/watch_bridge_contract.dart';
final class WatchBridgeConnectionEvent {
const WatchBridgeConnectionEvent({
required this.isReachable,
this.requestsResync = false,
});
final bool isReachable;
final bool requestsResync;
}
abstract interface class WatchBridgeNativeChannel {
Stream<WatchCommandEnvelope> get commands;
Stream<WatchSensorSummary> get sensorSummaries;
Stream<WatchSensorSample> get sensorSamples;
Stream<WatchBridgeConnectionEvent> get connectionEvents;
Future<void> publishProjection(WatchSessionProjection projection);
Future<void> sendCommandAck(
WatchCommandEnvelope command,
WatchCommandAck ack, {
int? revisionAtAck,
});
Future<void> requestCapabilityRefresh();
Future<void> startForegroundService();
Future<void> stopForegroundService();
}
final class MethodChannelWatchBridgeNativeChannel
implements WatchBridgeNativeChannel {
const MethodChannelWatchBridgeNativeChannel({
MethodChannel methodChannel = const MethodChannel(_methodChannelName),
EventChannel commandChannel = const EventChannel(_commandChannelName),
EventChannel sensorSummaryChannel = const EventChannel(
_sensorSummaryChannelName,
),
EventChannel sensorSampleChannel = const EventChannel(
_sensorSampleChannelName,
),
EventChannel connectionChannel = const EventChannel(_connectionChannelName),
}) : _methodChannel = methodChannel,
_commandChannel = commandChannel,
_sensorSummaryChannel = sensorSummaryChannel,
_sensorSampleChannel = sensorSampleChannel,
_connectionChannel = connectionChannel;
static const _methodChannelName = 'gametime.watch_bridge/methods';
static const _commandChannelName = 'gametime.watch_bridge/commands';
static const _sensorSummaryChannelName =
'gametime.watch_bridge/sensor_summaries';
static const _sensorSampleChannelName =
'gametime.watch_bridge/sensor_samples';
static const _connectionChannelName = 'gametime.watch_bridge/connection';
final MethodChannel _methodChannel;
final EventChannel _commandChannel;
final EventChannel _sensorSummaryChannel;
final EventChannel _sensorSampleChannel;
final EventChannel _connectionChannel;
@override
Stream<WatchCommandEnvelope> get commands {
return _commandChannel
.receiveBroadcastStream()
.where((event) {
return event is Map;
})
.map((event) {
return WatchCommandEnvelope.fromJson(_stringObjectMap(event));
});
}
@override
Stream<WatchSensorSummary> get sensorSummaries {
return _sensorSummaryChannel
.receiveBroadcastStream()
.where((event) {
return event is Map;
})
.map((event) {
return WatchSensorSummary.fromJson(_stringObjectMap(event));
});
}
@override
Stream<WatchSensorSample> get sensorSamples {
return _sensorSampleChannel
.receiveBroadcastStream()
.where((event) {
return event is Map;
})
.map((event) {
return WatchSensorSample.fromJson(_stringObjectMap(event));
});
}
@override
Stream<WatchBridgeConnectionEvent> get connectionEvents {
return _connectionChannel
.receiveBroadcastStream()
.where((event) {
return event is Map;
})
.map((event) {
final json = _stringObjectMap(event);
return WatchBridgeConnectionEvent(
isReachable: json['isReachable'] == true,
requestsResync: json['requestsResync'] == true,
);
});
}
@override
Future<void> publishProjection(WatchSessionProjection projection) {
return _invokeIgnoringMissingPlugin(
'publishProjection',
projection.toJson(),
);
}
@override
Future<void> requestCapabilityRefresh() {
return _invokeIgnoringMissingPlugin('requestCapabilityRefresh');
}
@override
Future<void> sendCommandAck(
WatchCommandEnvelope command,
WatchCommandAck ack, {
int? revisionAtAck,
}) {
return _invokeIgnoringMissingPlugin('sendCommandAck', {
'schemaVersion': watchBridgeSchemaVersion,
'commandId': command.commandId,
'sessionId': command.sessionId,
'expectedRevision': command.expectedRevision,
'status': ack.name,
'revisionAtAck': revisionAtAck,
'ackedAtEpochMs': DateTime.now().toUtc().millisecondsSinceEpoch,
});
}
@override
Future<void> startForegroundService() {
return _invokeIgnoringMissingPlugin('startForegroundService');
}
@override
Future<void> stopForegroundService() {
return _invokeIgnoringMissingPlugin('stopForegroundService');
}
Future<void> _invokeIgnoringMissingPlugin(
String method, [
Object? arguments,
]) {
return _methodChannel
.invokeMethod<void>(method, arguments)
.onError<MissingPluginException>((_, _) {});
}
}
Map<String, Object?> _stringObjectMap(Object? value) {
if (value is Map) {
return value.map((key, value) => MapEntry(key.toString(), value));
}
return const {};
}