emulator.dart 10.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'dart:math' as math;

9
import 'package:meta/meta.dart';
10
import 'package:process/process.dart';
11

12
import 'android/android_emulator.dart';
13
import 'android/android_sdk.dart';
14
import 'android/android_workflow.dart';
15
import 'base/context.dart';
16 17
import 'base/file_system.dart';
import 'base/logger.dart';
18
import 'base/process.dart';
19
import 'device.dart';
20
import 'ios/ios_emulators.dart';
21

22
EmulatorManager get emulatorManager => context.get<EmulatorManager>();
23 24 25

/// A class to get all available emulators.
class EmulatorManager {
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
  EmulatorManager({
    @required AndroidSdk androidSdk,
    @required Logger logger,
    @required ProcessManager processManager,
    @required AndroidWorkflow androidWorkflow,
    @required FileSystem fileSystem,
  }) : _androidSdk = androidSdk,
       _processUtils = ProcessUtils(logger: logger, processManager: processManager),
       _androidEmulators = AndroidEmulators(
        androidSdk: androidSdk,
        logger: logger,
        processManager: processManager,
        fileSystem: fileSystem,
        androidWorkflow: androidWorkflow
      ) {
    _emulatorDiscoverers.add(_androidEmulators);
42 43
  }

44 45 46 47 48 49 50 51 52
  final AndroidSdk _androidSdk;
  final AndroidEmulators _androidEmulators;
  final ProcessUtils _processUtils;

  // Constructing EmulatorManager is cheap; they only do expensive work if some
  // of their methods are called.
  final List<EmulatorDiscovery> _emulatorDiscoverers = <EmulatorDiscovery>[
    IOSEmulators(),
  ];
53

54 55
  Future<List<Emulator>> getEmulatorsMatching(String searchText) async {
    final List<Emulator> emulators = await getAllAvailableEmulators();
56
    searchText = searchText.toLowerCase();
57
    bool exactlyMatchesEmulatorId(Emulator emulator) =>
58 59
        emulator.id?.toLowerCase() == searchText ||
        emulator.name?.toLowerCase() == searchText;
60
    bool startsWithEmulatorId(Emulator emulator) =>
61 62
        emulator.id?.toLowerCase()?.startsWith(searchText) == true ||
        emulator.name?.toLowerCase()?.startsWith(searchText) == true;
63

64 65
    final Emulator exactMatch =
        emulators.firstWhere(exactlyMatchesEmulatorId, orElse: () => null);
66
    if (exactMatch != null) {
67
      return <Emulator>[exactMatch];
68 69 70
    }

    // Match on a id or name starting with [emulatorId].
71
    return emulators.where(startsWithEmulatorId).toList();
72 73 74 75 76 77
  }

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

Danny Tuppeny's avatar
Danny Tuppeny committed
78
  /// Return the list of all available emulators.
79 80
  Future<List<Emulator>> getAllAvailableEmulators() async {
    final List<Emulator> emulators = <Emulator>[];
81
    await Future.forEach<EmulatorDiscovery>(_platformDiscoverers, (EmulatorDiscovery discoverer) async {
82 83 84
      emulators.addAll(await discoverer.emulators);
    });
    return emulators;
85 86
  }

87
  /// Return the list of all available emulators.
88
  Future<CreateEmulatorResult> createEmulator({ String name }) async {
89
    if (name == null || name.isEmpty) {
90 91 92 93 94 95
      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
96
          .map<String>((Emulator e) => e.id)
97 98 99 100 101 102 103 104
          .where((String id) => id.startsWith(autoName))
          .toSet();
      int suffix = 1;
      name = autoName;
      while (takenNames.contains(name)) {
        name = '${autoName}_${++suffix}';
      }
    }
105 106 107 108 109
    if (!_androidEmulators.canLaunchAnything) {
      return CreateEmulatorResult(name,
        success: false, error: 'avdmanager is missing from the Android SDK'
      );
    }
110 111

    final String device = await _getPreferredAvailableDevice();
112
    if (device == null) {
113
      return CreateEmulatorResult(name,
114
          success: false, error: 'No device definitions are available');
115
    }
116 117

    final String sdkId = await _getPreferredSdkId();
118
    if (sdkId == null) {
119
      return CreateEmulatorResult(name,
120 121 122 123 124
          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"');
125
    }
126 127 128 129 130 131

    // 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) {
132
      if (error == null || error.trim() == '') {
133
        return null;
134
      }
135
      return error
136 137 138 139
          .split('\n')
          .where((String l) => l.trim() != 'null')
          .where((String l) =>
              l.trim() != 'Use --force if you want to replace it.')
140 141
          .join('\n')
          .trim();
142
    }
143
    final RunResult runResult = await _processUtils.run(<String>[
144
      _androidSdk?.avdManagerPath,
145 146 147 148 149 150 151
        'create',
        'avd',
        '-n', name,
        '-k', sdkId,
        '-d', device,
      ], environment: _androidSdk?.sdkManagerEnv,
    );
152
    return CreateEmulatorResult(
153 154 155 156 157 158 159
      name,
      success: runResult.exitCode == 0,
      output: runResult.stdout,
      error: cleanError(runResult.stderr),
    );
  }

160
  static const List<String> preferredDevices = <String>[
161 162 163
    'pixel',
    'pixel_xl',
  ];
164

165 166
  Future<String> _getPreferredAvailableDevice() async {
    final List<String> args = <String>[
167
      _androidSdk?.avdManagerPath,
168 169
      'list',
      'device',
170
      '-c',
171
    ];
172 173
    final RunResult runResult = await _processUtils.run(args,
        environment: _androidSdk?.sdkManagerEnv);
174
    if (runResult.exitCode != 0) {
175
      return null;
176
    }
177 178 179 180 181 182 183 184 185 186 187 188

    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,
    );
  }

