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

import 'dart:async';

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

import '../android/android_sdk.dart';
import '../android/android_workflow.dart';
Dan Field's avatar
Dan Field committed
12
import '../base/common.dart';
13
import '../base/file_system.dart';
Dan Field's avatar
Dan Field committed
14
import '../base/io.dart';
15
import '../base/logger.dart';
16
import '../base/process.dart';
Dan Field's avatar
Dan Field committed
17
import '../convert.dart';
18
import '../device.dart';
19 20 21 22
import '../emulator.dart';
import 'android_sdk.dart';

class AndroidEmulators extends EmulatorDiscovery {
23
  AndroidEmulators({
24 25 26 27 28
    AndroidSdk? androidSdk,
    required AndroidWorkflow androidWorkflow,
    required FileSystem fileSystem,
    required Logger logger,
    required ProcessManager processManager,
29 30 31 32 33 34 35 36
  }) : _androidSdk = androidSdk,
       _androidWorkflow = androidWorkflow,
       _fileSystem = fileSystem,
       _logger = logger,
       _processManager = processManager,
       _processUtils = ProcessUtils(logger: logger, processManager: processManager);

  final AndroidWorkflow _androidWorkflow;
37
  final AndroidSdk? _androidSdk;
38 39 40 41 42
  final FileSystem _fileSystem;
  final Logger _logger;
  final ProcessManager _processManager;
  final ProcessUtils _processUtils;

43 44 45 46
  @override
  bool get supportsPlatform => true;

  @override
47 48 49 50
  bool get canListAnything => _androidWorkflow.canListEmulators;

  @override
  bool get canLaunchAnything => _androidWorkflow.canListEmulators
51
    && _androidSdk?.getAvdManagerPath() != null;
52 53

  @override
54 55 56 57
  Future<List<Emulator>> get emulators => _getEmulatorAvds();

  /// Return the list of available emulator AVDs.
  Future<List<AndroidEmulator>> _getEmulatorAvds() async {
58
    final String? emulatorPath = _androidSdk?.emulatorPath;
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
    if (emulatorPath == null) {
      return <AndroidEmulator>[];
    }

    final String listAvdsOutput = (await _processUtils.run(
      <String>[emulatorPath, '-list-avds'])).stdout.trim();

    final List<AndroidEmulator> emulators = <AndroidEmulator>[];
    if (listAvdsOutput != null) {
      _extractEmulatorAvdInfo(listAvdsOutput, emulators);
    }
    return emulators;
  }

  /// Parse the given `emulator -list-avds` output in [text], and fill out the given list
  /// of emulators by reading information from the relevant ini files.
  void _extractEmulatorAvdInfo(String text, List<AndroidEmulator> emulators) {
    for (final String id in text.trim().split('\n').where((String l) => l != '')) {
      emulators.add(_loadEmulatorInfo(id));
    }
  }

  AndroidEmulator _loadEmulatorInfo(String id) {
    id = id.trim();
83
    final String? avdPath = _androidSdk?.getAvdPath();
84 85 86 87 88 89 90 91 92 93 94 95 96 97
    final AndroidEmulator androidEmulatorWithoutProperties = AndroidEmulator(
      id,
      processManager: _processManager,
      logger: _logger,
      androidSdk: _androidSdk,
    );
    if (avdPath == null) {
      return androidEmulatorWithoutProperties;
    }
    final File iniFile = _fileSystem.file(_fileSystem.path.join(avdPath, '$id.ini'));
    if (!iniFile.existsSync()) {
      return androidEmulatorWithoutProperties;
    }
    final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync());
98 99
    final String? path = ini['path'];
    if (path == null) {
100 101
      return androidEmulatorWithoutProperties;
    }
102
    final File configFile = _fileSystem.file(_fileSystem.path.join(path, 'config.ini'));
103 104 105 106 107 108 109 110 111 112 113 114
    if (!configFile.existsSync()) {
      return androidEmulatorWithoutProperties;
    }
    final Map<String, String> properties = parseIniLines(configFile.readAsLinesSync());
    return AndroidEmulator(
      id,
      properties: properties,
      processManager: _processManager,
      logger: _logger,
      androidSdk: _androidSdk,
    );
  }
115 116 117
}

