blob: c9777b7fb5f8aa869286f9d03f6901ba49628df9 [file] [log] [blame]
// 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:async';
import 'package:meta/meta.dart';
import '../android/android_sdk.dart';
import '../android/android_workflow.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/process.dart';
import '../base/utils.dart';
import '../convert.dart';
import '../device.dart';
import '../emulator.dart';
import '../globals.dart' as globals;
import 'android_sdk.dart';
class AndroidEmulators extends EmulatorDiscovery {
@override
bool get supportsPlatform => true;
@override
bool get canListAnything => androidWorkflow.canListEmulators;
@override
Future<List<Emulator>> get emulators async => getEmulatorAvds();
}
class AndroidEmulator extends Emulator {
AndroidEmulator(String id, [this._properties])
: super(id, _properties != null && _properties.isNotEmpty);
final Map<String, String> _properties;
// Android Studio uses the ID with underscores replaced with spaces
// for the name if displayname is not set so we do the same.
@override
String get name => _prop('avd.ini.displayname') ?? id.replaceAll('_', ' ').trim();
@override
String get manufacturer => _prop('hw.device.manufacturer');
@override
Category get category => Category.mobile;
@override
PlatformType get platformType => PlatformType.android;
String _prop(String name) => _properties != null ? _properties[name] : null;
@override
Future<void> launch() async {
final Process process = await processUtils.start(
<String>[getEmulatorPath(globals.androidSdk), '-avd', id],
);
// Record output from the emulator process.
final List<String> stdoutList = <String>[];
final List<String> stderrList = <String>[];
final StreamSubscription<String> stdoutSubscription = process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(stdoutList.add);
final StreamSubscription<String> stderrSubscription = process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(stderrList.add);
final Future<void> stdioFuture = waitGroup<void>(<Future<void>>[
stdoutSubscription.asFuture<void>(),
stderrSubscription.asFuture<void>(),
]);
// The emulator continues running on success, so we don't wait for the
// process to complete before continuing. However, if the process fails
// after the startup phase (3 seconds), then we only echo its output if
// its error code is non-zero and its stderr is non-empty.
bool earlyFailure = true;
unawaited(process.exitCode.then((int status) async {
if (status == 0) {
globals.printTrace('The Android emulator exited successfully');
return;
}
// Make sure the process' stdout and stderr are drained.
await stdioFuture;
unawaited(stdoutSubscription.cancel());
unawaited(stderrSubscription.cancel());
if (stdoutList.isNotEmpty) {
globals.printTrace('Android emulator stdout:');
stdoutList.forEach(globals.printTrace);
}
if (!earlyFailure && stderrList.isEmpty) {
globals.printStatus('The Android emulator exited with code $status');
return;
}
final String when = earlyFailure ? 'during startup' : 'after startup';
globals.printError('The Android emulator exited with code $status $when');
globals.printError('Android emulator stderr:');
stderrList.forEach(globals.printError);
globals.printError('Address these issues and try again.');
}));
// Wait a few seconds for the emulator to start.
await Future<void>.delayed(const Duration(seconds: 3));
earlyFailure = false;
return;
}
}
/// Return the list of available emulator AVDs.
List<AndroidEmulator> getEmulatorAvds() {
final String emulatorPath = getEmulatorPath(globals.androidSdk);
if (emulatorPath == null) {
return <AndroidEmulator>[];
}
final String listAvdsOutput = processUtils.runSync(
<String>[emulatorPath, '-list-avds']).stdout.trim();
final List<AndroidEmulator> emulators = <AndroidEmulator>[];
if (listAvdsOutput != null) {
extractEmulatorAvdInfo(listAvdsOutput, emulators);
}
return emulators;
}
/// Parse the given `emulator -list-avds` output in [text], and fill out the given list
/// of emulators by reading information from the relevant ini files.
void extractEmulatorAvdInfo(String text, List<AndroidEmulator> emulators) {
for (final String id in text.trim().split('\n').where((String l) => l != '')) {
emulators.add(_loadEmulatorInfo(id));
}
}
AndroidEmulator _loadEmulatorInfo(String id) {
id = id.trim();
final String avdPath = getAvdPath();
if (avdPath != null) {
final File iniFile = globals.fs.file(globals.fs.path.join(avdPath, '$id.ini'));
if (iniFile.existsSync()) {
final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync());
if (ini['path'] != null) {
final File configFile =
globals.fs.file(globals.fs.path.join(ini['path'], 'config.ini'));
if (configFile.existsSync()) {
final Map<String, String> properties =
parseIniLines(configFile.readAsLinesSync());
return AndroidEmulator(id, properties);
}
}
}
}
return AndroidEmulator(id);
}
@visibleForTesting
Map<String, String> parseIniLines(List<String> contents) {
final Map<String, String> results = <String, String>{};
final Iterable<List<String>> properties = contents
.map<String>((String l) => l.trim())
// Strip blank lines/comments
.where((String l) => l != '' && !l.startsWith('#'))
// Discard anything that isn't simple name=value
.where((String l) => l.contains('='))
// Split into name/value
.map<List<String>>((String l) => l.split('='));
for (final List<String> property in properties) {
results[property[0].trim()] = property[1].trim();
}
return results;
}