Unverified Commit 2a73ce9b authored by Lau Ching Jun's avatar Lau Ching Jun Committed by GitHub

Refactor `DeviceManager.findTargetDevices()` and...

Refactor `DeviceManager.findTargetDevices()` and `FlutterCommand.findAllTargetDevices()`, and add a flag to not show prompt. (#112223)
parent eefd5d4c
......@@ -205,7 +205,6 @@ Future<T> runInContext<T>(
),
fuchsiaSdk: globals.fuchsiaSdk!,
operatingSystemUtils: globals.os,
terminal: globals.terminal,
customDevicesConfig: globals.customDevicesConfig,
),
DevtoolsLauncher: () => DevtoolsServerLauncher(
......
......@@ -9,13 +9,10 @@ import 'package:meta/meta.dart';
import 'application_package.dart';
import 'artifacts.dart';
import 'base/common.dart';
import 'base/context.dart';
import 'base/dds.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/terminal.dart';
import 'base/user_messages.dart' hide userMessages;
import 'base/utils.dart';
import 'build_info.dart';
import 'devfs.dart';
......@@ -83,15 +80,9 @@ class PlatformType {
abstract class DeviceManager {
DeviceManager({
required Logger logger,
required Terminal terminal,
required UserMessages userMessages,
}) : _logger = logger,
_terminal = terminal,
_userMessages = userMessages;
}) : _logger = logger;
final Logger _logger;
final Terminal _terminal;
final UserMessages _userMessages;
/// Constructing DeviceManagers is cheap; they only do expensive work if some
/// of their methods are called.
......@@ -219,7 +210,12 @@ abstract class DeviceManager {
];
}
/// Find and return a list of devices based on the current project and environment.
/// Find and return all target [Device]s based upon currently connected
/// devices, the current project, and criteria entered by the user on
/// the command line.
///
/// If no device can be found that meets specified criteria,
/// then print an error message and return null.
///
/// Returns a list of devices specified by the user.
///
......@@ -233,9 +229,13 @@ abstract class DeviceManager {
/// device connected, then filter out unsupported devices and prioritize
/// ephemeral devices.
///
/// * If [flutterProject] is null, then assume the project supports all
/// device types.
Future<List<Device>> findTargetDevices(FlutterProject? flutterProject, { Duration? timeout }) async {
/// * If [promptUserToChooseDevice] is true, and there are more than one
/// device after the aforementioned filters, and the user is connected to a
/// terminal, then show a prompt asking the user to choose one.
Future<List<Device>> findTargetDevices(
FlutterProject? flutterProject, {
Duration? timeout,
}) async {
if (timeout != null) {
// Reset the cache with the specified timeout.
await refreshAllConnectedDevices(timeout: timeout);
......@@ -244,95 +244,54 @@ abstract class DeviceManager {
List<Device> devices = (await getDevices())
.where((Device device) => device.isSupported()).toList();
// Always remove web and fuchsia devices from `--all`. This setting
// currently requires devices to share a frontend_server and resident
// runner instance. Both web and fuchsia require differently configured
// compilers, and web requires an entirely different resident runner.
if (hasSpecifiedAllDevices) {
// User has specified `--device all`.
//
// Always remove web and fuchsia devices from `--all`. This setting
// currently requires devices to share a frontend_server and resident
// runner instance. Both web and fuchsia require differently configured
// compilers, and web requires an entirely different resident runner.
devices = <Device>[
for (final Device device in devices)
if (await device.targetPlatform != TargetPlatform.fuchsia_arm64 &&
await device.targetPlatform != TargetPlatform.fuchsia_x64 &&
await device.targetPlatform != TargetPlatform.web_javascript)
await device.targetPlatform != TargetPlatform.web_javascript &&
isDeviceSupportedForProject(device, flutterProject))
device,
];
}
} else if (!hasSpecifiedDeviceId) {
// User did not specify the device.
// If there is no specified device, the remove all devices which are not
// supported by the current application. For example, if there was no
// 'android' folder then don't attempt to launch with an Android device.
if (devices.length > 1 && !hasSpecifiedDeviceId) {
// Remove all devices which are not supported by the current application.
// For example, if there was no 'android' folder then don't attempt to
// launch with an Android device.
devices = <Device>[
for (final Device device in devices)
if (isDeviceSupportedForProject(device, flutterProject))
device,
];
} else if (devices.length == 1 &&
!hasSpecifiedDeviceId &&
!isDeviceSupportedForProject(devices.single, flutterProject)) {
// If there is only a single device but it is not supported, then return
// early.
return <Device>[];
}
// If there are still multiple devices and the user did not specify to run
// all, then attempt to prioritize ephemeral devices. For example, if the
// user only typed 'flutter run' and both an Android device and desktop
// device are available, choose the Android device.
if (devices.length > 1 && !hasSpecifiedAllDevices) {
// Note: ephemeral is nullable for device types where this is not well
// defined.
if (devices.any((Device device) => device.ephemeral == true)) {
// if there is only one ephemeral device, get it
final List<Device> ephemeralDevices = devices
.where((Device device) => device.ephemeral == true)
.toList();
if (ephemeralDevices.length == 1) {
devices = ephemeralDevices;
}
if (devices.length > 1) {
// If there are still multiple devices and the user did not specify to run
// all, then attempt to prioritize ephemeral devices. For example, if the
// user only typed 'flutter run' and both an Android device and desktop
// device are available, choose the Android device.
// Note: ephemeral is nullable for device types where this is not well
// defined.
final List<Device> ephemeralDevices = <Device>[
for (final Device device in devices)
if (device.ephemeral == true)
device,
];
if (ephemeralDevices.length == 1) {
devices = ephemeralDevices;
}
}
// If it was not able to prioritize a device. For example, if the user
// has two active Android devices running, then we request the user to
// choose one. If the user has two nonEphemeral devices running, we also
// request input to choose one.
if (devices.length > 1 && _terminal.stdinHasTerminal) {
_logger.printStatus(_userMessages.flutterMultipleDevicesFound);
await Device.printDevices(devices, _logger);
final Device chosenDevice = await _chooseOneOfAvailableDevices(devices);
specifiedDeviceId = chosenDevice.id;
devices = <Device>[chosenDevice];
}
}
return devices;
}
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(_userMessages.flutterChooseDevice(count, device.name, device.id));
count++;
}
}
Future<String> _readUserInput(int deviceCount) async {
_terminal.usesTerminalUi = true;
final String result = await _terminal.promptForCharInput(
<String>[ for (int i = 0; i < deviceCount; i++) '${i + 1}', 'q', 'Q'],
displayAcceptedCharacters: false,
logger: _logger,
prompt: _userMessages.flutterChooseOne,
);
return result;
return devices;
}
/// Returns whether the device is supported for the project.
......
......@@ -11,6 +11,7 @@ import 'artifacts.dart';
import 'base/file_system.dart';
import 'base/os.dart';
import 'base/platform.dart';
import 'base/user_messages.dart';
import 'custom_devices/custom_device.dart';
import 'custom_devices/custom_devices_config.dart';
import 'device.dart';
......@@ -51,10 +52,9 @@ class FlutterDeviceManager extends DeviceManager {
required Artifacts artifacts,
required MacOSWorkflow macOSWorkflow,
required FuchsiaSdk fuchsiaSdk,
required super.userMessages,
required UserMessages userMessages,
required OperatingSystemUtils operatingSystemUtils,
required WindowsWorkflow windowsWorkflow,
required super.terminal,
required CustomDevicesConfig customDevicesConfig,
}) : deviceDiscoverers = <DeviceDiscovery>[
AndroidDevices(
......
......@@ -1413,52 +1413,105 @@ abstract class FlutterCommand extends Command<void> {
timeout: deviceDiscoveryTimeout,
);
if (devices.isEmpty && deviceManager.hasSpecifiedDeviceId) {
globals.printStatus(userMessages.flutterNoMatchingDevice(deviceManager.specifiedDeviceId!));
final List<Device> allDevices = await deviceManager.getAllConnectedDevices();
if (allDevices.isNotEmpty) {
globals.printStatus('');
globals.printStatus('The following devices were found:');
await Device.printDevices(allDevices, globals.logger);
}
return null;
} else if (devices.isEmpty) {
if (deviceManager.hasSpecifiedAllDevices) {
globals.printStatus(userMessages.flutterNoDevicesFound);
if (devices.isEmpty) {
if (deviceManager.hasSpecifiedDeviceId) {
globals.logger.printStatus(userMessages.flutterNoMatchingDevice(deviceManager.specifiedDeviceId!));
final List<Device> allDevices = await deviceManager.getAllConnectedDevices();
if (allDevices.isNotEmpty) {
globals.logger.printStatus('');
globals.logger.printStatus('The following devices were found:');
await Device.printDevices(allDevices, globals.logger);
}
return null;
} else if (deviceManager.hasSpecifiedAllDevices) {
globals.logger.printStatus(userMessages.flutterNoDevicesFound);
await _printUnsupportedDevice(deviceManager);
return null;
} else {
globals.printStatus(userMessages.flutterNoSupportedDevices);
}
final List<Device> unsupportedDevices = await deviceManager.getDevices();
if (unsupportedDevices.isNotEmpty) {
final StringBuffer result = StringBuffer();
result.writeln(userMessages.flutterFoundButUnsupportedDevices);
result.writeAll(
(await Device.descriptions(unsupportedDevices))
.map((String desc) => desc)
.toList(),
'\n',
);
result.writeln();
result.writeln(userMessages.flutterMissPlatformProjects(
Device.devicesPlatformTypes(unsupportedDevices),
));
globals.printStatus(result.toString());
globals.logger.printStatus(userMessages.flutterNoSupportedDevices);
await _printUnsupportedDevice(deviceManager);
return null;
}
return null;
} else if (devices.length > 1 && !deviceManager.hasSpecifiedAllDevices) {
} else if (devices.length > 1) {
if (deviceManager.hasSpecifiedDeviceId) {
globals.printStatus(userMessages.flutterFoundSpecifiedDevices(devices.length, deviceManager.specifiedDeviceId!));
} else {
globals.printStatus(userMessages.flutterSpecifyDeviceWithAllOption);
devices = await deviceManager.getAllConnectedDevices();
globals.logger.printStatus(userMessages.flutterFoundSpecifiedDevices(devices.length, deviceManager.specifiedDeviceId!));
return null;
} else if (!deviceManager.hasSpecifiedAllDevices) {
if (globals.terminal.stdinHasTerminal) {
// If DeviceManager was not able to prioritize a device. For example, if the user
// has two active Android devices running, then we request the user to
// choose one. If the user has two nonEphemeral devices running, we also
// request input to choose one.
globals.logger.printStatus(userMessages.flutterMultipleDevicesFound);
await Device.printDevices(devices, globals.logger);
final Device chosenDevice = await _chooseOneOfAvailableDevices(devices);
// Update the [DeviceManager.specifiedDeviceId] so that we will not be prompted again.
deviceManager.specifiedDeviceId = chosenDevice.id;
devices = <Device>[chosenDevice];
} else {
// Show an error message asking the user to specify `-d all` if they
// want to run on multiple devices.
final List<Device> allDevices = await deviceManager.getAllConnectedDevices();
globals.logger.printStatus(userMessages.flutterSpecifyDeviceWithAllOption);
globals.logger.printStatus('');
await Device.printDevices(allDevices, globals.logger);
return null;
}
}
globals.printStatus('');
await Device.printDevices(devices, globals.logger);
return null;
}
return devices;
}
Future<void> _printUnsupportedDevice(DeviceManager deviceManager) async {
final List<Device> unsupportedDevices = await deviceManager.getDevices();
if (unsupportedDevices.isNotEmpty) {
final StringBuffer result = StringBuffer();
result.writeln(userMessages.flutterFoundButUnsupportedDevices);
result.writeAll(
(await Device.descriptions(unsupportedDevices))
.map((String desc) => desc)
.toList(),
'\n',
);
result.writeln();
result.writeln(userMessages.flutterMissPlatformProjects(
Device.devicesPlatformTypes(unsupportedDevices),
));
globals.logger.printStatus(result.toString());
}
}
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) {
globals.logger.printStatus(userMessages.flutterChooseDevice(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: globals.logger,
prompt: userMessages.flutterChooseOne,
);
return result;
}
/// Find and return the target [Device] based upon currently connected
/// devices and criteria entered by the user on the command line.
/// If a device cannot be found that meets specified criteria,
......
......@@ -6,8 +6,6 @@ import 'dart:convert';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/devices.dart';
import 'package:flutter_tools/src/device.dart';
......@@ -133,7 +131,7 @@ webby (mobile) • webby • web-javascript • Web SDK (1.2.4) (emulato
}
class _FakeDeviceManager extends DeviceManager {
_FakeDeviceManager() : super(logger: testLogger, terminal: Terminal.test(), userMessages: userMessages);
_FakeDeviceManager() : super(logger: testLogger);
@override
Future<List<Device>> getAllConnectedDevices() =>
......@@ -153,7 +151,7 @@ class _FakeDeviceManager extends DeviceManager {
}
class NoDevicesManager extends DeviceManager {
NoDevicesManager() : super(logger: testLogger, terminal: Terminal.test(), userMessages: userMessages);
NoDevicesManager() : super(logger: testLogger);
@override
Future<List<Device>> getAllConnectedDevices() async => <Device>[];
......
......@@ -328,7 +328,7 @@ class FakeDeviceManager extends Fake implements DeviceManager {
Future<List<Device>> getDevices() async => devices;
@override
Future<List<Device>> findTargetDevices(FlutterProject? flutterProject, {Duration? timeout}) async => devices;
Future<List<Device>> findTargetDevices(FlutterProject? flutterProject, {Duration? timeout, bool promptUserToChooseDevice = true}) async => devices;
}
class FailingFakeFlutterDriverFactory extends Fake implements FlutterDriverFactory {
......
......@@ -10,8 +10,6 @@ import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/test.dart';
import 'package:flutter_tools/src/device.dart';
......@@ -851,7 +849,7 @@ class FakePackageTest implements TestWrapper {
}
class _FakeDeviceManager extends DeviceManager {
_FakeDeviceManager(this._connectedDevices) : super(logger: testLogger, terminal: Terminal.test(), userMessages: userMessages);
_FakeDeviceManager(this._connectedDevices) : super(logger: testLogger);
final List<Device> _connectedDevices;
......
......@@ -11,11 +11,15 @@ import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/error_handling_io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/pre_run_validator.dart';
import 'package:flutter_tools/src/project.dart';
......@@ -25,6 +29,7 @@ import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_devices.dart';
import '../../src/test_flutter_command_runner.dart';
import 'utils.dart';
......@@ -669,6 +674,138 @@ void main() {
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
group('findAllTargetDevices', () {
final FakeDevice device1 = FakeDevice('device1', 'device1');
final FakeDevice device2 = FakeDevice('device2', 'device2');
group('when specified device id', () {
testUsingContext('returns device when device is found', () async {
testDeviceManager.specifiedDeviceId = 'device-id';
testDeviceManager.addDevice(device1);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device1]);
});
testUsingContext('show error when no device found', () async {
testDeviceManager.specifiedDeviceId = 'device-id';
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, null);
expect(testLogger.statusText, contains(UserMessages().flutterNoMatchingDevice('device-id')));
});
testUsingContext('show error when multiple devices found', () async {
testDeviceManager.specifiedDeviceId = 'device-id';
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, null);
expect(testLogger.statusText, contains(UserMessages().flutterFoundSpecifiedDevices(2, 'device-id')));
});
});
group('when specified all', () {
testUsingContext('can return one device', () async {
testDeviceManager.specifiedDeviceId = 'all';
testDeviceManager.addDevice(device1);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device1]);
});
testUsingContext('can return multiple devices', () async {
testDeviceManager.specifiedDeviceId = 'all';
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device1, device2]);
});
testUsingContext('show error when no device found', () async {
testDeviceManager.specifiedDeviceId = 'all';
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, null);
expect(testLogger.statusText, contains(UserMessages().flutterNoDevicesFound));
});
});
group('when device not specified', () {
testUsingContext('returns one device when only one device connected', () async {
testDeviceManager.addDevice(device1);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device1]);
});
testUsingContext('show error when no device found', () async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, null);
expect(testLogger.statusText, contains(UserMessages().flutterNoSupportedDevices));
});
testUsingContext('show error when multiple devices found and not connected to terminal', () async {
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, null);
expect(testLogger.statusText, contains(UserMessages().flutterSpecifyDeviceWithAllOption));
}, overrides: <Type, Generator>{
AnsiTerminal: () => FakeTerminal(stdinHasTerminal: false),
});
// Prompt to choose device when multiple devices found and connected to terminal
group('show prompt', () {
late FakeTerminal terminal;
setUp(() {
terminal = FakeTerminal();
});
testUsingContext('choose first device', () async {
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
terminal.setPrompt(<String>['1', '2', 'q', 'Q'], '1');
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device1]);
}, overrides: <Type, Generator>{
AnsiTerminal: () => terminal,
});
testUsingContext('choose second device', () async {
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
terminal.setPrompt(<String>['1', '2', 'q', 'Q'], '2');
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device2]);
}, overrides: <Type, Generator>{
AnsiTerminal: () => terminal,
});
testUsingContext('exits without choosing device', () async {
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
terminal.setPrompt(<String>['1', '2', 'q', 'Q'], 'q');
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
await expectLater(
flutterCommand.findAllTargetDevices(),
throwsToolExit(),
);
}, overrides: <Type, Generator>{
AnsiTerminal: () => terminal,
});
});
});
});
});
}
......@@ -823,3 +960,33 @@ class FakePub extends Fake implements Pub {
bool printProgress = true,
}) async { }
}
class FakeTerminal extends Fake implements AnsiTerminal {
FakeTerminal({this.stdinHasTerminal = true});
@override
final bool stdinHasTerminal;
@override
bool usesTerminalUi = true;
void setPrompt(List<String> characters, String result) {
_nextPrompt = characters;
_nextResult = result;
}
List<String>? _nextPrompt;
late String _nextResult;
@override
Future<String> promptForCharInput(
List<String> acceptedCharacters, {
Logger? logger,
String? prompt,
int? defaultChoiceIndex,
bool displayAcceptedCharacters = true,
}) async {
expect(acceptedCharacters, _nextPrompt);
return _nextResult;
}
}
......@@ -241,7 +241,7 @@ class FakeDeviceManager implements DeviceManager {
}
@override
Future<List<Device>> findTargetDevices(FlutterProject? flutterProject, { Duration? timeout }) async {
Future<List<Device>> findTargetDevices(FlutterProject? flutterProject, { Duration? timeout, bool promptUserToChooseDevice = true }) async {
return devices;
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment