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

import 'dart:math' as math;

7
import 'package:meta/meta.dart';
8
import 'package:process/process.dart';
9

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

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

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

45
  final Java? _java;
46
  final AndroidSdk? _androidSdk;
47 48 49 50 51 52 53 54
  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(),
  ];
55

56 57
  Future<List<Emulator>> getEmulatorsMatching(String searchText) async {
    final List<Emulator> emulators = await getAllAvailableEmulators();
58
    searchText = searchText.toLowerCase();
59
    bool exactlyMatchesEmulatorId(Emulator emulator) =>
60 61
        emulator.id.toLowerCase() == searchText ||
        emulator.name.toLowerCase() == searchText;
62
    bool startsWithEmulatorId(Emulator emulator) =>
63 64
        emulator.id.toLowerCase().startsWith(searchText) ||
        emulator.name.toLowerCase().startsWith(searchText);
65 66 67 68 69 70 71 72

    Emulator? exactMatch;
    for (final Emulator emulator in emulators) {
      if (exactlyMatchesEmulatorId(emulator)) {
        exactMatch = emulator;
        break;
      }
    }
73
    if (exactMatch != null) {
74
      return <Emulator>[exactMatch];
75 76 77
    }

    // Match on a id or name starting with [emulatorId].
78
    return emulators.where(startsWithEmulatorId).toList();
79 80 81 82 83 84
  }

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

Danny Tuppeny's avatar
Danny Tuppeny committed
85
  /// Return the list of all available emulators.
86 87
  Future<List<Emulator>> getAllAvailableEmulators() async {
    final List<Emulator> emulators = <Emulator>[];
88
    await Future.forEach<EmulatorDiscovery>(_platformDiscoverers, (EmulatorDiscovery discoverer) async {
89 90 91
      emulators.addAll(await discoverer.emulators);
    });
    return emulators;
92 93
  }

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

120
    final String? device = await _getPreferredAvailableDevice(avdManagerPath);
121
    if (device == null) {
122
      return CreateEmulatorResult(emulatorName,
123
          success: false, error: 'No device definitions are available');
124
    }
125

126
    final String? sdkId = await _getPreferredSdkId(avdManagerPath);
127
    if (sdkId == null) {
128
      return CreateEmulatorResult(emulatorName,
129 130 131 132 133
          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"');
134
    }
135 136 137 138 139

    // 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
140
    String? cleanError(String? error) {
141
      if (error == null || error.trim() == '') {
142
        return null;
143
      }
144
      return error
145 146 147 148
          .split('\n')
          .where((String l) => l.trim() != 'null')
          .where((String l) =>
              l.trim() != 'Use --force if you want to replace it.')
149 150
          .join('\n')
          .trim();
151
    }
152
    final RunResult runResult = await _processUtils.run(<String>[
153
        avdManagerPath,
154 155
        'create',
        'avd',
156
        '-n', emulatorName,
157 158
        '-k', sdkId,
        '-d', device,
159
      ], environment: _java?.environment,
160
    );
161
    return CreateEmulatorResult(
162
      emulatorName,
163 164 165 166 167 168
      success: runResult.exitCode == 0,
      output: runResult.stdout,
      error: cleanError(runResult.stderr),
    );
  }

169
  static const List<String> preferredDevices = <String>[
170 171 172
    'pixel',
    'pixel_xl',
  ];
173

174
  Future<String?> _getPreferredAvailableDevice(String avdManagerPath) async {
175
    final List<String> args = <String>[
176
      avdManagerPath,
177 178
      'list',
      'device',
179
      '-c',
180
    ];
181
    final RunResult runResult = await _processUtils.run(args,
182
        environment: _java?.environment);
183
    if (runResult.exitCode != 0) {
184
      return null;
185
    }
186 187 188 189 190 191

    final List<String> availableDevices = runResult.stdout
        .split('\n')
        .where((String l) => preferredDevices.contains(l.trim()))
        .toList();

192 193 194 195 196 197
    for (final String device in preferredDevices) {
      if (availableDevices.contains(device)) {
        return device;
      }
    }
    return null;
198 199
  }

