| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'package:fidl_fuchsia_bluetooth/fidl_async.dart'; |
| import 'package:fidl_fuchsia_bluetooth_control/fidl_async.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:fuchsia_services/services.dart'; |
| import 'package:lib.widgets/model.dart'; |
| |
| const Duration _deviceListRefreshInterval = Duration(seconds: 5); |
| |
| /// Model containing state needed for the bluetooth settings app. |
| class BluetoothSettingsModel extends Model { |
| /// Bluetooth controller proxy. |
| final ControlProxy _control = new ControlProxy(); |
| final BluetoothSettingsPairingDelegate _pairingDelegate = |
| BluetoothSettingsPairingDelegate(); |
| |
| List<AdapterInfo> _adapters; |
| AdapterInfo _activeAdapter; |
| final List<RemoteDevice> _remoteDevices = []; |
| Timer _sortListTimer; |
| bool _discoverable = true; |
| final List<StreamSubscription> _listeners = []; |
| |
| BluetoothSettingsModel() { |
| _onStart(); |
| } |
| |
| Future<void> setDiscoverable({bool discoverable}) async { |
| await _control.setDiscoverable(discoverable); |
| _discoverable = discoverable; |
| notifyListeners(); |
| } |
| |
| bool get discoverable => _discoverable; |
| |
| /// TODO(ejia): handle failures and error messages |
| Future<void> connect(RemoteDevice device) async { |
| await _control.connect(device.identifier); |
| } |
| |
| Future<void> disconnect(RemoteDevice device) async { |
| await _control.disconnect(device.identifier); |
| } |
| |
| /// Bluetooth devices that are seen, but are not connected. |
| Iterable<RemoteDevice> get availableDevices => |
| _remoteDevices.where((device) => !device.bonded); |
| |
| /// Bluetooth devices that are connected to the current adapter. |
| Iterable<RemoteDevice> get knownDevices => |
| _remoteDevices.where((remoteDevice) => remoteDevice.bonded); |
| |
| /// The current adapter that is being used |
| AdapterInfo get activeAdapter => _activeAdapter; |
| |
| /// All adapters that are not currently active. |
| Iterable<AdapterInfo> get inactiveAdapters => |
| _adapters?.where((adapter) => activeAdapter.address != adapter.address) ?? |
| []; |
| |
| PairingStatus get pairingStatus => _pairingDelegate.pairingStatus; |
| |
| Future<void> _onStart() async { |
| StartupContext.fromStartupInfo().incoming.connectToService(_control); |
| await _refresh(); |
| |
| // Sort the list by signal strength every few seconds. |
| _sortListTimer = Timer.periodic(_deviceListRefreshInterval, (_) { |
| _remoteDevices |
| .sort((a, b) => (b.rssi?.value ?? 0).compareTo(a.rssi?.value ?? 0)); |
| _refresh(); |
| }); |
| |
| // Just for first draft purposes, refresh whenever there are any changes. |
| // TODO: handle errors, refresh more gracefully |
| _listeners |
| ..add(_control.onActiveAdapterChanged.listen((_) => _refresh())) |
| ..add(_control.onAdapterRemoved.listen((_) => _refresh())) |
| ..add(_control.onAdapterUpdated.listen((_) => _refresh())) |
| ..add(_control.onDeviceUpdated.listen((device) { |
| int index = |
| _remoteDevices.indexWhere((d) => d.identifier == device.identifier); |
| if (index != -1) { |
| // Existing device, just update in-place. |
| _remoteDevices[index] = device; |
| } else { |
| // New device, add to bottom of list. |
| _remoteDevices.add(device); |
| } |
| notifyListeners(); |
| })) |
| ..add(_control.onDeviceRemoved.listen((deviceId) { |
| _removeDeviceFromList(deviceId); |
| notifyListeners(); |
| })); |
| |
| await _control.requestDiscovery(true); |
| await _control.setDiscoverable(true); |
| await _control |
| .setPairingDelegate(PairingDelegateBinding().wrap(_pairingDelegate)); |
| } |
| |
| void _removeDeviceFromList(String deviceId) { |
| _remoteDevices.removeWhere((device) => device.identifier == deviceId); |
| } |
| |
| /// Updates all the state that the model gets from bluetooth. |
| Future<void> _refresh() async { |
| _adapters = await _control.getAdapters(); |
| _activeAdapter = await _control.getActiveAdapterInfo(); |
| notifyListeners(); |
| } |
| |
| /// Closes the connection to the bluetooth control, thus ending active |
| /// scanning. |
| void dispose() { |
| _control.ctrl.close(); |
| _sortListTimer.cancel(); |
| _pairingDelegate.dispose(); |
| for (StreamSubscription subscription in _listeners) { |
| subscription.cancel(); |
| } |
| } |
| } |
| |
| class PairingStatus { |
| final String displayedPassKey; |
| final PairingMethod pairingMethod; |
| final RemoteDevice device; |
| int digitsEntered = 0; |
| bool completing = false; |
| |
| PairingStatus(this.displayedPassKey, this.pairingMethod, this.device); |
| } |
| |
| /// Used to capture events from bluetooth pairing. |
| class BluetoothSettingsPairingDelegate extends PairingDelegate |
| with ChangeNotifier { |
| PairingStatus pairingStatus; |
| |
| final StreamController<PairingDelegate$OnLocalKeypress$Response> |
| _localKeypressController = StreamController.broadcast(); |
| |
| void localKeypress(PairingKeypressType pressType) { |
| _localKeypressController.add(PairingDelegate$OnLocalKeypress$Response( |
| pairingStatus.device.identifier, pressType)); |
| } |
| |
| @override |
| Stream<PairingDelegate$OnLocalKeypress$Response> get onLocalKeypress => |
| _localKeypressController.stream; |
| |
| @override |
| Future<void> onPairingComplete(String deviceId, Status status) async { |
| pairingStatus = null; |
| notifyListeners(); |
| } |
| |
| @override |
| Future<PairingDelegate$OnPairingRequest$Response> onPairingRequest( |
| RemoteDevice device, |
| PairingMethod method, |
| String displayedPasskey) async { |
| // TODO: properly display passkey before accepting request |
| return PairingDelegate$OnPairingRequest$Response(true, displayedPasskey); |
| } |
| |
| @override |
| Future<void> onRemoteKeypress( |
| String deviceId, PairingKeypressType keypress) async { |
| assert(pairingStatus.device.identifier == deviceId); |
| switch (keypress) { |
| case PairingKeypressType.digitEntered: |
| pairingStatus.digitsEntered++; |
| break; |
| case PairingKeypressType.digitErased: |
| pairingStatus.digitsEntered++; |
| break; |
| case PairingKeypressType.passkeyCleared: |
| pairingStatus.digitsEntered = 0; |
| break; |
| case PairingKeypressType.passkeyEntered: |
| pairingStatus.completing = true; |
| } |
| notifyListeners(); |
| } |
| |
| @override |
| void dispose() { |
| super.dispose(); |
| _localKeypressController.close(); |
| } |
| } |