| // 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; |
| |
| Timer? _timer; |
| int scanIntervalInSeconds = 20; |
| final _scannedNetworks = <policy.ScanResult>{}; |
| String _targetNetwork = ''; |
| final _savedNetworks = <policy.NetworkConfig>{}; |
| |
| WiFiService(); |
| |
| @override |
| Future<void> start() async { |
| _clientProvider = policy.ClientProviderProxy(); |
| _clientController = policy.ClientControllerProxy(); |
| _monitor = ClientStateUpdatesMonitor(onChanged); |
| |
| Incoming.fromSvcPath().connectToService(_clientProvider); |
| |
| await _clientProvider?.getController( |
| InterfaceRequest(_clientController?.ctrl.request().passChannel()), |
| _monitor.getInterfaceHandle()); |
| |
| final requestStatus = await _clientController?.startClientConnections(); |
| if (requestStatus != RequestStatus.acknowledged) { |
| log.warning( |
| 'Failed to start wlan client connection. Request status: $requestStatus'); |
| } |
| |
| 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(); |
| dispose(); |
| } |
| |
| @override |
| void dispose() { |
| _clientProvider?.ctrl.close(); |
| _clientProvider = policy.ClientProviderProxy(); |
| _clientController?.ctrl.close(); |
| _clientController = policy.ClientControllerProxy(); |
| _scanResultIteratorProvider?.ctrl.close(); |
| _scanResultIteratorProvider = policy.ScanResultIteratorProxy(); |
| } |
| |
| String get targetNetwork => _targetNetwork; |
| set targetNetwork(String network) { |
| _targetNetwork = network; |
| onChanged(); |
| } |
| |
| Future<void> scanForNetworks() async { |
| _scanForNetworksSubscription = () async { |
| _scanResultIteratorProvider = policy.ScanResultIteratorProxy(); |
| await _clientController?.scanForNetworks(InterfaceRequest( |
| _scanResultIteratorProvider?.ctrl.request().passChannel())); |
| |
| _scannedNetworks.clear(); |
| List<policy.ScanResult>? scanResults; |
| try { |
| scanResults = await _scanResultIteratorProvider?.getNext(); |
| while (scanResults != null && scanResults.isNotEmpty) { |
| _scannedNetworks.addAll(scanResults); |
| scanResults = await _scanResultIteratorProvider?.getNext(); |
| } |
| } on Exception catch (e) { |
| log.warning('Error encountered during scan: $e'); |
| return; |
| } |
| onChanged(); |
| }() |
| .asStream() |
| .listen((_) {}); |
| } |
| |
| List<NetworkInformation> get scannedNetworks => _scannedNetworks |
| .map((network) => NetworkInformation( |
| name: nameFromScannedNetwork(network), |
| compatible: compatibleFromScannedNetwork(network), |
| icon: iconFromScannedNetwork(network))) |
| .toList(); |
| |
| String nameFromScannedNetwork(policy.ScanResult network) { |
| return utf8.decode(network.id!.ssid.toList()); |
| } |
| |
| IconData iconFromScannedNetwork(policy.ScanResult network) { |
| return network.id!.type == policy.SecurityType.none |
| ? Icons.signal_wifi_4_bar |
| : Icons.wifi_lock; |
| } |
| |
| bool compatibleFromScannedNetwork(policy.ScanResult network) { |
| return network.compatibility == policy.Compatibility.supported; |
| } |
| |
| Future<void> connectToWPA2Network(String password) async { |
| try { |
| _connectToWPA2NetworkSubscription = () async { |
| final utf8password = Uint8List.fromList(password.codeUnits); |
| final credential = policy.Credential.withPassword(utf8password); |
| policy.ScanResult? network = _scannedNetworks.firstWhereOrNull( |
| (network) => nameFromScannedNetwork(network) == _targetNetwork); |
| |
| if (network == null) { |
| throw Exception( |
| '$targetNetwork network not found in scanned networks.'); |
| } |
| |
| final networkConfig = |
| policy.NetworkConfig(id: network.id, credential: credential); |
| |
| // TODO(fxb/79885): Separate save and connect functionality. |
| 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(); |
| |
| bool get incorrectPassword => _monitor.incorrectPassword(); |
| |
| 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(); |
| }() |
| .asStream() |
| .listen((_) {}); |
| } on Exception catch (e) { |
| log.warning('Removing $network failed: $e'); |
| } |
| } |
| |
| List<NetworkInformation> get savedNetworks => _savedNetworks |
| .map((network) => NetworkInformation( |
| name: nameFromNetworkConfig(network), |
| compatible: true, |
| icon: iconFromNetworkConfig(network))) |
| .toList(); |
| |
| String nameFromNetworkConfig(policy.NetworkConfig network) { |
| return utf8.decode(network.id!.ssid.toList()); |
| } |
| |
| IconData iconFromNetworkConfig(policy.NetworkConfig network) { |
| return network.id!.type == policy.SecurityType.none |
| ? Icons.signal_wifi_4_bar |
| : Icons.wifi_lock; |
| } |
| } |
| |
| class ClientStateUpdatesMonitor extends policy.ClientStateUpdates { |
| final _binding = policy.ClientStateUpdatesBinding(); |
| policy.ClientStateSummary? _summary; |
| late final VoidCallback _onChanged; |
| |
| ClientStateUpdatesMonitor(this._onChanged); |
| |
| 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); |
| } |
| |
| // TODO(fxb/79855): ensure that failed password status is for target network |
| bool incorrectPassword() { |
| return _summary?.networks?.firstWhereOrNull((network) => |
| network.status == policy.DisconnectStatus.credentialsFailed) != |
| null; |
| } |
| |
| @override |
| Future<void> onClientStateUpdate(policy.ClientStateSummary summary) async { |
| _summary = summary; |
| _onChanged(); |
| } |
| } |
| |
| /// Network information needed for UI |
| class NetworkInformation { |
| String name; |
| bool compatible; |
| IconData icon; |
| |
| NetworkInformation( |
| {this.name = '', |
| this.compatible = false, |
| this.icon = Icons.signal_wifi_4_bar}); |
| } |