1// Copyright 2018 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:meta/meta.dart'; 8 9import '../android/android_sdk.dart'; 10import '../android/android_workflow.dart'; 11import '../base/file_system.dart'; 12import '../base/io.dart'; 13import '../base/process_manager.dart'; 14import '../device.dart'; 15import '../emulator.dart'; 16import 'android_sdk.dart'; 17 18class AndroidEmulators extends EmulatorDiscovery { 19 @override 20 bool get supportsPlatform => true; 21 22 @override 23 bool get canListAnything => androidWorkflow.canListEmulators; 24 25 @override 26 Future<List<Emulator>> get emulators async => getEmulatorAvds(); 27} 28 29class AndroidEmulator extends Emulator { 30 AndroidEmulator(String id, [this._properties]) 31 : super(id, _properties != null && _properties.isNotEmpty); 32 33 final Map<String, String> _properties; 34 35 // Android Studio uses the ID with underscores replaced with spaces 36 // for the name if displayname is not set so we do the same. 37 @override 38 String get name => _prop('avd.ini.displayname') ?? id.replaceAll('_', ' ').trim(); 39 40 @override 41 String get manufacturer => _prop('hw.device.manufacturer'); 42 43 @override 44 Category get category => Category.mobile; 45 46 @override 47 PlatformType get platformType => PlatformType.android; 48 49 String _prop(String name) => _properties != null ? _properties[name] : null; 50 51 @override 52 Future<void> launch() async { 53 final Future<void> launchResult = 54 processManager.run(<String>[getEmulatorPath(), '-avd', id]) 55 .then((ProcessResult runResult) { 56 if (runResult.exitCode != 0) { 57 throw '${runResult.stdout}\n${runResult.stderr}'.trimRight(); 58 } 59 }); 60 // The emulator continues running on a successful launch, so if it hasn't 61 // quit within 3 seconds we assume that's a success and just return. This 62 // means that on a slow machine, a failure that takes more than three 63 // seconds won't be recognized as such... :-/ 64 return Future.any<void>(<Future<void>>[ 65 launchResult, 66 Future<void>.delayed(const Duration(seconds: 3)), 67 ]); 68 } 69} 70 71/// Return the list of available emulator AVDs. 72List<AndroidEmulator> getEmulatorAvds() { 73 final String emulatorPath = getEmulatorPath(androidSdk); 74 if (emulatorPath == null) { 75 return <AndroidEmulator>[]; 76 } 77 78 final String listAvdsOutput = processManager.runSync(<String>[emulatorPath, '-list-avds']).stdout; 79 80 final List<AndroidEmulator> emulators = <AndroidEmulator>[]; 81 if (listAvdsOutput != null) { 82 extractEmulatorAvdInfo(listAvdsOutput, emulators); 83 } 84 return emulators; 85} 86 87/// Parse the given `emulator -list-avds` output in [text], and fill out the given list 88/// of emulators by reading information from the relevant ini files. 89void extractEmulatorAvdInfo(String text, List<AndroidEmulator> emulators) { 90 for (String id in text.trim().split('\n').where((String l) => l != '')) { 91 emulators.add(_loadEmulatorInfo(id)); 92 } 93} 94 95AndroidEmulator _loadEmulatorInfo(String id) { 96 id = id.trim(); 97 final String avdPath = getAvdPath(); 98 if (avdPath != null) { 99 final File iniFile = fs.file(fs.path.join(avdPath, '$id.ini')); 100 if (iniFile.existsSync()) { 101 final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync()); 102 if (ini['path'] != null) { 103 final File configFile = 104 fs.file(fs.path.join(ini['path'], 'config.ini')); 105 if (configFile.existsSync()) { 106 final Map<String, String> properties = 107 parseIniLines(configFile.readAsLinesSync()); 108 return AndroidEmulator(id, properties); 109 } 110 } 111 } 112 } 113 114 return AndroidEmulator(id); 115} 116 117@visibleForTesting 118Map<String, String> parseIniLines(List<String> contents) { 119 final Map<String, String> results = <String, String>{}; 120 121 final Iterable<List<String>> properties = contents 122 .map<String>((String l) => l.trim()) 123 // Strip blank lines/comments 124 .where((String l) => l != '' && !l.startsWith('#')) 125 // Discard anything that isn't simple name=value 126 .where((String l) => l.contains('=')) 127 // Split into name/value 128 .map<List<String>>((String l) => l.split('=')); 129 130 for (List<String> property in properties) { 131 results[property[0].trim()] = property[1].trim(); 132 } 133 134 return results; 135} 136