Add basic support for listing Android AVDs
Very basic support for "flutter emulators" which just lists the available Android AVDs.
Relates to:
https://github.com/flutter/flutter/issues/14822
https://github.com/Dart-Code/Dart-Code/issues/490
https://github.com/flutter/flutter/issues/13379
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index 195fc4a..2d466d4 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -15,6 +15,7 @@
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 @@
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 0000000..8429346
--- /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<List<Emulator>> get emulators async => getEmulatorAvds();
+}
+
+class AndroidEmulator extends Emulator {
+ AndroidEmulator(
+ String id
+ ) : super(id);
+
+ @override
+ String get name => id;
+
+ // @override
+ // Future<bool> launch() async {
+ // // TODO: ...
+ // return null;Í
+ // }
+}
+
+/// Return the list of available emulator AVDs.
+List<AndroidEmulator> getEmulatorAvds() {
+ final String emulatorPath = getEmulatorPath(androidSdk);
+ if (emulatorPath == null)
+ return <AndroidEmulator>[];
+ final String text = runSync(<String>[emulatorPath, '-list-avds']);
+ final List<AndroidEmulator> devices = <AndroidEmulator>[];
+ 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<AndroidEmulator> 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 ddab13c..1875c48 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 @@
}
}
+/// 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 @@
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<String> validateSdkWellFormed() {
@@ -216,6 +235,10 @@
return fs.path.join(directory, 'platform-tools', binaryName);
}
+ String getToolsPath(String binaryName) {
+ return fs.path.join(directory, 'tools', binaryName);
+ }
+
void _init() {
Iterable<Directory> platforms = <Directory>[]; // 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 14cc860..1187409 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 @@
@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 0000000..516ab3d
--- /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<Null> runCommand() async {
+ if (!doctor.canListAnything) {
+ throwToolExit(
+ "Unable to locate emulators; please run 'flutter doctor' for "
+ 'information about installing additional components.',
+ exitCode: 1);
+ }
+
+ final List<Emulator> 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<String> 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 5da0f2b..d5c23c3 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -26,6 +26,7 @@
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 @@
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 bbb3f3a..e8677a2 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -209,6 +209,12 @@
/// 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 0000000..673e4b5
--- /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<EmulatorDiscovery> _emulatorDiscoverers = <EmulatorDiscovery>[];
+
+ 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<Emulator> getEmulatorsById(String emulatorId) async* {
+ final List<Emulator> 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<Emulator> getEmulators() {
+ return hasSpecifiedEmulatorId
+ ? getEmulatorsById(specifiedEmulatorId)
+ : getAllAvailableEmulators();
+ }
+
+ Iterable<EmulatorDiscovery> get _platformDiscoverers {
+ return _emulatorDiscoverers.where((EmulatorDiscovery discoverer) => discoverer.supportsPlatform);
+ }
+
+ /// Return the list of all connected emulators.
+ Stream<Emulator> 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<List<String>> getEmulatorDiagnostics() async {
+ final List<String> diagnostics = <String>[];
+ 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<List<Emulator>> 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<List<String>> getDiagnostics() => new Future<List<String>>.value(<String>[]);
+}
+
+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<String> descriptions(List<Emulator> emulators) async* {
+ if (emulators.isEmpty)
+ return;
+
+ // Extract emulators information
+ final List<List<String>> table = <List<String>>[];
+ for (Emulator emulator in emulators) {
+ table.add(<String>[
+ emulator.name,
+ emulator.id,
+ ]);
+ }
+
+ // Calculate column widths
+ final List<int> indices = new List<int>.generate(table[0].length - 1, (int i) => i);
+ List<int> widths = indices.map((int i) => 0).toList();
+ for (List<String> row in table) {
+ widths = indices.map((int i) => math.max(widths[i], row[i].length)).toList();
+ }
+
+ // Join columns into lines of text
+ for (List<String> row in table) {
+ yield indices.map((int i) => row[i].padRight(widths[i])).join(' • ') + ' • ${row.last}';
+ }
+ }
+
+ static Future<Null> printEmulators(List<Emulator> 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 8c87671..f928383 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 @@
@override
bool get canLaunchDevices => xcode.isInstalledAndMeetsVersionCheck;
+ @override
+ bool get canListEmulators => false;
+
+ @override
+ bool get canLaunchEmulators => false;
+
Future<bool> get hasIDeviceInstaller => exitsHappyAsync(<String>['ideviceinstaller', '-h']);
Future<bool> get hasIosDeploy => exitsHappyAsync(<String>['ios-deploy', '--version']);