emulator.dart 9.35 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 'device.dart';
14
import 'globals.dart';
15
import 'ios/ios_emulators.dart';
16

17
EmulatorManager get emulatorManager => context.get<EmulatorManager>();
18 19 20 21 22 23 24

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

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

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

41 42
    final Emulator exactMatch =
        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 52 53 54
  }

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

Danny Tuppeny's avatar
Danny Tuppeny committed
55
  /// Return the list of all available emulators.
56 57
  Future<List<Emulator>> getAllAvailableEmulators() async {
    final List<Emulator> emulators = <Emulator>[];
58
    await Future.forEach<EmulatorDiscovery>(_platformDiscoverers, (EmulatorDiscovery discoverer) async {
59 60 61
      emulators.addAll(await discoverer.emulators);
    });
    return emulators;
62 63
  }

64
  /// Return the list of all available emulators.
65
  Future<CreateEmulatorResult> createEmulator({ String name }) async {
66 67 68 69 70 71 72
    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
73
          .map<String>((Emulator e) => e.id)
74 75 76 77 78 79 80 81 82 83 84
          .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)
85
      return CreateEmulatorResult(name,
86 87 88 89
          success: false, error: 'No device definitions are available');

    final String sdkId = await _getPreferredSdkId();
    if (sdkId == null)
90
      return CreateEmulatorResult(name,
91 92 93 94 95 96 97 98 99 100 101
          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) {
102 103 104
      if (error == null || error.trim() == '')
        return null;
      return error
105 106 107 108
          .split('\n')
          .where((String l) => l.trim() != 'null')
          .where((String l) =>
              l.trim() != 'Use --force if you want to replace it.')
109 110
          .join('\n')
          .trim();
111 112 113 114 115 116 117 118
    }

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

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

158
  RegExp androidApiVersion = RegExp(r';android-(\d+);');
159 160 161 162 163 164 165 166 167
  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',
    ];
168 169
    final ProcessResult runResult = processManager.runSync(args,
        environment: androidSdk?.sdkManagerEnv);
170 171 172 173 174 175 176 177 178 179

    // 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
180 181
        .map<String>((String id) => androidApiVersion.firstMatch(id).group(1))
        .map<int>((String apiVersion) => int.parse(apiVersion))
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
        .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,
    );
  }

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

  final String id;
218 219 220
  final bool hasConfig;
  String get name;
  String get manufacturer;
221 222
  Category get category;
  PlatformType get platformType;
223 224 225 226 227 228 229 230 231 232 233 234 235

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

236
  Future<void> launch();
237

238 239 240
  @override
  String toString() => name;

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

    // 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
249 250
        emulator.id ?? '',
        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 270 271
                  .join(' • ') +
              ' • ${row.last}';
        })
272
        .map<String>((String line) => line.replaceAll(whiteSpaceAndDots, ''))
273
        .toList();
274 275
  }

276 277
  static void printEmulators(List<Emulator> emulators) {
    descriptions(emulators).forEach(printStatus);
278 279
  }
}
280 281

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

284 285 286 287 288
  final bool success;
  final String emulatorName;
  final String output;
  final String error;
}