1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
// 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 'package:process/process.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/logger.dart';
import '../base/process.dart';
import '../base/utils.dart';
import '../convert.dart';
import '../device.dart';
import '../emulator.dart';
import 'android_sdk.dart';
class AndroidEmulators extends EmulatorDiscovery {
AndroidEmulators({
@required AndroidSdk androidSdk,
@required AndroidWorkflow androidWorkflow,
@required FileSystem fileSystem,
@required Logger logger,
@required ProcessManager processManager,
}) : _androidSdk = androidSdk,
_androidWorkflow = androidWorkflow,
_fileSystem = fileSystem,
_logger = logger,
_processManager = processManager,
_processUtils = ProcessUtils(logger: logger, processManager: processManager);
final AndroidWorkflow _androidWorkflow;
final AndroidSdk _androidSdk;
final FileSystem _fileSystem;
final Logger _logger;
final ProcessManager _processManager;
final ProcessUtils _processUtils;
@override
bool get supportsPlatform => true;
@override
bool get canListAnything => _androidWorkflow.canListEmulators;
@override
bool get canLaunchAnything => _androidWorkflow.canListEmulators
&& _androidSdk.getAvdManagerPath() != null;
@override
Future<List<Emulator>> get emulators => _getEmulatorAvds();
/// Return the list of available emulator AVDs.
Future<List<AndroidEmulator>> _getEmulatorAvds() async {
final String emulatorPath = getEmulatorPath(_androidSdk);
if (emulatorPath == null) {
return <AndroidEmulator>[];
}
final String listAvdsOutput = (await _processUtils.run(
<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();
final AndroidEmulator androidEmulatorWithoutProperties = AndroidEmulator(
id,
processManager: _processManager,
logger: _logger,
androidSdk: _androidSdk,
);
if (avdPath == null) {
return androidEmulatorWithoutProperties;
}
final File iniFile = _fileSystem.file(_fileSystem.path.join(avdPath, '$id.ini'));
if (!iniFile.existsSync()) {
return androidEmulatorWithoutProperties;
}
final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync());
if (ini['path'] == null) {
return androidEmulatorWithoutProperties;
}
final File configFile = _fileSystem.file(_fileSystem.path.join(ini['path'], 'config.ini'));
if (!configFile.existsSync()) {
return androidEmulatorWithoutProperties;
}
final Map<String, String> properties = parseIniLines(configFile.readAsLinesSync());
return AndroidEmulator(
id,
properties: properties,
processManager: _processManager,
logger: _logger,
androidSdk: _androidSdk,
);
}
}
class AndroidEmulator extends Emulator {
AndroidEmulator(String id, {
Map<String, String> properties,
@required Logger logger,
@required AndroidSdk androidSdk,
@required ProcessManager processManager,
}) : _properties = properties,
_logger = logger,
_androidSdk = androidSdk,
_processUtils = ProcessUtils(logger: logger, processManager: processManager),
super(id, properties != null && properties.isNotEmpty);
final Map<String, String> _properties;
final Logger _logger;
final ProcessUtils _processUtils;
final AndroidSdk _androidSdk;
// 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(_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) {
_logger.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) {
_logger.printTrace('Android emulator stdout:');
stdoutList.forEach(_logger.printTrace);
}
if (!earlyFailure && stderrList.isEmpty) {
_logger.printStatus('The Android emulator exited with code $status');
return;
}
final String when = earlyFailure ? 'during startup' : 'after startup';
_logger.printError('The Android emulator exited with code $status $when');
_logger.printError('Android emulator stderr:');
stderrList.forEach(_logger.printError);
_logger.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;
}
}
@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;
}