emulator.dart 9.22 KB
Newer Older
1 2 3 4 5 6 7 8
// 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';
9
import 'android/android_sdk.dart';
10
import 'base/context.dart';
11 12
import 'base/io.dart' show ProcessResult;
import 'base/process_manager.dart';
13
import 'globals.dart';
14
import 'ios/ios_emulators.dart';
15 16 17 18 19 20 21 22 23 24

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(new AndroidEmulators());
25
    _emulatorDiscoverers.add(new IOSEmulators());
26 27 28 29
  }

  final List<EmulatorDiscovery> _emulatorDiscoverers = <EmulatorDiscovery>[];

30 31
  Future<List<Emulator>> getEmulatorsMatching(String searchText) async {
    final List<Emulator> emulators = await getAllAvailableEmulators();
32
    searchText = searchText.toLowerCase();
33
    bool exactlyMatchesEmulatorId(Emulator emulator) =>
34 35
        emulator.id?.toLowerCase() == searchText ||
        emulator.name?.toLowerCase() == searchText;
36
    bool startsWithEmulatorId(Emulator emulator) =>
37 38
        emulator.id?.toLowerCase()?.startsWith(searchText) == true ||
        emulator.name?.toLowerCase()?.startsWith(searchText) == true;
39

40 41
    final Emulator exactMatch =
        emulators.firstWhere(exactlyMatchesEmulatorId, orElse: () => null);
42
    if (exactMatch != null) {
43
      return <Emulator>[exactMatch];
44 45 46
    }

    // Match on a id or name starting with [emulatorId].
47
    return emulators.where(startsWithEmulatorId).toList();
48 49 50 51 52 53
  }

  Iterable<EmulatorDiscovery> get _platformDiscoverers {
    return _emulatorDiscoverers.where((EmulatorDiscovery discoverer) => discoverer.supportsPlatform);
  }

Danny Tuppeny's avatar
Danny Tuppeny committed
54
  /// Return the list of all available emulators.
55 56
  Future<List<Emulator>> getAllAvailableEmulators() async {
    final List<Emulator> emulators = <Emulator>[];
Danny Tuppeny's avatar
Danny Tuppeny committed
57
    await Future.forEach(_platformDiscoverers, (EmulatorDiscovery discoverer) async {
58 59 60
      emulators.addAll(await discoverer.emulators);
    });
    return emulators;
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
  /// 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((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 new CreateEmulatorResult(name,
          success: false, error: 'No device definitions are available');

    final String sdkId = await _getPreferredSdkId();
    if (sdkId == null)
      return new 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) {
101 102 103
      if (error == null || error.trim() == '')
        return null;
      return error
104 105 106 107
          .split('\n')
          .where((String l) => l.trim() != 'null')
          .where((String l) =>
              l.trim() != 'Use --force if you want to replace it.')
108 109
          .join('\n')
          .trim();
110 111 112 113 114 115 116 117 118 119
    }

    final List<String> args = <String>[
      getAvdManagerPath(androidSdk),
      'create',
      'avd',
      '-n', name,
      '-k', sdkId,
      '-d', device
    ];
120 121
    final ProcessResult runResult = processManager.runSync(args,
        environment: androidSdk?.sdkManagerEnv);
122 123 124 125 126 127 128 129
    return new CreateEmulatorResult(
      name,
      success: runResult.exitCode == 0,
      output: runResult.stdout,
      error: cleanError(runResult.stderr),
    );
  }

130
  static const List<String> preferredDevices = <String>[
131 132 133 134 135 136 137 138 139 140
    'pixel',
    'pixel_xl',
  ];
  Future<String> _getPreferredAvailableDevice() async {
    final List<String> args = <String>[
      getAvdManagerPath(androidSdk),
      'list',
      'device',
      '-c'
    ];
141 142
    final ProcessResult runResult = processManager.runSync(args,
        environment: androidSdk?.sdkManagerEnv);
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
    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 = new 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',
    ];
167 168
    final ProcessResult runResult = processManager.runSync(args,
        environment: androidSdk?.sdkManagerEnv);
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

    // 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 id) => androidApiVersion.firstMatch(id).group(1))
        .map((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,
    );
  }

196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
  /// 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 {
214
  Emulator(this.id, this.hasConfig);
215 216

  final String id;
217 218 219 220
  final bool hasConfig;
  String get name;
  String get manufacturer;
  String get label;
221 222 223 224 225 226 227 228 229 230 231 232 233

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

234
  Future<void> launch();
235

236 237 238
  @override
  String toString() => name;

239
  static List<String> descriptions(List<Emulator> emulators) {
240
    if (emulators.isEmpty)
241
      return <String>[];
242 243 244 245 246

    // Extract emulators information
    final List<List<String>> table = <List<String>>[];
    for (Emulator emulator in emulators) {
      table.add(<String>[
Danny Tuppeny's avatar
Danny Tuppeny committed
247 248
        emulator.id ?? '',
        emulator.name ?? '',
249 250
        emulator.manufacturer ?? '',
        emulator.label ?? '',
251 252 253 254 255 256 257 258 259 260 261
      ]);
    }

    // Calculate column widths
    final List<int> indices = new List<int>.generate(table[0].length - 1, (int i) => i);
    List<int> widths = indices.map((int i) => 0).toList();
    for (List<String> row in table) {
      widths = indices.map((int i) => math.max(widths[i], row[i].length)).toList();
    }

    // Join columns into lines of text
262
    final RegExp whiteSpaceAndDots = new RegExp(r'[•\s]+$');
263 264 265 266 267 268 269
    return table
        .map((List<String> row) {
          return indices
                  .map((int i) => row[i].padRight(widths[i]))
                  .join(' • ') +
              ' • ${row.last}';
        })
270 271
        .map((String line) => line.replaceAll(whiteSpaceAndDots, ''))
        .toList();
272 273
  }

274 275
  static void printEmulators(List<Emulator> emulators) {
    descriptions(emulators).forEach(printStatus);
276 277
  }
}
278 279 280 281 282 283 284 285 286

class CreateEmulatorResult {
  final bool success;
  final String emulatorName;
  final String output;
  final String error;

  CreateEmulatorResult(this.emulatorName, {this.success, this.output, this.error});
}