// Copyright 2014 The Flutter 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:math' as math; import 'package:meta/meta.dart'; import 'package:process/process.dart'; import 'android/android_emulator.dart'; import 'android/android_sdk.dart'; import 'android/android_workflow.dart'; import 'base/context.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/process.dart'; import 'device.dart'; import 'ios/ios_emulators.dart'; EmulatorManager? get emulatorManager => context.get<EmulatorManager>(); /// A class to get all available emulators. class EmulatorManager { EmulatorManager({ AndroidSdk? androidSdk, required Logger logger, required ProcessManager processManager, required AndroidWorkflow androidWorkflow, required FileSystem fileSystem, }) : _androidSdk = androidSdk, _processUtils = ProcessUtils(logger: logger, processManager: processManager), _androidEmulators = AndroidEmulators( androidSdk: androidSdk, logger: logger, processManager: processManager, fileSystem: fileSystem, androidWorkflow: androidWorkflow ) { _emulatorDiscoverers.add(_androidEmulators); } final AndroidSdk? _androidSdk; final AndroidEmulators _androidEmulators; final ProcessUtils _processUtils; // Constructing EmulatorManager is cheap; they only do expensive work if some // of their methods are called. final List<EmulatorDiscovery> _emulatorDiscoverers = <EmulatorDiscovery>[ IOSEmulators(), ]; 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; Emulator? exactMatch; for (final Emulator emulator in emulators) { if (exactlyMatchesEmulatorId(emulator)) { exactMatch = emulator; break; } } 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.isEmpty) { 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 emulatorName = name!; final String? avdManagerPath = _androidSdk?.avdManagerPath; if (avdManagerPath == null || !_androidEmulators.canLaunchAnything) { return CreateEmulatorResult(emulatorName, success: false, error: 'avdmanager is missing from the Android SDK' ); } final String? device = await _getPreferredAvailableDevice(avdManagerPath); if (device == null) { return CreateEmulatorResult(emulatorName, success: false, error: 'No device definitions are available'); } final String? sdkId = await _getPreferredSdkId(avdManagerPath); if (sdkId == null) { return CreateEmulatorResult(emulatorName, 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 RunResult runResult = await _processUtils.run(<String>[ avdManagerPath, 'create', 'avd', '-n', emulatorName, '-k', sdkId, '-d', device, ], environment: _androidSdk?.sdkManagerEnv, ); return CreateEmulatorResult( emulatorName, success: runResult.exitCode == 0, output: runResult.stdout, error: cleanError(runResult.stderr), ); } static const List<String> preferredDevices = <String>[ 'pixel', 'pixel_xl', ]; Future<String?> _getPreferredAvailableDevice(String avdManagerPath) async { final List<String> args = <String>[ avdManagerPath, 'list', 'device', '-c', ]; final RunResult runResult = await _processUtils.run(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(); for (final String device in preferredDevices) { if (availableDevices.contains(device)) { return device; } } return null; } static final RegExp _androidApiVersion = RegExp(r';android-(\d+);'); Future<String?> _getPreferredSdkId(String avdManagerPath) 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>[ avdManagerPath, 'create', 'avd', '-n', 'temp', ]; final RunResult runResult = await _processUtils.run(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. for (final String id in availableIDs) { if (id.contains(';android-$apiVersion;')) { return id; } } return 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. bool get canListAnything; /// Whether this emulator discovery is capable of launching new emulators. bool get canLaunchAnything; Future<List<Emulator>> get emulators; } @immutable abstract class Emulator { const Emulator(this.id, this.hasConfig); final String id; final bool hasConfig; String get name; String? get manufacturer; Category get category; PlatformType get platformType; @override int get hashCode => id.hashCode; @override bool operator ==(Object other) { if (identical(this, other)) { return true; } return other is Emulator && other.id == id; } Future<void> launch({bool coldBoot}); @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 (final Emulator emulator in emulators) <String>[ emulator.id, emulator.name, emulator.manufacturer ?? '', emulator.platformType.toString(), ], ]; // 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 (final 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])) .followedBy(<String>[row.last]) .join(' • '); }) .map<String>((String line) => line.replaceAll(whiteSpaceAndDots, '')) .toList(); } static void printEmulators(List<Emulator> emulators, Logger logger) { descriptions(emulators).forEach(logger.printStatus); } } class CreateEmulatorResult { CreateEmulatorResult(this.emulatorName, {required this.success, this.output, this.error}); final bool success; final String emulatorName; final String? output; final String? error; }