200 201
  static final RegExp _androidApiVersion = RegExp(r';android-(\d+);');

202
  Future<String?> _getPreferredSdkId(String avdManagerPath) async {
203 204 205
    // 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>[
206
      avdManagerPath,
207 208 209 210
      'create',
      'avd',
      '-n', 'temp',
    ];
211
    final RunResult runResult = await _processUtils.run(args,
212
        environment: _java?.environment);
213 214 215 216

    // Get the list of IDs that match our criteria
    final List<String> availableIDs = runResult.stderr
        .split('\n')
217
        .where((String l) => _androidApiVersion.hasMatch(l))
218 219 220 221 222
        .where((String l) => l.contains('system-images'))
        .where((String l) => l.contains('google_apis_playstore'))
        .toList();

    final List<int> availableApiVersions = availableIDs
223
        .map<String>((String id) => _androidApiVersion.firstMatch(id)!.group(1)!)
224
        .map<int>((String apiVersion) => int.parse(apiVersion))
225 226 227 228 229 230 231 232 233
        .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.
234 235 236 237 238 239
    for (final String id in availableIDs) {
      if (id.contains(';android-$apiVersion;')) {
        return id;
      }
    }
    return null;
240 241
  }

242 243 244 245 246 247 248 249 250 251
  /// 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;

252
  /// Whether this emulator discovery is capable of listing any emulators.
253 254
  bool get canListAnything;

255
  /// Whether this emulator discovery is capable of launching new emulators.
256 257
  bool get canLaunchAnything;

258 259 260
  Future<List<Emulator>> get emulators;
}

261
@immutable
262
abstract class Emulator {
263
  const Emulator(this.id, this.hasConfig);
264 265

  final String id;
266 267
  final bool hasConfig;
  String get name;
268
  String? get manufacturer;
269
  Category get category;
270
  PlatformType get platformType;
271 272 273 274 275

  @override
  int get hashCode => id.hashCode;

  @override
276
  bool operator ==(Object other) {
277
    if (identical(this, other)) {
278
      return true;
279
    }
280 281
    return other is Emulator
        && other.id == id;
282 283
  }

284
  Future<void> launch({bool coldBoot});
285

286 287 288
  @override
  String toString() => name;

289
  static List<String> descriptions(List<Emulator> emulators) {
290
    if (emulators.isEmpty) {
291
      return <String>[];
292
    }
293

294 295 296 297 298 299 300
    const List<String> tableHeader = <String>[
      'Id',
      'Name',
      'Manufacturer',
      'Platform',
    ];

301
    // Extract emulators information
302
    final List<List<String>> table = <List<String>>[
303
      tableHeader,
304
      for (final Emulator emulator in emulators)
305
        <String>[
306 307
          emulator.id,
          emulator.name,
308
          emulator.manufacturer ?? '',
309
          emulator.platformType.toString(),
310 311
        ],
    ];
312 313

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

    // Join columns into lines of text
321
    final RegExp whiteSpaceAndDots = RegExp(r'[•\s]+$');
322
    return table
323
        .map<String>((List<String> row) {
324
          return indices
325 326 327
            .map<String>((int i) => row[i].padRight(widths[i]))
            .followedBy(<String>[row.last])
            .join(' • ');
328
        })
329
        .map<String>((String line) => line.replaceAll(whiteSpaceAndDots, ''))
330
        .toList();
331 332
  }

333
  static void printEmulators(List<Emulator> emulators, Logger logger) {
334 335 336 337
    final List<String> emulatorDescriptions = descriptions(emulators);
    // Prints the first description as the table header, followed by a newline.
    logger.printStatus('${emulatorDescriptions.first}\n');
    emulatorDescriptions.sublist(1).forEach(logger.printStatus);
338 339
  }
}
340 341

class CreateEmulatorResult {
342
  CreateEmulatorResult(this.emulatorName, {required this.success, this.output, this.error});
343

344 345
  final bool success;
  final String emulatorName;
346 347
  final String? output;
  final String? error;
348
}