| // 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 'android/android_sdk.dart'; | |
| import 'base/context.dart'; | |
| import 'base/io.dart' show ProcessResult; | |
| import 'base/process_manager.dart'; | |
| import 'globals.dart'; | |
| import 'ios/ios_emulators.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(AndroidEmulators()); | |
| _emulatorDiscoverers.add(IOSEmulators()); | |
| } | |
| final List<EmulatorDiscovery> _emulatorDiscoverers = <EmulatorDiscovery>[]; | |
| Future<List<Emulator>> getEmulatorsMatching(String searchText) async { | |
| final List<Emulator> emulators = await getAllAvailableEmulators(); | |
| searchText = searchText.toLowerCase(); | |
| bool exactlyMatchesEmulatorId(Emulator emulator) => | |
| emulator.id?.toLowerCase() == searchText || | |
| emulator.name?.toLowerCase() == searchText; | |
| bool startsWithEmulatorId(Emulator emulator) => | |
| emulator.id?.toLowerCase()?.startsWith(searchText) == true || | |
| emulator.name?.toLowerCase()?.startsWith(searchText) == true; | |
| final Emulator exactMatch = | |
| emulators.firstWhere(exactlyMatchesEmulatorId, orElse: () => null); | |
| if (exactMatch != null) { | |
| return <Emulator>[exactMatch]; | |
| } | |
| // Match on a id or name starting with [emulatorId]. | |
| return emulators.where(startsWithEmulatorId).toList(); | |
| } | |
| Iterable<EmulatorDiscovery> get _platformDiscoverers { | |
| return _emulatorDiscoverers.where((EmulatorDiscovery discoverer) => discoverer.supportsPlatform); | |
| } | |
| /// Return the list of all available emulators. | |
| Future<List<Emulator>> getAllAvailableEmulators() async { | |
| final List<Emulator> emulators = <Emulator>[]; | |
| await Future.forEach<EmulatorDiscovery>(_platformDiscoverers, (EmulatorDiscovery discoverer) async { | |
| emulators.addAll(await discoverer.emulators); | |
| }); | |
| return emulators; | |
| } | |
| /// Return the list of all available emulators. | |
| Future<CreateEmulatorResult> createEmulator({String name}) async { | |
| if (name == null || name == '') { | |
| const String autoName = 'flutter_emulator'; | |
| // Don't use getEmulatorsMatching here, as it will only return one | |
| // if there's an exact match and we need all those with this prefix | |
| // so we can keep adding suffixes until we miss. | |
| final List<Emulator> all = await getAllAvailableEmulators(); | |
| final Set<String> takenNames = all | |
| .map<String>((Emulator e) => e.id) | |
| .where((String id) => id.startsWith(autoName)) | |
| .toSet(); | |
| int suffix = 1; | |
| name = autoName; | |
| while (takenNames.contains(name)) { | |
| name = '${autoName}_${++suffix}'; | |
| } | |
| } | |
| final String device = await _getPreferredAvailableDevice(); | |
| if (device == null) | |
| return CreateEmulatorResult(name, | |
| success: false, error: 'No device definitions are available'); | |
| final String sdkId = await _getPreferredSdkId(); | |
| if (sdkId == null) | |
| return CreateEmulatorResult(name, | |
| success: false, | |
| error: | |
| 'No suitable Android AVD system images are available. You may need to install these' | |
| ' using sdkmanager, for example:\n' | |
| ' sdkmanager "system-images;android-27;google_apis_playstore;x86"'); | |
| // Cleans up error output from avdmanager to make it more suitable to show | |
| // to flutter users. Specifically: | |
| // - Removes lines that say "null" (!) | |
| // - Removes lines that tell the user to use '--force' to overwrite emulators | |
| String cleanError(String error) { | |
| if (error == null || error.trim() == '') | |
| return null; | |
| return error | |
| .split('\n') | |
| .where((String l) => l.trim() != 'null') | |
| .where((String l) => | |
| l.trim() != 'Use --force if you want to replace it.') | |
| .join('\n') | |
| .trim(); | |
| } | |
| final List<String> args = <String>[ | |
| getAvdManagerPath(androidSdk), | |
| 'create', | |
| 'avd', | |
| '-n', name, | |
| '-k', sdkId, | |
| '-d', device | |
| ]; | |
| final ProcessResult runResult = processManager.runSync(args, | |
| environment: androidSdk?.sdkManagerEnv); | |
| return CreateEmulatorResult( | |
| name, | |
| success: runResult.exitCode == 0, | |
| output: runResult.stdout, | |
| error: cleanError(runResult.stderr), | |
| ); | |
| } | |
| static const List<String> preferredDevices = <String>[ | |
| 'pixel', | |
| 'pixel_xl', | |
| ]; | |
| Future<String> _getPreferredAvailableDevice() async { | |
| final List<String> args = <String>[ | |
| getAvdManagerPath(androidSdk), | |
| 'list', | |
| 'device', | |
| '-c' | |
| ]; | |
| final ProcessResult runResult = processManager.runSync(args, | |
| environment: androidSdk?.sdkManagerEnv); | |
| if (runResult.exitCode != 0) | |
| return null; | |
| final List<String> availableDevices = runResult.stdout | |
| .split('\n') | |
| .where((String l) => preferredDevices.contains(l.trim())) | |
| .toList(); | |
| return preferredDevices.firstWhere( | |
| (String d) => availableDevices.contains(d), | |
| orElse: () => null, | |
| ); | |
| } | |
| RegExp androidApiVersion = RegExp(r';android-(\d+);'); | |
| Future<String> _getPreferredSdkId() async { | |
| // It seems that to get the available list of images, we need to send a | |
| // request to create without the image and it'll provide us a list :-( | |
| final List<String> args = <String>[ | |
| getAvdManagerPath(androidSdk), | |
| 'create', | |
| 'avd', | |
| '-n', 'temp', | |
| ]; | |
| final ProcessResult runResult = processManager.runSync(args, | |
| environment: androidSdk?.sdkManagerEnv); | |
| // Get the list of IDs that match our criteria | |
| final List<String> availableIDs = runResult.stderr | |
| .split('\n') | |
| .where((String l) => androidApiVersion.hasMatch(l)) | |
| .where((String l) => l.contains('system-images')) | |
| .where((String l) => l.contains('google_apis_playstore')) | |
| .toList(); | |
| final List<int> availableApiVersions = availableIDs | |
| .map<String>((String id) => androidApiVersion.firstMatch(id).group(1)) | |
| .map<int>((String apiVersion) => int.parse(apiVersion)) | |
| .toList(); | |
| // Get the highest Android API version or whats left | |
| final int apiVersion = availableApiVersions.isNotEmpty | |
| ? availableApiVersions.reduce(math.max) | |
| : -1; // Don't match below | |
| // We're out of preferences, we just have to return the first one with the high | |
| // API version. | |
| return availableIDs.firstWhere( | |
| (String id) => id.contains(';android-$apiVersion;'), | |
| orElse: () => null, | |
| ); | |
| } | |
| /// Whether we're capable of listing any emulators given the current environment configuration. | |
| bool get canListAnything { | |
| return _platformDiscoverers.any((EmulatorDiscovery discoverer) => discoverer.canListAnything); | |
| } | |
| } | |
| /// 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; | |
| } | |
| abstract class Emulator { | |
| Emulator(this.id, this.hasConfig); | |
| final String id; | |
| final bool hasConfig; | |
| String get name; | |
| String get manufacturer; | |
| String get label; | |
| @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; | |
| } | |
| Future<void> launch(); | |
| @override | |
| String toString() => name; | |
| static List<String> descriptions(List<Emulator> emulators) { | |
| if (emulators.isEmpty) | |
| return <String>[]; | |
| // Extract emulators information | |
| final List<List<String>> table = <List<String>>[]; | |
| for (Emulator emulator in emulators) { | |
| table.add(<String>[ | |
| emulator.id ?? '', | |
| emulator.name ?? '', | |
| emulator.manufacturer ?? '', | |
| emulator.label ?? '', | |
| ]); | |
| } | |
| // Calculate column widths | |
| final List<int> indices = List<int>.generate(table[0].length - 1, (int i) => i); | |
| List<int> widths = indices.map<int>((int i) => 0).toList(); | |
| for (List<String> row in table) { | |
| widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList(); | |
| } | |
| // Join columns into lines of text | |
| final RegExp whiteSpaceAndDots = RegExp(r'[•\s]+$'); | |
| return table | |
| .map<String>((List<String> row) { | |
| return indices | |
| .map<String>((int i) => row[i].padRight(widths[i])) | |
| .join(' • ') + | |
| ' • ${row.last}'; | |
| }) | |
| .map<String>((String line) => line.replaceAll(whiteSpaceAndDots, '')) | |
| .toList(); | |
| } | |
| static void printEmulators(List<Emulator> emulators) { | |
| descriptions(emulators).forEach(printStatus); | |
| } | |
| } | |
| class CreateEmulatorResult { | |
| CreateEmulatorResult(this.emulatorName, {this.success, this.output, this.error}); | |
| final bool success; | |
| final String emulatorName; | |
| final String output; | |
| final String error; | |
| } |