|
|
|
@ -18,60 +18,44 @@ enum DeviceType { |
|
|
|
class BluetoothService extends GetxService { |
|
|
|
static BluetoothService get to => Get.find<BluetoothService>(); |
|
|
|
|
|
|
|
// 单例实例 |
|
|
|
// 内部实例 |
|
|
|
BluetoothService._internal(); |
|
|
|
static final BluetoothService _instance = BluetoothService._internal(); |
|
|
|
factory BluetoothService() => _instance; |
|
|
|
BluetoothService._internal(); |
|
|
|
|
|
|
|
// 设备状态 |
|
|
|
// ====== 状态管理 ====== |
|
|
|
// 蓝牙适配器状态 |
|
|
|
final _isAvailable = false.obs; |
|
|
|
final _isOn = false.obs; |
|
|
|
final _isInitialized = false.obs; |
|
|
|
|
|
|
|
// 已连接的设备 |
|
|
|
final _connectedDevices = <BluetoothDevice>[].obs; |
|
|
|
final _systemPairedDevices = <BluetoothDevice>[].obs; |
|
|
|
|
|
|
|
// 扫描状态 |
|
|
|
// 操作状态 |
|
|
|
final _isScanning = false.obs; |
|
|
|
final _permissionsGranted = false.obs; |
|
|
|
|
|
|
|
// 扫描结果 |
|
|
|
// 设备管理 |
|
|
|
final _connectedDevices = <BluetoothDevice>[].obs; |
|
|
|
final _systemPairedDevices = <BluetoothDevice>[].obs; |
|
|
|
final _scanResults = <ScanResult>[].obs; |
|
|
|
|
|
|
|
// 当前选中的设备 |
|
|
|
final _selectedDevice = Rx<BluetoothDevice?>(null); |
|
|
|
final _deviceType = Rx<DeviceType>(DeviceType.unknown); |
|
|
|
|
|
|
|
// 连接状态 |
|
|
|
final _connectionState = Rx<BluetoothConnectionState>(BluetoothConnectionState.disconnected); |
|
|
|
|
|
|
|
// 存储最后连接的设备信息,供其他控制器使用 |
|
|
|
final _lastConnectedDeviceInfo = Rx<Map<String, dynamic>>({}); |
|
|
|
|
|
|
|
// 订阅 |
|
|
|
// 流订阅 |
|
|
|
StreamSubscription? _stateSubscription; |
|
|
|
StreamSubscription? _scanSubscription; |
|
|
|
StreamSubscription? _connectionSubscription; |
|
|
|
|
|
|
|
// 连接锁,防止并发操作 |
|
|
|
bool _isConnecting = false; |
|
|
|
|
|
|
|
// 记录连接尝试次数 |
|
|
|
int _connectionAttempts = 0; |
|
|
|
static const int MAX_CONNECTION_ATTEMPTS = 2; |
|
|
|
|
|
|
|
// 权限状态 |
|
|
|
final _permissionsGranted = false.obs; |
|
|
|
bool get permissionsGranted => _permissionsGranted.value; |
|
|
|
|
|
|
|
// 初始化完成标志 |
|
|
|
final _isInitialized = false.obs; |
|
|
|
bool get isInitialized => _isInitialized.value; |
|
|
|
RxBool get isInitializedObs => _isInitialized; |
|
|
|
|
|
|
|
// Getters |
|
|
|
// ====== Getters ====== |
|
|
|
// 基本getter |
|
|
|
bool get isAvailable => _isAvailable.value; |
|
|
|
bool get isOn => _isOn.value; |
|
|
|
bool get isInitialized => _isInitialized.value; |
|
|
|
bool get isScanning => _isScanning.value; |
|
|
|
bool get permissionsGranted => _permissionsGranted.value; |
|
|
|
|
|
|
|
// 设备相关getter |
|
|
|
List<BluetoothDevice> get connectedDevices => _connectedDevices; |
|
|
|
List<BluetoothDevice> get systemPairedDevices => _systemPairedDevices; |
|
|
|
List<ScanResult> get scanResults => _scanResults; |
|
|
|
@ -80,9 +64,12 @@ class BluetoothService extends GetxService { |
|
|
|
BluetoothConnectionState get connectionState => _connectionState.value; |
|
|
|
Map<String, dynamic> get lastConnectedDeviceInfo => _lastConnectedDeviceInfo.value; |
|
|
|
|
|
|
|
// Observable getters (for reactive programming) |
|
|
|
// Observable getters |
|
|
|
RxBool get isAvailableObs => _isAvailable; |
|
|
|
RxBool get isOnObs => _isOn; |
|
|
|
RxBool get isInitializedObs => _isInitialized; |
|
|
|
RxBool get isScanningObs => _isScanning; |
|
|
|
RxBool get permissionsGrantedObs => _permissionsGranted; |
|
|
|
RxList<BluetoothDevice> get connectedDevicesObs => _connectedDevices; |
|
|
|
RxList<ScanResult> get scanResultsObs => _scanResults; |
|
|
|
Rx<BluetoothDevice?> get selectedDeviceObs => _selectedDevice; |
|
|
|
@ -90,242 +77,212 @@ class BluetoothService extends GetxService { |
|
|
|
Rx<BluetoothConnectionState> get connectionStateObs => _connectionState; |
|
|
|
Rx<Map<String, dynamic>> get lastConnectedDeviceInfoObs => _lastConnectedDeviceInfo; |
|
|
|
|
|
|
|
// 电池服务UUID |
|
|
|
static const String BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb"; |
|
|
|
// 电池电量特征UUID |
|
|
|
static const String BATTERY_CHARACTERISTIC_UUID = "00002a19-0000-1000-8000-00805f9b34fb"; |
|
|
|
|
|
|
|
/// 初始化蓝牙服务 |
|
|
|
Future<BluetoothService> init() async { |
|
|
|
try { |
|
|
|
if (_isInitialized.value) { |
|
|
|
Logger.info('蓝牙服务已初始化,无需重复初始化'); |
|
|
|
return this; |
|
|
|
} |
|
|
|
|
|
|
|
Logger.info('开始初始化蓝牙服务'); |
|
|
|
|
|
|
|
// 先请求权限,确保权限请求完成后再进行后续操作 |
|
|
|
if (Platform.isAndroid) { |
|
|
|
Logger.info('Android平台,请求蓝牙权限'); |
|
|
|
await _checkAndRequestPermissions(); |
|
|
|
try { |
|
|
|
// 1. 设置蓝牙状态监听 |
|
|
|
_setupBluetoothListeners(); |
|
|
|
|
|
|
|
// 2. 初始检查蓝牙状态 |
|
|
|
_checkBluetoothState(); |
|
|
|
|
|
|
|
// 3. 标记初始化完成 |
|
|
|
_isInitialized.value = true; |
|
|
|
Logger.info('蓝牙服务初始化完成'); |
|
|
|
return this; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('初始化蓝牙服务失败: $e'); |
|
|
|
return this; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 设置蓝牙状态监听 |
|
|
|
/// 设置蓝牙状态监听 |
|
|
|
void _setupBluetoothListeners() { |
|
|
|
// 取消旧订阅(如果有) |
|
|
|
_stateSubscription?.cancel(); |
|
|
|
|
|
|
|
// 监听蓝牙适配器状态变化 |
|
|
|
_stateSubscription = FlutterBluePlus.adapterState.listen((state) { |
|
|
|
_isOn.value = state == BluetoothAdapterState.on; |
|
|
|
_isAvailable.value = state != BluetoothAdapterState.unavailable; |
|
|
|
|
|
|
|
if (state == BluetoothAdapterState.on) { |
|
|
|
// 蓝牙打开时,获取已连接和已配对设备 |
|
|
|
Logger.info('蓝牙已开启'); |
|
|
|
if (_permissionsGranted.value) { |
|
|
|
_refreshSystemDevices(); |
|
|
|
} |
|
|
|
} else if (state == BluetoothAdapterState.off) { |
|
|
|
// 蓝牙关闭时,清空设备列表 |
|
|
|
_connectedDevices.clear(); |
|
|
|
_systemPairedDevices.clear(); |
|
|
|
_scanResults.clear(); |
|
|
|
_selectedDevice.value = null; |
|
|
|
_lastConnectedDeviceInfo.value = {}; |
|
|
|
_deviceType.value = DeviceType.unknown; |
|
|
|
Logger.info('蓝牙已关闭'); |
|
|
|
_resetDeviceState(); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 检查蓝牙状态 |
|
|
|
/// 初始检查蓝牙状态 |
|
|
|
Future<void> _checkBluetoothState() async { |
|
|
|
try { |
|
|
|
bool isOn = await FlutterBluePlus.isOn |
|
|
|
.timeout(const Duration(seconds: 5), onTimeout: () { |
|
|
|
Logger.error('检查蓝牙状态超时'); |
|
|
|
return false; |
|
|
|
}); |
|
|
|
.timeout(const Duration(seconds: 5), onTimeout: () => false); |
|
|
|
|
|
|
|
_isOn.value = isOn; |
|
|
|
_isAvailable.value = isOn; |
|
|
|
|
|
|
|
if (isOn) { |
|
|
|
// 刷新设备状态 |
|
|
|
await _refreshSystemDevices(); |
|
|
|
} |
|
|
|
Logger.info('当前蓝牙是否已开启: $isOn'); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('检查蓝牙状态失败: $e'); |
|
|
|
_isOn.value = false; |
|
|
|
_isAvailable.value = false; |
|
|
|
} |
|
|
|
|
|
|
|
// 标记初始化完成 |
|
|
|
_isInitialized.value = true; |
|
|
|
Logger.info('蓝牙服务初始化完成'); |
|
|
|
return this; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('初始化蓝牙服务失败: $e'); |
|
|
|
return this; |
|
|
|
} |
|
|
|
|
|
|
|
/// 重置设备状态 |
|
|
|
void _resetDeviceState() { |
|
|
|
_connectedDevices.clear(); |
|
|
|
_systemPairedDevices.clear(); |
|
|
|
_scanResults.clear(); |
|
|
|
_selectedDevice.value = null; |
|
|
|
_lastConnectedDeviceInfo.value = {}; |
|
|
|
_deviceType.value = DeviceType.unknown; |
|
|
|
|
|
|
|
// 取消正在进行的操作 |
|
|
|
_scanSubscription?.cancel(); |
|
|
|
_connectionSubscription?.cancel(); |
|
|
|
_isScanning.value = false; |
|
|
|
} |
|
|
|
|
|
|
|
/// 检查并请求蓝牙所需权限(Android 12+) |
|
|
|
Future<bool> _checkAndRequestPermissions() async { |
|
|
|
/// 检查并请求蓝牙权限 |
|
|
|
Future<bool> requestPermissions() async { |
|
|
|
try { |
|
|
|
if (Platform.isAndroid) { |
|
|
|
// 对于Android 12+,需要BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限 |
|
|
|
Map<Permission, PermissionStatus> statuses = await [ |
|
|
|
// iOS无需额外权限 |
|
|
|
if (!Platform.isAndroid) { |
|
|
|
_permissionsGranted.value = true; |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
// 请求所有必要权限 |
|
|
|
final permissions = <Permission>[ |
|
|
|
Permission.bluetoothScan, |
|
|
|
Permission.bluetoothConnect, |
|
|
|
Permission.location, |
|
|
|
].request(); |
|
|
|
Permission.location, // Android需要定位权限来扫描BLE设备 |
|
|
|
Permission.microphone |
|
|
|
]; |
|
|
|
|
|
|
|
// 一次性请求所有权限 |
|
|
|
Map<Permission, PermissionStatus> statuses = await permissions.request(); |
|
|
|
|
|
|
|
// 检查所有权限是否已授予 |
|
|
|
bool allGranted = statuses.values.every((status) => status.isGranted); |
|
|
|
_permissionsGranted.value = allGranted; |
|
|
|
|
|
|
|
if (!allGranted) { |
|
|
|
Logger.warning('蓝牙权限未完全授予: $statuses'); |
|
|
|
// 检查哪些权限被拒绝 |
|
|
|
List<String> deniedPermissions = []; |
|
|
|
statuses.forEach((permission, status) { |
|
|
|
if (!status.isGranted) { |
|
|
|
deniedPermissions.add(permission.toString().split('.').last); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
return allGranted; |
|
|
|
Logger.warning('权限被拒绝: ${deniedPermissions.join(", ")}'); |
|
|
|
} |
|
|
|
|
|
|
|
// iOS不需要显式请求蓝牙权限 |
|
|
|
_permissionsGranted.value = true; |
|
|
|
return true; |
|
|
|
return allGranted; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('检查蓝牙权限失败: $e'); |
|
|
|
Logger.error('请求权限失败: $e'); |
|
|
|
_permissionsGranted.value = false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 再次请求权限 |
|
|
|
Future<bool> requestPermissions() async { |
|
|
|
return await _checkAndRequestPermissions(); |
|
|
|
} |
|
|
|
|
|
|
|
/// 刷新系统设备状态(已连接和已配对设备) |
|
|
|
/// 刷新系统蓝牙设备(已连接和已配对) |
|
|
|
Future<void> _refreshSystemDevices() async { |
|
|
|
try { |
|
|
|
// 避免重复刷新,使用标志位 |
|
|
|
bool _isRefreshing = false; |
|
|
|
if (_isRefreshing) return; |
|
|
|
|
|
|
|
_isRefreshing = true; |
|
|
|
if (!_permissionsGranted.value || !_isOn.value) { |
|
|
|
Logger.warning('无法刷新系统设备:蓝牙未开启或缺少权限'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 获取系统已连接设备 |
|
|
|
try { |
|
|
|
// 获取已连接的系统设备 |
|
|
|
List<BluetoothDevice> devices = await FlutterBluePlus.connectedSystemDevices |
|
|
|
.timeout(const Duration(seconds: 5), onTimeout: () { |
|
|
|
Logger.error('获取已连接设备超时'); |
|
|
|
return <BluetoothDevice>[]; |
|
|
|
}); |
|
|
|
.timeout(const Duration(seconds: 10), onTimeout: () => <BluetoothDevice>[]); |
|
|
|
|
|
|
|
_connectedDevices.value = devices; |
|
|
|
Logger.info('已连接系统设备数量: ${_connectedDevices.length}'); |
|
|
|
Logger.info('获取到${devices.length}个系统已连接设备'); |
|
|
|
|
|
|
|
// 如果有当前选中设备,更新其状态 |
|
|
|
// 更新选中设备的连接状态(如果有) |
|
|
|
if (_selectedDevice.value != null) { |
|
|
|
// 检查选中设备是否仍然在已连接列表中 |
|
|
|
bool stillConnected = _connectedDevices.any( |
|
|
|
(d) => d.remoteId.str == _selectedDevice.value!.remoteId.str |
|
|
|
); |
|
|
|
|
|
|
|
_connectionState.value = stillConnected |
|
|
|
? BluetoothConnectionState.connected |
|
|
|
: BluetoothConnectionState.disconnected; |
|
|
|
|
|
|
|
if (stillConnected) { |
|
|
|
_connectionState.value = BluetoothConnectionState.connected; |
|
|
|
_updateLastConnectedDeviceInfo(); |
|
|
|
} else { |
|
|
|
_connectionState.value = BluetoothConnectionState.disconnected; |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
Logger.error('获取已连接设备失败: $e'); |
|
|
|
} |
|
|
|
|
|
|
|
// 获取已配对设备 |
|
|
|
try { |
|
|
|
// 获取系统已配对设备 |
|
|
|
List<BluetoothDevice> pairedDevices = await FlutterBluePlus.bondedDevices |
|
|
|
.timeout(const Duration(seconds: 5), onTimeout: () { |
|
|
|
Logger.error('获取已配对设备超时'); |
|
|
|
return <BluetoothDevice>[]; |
|
|
|
}); |
|
|
|
.timeout(const Duration(seconds: 10), onTimeout: () => <BluetoothDevice>[]); |
|
|
|
|
|
|
|
_systemPairedDevices.value = pairedDevices; |
|
|
|
Logger.info('系统已配对设备数量: ${_systemPairedDevices.length}'); |
|
|
|
Logger.info('获取到${pairedDevices.length}个系统已配对设备'); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('获取已配对设备失败: $e'); |
|
|
|
Logger.error('刷新系统设备失败: $e'); |
|
|
|
} |
|
|
|
|
|
|
|
// 如果有已连接的设备但没有选中设备,自动选择第一个 |
|
|
|
if (_connectedDevices.isNotEmpty && _selectedDevice.value == null) { |
|
|
|
_selectedDevice.value = _connectedDevices[0]; |
|
|
|
_determineDeviceType(_selectedDevice.value!); |
|
|
|
_connectionState.value = BluetoothConnectionState.connected; |
|
|
|
_updateLastConnectedDeviceInfo(); |
|
|
|
} |
|
|
|
|
|
|
|
_isRefreshing = false; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('刷新系统设备状态失败: $e'); |
|
|
|
} |
|
|
|
/// 开始扫描附近的蓝牙设备 |
|
|
|
Future<List<ScanResult>> startScan({Duration? timeout}) async { |
|
|
|
// 检查条件 |
|
|
|
if (!_permissionsGranted.value) { |
|
|
|
Logger.warning('缺少蓝牙扫描权限,无法扫描设备'); |
|
|
|
return []; |
|
|
|
} |
|
|
|
|
|
|
|
/// 确定设备类型(经典蓝牙/BLE/双模) |
|
|
|
void _determineDeviceType(BluetoothDevice device) { |
|
|
|
try { |
|
|
|
// 默认设为未知类型 |
|
|
|
_deviceType.value = DeviceType.unknown; |
|
|
|
|
|
|
|
// 设备名称包含耳机/headphone/earphone/earbuds等关键词可能是音频设备 |
|
|
|
final String deviceName = device.platformName.toLowerCase(); |
|
|
|
final List<String> audioKeywords = [ |
|
|
|
'headphone', 'earphone', 'earbuds', 'headset', |
|
|
|
'speaker', 'audio', '耳机', '音箱', '音响' |
|
|
|
]; |
|
|
|
|
|
|
|
bool hasAudioKeyword = audioKeywords.any((keyword) => deviceName.contains(keyword)); |
|
|
|
|
|
|
|
// 如果在已连接设备列表中,很可能是已经通过系统连接的经典蓝牙 |
|
|
|
bool inConnectedList = _connectedDevices.any((d) => d.remoteId.str == device.remoteId.str); |
|
|
|
|
|
|
|
if (hasAudioKeyword || inConnectedList) { |
|
|
|
_deviceType.value = DeviceType.classicAudio; |
|
|
|
Logger.info('设备${device.platformName}被判定为经典蓝牙音频设备'); |
|
|
|
} else { |
|
|
|
// 不确定的情况,先假设是BLE设备,连接后可能会更新 |
|
|
|
_deviceType.value = DeviceType.ble; |
|
|
|
Logger.info('设备${device.platformName}被判定为BLE设备'); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
Logger.error('确定设备类型失败: $e'); |
|
|
|
_deviceType.value = DeviceType.unknown; |
|
|
|
} |
|
|
|
if (!_isOn.value) { |
|
|
|
Logger.warning('蓝牙未开启,无法扫描设备'); |
|
|
|
return []; |
|
|
|
} |
|
|
|
|
|
|
|
/// 开始扫描附近的蓝牙设备 |
|
|
|
Future<List<ScanResult>> startScan({Duration? timeout}) async { |
|
|
|
// 如果已经在扫描,先停止 |
|
|
|
if (_isScanning.value) { |
|
|
|
await stopScan(); |
|
|
|
} |
|
|
|
|
|
|
|
// 清空旧的扫描结果 |
|
|
|
_scanResults.clear(); |
|
|
|
Logger.info('开始扫描附近蓝牙设备...'); |
|
|
|
|
|
|
|
try { |
|
|
|
// 确保拥有扫描权限 |
|
|
|
if (Platform.isAndroid && !await Permission.bluetoothScan.isGranted) { |
|
|
|
Logger.warning('没有蓝牙扫描权限,尝试请求权限'); |
|
|
|
await _checkAndRequestPermissions(); |
|
|
|
} |
|
|
|
|
|
|
|
// 设置扫描监听 |
|
|
|
// 设置结果监听 |
|
|
|
_scanSubscription?.cancel(); |
|
|
|
_scanSubscription = FlutterBluePlus.scanResults.listen((results) { |
|
|
|
_scanResults.value = results; |
|
|
|
}); |
|
|
|
|
|
|
|
// 设置扫描中状态 |
|
|
|
_isScanning.value = true; |
|
|
|
|
|
|
|
// 开始扫描 |
|
|
|
_isScanning.value = true; |
|
|
|
await FlutterBluePlus.startScan( |
|
|
|
timeout: timeout ?? const Duration(seconds: 5), |
|
|
|
timeout: timeout ?? const Duration(seconds: 10), |
|
|
|
androidScanMode: AndroidScanMode.lowLatency, |
|
|
|
); |
|
|
|
|
|
|
|
// 扫描完成 |
|
|
|
_isScanning.value = false; |
|
|
|
Logger.info('蓝牙扫描完成,发现 ${_scanResults.length} 个设备'); |
|
|
|
|
|
|
|
Logger.info('扫描完成,发现${_scanResults.length}个设备'); |
|
|
|
return _scanResults; |
|
|
|
} catch (e) { |
|
|
|
_isScanning.value = false; |
|
|
|
Logger.error('蓝牙扫描失败: $e'); |
|
|
|
Logger.error('扫描设备失败: $e'); |
|
|
|
return []; |
|
|
|
} |
|
|
|
} |
|
|
|
@ -337,246 +294,261 @@ class BluetoothService extends GetxService { |
|
|
|
try { |
|
|
|
await FlutterBluePlus.stopScan(); |
|
|
|
_isScanning.value = false; |
|
|
|
Logger.info('已停止蓝牙扫描'); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('停止蓝牙扫描失败: $e'); |
|
|
|
Logger.error('停止扫描失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 连接到指定的蓝牙设备 |
|
|
|
/// 针对不同类型设备使用不同的连接策略 |
|
|
|
/// 连接到指定蓝牙设备 |
|
|
|
Future<bool> connectToDevice(BluetoothDevice device) async { |
|
|
|
// 防止并发连接操作 |
|
|
|
if (_isConnecting) { |
|
|
|
Logger.warning('已有正在进行的连接操作,请等待完成'); |
|
|
|
// 检查条件 |
|
|
|
if (!_permissionsGranted.value) { |
|
|
|
Logger.warning('缺少蓝牙连接权限,无法连接设备'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
_isConnecting = true; |
|
|
|
_connectionAttempts = 0; |
|
|
|
if (!_isOn.value) { |
|
|
|
Logger.warning('蓝牙未开启,无法连接设备'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
Logger.info('尝试连接设备: ${device.platformName} (${device.remoteId.str})'); |
|
|
|
Logger.info('尝试连接设备: ${device.platformName}'); |
|
|
|
|
|
|
|
try { |
|
|
|
// 确定设备类型 |
|
|
|
_determineDeviceType(device); |
|
|
|
|
|
|
|
// 检查设备是否已在系统连接列表中 |
|
|
|
// 检查设备是否已连接 |
|
|
|
bool alreadyConnected = _connectedDevices.any((d) => d.remoteId.str == device.remoteId.str); |
|
|
|
|
|
|
|
// 如果是当前已选择且已连接的设备,直接返回成功 |
|
|
|
// 如果当前已选择该设备且已连接,则无需再连接 |
|
|
|
if (_selectedDevice.value == device && alreadyConnected) { |
|
|
|
Logger.info('设备已连接,无需重新连接'); |
|
|
|
Logger.info('设备已连接,无需重复连接'); |
|
|
|
_connectionState.value = BluetoothConnectionState.connected; |
|
|
|
_updateLastConnectedDeviceInfo(); |
|
|
|
_isConnecting = false; |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
// 如果有其他已选择的设备,先取消监听并清除 |
|
|
|
// 如果已选择了其他设备,先断开 |
|
|
|
if (_selectedDevice.value != null && _selectedDevice.value != device) { |
|
|
|
_connectionSubscription?.cancel(); |
|
|
|
_connectionSubscription = null; |
|
|
|
await disconnectFromDevice(); |
|
|
|
} |
|
|
|
|
|
|
|
// 更新选中的设备 |
|
|
|
// 设置新的目标设备 |
|
|
|
_selectedDevice.value = device; |
|
|
|
// 对于经典蓝牙音频设备,不尝试BLE连接,直接检查系统连接状态 |
|
|
|
|
|
|
|
// 针对不同类型设备采取不同连接策略 |
|
|
|
if (_deviceType.value == DeviceType.classicAudio) { |
|
|
|
Logger.info('经典蓝牙音频设备,检查系统连接状态'); |
|
|
|
// 经典蓝牙设备(如音频设备)依赖系统连接 |
|
|
|
return _connectToClassicAudioDevice(device); |
|
|
|
} else { |
|
|
|
// BLE设备直接通过库连接 |
|
|
|
return _connectToBLEDevice(device); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
Logger.error('连接设备过程中出错: $e'); |
|
|
|
_connectionState.value = BluetoothConnectionState.disconnected; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 连接经典蓝牙音频设备 |
|
|
|
Future<bool> _connectToClassicAudioDevice(BluetoothDevice device) async { |
|
|
|
Logger.info('尝试连接经典蓝牙音频设备'); |
|
|
|
|
|
|
|
// 刷新系统连接设备列表 |
|
|
|
try { |
|
|
|
// 刷新系统设备列表 |
|
|
|
await _refreshSystemDevices(); |
|
|
|
|
|
|
|
// 再次检查设备是否在已连接列表中 |
|
|
|
alreadyConnected = _connectedDevices.any((d) => d.remoteId.str == device.remoteId.str); |
|
|
|
// 检查设备是否在系统已连接列表中 |
|
|
|
bool connected = _connectedDevices.any((d) => d.remoteId.str == device.remoteId.str); |
|
|
|
|
|
|
|
if (alreadyConnected) { |
|
|
|
Logger.info('设备已在系统连接列表中,视为已连接'); |
|
|
|
if (connected) { |
|
|
|
Logger.info('经典蓝牙设备已在系统连接列表中'); |
|
|
|
_connectionState.value = BluetoothConnectionState.connected; |
|
|
|
_monitorSystemConnectionState(device); |
|
|
|
_updateLastConnectedDeviceInfo(); |
|
|
|
_isConnecting = false; |
|
|
|
return true; |
|
|
|
} else { |
|
|
|
Logger.info('经典蓝牙设备未在系统连接列表中,需要在系统设置中配对和连接'); |
|
|
|
Logger.info('经典蓝牙设备未在系统连接列表中,需要在系统设置中进行配对'); |
|
|
|
_connectionState.value = BluetoothConnectionState.disconnected; |
|
|
|
_isConnecting = false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 对于BLE或双模设备,尝试进行BLE连接 |
|
|
|
Logger.info('尝试进行BLE连接'); |
|
|
|
return await _attemptBleConnection(device); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('连接设备 ${device.platformName} 失败: $e'); |
|
|
|
Logger.error('连接经典蓝牙设备失败: $e'); |
|
|
|
_connectionState.value = BluetoothConnectionState.disconnected; |
|
|
|
_isConnecting = false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 尝试进行BLE连接 |
|
|
|
Future<bool> _attemptBleConnection(BluetoothDevice device) async { |
|
|
|
try { |
|
|
|
/// 连接BLE设备 |
|
|
|
Future<bool> _connectToBLEDevice(BluetoothDevice device) async { |
|
|
|
Logger.info('尝试连接BLE设备'); |
|
|
|
_connectionState.value = BluetoothConnectionState.connecting; |
|
|
|
|
|
|
|
// 增加连接尝试次数 |
|
|
|
_connectionAttempts++; |
|
|
|
|
|
|
|
// 不使用autoConnect=true来避免与mtu参数冲突 |
|
|
|
// 根据错误日志,mtu和autoConnect是不兼容的 |
|
|
|
bool autoConnect = false; // 始终使用false避免兼容性问题 |
|
|
|
Logger.info('BLE连接尝试 #$_connectionAttempts,autoConnect=$autoConnect'); |
|
|
|
try { |
|
|
|
// 设置连接状态监听 |
|
|
|
_connectionSubscription?.cancel(); |
|
|
|
_connectionSubscription = device.connectionState.listen((state) { |
|
|
|
Logger.info('BLE连接状态变化: $state'); |
|
|
|
_connectionState.value = state; |
|
|
|
|
|
|
|
// 开始监听连接状态 |
|
|
|
_monitorConnectionState(device); |
|
|
|
if (state == BluetoothConnectionState.connected) { |
|
|
|
_updateLastConnectedDeviceInfo(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 尝试连接,不同的尝试使用不同的超时时间 |
|
|
|
if (_connectionAttempts == 1) { |
|
|
|
// 第一次尝试:短超时 |
|
|
|
Logger.info('第一次尝试使用短超时(4秒)'); |
|
|
|
// 尝试连接 |
|
|
|
await device.connect( |
|
|
|
autoConnect: autoConnect, |
|
|
|
timeout: const Duration(seconds: 4) |
|
|
|
autoConnect: false, |
|
|
|
timeout: const Duration(seconds: 10) |
|
|
|
); |
|
|
|
|
|
|
|
// 等待连接状态 |
|
|
|
final completer = Completer<bool>(); |
|
|
|
bool isCompleted = false; // 添加一个本地变量跟踪完成状态 |
|
|
|
|
|
|
|
// 检查当前状态 |
|
|
|
if (_connectionState.value == BluetoothConnectionState.connected) { |
|
|
|
completer.complete(true); |
|
|
|
isCompleted = true; |
|
|
|
} else { |
|
|
|
// 第二次尝试:长超时 |
|
|
|
Logger.info('第二次尝试使用长超时(8秒)'); |
|
|
|
await device.connect( |
|
|
|
autoConnect: autoConnect, |
|
|
|
timeout: const Duration(seconds: 8) |
|
|
|
); |
|
|
|
// 设置超时 |
|
|
|
Timer(const Duration(seconds: 12), () { |
|
|
|
if (!isCompleted) { // 使用本地变量而不是 completer.isCompleted |
|
|
|
Logger.warning('连接设备超时'); |
|
|
|
completer.complete(false); |
|
|
|
isCompleted = true; |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 如果连接成功,更新状态 |
|
|
|
_connectionState.value = BluetoothConnectionState.connected; |
|
|
|
bool result = await completer.future; |
|
|
|
|
|
|
|
if (result) { |
|
|
|
Logger.info('BLE设备连接成功'); |
|
|
|
_updateLastConnectedDeviceInfo(); |
|
|
|
} else { |
|
|
|
Logger.warning('BLE设备连接失败'); |
|
|
|
// 尝试断开连接 |
|
|
|
await device.disconnect().catchError((_) {}); |
|
|
|
} |
|
|
|
|
|
|
|
Logger.info('设备 ${device.platformName} 连接成功'); |
|
|
|
_isConnecting = false; |
|
|
|
return true; |
|
|
|
return result; |
|
|
|
} catch (e) { |
|
|
|
// 连接失败,检查是否需要重试 |
|
|
|
Logger.error('BLE连接尝试 #$_connectionAttempts 失败: $e'); |
|
|
|
|
|
|
|
if (_connectionAttempts < MAX_CONNECTION_ATTEMPTS) { |
|
|
|
Logger.info('尝试使用不同参数重新连接'); |
|
|
|
Logger.error('连接BLE设备失败: $e'); |
|
|
|
_connectionState.value = BluetoothConnectionState.disconnected; |
|
|
|
|
|
|
|
// 断开连接并等待一段时间 |
|
|
|
// 尝试断开连接 |
|
|
|
try { |
|
|
|
await device.disconnect(); |
|
|
|
} catch (disconnectError) { |
|
|
|
// 忽略断开连接的错误 |
|
|
|
Logger.error('断开连接时出错,继续尝试: $disconnectError'); |
|
|
|
} |
|
|
|
|
|
|
|
// 等待时间更长,确保设备和系统状态重置 |
|
|
|
await Future.delayed(const Duration(milliseconds: 2000)); |
|
|
|
} catch (_) {} |
|
|
|
|
|
|
|
// 重试连接 |
|
|
|
return await _attemptBleConnection(device); |
|
|
|
} else { |
|
|
|
// 连接失败且达到最大尝试次数 |
|
|
|
_connectionState.value = BluetoothConnectionState.disconnected; |
|
|
|
_isConnecting = false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 断开与当前设备的连接 |
|
|
|
/// 断开当前设备连接 |
|
|
|
Future<bool> disconnectFromDevice() async { |
|
|
|
// 没有已选择的设备 |
|
|
|
if (_selectedDevice.value == null) { |
|
|
|
Logger.info('没有当前选中的设备,无需断开连接'); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
// 取消设备监听 |
|
|
|
// 取消连接状态监听 |
|
|
|
_connectionSubscription?.cancel(); |
|
|
|
_connectionSubscription = null; |
|
|
|
|
|
|
|
try { |
|
|
|
// 对于经典蓝牙设备,不调用disconnect |
|
|
|
if (_deviceType.value == DeviceType.classicAudio) { |
|
|
|
Logger.info('经典蓝牙设备,仅清除选择状态,不调用disconnect'); |
|
|
|
_selectedDevice.value = null; |
|
|
|
_connectionState.value = BluetoothConnectionState.disconnected; |
|
|
|
return true; |
|
|
|
// 对BLE设备才需要执行断开操作 |
|
|
|
if (_deviceType.value != DeviceType.classicAudio) { |
|
|
|
await _selectedDevice.value!.disconnect(); |
|
|
|
} |
|
|
|
|
|
|
|
// 对于BLE设备,调用disconnect |
|
|
|
Logger.info('正在断开设备 ${_selectedDevice.value!.platformName} 的连接'); |
|
|
|
await _selectedDevice.value!.disconnect(); |
|
|
|
// 清除设备状态 |
|
|
|
_selectedDevice.value = null; |
|
|
|
_connectionState.value = BluetoothConnectionState.disconnected; |
|
|
|
|
|
|
|
return true; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('断开蓝牙设备连接失败: $e'); |
|
|
|
// 即使失败也清除状态 |
|
|
|
Logger.error('断开设备连接失败: $e'); |
|
|
|
|
|
|
|
// 即使失败,也清除选中设备 |
|
|
|
_selectedDevice.value = null; |
|
|
|
_connectionState.value = BluetoothConnectionState.disconnected; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 监控BLE连接状态 |
|
|
|
void _monitorConnectionState(BluetoothDevice device) { |
|
|
|
Logger.info('设置设备 ${device.platformName} 的BLE连接状态监听'); |
|
|
|
/// 查找系统已连接设备 |
|
|
|
Future<BluetoothDevice?> findConnectedSystemDevice() async { |
|
|
|
if (!_permissionsGranted.value || !_isOn.value) { |
|
|
|
Logger.warning('蓝牙未开启或缺少权限,无法查找系统连接设备'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
// 取消之前的监听 |
|
|
|
_connectionSubscription?.cancel(); |
|
|
|
try { |
|
|
|
// 刷新系统设备列表 |
|
|
|
await _refreshSystemDevices(); |
|
|
|
|
|
|
|
// 设置新的监听 |
|
|
|
_connectionSubscription = device.connectionState.listen((state) { |
|
|
|
Logger.info('设备 ${device.platformName} BLE连接状态变更为: $state'); |
|
|
|
_connectionState.value = state; |
|
|
|
if (_connectedDevices.isEmpty) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
if (state == BluetoothConnectionState.disconnected) { |
|
|
|
Logger.info('设备 ${device.platformName} BLE连接已断开'); |
|
|
|
// 设备断开连接时,不清除设备信息,以便下次可以重新连接 |
|
|
|
} else if (state == BluetoothConnectionState.connected) { |
|
|
|
Logger.info('设备 ${device.platformName} BLE连接已成功'); |
|
|
|
// 设备连接成功时,更新最后连接的设备信息 |
|
|
|
// 优先找出可能的音频设备 |
|
|
|
BluetoothDevice device = _findAudioDevice(_connectedDevices) ?? _connectedDevices.first; |
|
|
|
|
|
|
|
// 自动选择设备 |
|
|
|
_selectedDevice.value = device; |
|
|
|
_determineDeviceType(device); |
|
|
|
_connectionState.value = BluetoothConnectionState.connected; |
|
|
|
_updateLastConnectedDeviceInfo(); |
|
|
|
|
|
|
|
return device; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('查找系统已连接设备失败: $e'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
/// 监控系统连接状态(针对经典蓝牙设备) |
|
|
|
void _monitorSystemConnectionState(BluetoothDevice device) { |
|
|
|
Logger.info('设置设备 ${device.platformName} 的系统连接状态监听'); |
|
|
|
/// 查找音频设备 |
|
|
|
BluetoothDevice? _findAudioDevice(List<BluetoothDevice> devices) { |
|
|
|
if (devices.isEmpty) return null; |
|
|
|
|
|
|
|
// 使用定时器定期检查系统连接状态 |
|
|
|
Timer.periodic(const Duration(seconds: 5), (timer) async { |
|
|
|
// 如果设备已变更或服务关闭,停止监控 |
|
|
|
if (_selectedDevice.value == null || |
|
|
|
_selectedDevice.value!.remoteId.str != device.remoteId.str) { |
|
|
|
timer.cancel(); |
|
|
|
return; |
|
|
|
// 音频设备的可能名称关键词 |
|
|
|
final List<String> audioKeywords = [ |
|
|
|
'headphone', 'earphone', 'earbuds', 'headset', |
|
|
|
'speaker', 'audio', '耳机', '音箱', '音响' |
|
|
|
]; |
|
|
|
|
|
|
|
// 尝试匹配音频设备名称 |
|
|
|
for (BluetoothDevice device in devices) { |
|
|
|
String name = device.platformName.toLowerCase(); |
|
|
|
if (audioKeywords.any((keyword) => name.contains(keyword))) { |
|
|
|
return device; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
await _refreshSystemDevices(); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查设备是否仍在系统连接列表中 |
|
|
|
bool stillConnected = _connectedDevices.any( |
|
|
|
(d) => d.remoteId.str == device.remoteId.str |
|
|
|
); |
|
|
|
/// 确定设备类型 |
|
|
|
void _determineDeviceType(BluetoothDevice device) { |
|
|
|
final String deviceName = device.platformName.toLowerCase(); |
|
|
|
|
|
|
|
if (stillConnected && _connectionState.value != BluetoothConnectionState.connected) { |
|
|
|
Logger.info('设备 ${device.platformName} 在系统连接列表中,更新为已连接状态'); |
|
|
|
_connectionState.value = BluetoothConnectionState.connected; |
|
|
|
_updateLastConnectedDeviceInfo(); |
|
|
|
} else if (!stillConnected && _connectionState.value == BluetoothConnectionState.connected) { |
|
|
|
Logger.info('设备 ${device.platformName} 不在系统连接列表中,更新为断开状态'); |
|
|
|
_connectionState.value = BluetoothConnectionState.disconnected; |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
Logger.error('监控系统连接状态失败: $e'); |
|
|
|
// 音频设备关键词 |
|
|
|
final List<String> audioKeywords = [ |
|
|
|
'headphone', 'earphone', 'earbuds', 'headset', |
|
|
|
'speaker', 'audio', '耳机', '音箱', '音响' |
|
|
|
]; |
|
|
|
|
|
|
|
bool hasAudioKeyword = audioKeywords.any((keyword) => deviceName.contains(keyword)); |
|
|
|
bool inConnectedList = _connectedDevices.any((d) => d.remoteId.str == device.remoteId.str); |
|
|
|
|
|
|
|
if (hasAudioKeyword || inConnectedList) { |
|
|
|
_deviceType.value = DeviceType.classicAudio; |
|
|
|
} else { |
|
|
|
_deviceType.value = DeviceType.ble; |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
/// 获取已连接设备的信息 |
|
|
|
@ -603,9 +575,6 @@ class BluetoothService extends GetxService { |
|
|
|
'deviceType': _deviceType.value.toString().split('.').last |
|
|
|
}; |
|
|
|
|
|
|
|
// 更新最后连接的设备信息 |
|
|
|
_lastConnectedDeviceInfo.value = deviceInfo; |
|
|
|
|
|
|
|
return deviceInfo; |
|
|
|
} |
|
|
|
|
|
|
|
@ -616,123 +585,17 @@ class BluetoothService extends GetxService { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 一个综合方法,查找且尝试使用系统已连接的设备 |
|
|
|
/// 针对经典蓝牙耳机优化,避免使用BLE连接 |
|
|
|
Future<BluetoothDevice?> findConnectedSystemDevice() async { |
|
|
|
try { |
|
|
|
// 先刷新系统设备列表 |
|
|
|
await _refreshSystemDevices(); |
|
|
|
|
|
|
|
// 1. 检查当前是否有已选择且已连接的设备 |
|
|
|
if (_selectedDevice.value != null && |
|
|
|
_connectionState.value == BluetoothConnectionState.connected) { |
|
|
|
Logger.info('使用当前已连接设备: ${_selectedDevice.value!.platformName}'); |
|
|
|
return _selectedDevice.value; |
|
|
|
} |
|
|
|
|
|
|
|
// 2. 检查系统已连接设备列表 |
|
|
|
if (_connectedDevices.isNotEmpty) { |
|
|
|
// 优先选择可能是音频设备的 |
|
|
|
BluetoothDevice audioDevice = _connectedDevices.firstWhere( |
|
|
|
(device) { |
|
|
|
final name = device.platformName.toLowerCase(); |
|
|
|
return name.contains('headphone') || |
|
|
|
name.contains('earphone') || |
|
|
|
name.contains('earbuds') || |
|
|
|
name.contains('headset') || |
|
|
|
name.contains('speaker') || |
|
|
|
name.contains('耳机') || |
|
|
|
name.contains('音箱'); |
|
|
|
}, |
|
|
|
orElse: () => _connectedDevices.first |
|
|
|
); |
|
|
|
|
|
|
|
Logger.info('在系统已连接设备中找到设备: ${audioDevice.platformName}'); |
|
|
|
|
|
|
|
// 设置为当前设备并更新状态 |
|
|
|
_selectedDevice.value = audioDevice; |
|
|
|
_determineDeviceType(audioDevice); |
|
|
|
_connectionState.value = BluetoothConnectionState.connected; |
|
|
|
_monitorSystemConnectionState(audioDevice); |
|
|
|
_updateLastConnectedDeviceInfo(); |
|
|
|
|
|
|
|
return audioDevice; |
|
|
|
} |
|
|
|
|
|
|
|
// 3. 如果没有已连接设备,检查已配对设备 |
|
|
|
return null; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('查找系统已连接设备失败: $e'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 获取设备电量 |
|
|
|
Future<int?> getBatteryLevel() async { |
|
|
|
if (_selectedDevice.value == null || |
|
|
|
_connectionState.value != BluetoothConnectionState.connected) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// 对于BLE设备,尝试读取电池服务 |
|
|
|
if (_deviceType.value == DeviceType.ble || _deviceType.value == DeviceType.dual) { |
|
|
|
// 发现设备服务 |
|
|
|
List<dynamic> services = await _selectedDevice.value!.discoverServices(); |
|
|
|
|
|
|
|
// 查找电池服务 |
|
|
|
for (var service in services) { |
|
|
|
if (service.uuid.toString().toLowerCase() == BATTERY_SERVICE_UUID) { |
|
|
|
// 查找电池特征 |
|
|
|
for (var characteristic in service.characteristics) { |
|
|
|
if (characteristic.uuid.toString().toLowerCase() == BATTERY_CHARACTERISTIC_UUID) { |
|
|
|
// 读取电池电量 |
|
|
|
List<int> value = await characteristic.read(); |
|
|
|
if (value.isNotEmpty) { |
|
|
|
return value[0]; // 电量百分比 |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 对于经典蓝牙设备,可能需要平台特定的方法 |
|
|
|
if (_deviceType.value == DeviceType.classicAudio) { |
|
|
|
// 这里需要使用平台通道或其他插件获取经典蓝牙设备电量 |
|
|
|
// 目前根据设备名称特征尝试估计电量 |
|
|
|
// 部分设备会在名称后附加电量信息,如"XXX耳机(80%)" |
|
|
|
if (_selectedDevice.value != null) { |
|
|
|
final deviceName = _selectedDevice.value!.platformName; |
|
|
|
final percentRegex = RegExp(r'(\d+)%'); |
|
|
|
final match = percentRegex.firstMatch(deviceName); |
|
|
|
if (match != null && match.groupCount >= 1) { |
|
|
|
final percentStr = match.group(1); |
|
|
|
if (percentStr != null) { |
|
|
|
final percent = int.tryParse(percentStr); |
|
|
|
if (percent != null && percent >= 0 && percent <= 100) { |
|
|
|
return percent; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Logger.info('无法从设备名称中提取电量信息'); |
|
|
|
} |
|
|
|
|
|
|
|
// 如果以上方法都无法获取电量,返回null |
|
|
|
return null; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('获取设备电量失败: $e'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 清理资源 |
|
|
|
@override |
|
|
|
void onClose() { |
|
|
|
// 取消所有订阅 |
|
|
|
_stateSubscription?.cancel(); |
|
|
|
_scanSubscription?.cancel(); |
|
|
|
_connectionSubscription?.cancel(); |
|
|
|
|
|
|
|
// 断开设备连接 |
|
|
|
disconnectFromDevice(); |
|
|
|
|
|
|
|
super.onClose(); |
|
|
|
} |
|
|
|
} |