blob: 74db3630e359bb76dcfb94d3e38dde43349d3005 [file] [log] [blame]
// 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();
}
}