feat(watch): Wear OS companion app - UX surfaces + bridge client (#91-E, #91-F)
This commit is contained in:
@ -0,0 +1,159 @@
|
||||
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;
|
||||
}
|
||||
|
||||
final class WatchCommandAckEvent {
|
||||
const WatchCommandAckEvent({
|
||||
required this.commandId,
|
||||
required this.status,
|
||||
required this.sessionId,
|
||||
this.revisionAtAck,
|
||||
this.reasonCode,
|
||||
});
|
||||
|
||||
final String commandId;
|
||||
final WatchCommandAck status;
|
||||
final String sessionId;
|
||||
final int? revisionAtAck;
|
||||
final String? reasonCode;
|
||||
}
|
||||
|
||||
abstract interface class NativeWatchBridgeClient {
|
||||
Stream<WatchSessionProjection> get projections;
|
||||
|
||||
Stream<WatchCommandAckEvent> get acks;
|
||||
|
||||
Stream<WatchBridgeConnectionEvent> get connectionEvents;
|
||||
|
||||
Future<void> sendCommand(WatchCommandEnvelope command);
|
||||
|
||||
Future<void> requestResync();
|
||||
|
||||
Future<void> requestCapabilityRefresh();
|
||||
}
|
||||
|
||||
final class MethodChannelNativeWatchBridgeClient
|
||||
implements NativeWatchBridgeClient {
|
||||
const MethodChannelNativeWatchBridgeClient({
|
||||
MethodChannel methodChannel = const MethodChannel(_methodChannelName),
|
||||
EventChannel projectionChannel = const EventChannel(
|
||||
_projectionChannelName,
|
||||
),
|
||||
EventChannel ackChannel = const EventChannel(_ackChannelName),
|
||||
EventChannel connectionChannel = const EventChannel(
|
||||
_connectionChannelName,
|
||||
),
|
||||
}) : _methodChannel = methodChannel,
|
||||
_projectionChannel = projectionChannel,
|
||||
_ackChannel = ackChannel,
|
||||
_connectionChannel = connectionChannel;
|
||||
|
||||
static const _methodChannelName = 'gametime.watch_bridge/methods';
|
||||
static const _projectionChannelName = 'gametime.watch_bridge/projections';
|
||||
static const _ackChannelName = 'gametime.watch_bridge/acks';
|
||||
static const _connectionChannelName = 'gametime.watch_bridge/connection';
|
||||
|
||||
final MethodChannel _methodChannel;
|
||||
final EventChannel _projectionChannel;
|
||||
final EventChannel _ackChannel;
|
||||
final EventChannel _connectionChannel;
|
||||
|
||||
@override
|
||||
Stream<WatchSessionProjection> get projections {
|
||||
return _projectionChannel
|
||||
.receiveBroadcastStream()
|
||||
.where((event) => event is Map)
|
||||
.map((event) {
|
||||
return WatchSessionProjection.fromJson(_stringObjectMap(event));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<WatchCommandAckEvent> get acks {
|
||||
return _ackChannel
|
||||
.receiveBroadcastStream()
|
||||
.where((event) => event is Map)
|
||||
.map((event) {
|
||||
final json = _stringObjectMap(event);
|
||||
return WatchCommandAckEvent(
|
||||
commandId: _stringFromJson(json['commandId']),
|
||||
status: _enumFromJson(
|
||||
json['status'],
|
||||
WatchCommandAck.values,
|
||||
WatchCommandAck.rejectedPhoneBusy,
|
||||
),
|
||||
sessionId: _stringFromJson(json['sessionId']),
|
||||
revisionAtAck: _nullableIntFromJson(json['revisionAtAck']),
|
||||
reasonCode: _nullableStringFromJson(json['reasonCode']),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<WatchBridgeConnectionEvent> get connectionEvents {
|
||||
return _connectionChannel
|
||||
.receiveBroadcastStream()
|
||||
.where((event) => event is Map)
|
||||
.map((event) {
|
||||
final json = _stringObjectMap(event);
|
||||
return WatchBridgeConnectionEvent(
|
||||
isReachable: json['isReachable'] == true,
|
||||
requestsResync: json['requestsResync'] == true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> sendCommand(WatchCommandEnvelope command) {
|
||||
return _methodChannel.invokeMethod<void>('sendCommand', command.toJson());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestCapabilityRefresh() {
|
||||
return _methodChannel.invokeMethod<void>('requestCapabilityRefresh');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestResync() {
|
||||
return _methodChannel.invokeMethod<void>('requestResync');
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object?> _stringObjectMap(Object? value) {
|
||||
if (value is Map) {
|
||||
return value.map((key, value) => MapEntry(key.toString(), value));
|
||||
}
|
||||
return const {};
|
||||
}
|
||||
|
||||
String _stringFromJson(Object? value) {
|
||||
return value is String ? value : '';
|
||||
}
|
||||
|
||||
String? _nullableStringFromJson(Object? value) {
|
||||
return value is String ? value : null;
|
||||
}
|
||||
|
||||
int? _nullableIntFromJson(Object? value) {
|
||||
return value is int ? value : value is num ? value.toInt() : null;
|
||||
}
|
||||
|
||||
T _enumFromJson<T extends Enum>(Object? value, List<T> values, T fallback) {
|
||||
if (value is String) {
|
||||
for (final enumValue in values) {
|
||||
if (enumValue.name == value) {
|
||||
return enumValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user