android_emulator.dart 5.98 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 8 9 10
// 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';

import '../android/android_sdk.dart';
import '../android/android_workflow.dart';
Dan Field's avatar
Dan Field committed
11
import '../base/common.dart';
12
import '../base/file_system.dart';
Dan Field's avatar
Dan Field committed
13
import '../base/io.dart';
14
import '../base/process.dart';
Dan Field's avatar
Dan Field committed
15 16
import '../base/utils.dart';
import '../convert.dart';
17
import '../device.dart';
18
import '../emulator.dart';
19
import '../globals.dart' as globals;
20 21 22 23 24 25 26
import 'android_sdk.dart';

class AndroidEmulators extends EmulatorDiscovery {
  @override
  bool get supportsPlatform => true;

  @override
27
  bool get canListAnything => androidWorkflow.canListEmulators;
28 29 30 31 32 33

  @override
  Future<List<Emulator>> get emulators async => getEmulatorAvds();
}

class AndroidEmulator extends Emulator {
34
  AndroidEmulator(String id, [this._properties])
35
    : super(id, _properties != null && _properties.isNotEmpty);
36

37
  final Map<String, String> _properties;
38

39 40
  // Android Studio uses the ID with underscores replaced with spaces
  // for the name if displayname is not set so we do the same.
41
  @override
42
  String get name => _prop('avd.ini.displayname') ?? id.replaceAll('_', ' ').trim();
43 44

  @override
45
  String get manufacturer => _prop('hw.device.manufacturer');
46

47 48 49 50 51 52
  @override
  Category get category => Category.mobile;

  @override
  PlatformType get platformType => PlatformType.android;

53 54
  String _prop(String name) => _properties != null ? _properties[name] : null;

55
  @override
56
  Future<void> launch() async {
57
    final Process process = await processUtils.start(
Dan Field's avatar
Dan Field committed
58
      <String>[getEmulatorPath(androidSdk), '-avd', id],
59
    );
Dan Field's avatar
Dan Field committed
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74

    // 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);
    final Future<void> stdioFuture = waitGroup<void>(<Future<void>>[
      stdoutSubscription.asFuture<void>(),
      stderrSubscription.asFuture<void>(),
75
    ]);
Dan Field's avatar
Dan Field committed
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 101 102 103 104 105 106 107 108 109

    // 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) {
        globals.printTrace('The Android emulator exited successfully');
        return;
      }
      // Make sure the process' stdout and stderr are drained.
      await stdioFuture;
      unawaited(stdoutSubscription.cancel());
      unawaited(stderrSubscription.cancel());
      if (stdoutList.isNotEmpty) {
        globals.printTrace('Android emulator stdout:');
        stdoutList.forEach(globals.printTrace);
      }
      if (!earlyFailure && stderrList.isEmpty) {
        globals.printStatus('The Android emulator exited with code $status');
        return;
      }
      final String when = earlyFailure ? 'during startup' : 'after startup';
      globals.printError('The Android emulator exited with code $status $when');
      globals.printError('Android emulator stderr:');
      stderrList.forEach(globals.printError);
      globals.printError('Address these issues and try again.');
    }));

    // Wait a few seconds for the emulator to start.
    await Future<void>.delayed(const Duration(seconds: 3));
    earlyFailure = false;
    return;
110
  }
111 112 113 114 115
}

/// Return the list of available emulator AVDs.
List<AndroidEmulator> getEmulatorAvds() {
  final String emulatorPath = getEmulatorPath(androidSdk);
116
  if (emulatorPath == null) {
117
    return <AndroidEmulator>[];
118
  }
Danny Tuppeny's avatar
Danny Tuppeny committed
119

120
  final String listAvdsOutput = processUtils.runSync(
121
    <String>[emulatorPath, '-list-avds']).stdout.trim();
122

123
  final List<AndroidEmulator> emulators = <AndroidEmulator>[];
124 125 126
  if (listAvdsOutput != null) {
    extractEmulatorAvdInfo(listAvdsOutput, emulators);
  }
127
  return emulators;
128 129 130
}

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

138
AndroidEmulator _loadEmulatorInfo(String id) {
139
  id = id.trim();
140 141
  final String avdPath = getAvdPath();
  if (avdPath != null) {
142
    final File iniFile = globals.fs.file(globals.fs.path.join(avdPath, '$id.ini'));
143 144 145 146
    if (iniFile.existsSync()) {
      final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync());
      if (ini['path'] != null) {
        final File configFile =
147
            globals.fs.file(globals.fs.path.join(ini['path'], 'config.ini'));
148 149 150
        if (configFile.existsSync()) {
          final Map<String, String> properties =
              parseIniLines(configFile.readAsLinesSync());
151
          return AndroidEmulator(id, properties);
152 153
        }
      }
154 155 156
    }
  }

157
  return AndroidEmulator(id);
158 159
}

160
@visibleForTesting
161
Map<String, String> parseIniLines(List<String> contents) {
162 163 164
  final Map<String, String> results = <String, String>{};

  final Iterable<List<String>> properties = contents
165
      .map<String>((String l) => l.trim())
Danny Tuppeny's avatar
Danny Tuppeny committed
166 167 168 169 170
      // 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
171
      .map<List<String>>((String l) => l.split('='));
172

173
  for (final List<String> property in properties) {
174
    results[property[0].trim()] = property[1].trim();
175
  }
176 177

  return results;
178
}