class AndroidEmulator extends Emulator {
118
  AndroidEmulator(String id, {
119 120 121 122
    Map<String, String>? properties,
    required Logger logger,
    AndroidSdk? androidSdk,
    required ProcessManager processManager,
123 124 125 126 127
  }) : _properties = properties,
       _logger = logger,
       _androidSdk = androidSdk,
       _processUtils = ProcessUtils(logger: logger, processManager: processManager),
       super(id, properties != null && properties.isNotEmpty);
128

129
  final Map<String, String>? _properties;
130 131
  final Logger _logger;
  final ProcessUtils _processUtils;
132
  final AndroidSdk? _androidSdk;
133

134 135
  // Android Studio uses the ID with underscores replaced with spaces
  // for the name if displayname is not set so we do the same.
136
  @override
137
  String get name => _prop('avd.ini.displayname') ?? id.replaceAll('_', ' ').trim();
138 139

  @override
140
  String? get manufacturer => _prop('hw.device.manufacturer');
141

142 143 144 145
  @override
  Category get category => Category.mobile;

  @override
146
  PlatformType get platformType => PlatformType.android;
147

148
  String? _prop(String name) => _properties != null ? _properties![name] : null;
149

150
  @override
151 152 153 154 155
  Future<void> launch({@visibleForTesting Duration? startupDuration, bool coldBoot = false}) async {
    final String? emulatorPath = _androidSdk?.emulatorPath;
    if (emulatorPath == null) {
      throw Exception('Emulator is missing from the Android SDK');
    }
156
    final List<String> command = <String>[
157
      emulatorPath,
158 159 160 161 162 163
      '-avd',
      id,
      if (coldBoot)
        '-no-snapshot-load'
    ];
    final Process process = await _processUtils.start(command);
Dan Field's avatar
Dan Field committed
164 165 166 167 168 169 170 171 172 173 174 175

    // Record output from the emulator process.
    final List<String> stdoutList = <String>[];
    final List<String> stderrList = <String>[];
    final StreamSubscription<String> stdoutSubscription = process.stdout
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
      .listen(stdoutList.add);
    final StreamSubscription<String> stderrSubscription = process.stderr
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
      .listen(stderrList.add);
176
    final Future<void> stdioFuture = Future.wait<void>(<Future<void>>[
Dan Field's avatar
Dan Field committed
177 178
      stdoutSubscription.asFuture<void>(),
      stderrSubscription.asFuture<void>(),
179
    ]);
Dan Field's avatar
Dan Field committed
180 181 182 183 184 185 186 187

    // The emulator continues running on success, so we don't wait for the
    // process to complete before continuing. However, if the process fails
    // after the startup phase (3 seconds), then we only echo its output if
    // its error code is non-zero and its stderr is non-empty.
    bool earlyFailure = true;
    unawaited(process.exitCode.then((int status) async {
      if (status == 0) {
188
        _logger.printTrace('The Android emulator exited successfully');
Dan Field's avatar
Dan Field committed
189 190 191 192 193 194 195
        return;
      }
      // Make sure the process' stdout and stderr are drained.
      await stdioFuture;
      unawaited(stdoutSubscription.cancel());
      unawaited(stderrSubscription.cancel());
      if (stdoutList.isNotEmpty) {
196 197
        _logger.printTrace('Android emulator stdout:');
        stdoutList.forEach(_logger.printTrace);
Dan Field's avatar
Dan Field committed
198 199
      }
      if (!earlyFailure && stderrList.isEmpty) {
200
        _logger.printStatus('The Android emulator exited with code $status');
Dan Field's avatar
Dan Field committed
201 202 203
        return;
      }
      final String when = earlyFailure ? 'during startup' : 'after startup';
204 205 206 207
      _logger.printError('The Android emulator exited with code $status $when');
      _logger.printError('Android emulator stderr:');
      stderrList.forEach(_logger.printError);
      _logger.printError('Address these issues and try again.');
Dan Field's avatar
Dan Field committed
208 209 210
    }));

    // Wait a few seconds for the emulator to start.
211
    await Future<void>.delayed(startupDuration ?? const Duration(seconds: 3));
Dan Field's avatar
Dan Field committed
212 213
    earlyFailure = false;
    return;
214
  }
215 216
}

217

218
@visibleForTesting
219
Map<String, String> parseIniLines(List<String> contents) {
220 221 222
  final Map<String, String> results = <String, String>{};

  final Iterable<List<String>> properties = contents
223
      .map<String>((String l) => l.trim())
Danny Tuppeny's avatar
Danny Tuppeny committed
224 225 226 227 228
      // Strip blank lines/comments
      .where((String l) => l != '' && !l.startsWith('#'))
      // Discard anything that isn't simple name=value
      .where((String l) => l.contains('='))
      // Split into name/value
229
      .map<List<String>>((String l) => l.split('='));
230

231
  for (final List<String> property in properties) {
232
    results[property[0].trim()] = property[1].trim();
233
  }
234 235

  return results;
236
}