| // 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; |
| } |