blob: c7426c001ba5c5b8a95bcc21294e6dd28183a424 [file] [log] [blame]
// Copyright 2021 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 'dart:convert';
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:ermine/src/services/settings/task_service.dart';
import 'package:fidl/fidl.dart' show InterfaceHandle, InterfaceRequest;
import 'package:fidl_fuchsia_wlan_common/fidl_async.dart';
import 'package:fidl_fuchsia_wlan_policy/fidl_async.dart' as policy;
import 'package:fuchsia_logger/logger.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fuchsia_services/services.dart';
/// Defines a [TaskService] for WiFi control.
class WiFiService implements TaskService {
late final VoidCallback onChanged;
policy.ClientProviderProxy? _clientProvider;
policy.ClientControllerProxy? _clientController;
late ClientStateUpdatesMonitor _monitor;
StreamSubscription? _scanForNetworksSubscription;
policy.ScanResultIteratorProxy? _scanResultIteratorProvider;
StreamSubscription? _connectToWPA2NetworkSubscription;
StreamSubscription? _savedNetworksSubscription;
StreamSubscription? _removeNetworkSubscription;
StreamSubscription? _startClientConnectionsSubscription;
StreamSubscription? _stopClientConnectionsSubscription;
Timer? _timer;
int scanIntervalInSeconds = 20;
final _scannedNetworks = <policy.ScanResult>{};
NetworkInformation _targetNetwork = NetworkInformation();
final _savedNetworks = <policy.NetworkConfig>{};
bool _clientConnectionsEnabled = false;
final _networksWithFailedCredentials = <policy.NetworkConfig>{};
WiFiService();
@override
Future<void> start() async {
_clientProvider = policy.ClientProviderProxy();
_clientController = policy.ClientControllerProxy();
_monitor = ClientStateUpdatesMonitor(
onChanged, _pollNetworksWithFailedCredentials);
Incoming.fromSvcPath().connectToService(_clientProvider);
await _clientProvider?.getController(
InterfaceRequest(_clientController?.ctrl.request().passChannel()),
_monitor.getInterfaceHandle());
clientConnectionsEnabled = true;
await getSavedNetworks();
_timer = Timer.periodic(
Duration(seconds: scanIntervalInSeconds), (_) => scanForNetworks());
}
@override
Future<void> stop() async {
_timer?.cancel();
await _scanForNetworksSubscription?.cancel();
await _connectToWPA2NetworkSubscription?.cancel();
await _savedNetworksSubscription?.cancel();
await _removeNetworkSubscription?.cancel();
await _startClientConnectionsSubscription?.cancel();
await _stopClientConnectionsSubscription?.cancel();
dispose();
}
@override
void dispose() {
_clientProvider?.ctrl.close();
_clientProvider = policy.ClientProviderProxy();
_clientController?.ctrl.close();
_clientController = policy.ClientControllerProxy();
_scanResultIteratorProvider?.ctrl.close();
_scanResultIteratorProvider = policy.ScanResultIteratorProxy();
}
NetworkInformation get targetNetwork => _targetNetwork;
set targetNetwork(NetworkInformation network) {
_targetNetwork = network;
onChanged();
}
bool get clientConnectionsEnabled => _clientConnectionsEnabled;
set clientConnectionsEnabled(bool enabled) {
_clientConnectionsEnabled = enabled;
if (enabled) {
_startClientConnections();
} else {
_stopClientConnections();
}
onChanged();
}
Future<void> _startClientConnections() async {
if (_stopClientConnectionsSubscription != null) {
await _stopClientConnectionsSubscription!.cancel();
}
_startClientConnectionsSubscription = () async {
final requestStatus = await _clientController?.startClientConnections();
if (requestStatus != RequestStatus.acknowledged) {
log.warning(
'Failed to start wlan client connection. Request status: $requestStatus');
}
}()
.asStream()
.listen((_) {});
}
Future<void> _stopClientConnections() async {
if (_startClientConnectionsSubscription != null) {
await _startClientConnectionsSubscription!.cancel();
}
_stopClientConnectionsSubscription = () async {
final requestStatus = await _clientController?.stopClientConnections();
if (requestStatus != RequestStatus.acknowledged) {
log.warning(
'Failed to stop wlan client connection. Request status: $requestStatus');
}
}()
.asStream()
.listen((_) {});
}
Future<void> scanForNetworks() async {
_scanForNetworksSubscription = () async {
_scanResultIteratorProvider = policy.ScanResultIteratorProxy();
await _clientController?.scanForNetworks(InterfaceRequest(
_scanResultIteratorProvider?.ctrl.request().passChannel()));
List<policy.ScanResult>? scanResults;
try {
scanResults = await _scanResultIteratorProvider?.getNext();
_scannedNetworks.clear();
while (scanResults != null && scanResults.isNotEmpty) {
_scannedNetworks.addAll(scanResults);
scanResults = await _scanResultIteratorProvider?.getNext();
}
} on Exception catch (e) {
log.warning('Error encountered during scan: $e');
// TODO(cwhitten): uncomment once fxb/87664 fixed
// return;
}
onChanged();
}()
.asStream()
.listen((_) {});
}
// TODO(cwhitten): simplify to _scannedNetworks.map(NetworkInformation.fromScanResult).toList();
// once passing named contructors is supported by dart.
List<NetworkInformation> get scannedNetworks =>
networkInformationFromScannedNetworks(_scannedNetworks);
List<NetworkInformation> networkInformationFromScannedNetworks(
Set<policy.ScanResult> networks) {
var networkInformationList = <NetworkInformation>[];
for (var network in networks) {
networkInformationList.add(NetworkInformation.fromScanResult(network));
}
return networkInformationList;
}
Uint8List ssidFromScannedNetwork(policy.ScanResult network) {
return network.id!.ssid;
}
Future<void> connectToNetwork(String password) async {
try {
_connectToWPA2NetworkSubscription = () async {
final credential = _targetNetwork.isOpen
? policy.Credential.withNone(policy.Empty())
: policy.Credential.withPassword(
Uint8List.fromList(password.codeUnits));
policy.ScanResult? network = _scannedNetworks.firstWhereOrNull(
(network) =>
ssidFromScannedNetwork(network) == _targetNetwork.ssid);
if (network == null) {
throw Exception(
'$targetNetwork network not found in scanned networks.');
}
final networkConfig =
policy.NetworkConfig(id: network.id, credential: credential);
await _clientController?.saveNetwork(networkConfig);
final requestStatus = await _clientController?.connect(network.id!);
if (requestStatus != RequestStatus.acknowledged) {
throw Exception(
'connecting to $targetNetwork rejected: $requestStatus.');
}
// Refresh list of saved networks
await getSavedNetworks();
}()
.asStream()
.listen((_) {});
} on Exception catch (e) {
log.warning('Connecting to $targetNetwork failed: $e');
}
}
String get currentNetwork => _monitor.currentNetwork();
bool get connectionsEnabled => _monitor.connectionsEnabled();
void _pollNetworksWithFailedCredentials(List<policy.NetworkIdentifier>? ids) {
if (ids == null) {
return;
}
for (var id in ids) {
final foundNetwork = _savedNetworks.firstWhereOrNull((savedNetwork) =>
listEquals(savedNetwork.id?.ssid, id.ssid) &&
savedNetwork.id?.type == id.type);
if (foundNetwork != null) {
_networksWithFailedCredentials.add(foundNetwork);
}
}
}
Future<void> getSavedNetworks() async {
_savedNetworksSubscription = () async {
final iterator = policy.NetworkConfigIteratorProxy();
await _clientController?.getSavedNetworks(
InterfaceRequest(iterator.ctrl.request().passChannel()));
_savedNetworks.clear();
var savedNetworkResults = await iterator.getNext();
while (savedNetworkResults.isNotEmpty) {
_savedNetworks.addAll(savedNetworkResults);
savedNetworkResults = await iterator.getNext();
}
onChanged();
}()
.asStream()
.listen((_) {});
}
// TODO(fxb/79885): Pass security type to ensure removing correct network
Future<void> remove(String network) async {
try {
_removeNetworkSubscription = () async {
final ssid = utf8.encode(network);
final foundNetwork = _savedNetworks.firstWhereOrNull(
(savedNetwork) => listEquals(savedNetwork.id?.ssid, ssid));
if (foundNetwork == null) {
throw Exception('$network not found in saved networks.');
}
final networkConfig = policy.NetworkConfig(
id: foundNetwork.id, credential: foundNetwork.credential);
await _clientController?.removeNetwork(networkConfig);
// Refresh list of saved networks
await getSavedNetworks();
_networksWithFailedCredentials.removeWhere((networkConfig) =>
listEquals(networkConfig.id?.ssid, foundNetwork.id?.ssid) &&
networkConfig.id?.type == foundNetwork.id?.type);
}()
.asStream()
.listen((_) {});
} on Exception catch (e) {
log.warning('Removing $network failed: $e');
}
}
// TODO(cwhitten): simplify to _savedNetworks.map(NetworkInformation.fromNetworkConfig).toList();
// once passing named contructors is supported by dart.
List<NetworkInformation> get savedNetworks =>
networkInformationFromSavedNetworks(_savedNetworks);
List<NetworkInformation> networkInformationFromSavedNetworks(
Set<policy.NetworkConfig> networks) {
var networkInformationList = <NetworkInformation>[];
for (var network in networks) {
networkInformationList.add(NetworkInformation.fromNetworkConfig(
network, _networksWithFailedCredentials));
}
return networkInformationList;
}
}
class ClientStateUpdatesMonitor extends policy.ClientStateUpdates {
final _binding = policy.ClientStateUpdatesBinding();
policy.ClientStateSummary? _summary;
late final VoidCallback _onChanged;
late final void Function(List<policy.NetworkIdentifier>?)
_pollNetworksWithFailedCredentials;
ClientStateUpdatesMonitor(
this._onChanged, this._pollNetworksWithFailedCredentials);
InterfaceHandle<policy.ClientStateUpdates> getInterfaceHandle() =>
_binding.wrap(this);
policy.ClientStateSummary? getState() => _summary;
bool connectionsEnabled() =>
_summary?.state == policy.WlanClientState.connectionsEnabled;
// Returns first found connected network.
// TODO(fxb/79885): expand to return multiple connected networks.
String currentNetwork() {
final foundNetwork = _summary?.networks
?.firstWhereOrNull(
(network) => network.state == policy.ConnectionState.connected)
?.id!
.ssid
.toList();
return foundNetwork == null ? '' : utf8.decode(foundNetwork);
}
// Check for failed credentials and poll networks with failed credentials
void _checkForFailedCredentials() {
var foundNetworks = _summary?.networks?.where((network) =>
network.status == policy.DisconnectStatus.credentialsFailed);
var networkIDs = foundNetworks?.map((network) => network.id!).toList();
if (networkIDs != null && networkIDs.isNotEmpty) {
_pollNetworksWithFailedCredentials(networkIDs);
}
}
@override
Future<void> onClientStateUpdate(policy.ClientStateSummary summary) async {
_summary = summary;
_checkForFailedCredentials();
_onChanged();
}
}
/// Network information needed for UI
class NetworkInformation {
// SSID used for lookup
Uint8List? _ssid;
// String representation of SSID in UI
String? _name;
// If network is able to be connected to
bool _compatible = false;
// Security type of network
policy.SecurityType? _securityType;
// If network has a failed connection attempt due to bad credentials
// Only set true if failed credentials found
bool credentialsFailed = false;
NetworkInformation();
// Constructor for network config
NetworkInformation.fromNetworkConfig(policy.NetworkConfig networkConfig,
[Set<policy.NetworkConfig> networksWithFailedCredentials = const {}]) {
_ssid = networkConfig.id?.ssid;
_name = networkConfig.id?.ssid.toList() != null
? utf8.decode(networkConfig.id!.ssid.toList())
: null;
_compatible = true;
_securityType = networkConfig.id?.type;
if (networksWithFailedCredentials.isNotEmpty) {
if (networksWithFailedCredentials
.map((networkConfig) => networkConfig.id!)
.firstWhereOrNull((networkIdentifier) =>
listEquals(networkIdentifier.ssid, _ssid) &&
networkIdentifier.type == _securityType) !=
null) {
credentialsFailed = true;
}
}
}
// Constructor for scan result
NetworkInformation.fromScanResult(policy.ScanResult scanResult) {
_ssid = scanResult.id?.ssid;
_name = scanResult.id?.ssid.toList() != null
? utf8.decode(scanResult.id!.ssid.toList())
: null;
// Only allow valid characters in UI representation of SSID
// TODO(fxb/92668): Allow special characters, such as emojis, in network names
if (_name != null) {
_name = _name!.replaceAll(RegExp(r'[^A-Za-z0-9()\[\]\s+.,;?&_-]'), '');
}
_compatible = scanResult.compatibility == policy.Compatibility.supported;
_securityType = scanResult.id?.type;
}
Uint8List? get ssid => _ssid;
String get name => _name ?? '';
bool get compatible => _compatible;
IconData get icon => _securityType == policy.SecurityType.none
? Icons.signal_wifi_4_bar
: Icons.wifi_lock;
bool get isOpen => _securityType == policy.SecurityType.none;
bool get isWEP => _securityType == policy.SecurityType.wep;
bool get isWPA => _securityType == policy.SecurityType.wpa;
bool get isWPA2 => _securityType == policy.SecurityType.wpa2;
bool get isWPA3 => _securityType == policy.SecurityType.wpa3;
}