blob: 202ac9075432bef8e472618cf32ea542f5beedb5 [file] [log] [blame]
// Copyright 2014 The Flutter 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 'package:meta/meta.dart';
import '../base/common.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/terminal.dart';
import '../base/user_messages.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../ios/devices.dart';
const String _checkingForWirelessDevicesMessage = 'Checking for wireless devices...';
const String _chooseOneMessage = 'Please choose one (or "q" to quit)';
const String _connectedDevicesMessage = 'Connected devices:';
const String _foundButUnsupportedDevicesMessage = 'The following devices were found, but are not supported by this project:';
const String _noAttachedCheckForWirelessMessage = 'No devices found yet. Checking for wireless devices...';
const String _noDevicesFoundMessage = 'No devices found.';
const String _noWirelessDevicesFoundMessage = 'No wireless devices were found.';
const String _wirelesslyConnectedDevicesMessage = 'Wirelessly connected devices:';
String _chooseDeviceOptionMessage(int option, String name, String deviceId) => '[$option]: $name ($deviceId)';
String _foundMultipleSpecifiedDevicesMessage(String deviceId) =>
'Found multiple devices with name or id matching $deviceId:';
String _foundSpecifiedDevicesMessage(int count, String deviceId) =>
'Found $count devices with name or id matching $deviceId:';
String _noMatchingDeviceMessage(String deviceId) => 'No supported devices found with name or id '
"matching '$deviceId'.";
String flutterSpecifiedDeviceDevModeDisabled(String deviceName) => 'To use '
"'$deviceName' for development, enable Developer Mode in Settings → Privacy & Security.";
/// This class handles functionality of finding and selecting target devices.
///
/// Target devices are devices that are supported and selectable to run
/// a flutter application on.
class TargetDevices {
factory TargetDevices({
required Platform platform,
required DeviceManager deviceManager,
required Logger logger,
DeviceConnectionInterface? deviceConnectionInterface,
}) {
if (platform.isMacOS) {
return TargetDevicesWithExtendedWirelessDeviceDiscovery(
deviceManager: deviceManager,
logger: logger,
deviceConnectionInterface: deviceConnectionInterface,
);
}
return TargetDevices._private(
deviceManager: deviceManager,
logger: logger,
deviceConnectionInterface: deviceConnectionInterface,
);
}
TargetDevices._private({
required DeviceManager deviceManager,
required Logger logger,
required this.deviceConnectionInterface,
}) : _deviceManager = deviceManager,
_logger = logger;
final DeviceManager _deviceManager;
final Logger _logger;
final DeviceConnectionInterface? deviceConnectionInterface;
bool get _includeAttachedDevices =>
deviceConnectionInterface == null ||
deviceConnectionInterface == DeviceConnectionInterface.attached;
bool get _includeWirelessDevices =>
deviceConnectionInterface == null ||
deviceConnectionInterface == DeviceConnectionInterface.wireless;
Future<List<Device>> _getAttachedDevices({
DeviceDiscoverySupportFilter? supportFilter,
}) async {
if (!_includeAttachedDevices) {
return <Device>[];
}
return _deviceManager.getDevices(
filter: DeviceDiscoveryFilter(
deviceConnectionInterface: DeviceConnectionInterface.attached,
supportFilter: supportFilter,
),
);
}
Future<List<Device>> _getWirelessDevices({
DeviceDiscoverySupportFilter? supportFilter,
}) async {
if (!_includeWirelessDevices) {
return <Device>[];
}
return _deviceManager.getDevices(
filter: DeviceDiscoveryFilter(
deviceConnectionInterface: DeviceConnectionInterface.wireless,
supportFilter: supportFilter,
),
);
}
Future<List<Device>> _getDeviceById({
bool includeDevicesUnsupportedByProject = false,
bool includeDisconnected = false,
}) async {
return _deviceManager.getDevices(
filter: DeviceDiscoveryFilter(
excludeDisconnected: !includeDisconnected,
supportFilter: _deviceManager.deviceSupportFilter(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
),
deviceConnectionInterface: deviceConnectionInterface,
),
);
}
DeviceDiscoverySupportFilter _defaultSupportFilter(
bool includeDevicesUnsupportedByProject,
) {
return _deviceManager.deviceSupportFilter(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
);
}
void startExtendedWirelessDeviceDiscovery({
Duration? deviceDiscoveryTimeout,
}) {}
/// Find and return all target [Device]s based upon criteria entered by the
/// user on the command line.
///
/// When the user has specified `all` devices, return all devices meeting criteria.
///
/// When the user has specified a device id/name, attempt to find an exact or
/// partial match. If an exact match or a single partial match is found,
/// return it immediately.
///
/// When multiple devices are found and there is a terminal attached to
/// stdin, allow the user to select which device to use. When a terminal
/// with stdin is not available, print a list of available devices and
/// return null.
///
/// When no devices meet user specifications, print a list of unsupported
/// devices and return null.
Future<List<Device>?> findAllTargetDevices({
Duration? deviceDiscoveryTimeout,
bool includeDevicesUnsupportedByProject = false,
}) async {
if (!globals.doctor!.canLaunchAnything) {
_logger.printError(userMessages.flutterNoDevelopmentDevice);
return null;
}
if (deviceDiscoveryTimeout != null) {
// Reset the cache with the specified timeout.
await _deviceManager.refreshAllDevices(timeout: deviceDiscoveryTimeout);
}
if (_deviceManager.hasSpecifiedDeviceId) {
// Must check for device match separately from `_getAttachedDevices` and
// `_getWirelessDevices` because if an exact match is found in one
// and a partial match is found in another, there is no way to distinguish
// between them.
final List<Device> devices = await _getDeviceById(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
);
if (devices.length == 1) {
return devices;
}
}
final List<Device> attachedDevices = await _getAttachedDevices(
supportFilter: _defaultSupportFilter(includeDevicesUnsupportedByProject),
);
final List<Device> wirelessDevices = await _getWirelessDevices(
supportFilter: _defaultSupportFilter(includeDevicesUnsupportedByProject),
);
final List<Device> allDevices = attachedDevices + wirelessDevices;
if (allDevices.isEmpty) {
return _handleNoDevices();
} else if (_deviceManager.hasSpecifiedAllDevices) {
return allDevices;
} else if (allDevices.length > 1) {
return _handleMultipleDevices(attachedDevices, wirelessDevices);
}
return allDevices;
}
/// When no supported devices are found, display a message and list of
/// unsupported devices found.
Future<List<Device>?> _handleNoDevices() async {
// Get connected devices from cache, including unsupported ones.
final List<Device> unsupportedDevices = await _deviceManager.getAllDevices(
filter: DeviceDiscoveryFilter(
deviceConnectionInterface: deviceConnectionInterface,
)
);
if (_deviceManager.hasSpecifiedDeviceId) {
_logger.printStatus(
_noMatchingDeviceMessage(_deviceManager.specifiedDeviceId!),
);
if (unsupportedDevices.isNotEmpty) {
_logger.printStatus('');
_logger.printStatus('The following devices were found:');
await Device.printDevices(unsupportedDevices, _logger);
}
return null;
}
_logger.printStatus(_deviceManager.hasSpecifiedAllDevices
? _noDevicesFoundMessage
: userMessages.flutterNoSupportedDevices);
await _printUnsupportedDevice(unsupportedDevices);
return null;
}
/// Determine which device to use when multiple found.
///
/// If user has not specified a device id/name, attempt to prioritize
/// ephemeral devices. If a single ephemeral device is found, return it
/// immediately.
///
/// Otherwise, prompt the user to select a device if there is a terminal
/// with stdin. If there is not a terminal, display the list of devices with
/// instructions to use a device selection flag.
Future<List<Device>?> _handleMultipleDevices(
List<Device> attachedDevices,
List<Device> wirelessDevices,
) async {
final List<Device> allDevices = attachedDevices + wirelessDevices;
final Device? ephemeralDevice = _deviceManager.getSingleEphemeralDevice(allDevices);
if (ephemeralDevice != null) {
return <Device>[ephemeralDevice];
}
if (globals.terminal.stdinHasTerminal) {
return _selectFromMultipleDevices(attachedDevices, wirelessDevices);
} else {
return _printMultipleDevices(attachedDevices, wirelessDevices);
}
}
/// Display a list of found devices. When the user has not specified the
/// device id/name, display devices unsupported by the project as well and
/// give instructions to use a device selection flag.
Future<List<Device>?> _printMultipleDevices(
List<Device> attachedDevices,
List<Device> wirelessDevices,
) async {
List<Device> supportedAttachedDevices = attachedDevices;
List<Device> supportedWirelessDevices = wirelessDevices;
if (_deviceManager.hasSpecifiedDeviceId) {
final int allDeviceLength = supportedAttachedDevices.length + supportedWirelessDevices.length;
_logger.printStatus(_foundSpecifiedDevicesMessage(
allDeviceLength,
_deviceManager.specifiedDeviceId!,
));
} else {
// Get connected devices from cache, including ones unsupported for the
// project but still supported by Flutter.
supportedAttachedDevices = await _getAttachedDevices(
supportFilter: DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutter(),
);
supportedWirelessDevices = await _getWirelessDevices(
supportFilter: DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutter(),
);
_logger.printStatus(userMessages.flutterSpecifyDeviceWithAllOption);
_logger.printStatus('');
}
await Device.printDevices(supportedAttachedDevices, _logger);
if (supportedWirelessDevices.isNotEmpty) {
if (_deviceManager.hasSpecifiedDeviceId || supportedAttachedDevices.isNotEmpty) {
_logger.printStatus('');
}
_logger.printStatus(_wirelesslyConnectedDevicesMessage);
await Device.printDevices(supportedWirelessDevices, _logger);
}
return null;
}
/// Display a list of selectable devices, prompt the user to choose one, and
/// wait for the user to select a valid option.
Future<List<Device>?> _selectFromMultipleDevices(
List<Device> attachedDevices,
List<Device> wirelessDevices,
) async {
final List<Device> allDevices = attachedDevices + wirelessDevices;
if (_deviceManager.hasSpecifiedDeviceId) {
_logger.printStatus(_foundSpecifiedDevicesMessage(
allDevices.length,
_deviceManager.specifiedDeviceId!,
));
} else {
_logger.printStatus(_connectedDevicesMessage);
}
await Device.printDevices(attachedDevices, _logger);
if (wirelessDevices.isNotEmpty) {
_logger.printStatus('');
_logger.printStatus(_wirelesslyConnectedDevicesMessage);
await Device.printDevices(wirelessDevices, _logger);
_logger.printStatus('');
}
final Device chosenDevice = await _chooseOneOfAvailableDevices(allDevices);
// Update the [DeviceManager.specifiedDeviceId] so that the user will not
// be prompted again.
_deviceManager.specifiedDeviceId = chosenDevice.id;
return <Device>[chosenDevice];
}
Future<void> _printUnsupportedDevice(List<Device> unsupportedDevices) async {
if (unsupportedDevices.isNotEmpty) {
final StringBuffer result = StringBuffer();
result.writeln();
result.writeln(_foundButUnsupportedDevicesMessage);
result.writeAll(
(await Device.descriptions(unsupportedDevices))
.map((String desc) => desc)
.toList(),
'\n',
);
result.writeln();
result.writeln(userMessages.flutterMissPlatformProjects(
Device.devicesPlatformTypes(unsupportedDevices),
));
_logger.printStatus(result.toString(), newline: false);
}
}
Future<Device> _chooseOneOfAvailableDevices(List<Device> devices) async {
_displayDeviceOptions(devices);
final String userInput = await _readUserInput(devices.length);
if (userInput.toLowerCase() == 'q') {
throwToolExit('');
}
return devices[int.parse(userInput) - 1];
}
void _displayDeviceOptions(List<Device> devices) {
int count = 1;
for (final Device device in devices) {
_logger.printStatus(_chooseDeviceOptionMessage(count, device.name, device.id));
count++;
}
}
Future<String> _readUserInput(int deviceCount) async {
globals.terminal.usesTerminalUi = true;
final String result = await globals.terminal.promptForCharInput(
<String>[ for (int i = 0; i < deviceCount; i++) '${i + 1}', 'q', 'Q'],
displayAcceptedCharacters: false,
logger: _logger,
prompt: _chooseOneMessage,
);
return result;
}
}
@visibleForTesting
class TargetDevicesWithExtendedWirelessDeviceDiscovery extends TargetDevices {
TargetDevicesWithExtendedWirelessDeviceDiscovery({
required super.deviceManager,
required super.logger,
super.deviceConnectionInterface,
}) : super._private();
Future<void>? _wirelessDevicesRefresh;
@visibleForTesting
bool waitForWirelessBeforeInput = false;
@visibleForTesting
late final TargetDeviceSelection deviceSelection = TargetDeviceSelection(_logger);
@override
void startExtendedWirelessDeviceDiscovery({
Duration? deviceDiscoveryTimeout,
}) {
if (deviceDiscoveryTimeout == null && _includeWirelessDevices) {
_wirelessDevicesRefresh ??= _deviceManager.refreshExtendedWirelessDeviceDiscoverers(
timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout,
);
}
return;
}
Future<List<Device>> _getRefreshedWirelessDevices({
bool includeDevicesUnsupportedByProject = false,
}) async {
if (!_includeWirelessDevices) {
return <Device>[];
}
startExtendedWirelessDeviceDiscovery();
return () async {
await _wirelessDevicesRefresh;
return _deviceManager.getDevices(
filter: DeviceDiscoveryFilter(
deviceConnectionInterface: DeviceConnectionInterface.wireless,
supportFilter: _defaultSupportFilter(includeDevicesUnsupportedByProject),
),
);
}();
}
Future<Device?> _waitForIOSDeviceToConnect(IOSDevice device) async {
for (final DeviceDiscovery discoverer in _deviceManager.deviceDiscoverers) {
if (discoverer is IOSDevices) {
_logger.printStatus('Waiting for ${device.name} to connect...');
final Status waitingStatus = _logger.startSpinner(
timeout: const Duration(seconds: 30),
warningColor: TerminalColor.red,
slowWarningCallback: () {
return 'The device was unable to connect after 30 seconds. Ensure the device is paired and unlocked.';
},
);
final Device? connectedDevice = await discoverer.waitForDeviceToConnect(device, _logger);
waitingStatus.stop();
return connectedDevice;
}
}
return null;
}
/// Find and return all target [Device]s based upon criteria entered by the
/// user on the command line.
///
/// When the user has specified `all` devices, return all devices meeting criteria.
///
/// When the user has specified a device id/name, attempt to find an exact or
/// partial match. If an exact match or a single partial match is found and
/// the device is connected, return it immediately. If an exact match or a
/// single partial match is found and the device is not connected and it's
/// an iOS device, wait for it to connect.
///
/// When multiple devices are found and there is a terminal attached to
/// stdin, allow the user to select which device to use. When a terminal
/// with stdin is not available, print a list of available devices and
/// return null.
///
/// When no devices meet user specifications, print a list of unsupported
/// devices and return null.
@override
Future<List<Device>?> findAllTargetDevices({
Duration? deviceDiscoveryTimeout,
bool includeDevicesUnsupportedByProject = false,
}) async {
if (!globals.doctor!.canLaunchAnything) {
_logger.printError(userMessages.flutterNoDevelopmentDevice);
return null;
}
// When a user defines the timeout or filters to only attached devices,
// use the super function that does not do longer wireless device
// discovery and does not wait for devices to connect.
if (deviceDiscoveryTimeout != null || deviceConnectionInterface == DeviceConnectionInterface.attached) {
return super.findAllTargetDevices(
deviceDiscoveryTimeout: deviceDiscoveryTimeout,
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
);
}
// Start polling for wireless devices that need longer to load if it hasn't
// already been started.
startExtendedWirelessDeviceDiscovery();
if (_deviceManager.hasSpecifiedDeviceId) {
// Get devices matching the specified device regardless of whether they
// are currently connected or not.
// If there is a single matching connected device, return it immediately.
// If the only device found is an iOS device that is not connected yet,
// wait for it to connect.
// If there are multiple matches, continue on to wait for all attached
// and wireless devices to load so the user can select between all
// connected matches.
final List<Device> specifiedDevices = await _getDeviceById(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
includeDisconnected: true,
);
if (specifiedDevices.length == 1) {
Device? matchedDevice = specifiedDevices.first;
// If the only matching device does not have Developer Mode enabled,
// print a warning
if (matchedDevice is IOSDevice && !matchedDevice.devModeEnabled) {
_logger.printStatus(
flutterSpecifiedDeviceDevModeDisabled(matchedDevice.name)
);
return null;
}
if (!matchedDevice.isConnected && matchedDevice is IOSDevice) {
matchedDevice = await _waitForIOSDeviceToConnect(matchedDevice);
}
if (matchedDevice != null && matchedDevice.isConnected) {
return <Device>[matchedDevice];
}
} else {
for (final Device device in specifiedDevices) {
// Print warning for every matching device that does not have Developer Mode enabled.
if (device is IOSDevice && !device.devModeEnabled) {
_logger.printStatus(
flutterSpecifiedDeviceDevModeDisabled(device.name)
);
}
}
}
}
final List<Device> attachedDevices = await _getAttachedDevices(
supportFilter: _defaultSupportFilter(includeDevicesUnsupportedByProject),
);
// _getRefreshedWirelessDevices must be run after _getAttachedDevices is
// finished to prevent non-iOS discoverers from running simultaneously.
// `AndroidDevices` may error if run simultaneously.
final Future<List<Device>> futureWirelessDevices = _getRefreshedWirelessDevices(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
);
if (attachedDevices.isEmpty) {
return _handleNoAttachedDevices(attachedDevices, futureWirelessDevices);
} else if (_deviceManager.hasSpecifiedAllDevices) {
return _handleAllDevices(attachedDevices, futureWirelessDevices);
}
// Even if there's only a single attached device, continue to
// `_handleRemainingDevices` since there might be wireless devices
// that are not loaded yet.
return _handleRemainingDevices(attachedDevices, futureWirelessDevices);
}
/// When no supported attached devices are found, wait for wireless devices
/// to load.
///
/// If no wireless devices are found, continue to `_handleNoDevices`.
///
/// If wireless devices are found, continue to `_handleMultipleDevices`.
Future<List<Device>?> _handleNoAttachedDevices(
List<Device> attachedDevices,
Future<List<Device>> futureWirelessDevices,
) async {
if (_includeAttachedDevices) {
_logger.printStatus(_noAttachedCheckForWirelessMessage);
} else {
_logger.printStatus(_checkingForWirelessDevicesMessage);
}
final List<Device> wirelessDevices = await futureWirelessDevices;
final List<Device> allDevices = attachedDevices + wirelessDevices;
if (allDevices.isEmpty) {
_logger.printStatus('');
return _handleNoDevices();
} else if (_deviceManager.hasSpecifiedAllDevices) {
return allDevices;
} else if (allDevices.length > 1) {
_logger.printStatus('');
return _handleMultipleDevices(attachedDevices, wirelessDevices);
}
return allDevices;
}
/// Wait for wireless devices to load and then return all attached and
/// wireless devices.
Future<List<Device>?> _handleAllDevices(
List<Device> devices,
Future<List<Device>> futureWirelessDevices,
) async {
_logger.printStatus(_checkingForWirelessDevicesMessage);
final List<Device> wirelessDevices = await futureWirelessDevices;
return devices + wirelessDevices;
}
/// Determine which device to use when one or more are found.
///
/// If user has not specified a device id/name, attempt to prioritize
/// ephemeral devices. If a single ephemeral device is found, return it
/// immediately.
///
/// Otherwise, prompt the user to select a device if there is a terminal
/// with stdin. If there is not a terminal, display the list of devices with
/// instructions to use a device selection flag.
Future<List<Device>?> _handleRemainingDevices(
List<Device> attachedDevices,
Future<List<Device>> futureWirelessDevices,
) async {
final Device? ephemeralDevice = _deviceManager.getSingleEphemeralDevice(attachedDevices);
if (ephemeralDevice != null) {
return <Device>[ephemeralDevice];
}
if (!globals.terminal.stdinHasTerminal || !_logger.supportsColor) {
_logger.printStatus(_checkingForWirelessDevicesMessage);
final List<Device> wirelessDevices = await futureWirelessDevices;
if (attachedDevices.length + wirelessDevices.length == 1) {
return attachedDevices + wirelessDevices;
}
_logger.printStatus('');
// If the terminal has stdin but does not support color/ANSI (which is
// needed to clear lines), fallback to standard selection of device.
if (globals.terminal.stdinHasTerminal && !_logger.supportsColor) {
return _handleMultipleDevices(attachedDevices, wirelessDevices);
}
// If terminal does not have stdin, print out device list.
return _printMultipleDevices(attachedDevices, wirelessDevices);
}
return _selectFromDevicesAndCheckForWireless(
attachedDevices,
futureWirelessDevices,
);
}
/// Display a list of selectable attached devices and prompt the user to
/// choose one.
///
/// Also, display a message about waiting for wireless devices to load. Once
/// wireless devices have loaded, update waiting message, device list, and
/// selection options.
///
/// Wait for the user to select a device.
Future<List<Device>?> _selectFromDevicesAndCheckForWireless(
List<Device> attachedDevices,
Future<List<Device>> futureWirelessDevices,
) async {
if (attachedDevices.length == 1 || !_deviceManager.hasSpecifiedDeviceId) {
_logger.printStatus(_connectedDevicesMessage);
} else if (_deviceManager.hasSpecifiedDeviceId) {
// Multiple devices were found with part of the name/id provided.
_logger.printStatus(_foundMultipleSpecifiedDevicesMessage(
_deviceManager.specifiedDeviceId!,
));
}
// Display list of attached devices.
await Device.printDevices(attachedDevices, _logger);
// Display waiting message.
_logger.printStatus('');
_logger.printStatus(_checkingForWirelessDevicesMessage);
_logger.printStatus('');
// Start user device selection so user can select device while waiting
// for wireless devices to load if they want.
_displayDeviceOptions(attachedDevices);
deviceSelection.devices = attachedDevices;
final Future<Device> futureChosenDevice = deviceSelection.userSelectDevice();
Device? chosenDevice;
// Once wireless devices are found, we clear out the waiting message (3),
// device option list (attachedDevices.length), and device option prompt (1).
int numLinesToClear = attachedDevices.length + 4;
futureWirelessDevices = futureWirelessDevices.then((List<Device> wirelessDevices) async {
// If device is already chosen, don't update terminal with
// wireless device list.
if (chosenDevice != null) {
return wirelessDevices;
}
final List<Device> allDevices = attachedDevices + wirelessDevices;
if (_logger.isVerbose) {
await _verbosePrintWirelessDevices(attachedDevices, wirelessDevices);
} else {
// Also clear any invalid device selections.
numLinesToClear += deviceSelection.invalidAttempts;
await _printWirelessDevices(wirelessDevices, numLinesToClear);
}
_logger.printStatus('');
// Reprint device option list.
_displayDeviceOptions(allDevices);
deviceSelection.devices = allDevices;
// Reprint device option prompt.
_logger.printStatus(
'$_chooseOneMessage: ',
emphasis: true,
newline: false,
);
return wirelessDevices;
});
// Used for testing.
if (waitForWirelessBeforeInput) {
await futureWirelessDevices;
}
// Wait for user to select a device.
chosenDevice = await futureChosenDevice;
// Update the [DeviceManager.specifiedDeviceId] so that the user will not
// be prompted again.
_deviceManager.specifiedDeviceId = chosenDevice.id;
return <Device>[chosenDevice];
}
/// Reprint list of attached devices before printing list of wireless devices.
Future<void> _verbosePrintWirelessDevices(
List<Device> attachedDevices,
List<Device> wirelessDevices,
) async {
if (wirelessDevices.isEmpty) {
_logger.printStatus(_noWirelessDevicesFoundMessage);
}
// The iOS xcdevice outputs once wireless devices are done loading, so
// reprint attached devices so they're grouped with the wireless ones.
_logger.printStatus(_connectedDevicesMessage);
await Device.printDevices(attachedDevices, _logger);
if (wirelessDevices.isNotEmpty) {
_logger.printStatus('');
_logger.printStatus(_wirelesslyConnectedDevicesMessage);
await Device.printDevices(wirelessDevices, _logger);
}
}
/// Clear [numLinesToClear] lines from terminal. Print message and list of
/// wireless devices.
Future<void> _printWirelessDevices(
List<Device> wirelessDevices,
int numLinesToClear,
) async {
_logger.printStatus(
globals.terminal.clearLines(numLinesToClear),
newline: false,
);
_logger.printStatus('');
if (wirelessDevices.isEmpty) {
_logger.printStatus(_noWirelessDevicesFoundMessage);
} else {
_logger.printStatus(_wirelesslyConnectedDevicesMessage);
await Device.printDevices(wirelessDevices, _logger);
}
}
}
@visibleForTesting
class TargetDeviceSelection {
TargetDeviceSelection(this._logger);
List<Device> devices = <Device>[];
final Logger _logger;
int invalidAttempts = 0;
/// Prompt user to select a device and wait until they select a valid device.
///
/// If the user selects `q`, exit the tool.
///
/// If the user selects an invalid number, reprompt them and continue waiting.
Future<Device> userSelectDevice() async {
Device? chosenDevice;
while (chosenDevice == null) {
final String userInputString = await readUserInput();
if (userInputString.toLowerCase() == 'q') {
throwToolExit('');
}
final int deviceIndex = int.parse(userInputString) - 1;
if (deviceIndex > -1 && deviceIndex < devices.length) {
chosenDevice = devices[deviceIndex];
}
}
return chosenDevice;
}
/// Prompt user to select a device and wait until they select a valid
/// character.
///
/// Only allow input of a number or `q`.
@visibleForTesting
Future<String> readUserInput() async {
final RegExp pattern = RegExp(r'\d+$|q', caseSensitive: false);
String? choice;
globals.terminal.singleCharMode = true;
while (choice == null || choice.length > 1 || !pattern.hasMatch(choice)) {
_logger.printStatus(_chooseOneMessage, emphasis: true, newline: false);
// prompt ends with ': '
_logger.printStatus(': ', emphasis: true, newline: false);
choice = (await globals.terminal.keystrokes.first).trim();
_logger.printStatus(choice);
invalidAttempts++;
}
globals.terminal.singleCharMode = false;
return choice;
}
}