• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2018 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:math' as math;
7
8import 'android/android_emulator.dart';
9import 'android/android_sdk.dart';
10import 'base/context.dart';
11import 'base/io.dart' show ProcessResult;
12import 'base/process_manager.dart';
13import 'device.dart';
14import 'globals.dart';
15import 'ios/ios_emulators.dart';
16
17EmulatorManager get emulatorManager => context.get<EmulatorManager>();
18
19/// A class to get all available emulators.
20class EmulatorManager {
21  /// Constructing EmulatorManager is cheap; they only do expensive work if some
22  /// of their methods are called.
23  EmulatorManager() {
24    // Register the known discoverers.
25    _emulatorDiscoverers.add(AndroidEmulators());
26    _emulatorDiscoverers.add(IOSEmulators());
27  }
28
29  final List<EmulatorDiscovery> _emulatorDiscoverers = <EmulatorDiscovery>[];
30
31  Future<List<Emulator>> getEmulatorsMatching(String searchText) async {
32    final List<Emulator> emulators = await getAllAvailableEmulators();
33    searchText = searchText.toLowerCase();
34    bool exactlyMatchesEmulatorId(Emulator emulator) =>
35        emulator.id?.toLowerCase() == searchText ||
36        emulator.name?.toLowerCase() == searchText;
37    bool startsWithEmulatorId(Emulator emulator) =>
38        emulator.id?.toLowerCase()?.startsWith(searchText) == true ||
39        emulator.name?.toLowerCase()?.startsWith(searchText) == true;
40
41    final Emulator exactMatch =
42        emulators.firstWhere(exactlyMatchesEmulatorId, orElse: () => null);
43    if (exactMatch != null) {
44      return <Emulator>[exactMatch];
45    }
46
47    // Match on a id or name starting with [emulatorId].
48    return emulators.where(startsWithEmulatorId).toList();
49  }
50
51  Iterable<EmulatorDiscovery> get _platformDiscoverers {
52    return _emulatorDiscoverers.where((EmulatorDiscovery discoverer) => discoverer.supportsPlatform);
53  }
54
55  /// Return the list of all available emulators.
56  Future<List<Emulator>> getAllAvailableEmulators() async {
57    final List<Emulator> emulators = <Emulator>[];
58    await Future.forEach<EmulatorDiscovery>(_platformDiscoverers, (EmulatorDiscovery discoverer) async {
59      emulators.addAll(await discoverer.emulators);
60    });
61    return emulators;
62  }
63
64  /// Return the list of all available emulators.
65  Future<CreateEmulatorResult> createEmulator({ String name }) async {
66    if (name == null || name == '') {
67      const String autoName = 'flutter_emulator';
68      // Don't use getEmulatorsMatching here, as it will only return one
69      // if there's an exact match and we need all those with this prefix
70      // so we can keep adding suffixes until we miss.
71      final List<Emulator> all = await getAllAvailableEmulators();
72      final Set<String> takenNames = all
73          .map<String>((Emulator e) => e.id)
74          .where((String id) => id.startsWith(autoName))
75          .toSet();
76      int suffix = 1;
77      name = autoName;
78      while (takenNames.contains(name)) {
79        name = '${autoName}_${++suffix}';
80      }
81    }
82
83    final String device = await _getPreferredAvailableDevice();
84    if (device == null)
85      return CreateEmulatorResult(name,
86          success: false, error: 'No device definitions are available');
87
88    final String sdkId = await _getPreferredSdkId();
89    if (sdkId == null)
90      return CreateEmulatorResult(name,
91          success: false,
92          error:
93              'No suitable Android AVD system images are available. You may need to install these'
94              ' using sdkmanager, for example:\n'
95              '  sdkmanager "system-images;android-27;google_apis_playstore;x86"');
96
97    // Cleans up error output from avdmanager to make it more suitable to show
98    // to flutter users. Specifically:
99    // - Removes lines that say "null" (!)
100    // - Removes lines that tell the user to use '--force' to overwrite emulators
101    String cleanError(String error) {
102      if (error == null || error.trim() == '')
103        return null;
104      return error
105          .split('\n')
106          .where((String l) => l.trim() != 'null')
107          .where((String l) =>
108              l.trim() != 'Use --force if you want to replace it.')
109          .join('\n')
110          .trim();
111    }
112
113    final List<String> args = <String>[
114      getAvdManagerPath(androidSdk),
115      'create',
116      'avd',
117      '-n', name,
118      '-k', sdkId,
119      '-d', device,
120    ];
121    final ProcessResult runResult = processManager.runSync(args,
122        environment: androidSdk?.sdkManagerEnv);
123    return CreateEmulatorResult(
124      name,
125      success: runResult.exitCode == 0,
126      output: runResult.stdout,
127      error: cleanError(runResult.stderr),
128    );
129  }
130
131  static const List<String> preferredDevices = <String>[
132    'pixel',
133    'pixel_xl',
134  ];
135  Future<String> _getPreferredAvailableDevice() async {
136    final List<String> args = <String>[
137      getAvdManagerPath(androidSdk),
138      'list',
139      'device',
140      '-c',
141    ];
142    final ProcessResult runResult = processManager.runSync(args,
143        environment: androidSdk?.sdkManagerEnv);
144    if (runResult.exitCode != 0)
145      return null;
146
147    final List<String> availableDevices = runResult.stdout
148        .split('\n')
149        .where((String l) => preferredDevices.contains(l.trim()))
150        .toList();
151
152    return preferredDevices.firstWhere(
153      (String d) => availableDevices.contains(d),
154      orElse: () => null,
155    );
156  }
157
158  RegExp androidApiVersion = RegExp(r';android-(\d+);');
159  Future<String> _getPreferredSdkId() async {
160    // It seems that to get the available list of images, we need to send a
161    // request to create without the image and it'll provide us a list :-(
162    final List<String> args = <String>[
163      getAvdManagerPath(androidSdk),
164      'create',
165      'avd',
166      '-n', 'temp',
167    ];
168    final ProcessResult runResult = processManager.runSync(args,
169        environment: androidSdk?.sdkManagerEnv);
170
171    // Get the list of IDs that match our criteria
172    final List<String> availableIDs = runResult.stderr
173        .split('\n')
174        .where((String l) => androidApiVersion.hasMatch(l))
175        .where((String l) => l.contains('system-images'))
176        .where((String l) => l.contains('google_apis_playstore'))
177        .toList();
178
179    final List<int> availableApiVersions = availableIDs
180        .map<String>((String id) => androidApiVersion.firstMatch(id).group(1))
181        .map<int>((String apiVersion) => int.parse(apiVersion))
182        .toList();
183
184    // Get the highest Android API version or whats left
185    final int apiVersion = availableApiVersions.isNotEmpty
186        ? availableApiVersions.reduce(math.max)
187        : -1; // Don't match below
188
189    // We're out of preferences, we just have to return the first one with the high
190    // API version.
191    return availableIDs.firstWhere(
192      (String id) => id.contains(';android-$apiVersion;'),
193      orElse: () => null,
194    );
195  }
196
197  /// Whether we're capable of listing any emulators given the current environment configuration.
198  bool get canListAnything {
199    return _platformDiscoverers.any((EmulatorDiscovery discoverer) => discoverer.canListAnything);
200  }
201}
202
203/// An abstract class to discover and enumerate a specific type of emulators.
204abstract class EmulatorDiscovery {
205  bool get supportsPlatform;
206
207  /// Whether this emulator discovery is capable of listing any emulators given the
208  /// current environment configuration.
209  bool get canListAnything;
210
211  Future<List<Emulator>> get emulators;
212}
213
214abstract class Emulator {
215  Emulator(this.id, this.hasConfig);
216
217  final String id;
218  final bool hasConfig;
219  String get name;
220  String get manufacturer;
221  Category get category;
222  PlatformType get platformType;
223
224  @override
225  int get hashCode => id.hashCode;
226
227  @override
228  bool operator ==(dynamic other) {
229    if (identical(this, other))
230      return true;
231    if (other is! Emulator)
232      return false;
233    return id == other.id;
234  }
235
236  Future<void> launch();
237
238  @override
239  String toString() => name;
240
241  static List<String> descriptions(List<Emulator> emulators) {
242    if (emulators.isEmpty)
243      return <String>[];
244
245    // Extract emulators information
246    final List<List<String>> table = <List<String>>[];
247    for (Emulator emulator in emulators) {
248      table.add(<String>[
249        emulator.id ?? '',
250        emulator.name ?? '',
251        emulator.manufacturer ?? '',
252        emulator.platformType?.toString() ?? '',
253      ]);
254    }
255
256    // Calculate column widths
257    final List<int> indices = List<int>.generate(table[0].length - 1, (int i) => i);
258    List<int> widths = indices.map<int>((int i) => 0).toList();
259    for (List<String> row in table) {
260      widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList();
261    }
262
263    // Join columns into lines of text
264    final RegExp whiteSpaceAndDots = RegExp(r'[•\s]+$');
265    return table
266        .map<String>((List<String> row) {
267          return indices
268                  .map<String>((int i) => row[i].padRight(widths[i]))
269                  .join(' • ') +
270              ' • ${row.last}';
271        })
272        .map<String>((String line) => line.replaceAll(whiteSpaceAndDots, ''))
273        .toList();
274  }
275
276  static void printEmulators(List<Emulator> emulators) {
277    descriptions(emulators).forEach(printStatus);
278  }
279}
280
281class CreateEmulatorResult {
282  CreateEmulatorResult(this.emulatorName, {this.success, this.output, this.error});
283
284  final bool success;
285  final String emulatorName;
286  final String output;
287  final String error;
288}
289