diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 195fc4a02a6..2d466d4010c 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -15,6 +15,7 @@ import 'src/commands/daemon.dart'; import 'src/commands/devices.dart'; import 'src/commands/doctor.dart'; import 'src/commands/drive.dart'; +import 'src/commands/emulators.dart'; import 'src/commands/format.dart'; import 'src/commands/fuchsia_reload.dart'; import 'src/commands/ide_config.dart'; @@ -57,6 +58,7 @@ Future main(List args) async { new DevicesCommand(), new DoctorCommand(verbose: verbose), new DriveCommand(), + new EmulatorsCommand(), new FormatCommand(), new FuchsiaReloadCommand(), new IdeConfigCommand(hidden: !verboseHelp), diff --git a/packages/flutter_tools/lib/src/android/android_emulator.dart b/packages/flutter_tools/lib/src/android/android_emulator.dart new file mode 100644 index 00000000000..8429346c264 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/android_emulator.dart @@ -0,0 +1,60 @@ +// Copyright 2018 The Chromium 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:meta/meta.dart'; + +import '../android/android_sdk.dart'; +import '../android/android_workflow.dart'; +import '../base/process.dart'; +import '../emulator.dart'; +import 'android_sdk.dart'; + +class AndroidEmulators extends EmulatorDiscovery { + @override + bool get supportsPlatform => true; + + @override + bool get canListAnything => androidWorkflow.canListDevices; + + @override + Future> get emulators async => getEmulatorAvds(); +} + +class AndroidEmulator extends Emulator { + AndroidEmulator( + String id + ) : super(id); + + @override + String get name => id; + + // @override + // Future launch() async { + // // TODO: ... + // return null;Í + // } +} + +/// Return the list of available emulator AVDs. +List getEmulatorAvds() { + final String emulatorPath = getEmulatorPath(androidSdk); + if (emulatorPath == null) + return []; + final String text = runSync([emulatorPath, '-list-avds']); + final List devices = []; + parseEmulatorAvdOutput(text, devices); + return devices; +} + +/// Parse the given `emulator -list-avds` output in [text], and fill out the given list +/// of emulators. +@visibleForTesting +void parseEmulatorAvdOutput(String text, + List emulators) { + for (String line in text.trim().split('\n')) { + emulators.add(new AndroidEmulator(line)); + } +} diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart index ddab13c0326..1875c48db61 100644 --- a/packages/flutter_tools/lib/src/android/android_sdk.dart +++ b/packages/flutter_tools/lib/src/android/android_sdk.dart @@ -59,6 +59,23 @@ String getAdbPath([AndroidSdk existingSdk]) { } } +/// Locate ADB. Prefer to use one from an Android SDK, if we can locate that. +/// This should be used over accessing androidSdk.adbPath directly because it +/// will work for those users who have Android Platform Tools installed but +/// not the full SDK. +String getEmulatorPath([AndroidSdk existingSdk]) { + if (existingSdk?.emulatorPath != null) + return existingSdk.emulatorPath; + + final AndroidSdk sdk = AndroidSdk.locateAndroidSdk(); + + if (sdk?.latestVersion == null) { + return os.which('emulator')?.path; + } else { + return sdk.emulatorPath; + } +} + class AndroidSdk { AndroidSdk(this.directory, [this.ndkDirectory, this.ndkCompiler, this.ndkCompilerArgs]) { @@ -200,6 +217,8 @@ class AndroidSdk { String get adbPath => getPlatformToolsPath('adb'); + String get emulatorPath => getToolsPath('emulator'); + /// Validate the Android SDK. This returns an empty list if there are no /// issues; otherwise, it returns a list of issues found. List validateSdkWellFormed() { @@ -216,6 +235,10 @@ class AndroidSdk { return fs.path.join(directory, 'platform-tools', binaryName); } + String getToolsPath(String binaryName) { + return fs.path.join(directory, 'tools', binaryName); + } + void _init() { Iterable platforms = []; // android-22, ... diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart index 14cc860a4e1..11874095196 100644 --- a/packages/flutter_tools/lib/src/android/android_workflow.dart +++ b/packages/flutter_tools/lib/src/android/android_workflow.dart @@ -42,6 +42,12 @@ class AndroidWorkflow extends DoctorValidator implements Workflow { @override bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed().isEmpty; + @override + bool get canListEmulators => getEmulatorPath(androidSdk) != null; + + @override + bool get canLaunchEmulators => androidSdk != null && androidSdk.validateSdkWellFormed().isEmpty; + static const String _kJdkDownload = 'https://www.oracle.com/technetwork/java/javase/downloads/'; /// Returns false if we cannot determine the Java version or if the version diff --git a/packages/flutter_tools/lib/src/commands/emulators.dart b/packages/flutter_tools/lib/src/commands/emulators.dart new file mode 100644 index 00000000000..516ab3df08f --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/emulators.dart @@ -0,0 +1,51 @@ +// Copyright 2018 The Chromium 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 '../base/common.dart'; +import '../base/utils.dart'; +import '../doctor.dart'; +import '../emulator.dart'; +import '../globals.dart'; +import '../runner/flutter_command.dart'; + +class EmulatorsCommand extends FlutterCommand { + @override + final String name = 'emulators'; + + @override + final String description = 'List all available emulators.'; + + @override + Future runCommand() async { + if (!doctor.canListAnything) { + throwToolExit( + "Unable to locate emulators; please run 'flutter doctor' for " + 'information about installing additional components.', + exitCode: 1); + } + + final List emulators = await emulatorManager.getAllAvailableEmulators().toList(); + + if (emulators.isEmpty) { + printStatus( + 'No emulators available.\n\n' + // TODO: Change these when we support creation + // 'You may need to create images using "flutter emulators --create"\n' + 'You may need to create one using Android Studio\n' + 'or visit https://flutter.io/setup/ for troubleshooting tips.'); + final List diagnostics = await emulatorManager.getEmulatorDiagnostics(); + if (diagnostics.isNotEmpty) { + printStatus(''); + for (String diagnostic in diagnostics) { + printStatus('• ${diagnostic.replaceAll('\n', '\n ')}'); + } + } + } else { + printStatus('${emulators.length} available ${pluralize('emulators', emulators.length)}:\n'); + await Emulator.printEmulators(emulators); + } + } +} diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 5da0f2b796f..d5c23c3e41e 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -26,6 +26,7 @@ import 'compile.dart'; import 'devfs.dart'; import 'device.dart'; import 'doctor.dart'; +import 'emulator.dart'; import 'ios/cocoapods.dart'; import 'ios/ios_workflow.dart'; import 'ios/mac.dart'; @@ -58,6 +59,7 @@ Future runInContext( DeviceManager: () => new DeviceManager(), Doctor: () => const Doctor(), DoctorValidatorsProvider: () => DoctorValidatorsProvider.defaultInstance, + EmulatorManager: () => new EmulatorManager(), Flags: () => const EmptyFlags(), FlutterVersion: () => new FlutterVersion(const Clock()), GenSnapshot: () => const GenSnapshot(), diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index bbb3f3aac15..e8677a28f4c 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -209,6 +209,12 @@ abstract class Workflow { /// Could this thing launch *something*? It may still have minor issues. bool get canLaunchDevices; + + /// Are we functional enough to list emulators? + bool get canListEmulators; + + /// Could this thing launch *something*? It may still have minor issues. + bool get canLaunchEmulators; } enum ValidationType { diff --git a/packages/flutter_tools/lib/src/emulator.dart b/packages/flutter_tools/lib/src/emulator.dart new file mode 100644 index 00000000000..673e4b55695 --- /dev/null +++ b/packages/flutter_tools/lib/src/emulator.dart @@ -0,0 +1,168 @@ +// Copyright 2018 The Chromium 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:math' as math; + +import 'android/android_emulator.dart'; +import 'base/context.dart'; +import 'globals.dart'; + +EmulatorManager get emulatorManager => context[EmulatorManager]; + +/// A class to get all available emulators. +class EmulatorManager { + /// Constructing EmulatorManager is cheap; they only do expensive work if some + /// of their methods are called. + EmulatorManager() { + // Register the known discoverers. + _emulatorDiscoverers.add(new AndroidEmulators()); + } + + final List _emulatorDiscoverers = []; + + String _specifiedEmulatorId; + + /// A user-specified emulator ID. + String get specifiedEmulatorId { + if (_specifiedEmulatorId == null || _specifiedEmulatorId == 'all') + return null; + return _specifiedEmulatorId; + } + + set specifiedEmulatorId(String id) { + _specifiedEmulatorId = id; + } + + /// True when the user has specified a single specific emulator. + bool get hasSpecifiedEmulatorId => specifiedEmulatorId != null; + + /// True when the user has specified all emulators by setting + /// specifiedEmulatorId = 'all'. + bool get hasSpecifiedAllEmulators => _specifiedEmulatorId == 'all'; + + Stream getEmulatorsById(String emulatorId) async* { + final List emulators = await getAllAvailableEmulators().toList(); + emulatorId = emulatorId.toLowerCase(); + bool exactlyMatchesEmulatorId(Emulator emulator) => + emulator.id.toLowerCase() == emulatorId || + emulator.name.toLowerCase() == emulatorId; + bool startsWithEmulatorId(Emulator emulator) => + emulator.id.toLowerCase().startsWith(emulatorId) || + emulator.name.toLowerCase().startsWith(emulatorId); + + final Emulator exactMatch = emulators.firstWhere( + exactlyMatchesEmulatorId, orElse: () => null); + if (exactMatch != null) { + yield exactMatch; + return; + } + + // Match on a id or name starting with [emulatorId]. + for (Emulator emulator in emulators.where(startsWithEmulatorId)) + yield emulator; + } + + /// Return the list of available emulators, filtered by any user-specified emulator id. + Stream getEmulators() { + return hasSpecifiedEmulatorId + ? getEmulatorsById(specifiedEmulatorId) + : getAllAvailableEmulators(); + } + + Iterable get _platformDiscoverers { + return _emulatorDiscoverers.where((EmulatorDiscovery discoverer) => discoverer.supportsPlatform); + } + + /// Return the list of all connected emulators. + Stream getAllAvailableEmulators() async* { + for (EmulatorDiscovery discoverer in _platformDiscoverers) { + for (Emulator emulator in await discoverer.emulators) { + yield emulator; + } + } + } + + /// Whether we're capable of listing any emulators given the current environment configuration. + bool get canListAnything { + return _platformDiscoverers.any((EmulatorDiscovery discoverer) => discoverer.canListAnything); + } + + /// Get diagnostics about issues with any emulators. + Future> getEmulatorDiagnostics() async { + final List diagnostics = []; + for (EmulatorDiscovery discoverer in _platformDiscoverers) { + diagnostics.addAll(await discoverer.getDiagnostics()); + } + return diagnostics; + } +} + +/// An abstract class to discover and enumerate a specific type of emulators. +abstract class EmulatorDiscovery { + bool get supportsPlatform; + + /// Whether this emulator discovery is capable of listing any emulators given the + /// current environment configuration. + bool get canListAnything; + + Future> get emulators; + + /// Gets a list of diagnostic messages pertaining to issues with any available + /// emulators (will be an empty list if there are no issues). + Future> getDiagnostics() => new Future>.value([]); +} + +abstract class Emulator { + Emulator(this.id); + + final String id; + + String get name; + + @override + int get hashCode => id.hashCode; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other is! Emulator) + return false; + return id == other.id; + } + + @override + String toString() => name; + + static Stream descriptions(List emulators) async* { + if (emulators.isEmpty) + return; + + // Extract emulators information + final List> table = >[]; + for (Emulator emulator in emulators) { + table.add([ + emulator.name, + emulator.id, + ]); + } + + // Calculate column widths + final List indices = new List.generate(table[0].length - 1, (int i) => i); + List widths = indices.map((int i) => 0).toList(); + for (List row in table) { + widths = indices.map((int i) => math.max(widths[i], row[i].length)).toList(); + } + + // Join columns into lines of text + for (List row in table) { + yield indices.map((int i) => row[i].padRight(widths[i])).join(' • ') + ' • ${row.last}'; + } + } + + static Future printEmulators(List emulators) async { + await descriptions(emulators).forEach(printStatus); + } +} diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart index 8c87671715f..f92838392f5 100644 --- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart +++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart @@ -30,6 +30,12 @@ class IOSWorkflow extends DoctorValidator implements Workflow { @override bool get canLaunchDevices => xcode.isInstalledAndMeetsVersionCheck; + @override + bool get canListEmulators => false; + + @override + bool get canLaunchEmulators => false; + Future get hasIDeviceInstaller => exitsHappyAsync(['ideviceinstaller', '-h']); Future get hasIosDeploy => exitsHappyAsync(['ios-deploy', '--version']);