189 190
  static final RegExp _androidApiVersion = RegExp(r';android-(\d+);');

191 192 193 194
  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>[
195
      _androidSdk?.avdManagerPath,
196 197 198 199
      'create',
      'avd',
      '-n', 'temp',
    ];
200 201
    final RunResult runResult = await _processUtils.run(args,
        environment: _androidSdk?.sdkManagerEnv);
202 203 204 205

    // Get the list of IDs that match our criteria
    final List<String> availableIDs = runResult.stderr
        .split('\n')
206
        .where((String l) => _androidApiVersion.hasMatch(l))
207 208 209 210 211
        .where((String l) => l.contains('system-images'))
        .where((String l) => l.contains('google_apis_playstore'))
        .toList();

    final List<int> availableApiVersions = availableIDs
212
        .map<String>((String id) => _androidApiVersion.firstMatch(id).group(1))
213
        .map<int>((String apiVersion) => int.parse(apiVersion))
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
        .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,
    );
  }

229 230 231 232 233 234 235 236 237 238
  /// 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;

239
  /// Whether this emulator discovery is capable of listing any emulators.
240 241
  bool get canListAnything;

242
  /// Whether this emulator discovery is capable of launching new emulators.
243 244
  bool get canLaunchAnything;

245 246 247
  Future<List<Emulator>> get emulators;
}

248
@immutable
249
abstract class Emulator {
250
  const Emulator(this.id, this.hasConfig);
251 252

  final String id;
253 254 255
  final bool hasConfig;
  String get name;
  String get manufacturer;
256
  Category get category;
257
  PlatformType get platformType;
258 259 260 261 262

  @override
  int get hashCode => id.hashCode;

  @override
263
  bool operator ==(Object other) {
264
    if (identical(this, other)) {
265
      return true;
266
    }
267 268
    return other is Emulator
        && other.id == id;
269 270
  }

271
  Future<void> launch({bool coldBoot});
272

273 274 275
  @override
  String toString() => name;

276
  static List<String> descriptions(List<Emulator> emulators) {
277
    if (emulators.isEmpty) {
278
      return <String>[];
279
    }
280 281

    // Extract emulators information
282
    final List<List<String>> table = <List<String>>[
283
      for (final Emulator emulator in emulators)
284 285 286 287
        <String>[
          emulator.id ?? '',
          emulator.name ?? '',
          emulator.manufacturer ?? '',
288
          emulator.platformType?.toString() ?? '',
289 290
        ],
    ];
291 292

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

    // Join columns into lines of text
300
    final RegExp whiteSpaceAndDots = RegExp(r'[•\s]+$');
301
    return table
302
        .map<String>((List<String> row) {
303
          return indices
304 305 306
            .map<String>((int i) => row[i].padRight(widths[i]))
            .followedBy(<String>[row.last])
            .join(' • ');
307
        })
308
        .map<String>((String line) => line.replaceAll(whiteSpaceAndDots, ''))
309
        .toList();
310 311
  }

312 313
  static void printEmulators(List<Emulator> emulators, Logger logger) {
    descriptions(emulators).forEach(logger.printStatus);
314 315
  }
}
316 317

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

320 321 322 323 324
  final bool success;
  final String emulatorName;
  final String output;
  final String error;
}