import 'dart:async'; import 'package:flutter/services.dart'; import '../models/bluetooth_device_info.dart'; import '../models/bluetooth_power_state.dart'; import '../models/gps_speed.dart'; import '../models/location_reading.dart'; import '../models/obd_state.dart'; class TelemetryException implements Exception { TelemetryException(this.message, {this.code}); final String message; /// Stable native error code, when one was supplied. final String? code; @override String toString() => message; } /// Bluetooth OBD and location boundary. Kept separate from LauncherPlatform so /// package/launcher fakes do not need telemetry methods. abstract class TelemetryPlatform { Future> listBondedDevices(); Future bluetoothState(); Future openBluetoothSettings(); Future ensureBluetoothPermission(); Future ensureLocationPermission(); Future getLocation(); Future connectObd(String address); Future disconnectObd(); Future getObdState(); /// Starts GPS speed updates only when precise location is already allowed. Future startGpsSpeed(); Future stopGpsSpeed(); Future getGpsSpeed(); } class NativeTelemetryBridge implements TelemetryPlatform { static const _channel = MethodChannel('com.wasp.cardash_launcher/telemetry'); static const _listTimeout = Duration(seconds: 6); static const _stateTimeout = Duration(seconds: 3); static const _permissionTimeout = Duration(seconds: 30); static const _connectTimeout = Duration(seconds: 5); static const _disconnectTimeout = Duration(seconds: 3); static const _obdStateTimeout = Duration(seconds: 2); static const _settingsTimeout = Duration(seconds: 3); @override Future> listBondedDevices() async { try { final raw = await _channel .invokeMethod('listBondedDevices') .timeout(_listTimeout); if (raw is! List) return const []; return raw .whereType() .map(BluetoothDeviceInfo.fromMap) .where((device) => device.address.isNotEmpty) .toList(growable: false); } on TimeoutException { throw TelemetryException('Bluetooth is not responding.', code: 'timeout'); } on PlatformException catch (error) { throw TelemetryException( error.message ?? 'Could not list Bluetooth devices.', code: error.code, ); } on MissingPluginException { throw TelemetryException('The telemetry bridge is unavailable.'); } } @override Future bluetoothState() async { try { final raw = await _channel .invokeMethod('bluetoothState') .timeout(_stateTimeout); return BluetoothPowerState.fromWire(raw); } on TimeoutException { return BluetoothPowerState.unknown; } on PlatformException { return BluetoothPowerState.unavailable; } on MissingPluginException { return BluetoothPowerState.unavailable; } } @override Future openBluetoothSettings() async { try { return await _channel .invokeMethod('openBluetoothSettings') .timeout(_settingsTimeout) ?? false; } on TimeoutException { return false; } on PlatformException { return false; } on MissingPluginException { return false; } } @override Future ensureBluetoothPermission() async { try { return await _channel .invokeMethod('ensureBluetoothPermission') .timeout(_permissionTimeout) ?? false; } on TimeoutException { throw TelemetryException( 'Bluetooth permission request timed out.', code: 'timeout', ); } on PlatformException { return false; } on MissingPluginException { return false; } } @override Future ensureLocationPermission() async { try { return await _channel .invokeMethod('ensureLocationPermission') .timeout(_permissionTimeout) ?? false; } on TimeoutException { return false; } on PlatformException { return false; } on MissingPluginException { return false; } } @override Future getLocation() async { try { final raw = await _channel .invokeMethod('getLocation') .timeout(const Duration(seconds: 20)); if (raw is! Map) { throw TelemetryException('Could not read the current location.'); } return LocationReading.fromMap(raw); } on TimeoutException { throw TelemetryException('Could not read the current location.'); } on PlatformException catch (error) { throw TelemetryException( error.message ?? 'Could not read location.', code: error.code, ); } on MissingPluginException { throw TelemetryException('The telemetry bridge is unavailable.'); } } @override Future connectObd(String address) async { try { await _channel .invokeMethod('connectObd', {'address': address}) .timeout(_connectTimeout); } on TimeoutException { throw TelemetryException('Bluetooth is not responding.', code: 'timeout'); } on PlatformException catch (error) { throw TelemetryException( error.message ?? 'Could not connect to OBD.', code: error.code, ); } on MissingPluginException { throw TelemetryException('The telemetry bridge is unavailable.'); } } @override Future disconnectObd() async { try { await _channel .invokeMethod('disconnectObd') .timeout(_disconnectTimeout); } on TimeoutException { // The native stop is best effort; never hold up the UI for it. } on PlatformException { // Nothing useful to do when already disconnected. } on MissingPluginException { // No-op outside Android. } } @override Future startGpsSpeed() async { try { return await _channel .invokeMethod('startGpsSpeed') .timeout(const Duration(seconds: 3)) ?? false; } on TimeoutException { return false; } on PlatformException { return false; } on MissingPluginException { return false; } } @override Future stopGpsSpeed() async { try { await _channel .invokeMethod('stopGpsSpeed') .timeout(const Duration(seconds: 2)); } on TimeoutException { // Best effort. } on PlatformException { // Nothing useful to do when already stopped. } on MissingPluginException { // No-op outside Android. } } @override Future getGpsSpeed() async { try { final raw = await _channel .invokeMethod('getGpsSpeed') .timeout(const Duration(seconds: 2)); return raw is Map ? GpsSpeed.fromMap(raw) : const GpsSpeed(); } on TimeoutException { return const GpsSpeed(); } on PlatformException { return const GpsSpeed(); } on MissingPluginException { return const GpsSpeed(); } } @override Future getObdState() async { try { final raw = await _channel .invokeMethod('getObdState') .timeout(_obdStateTimeout); return raw is Map ? ObdState.fromMap(raw) : const ObdState(); } on TimeoutException { throw TelemetryException('OBD state is not responding.', code: 'timeout'); } on PlatformException { return const ObdState(); } on MissingPluginException { return const ObdState(); } } }