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'; 6import 'dart:math' as math; 7 8import 'android/android_emulator.dart'; 9import 'android/android_sdk.dart'; 10import 'base/context.dart'; 11import 'base/io.dart' show ProcessResult; 12import 'base/process_manager.dart'; 13import 'device.dart'; 14import 'globals.dart'; 15import 'ios/ios_emulators.dart'; 16 17EmulatorManager get emulatorManager => context.get<EmulatorManager>(); 18 19/// A class to get all available emulators. 20class EmulatorManager { 21 /// Constructing EmulatorManager is cheap; they only do expensive work if some 22 /// of their methods are called. 23 EmulatorManager() { 24 // Register the known discoverers. 25 _emulatorDiscoverers.add(AndroidEmulators()); 26 _emulatorDiscoverers.add(IOSEmulators()); 27 } 28 29 final List<EmulatorDiscovery> _emulatorDiscoverers = <EmulatorDiscovery>[]; 30 31 Future<List<Emulator>> getEmulatorsMatching(String searchText) async { 32 final List<Emulator> emulators = await getAllAvailableEmulators(); 33 searchText = searchText.toLowerCase(); 34 bool exactlyMatchesEmulatorId(Emulator emulator) => 35 emulator.id?.toLowerCase() == searchText || 36 emulator.name?.toLowerCase() == searchText; 37 bool startsWithEmulatorId(Emulator emulator) => 38 emulator.id?.toLowerCase()?.startsWith(searchText) == true || 39 emulator.name?.toLowerCase()?.startsWith(searchText) == true; 40 41 final Emulator exactMatch = 42 emulators.firstWhere(exactlyMatchesEmulatorId, orElse: () => null); 43 if (exactMatch != null) { 44 return <Emulator>[exactMatch]; 45 } 46 47 // Match on a id or name starting with [emulatorId]. 48 return emulators.where(startsWithEmulatorId).toList(); 49 } 50 51 Iterable<EmulatorDiscovery> get _platformDiscoverers { 52 return _emulatorDiscoverers.where((EmulatorDiscovery discoverer) => discoverer.supportsPlatform); 53 } 54 55 /// Return the list of all available emulators. 56 Future<List<Emulator>> getAllAvailableEmulators() async { 57 final List<Emulator> emulators = <Emulator>[]; 58 await Future.forEach<EmulatorDiscovery>(_platformDiscoverers, (EmulatorDiscovery discoverer) async { 59 emulators.addAll(await discoverer.emulators); 60 }); 61 return emulators; 62 } 63 64 /// Return the list of all available emulators. 65 Future<CreateEmulatorResult> createEmulator({ String name }) async { 66 if (name == null || name == '') { 67 const String autoName = 'flutter_emulator'; 68 // Don't use getEmulatorsMatching here, as it will only return one 69 // if there's an exact match and we need all those with this prefix 70 // so we can keep adding suffixes until we miss. 71 final List<Emulator> all = await getAllAvailableEmulators(); 72 final Set<String> takenNames = all 73 .map<String>((Emulator e) => e.id) 74 .where((String id) => id.startsWith(autoName)) 75 .toSet(); 76 int suffix = 1; 77 name = autoName; 78 while (takenNames.contains(name)) { 79 name = '${autoName}_${++suffix}'; 80 } 81 } 82 83 final String device = await _getPreferredAvailableDevice(); 84 if (device == null) 85 return CreateEmulatorResult(name, 86 success: false, error: 'No device definitions are available'); 87 88 final String sdkId = await _getPreferredSdkId(); 89 if (sdkId == null) 90 return CreateEmulatorResult(name, 91 success: false, 92 error: 93 'No suitable Android AVD system images are available. You may need to install these' 94 ' using sdkmanager, for example:\n' 95 ' sdkmanager "system-images;android-27;google_apis_playstore;x86"'); 96 97 // Cleans up error output from avdmanager to make it more suitable to show 98 // to flutter users. Specifically: 99 // - Removes lines that say "null" (!) 100 // - Removes lines that tell the user to use '--force' to overwrite emulators 101 String cleanError(String error) { 102 if (error == null || error.trim() == '') 103 return null; 104 return error 105 .split('\n') 106 .where((String l) => l.trim() != 'null') 107 .where((String l) => 108 l.trim() != 'Use --force if you want to replace it.') 109 .join('\n') 110 .trim(); 111 } 112 113 final List<String> args = <String>[ 114 getAvdManagerPath(androidSdk), 115 'create', 116 'avd', 117 '-n', name, 118 '-k', sdkId, 119 '-d', device, 120 ]; 121 final ProcessResult runResult = processManager.runSync(args, 122 environment: androidSdk?.sdkManagerEnv); 123 return CreateEmulatorResult( 124 name, 125 success: runResult.exitCode == 0, 126 output: runResult.stdout, 127 error: cleanError(runResult.stderr), 128 ); 129 } 130 131 static const List<String> preferredDevices = <String>[ 132 'pixel', 133 'pixel_xl', 134 ]; 135 Future<String> _getPreferredAvailableDevice() async { 136 final List<String> args = <String>[ 137 getAvdManagerPath(androidSdk), 138 'list', 139 'device', 140 '-c', 141 ]; 142 final ProcessResult runResult = processManager.runSync(args, 143 environment: androidSdk?.sdkManagerEnv); 144 if (runResult.exitCode != 0) 145 return null; 146 147 final List<String> availableDevices = runResult.stdout 148 .split('\n') 149 .where((String l) => preferredDevices.contains(l.trim())) 150 .toList(); 151 152 return preferredDevices.firstWhere( 153 (String d) => availableDevices.contains(d), 154 orElse: () => null, 155 ); 156 } 157 158 RegExp androidApiVersion = RegExp(r';android-(\d+);'); 159 Future<String> _getPreferredSdkId() async { 160 // It seems that to get the available list of images, we need to send a 161 // request to create without the image and it'll provide us a list :-( 162 final List<String> args = <String>[ 163 getAvdManagerPath(androidSdk), 164 'create', 165 'avd', 166 '-n', 'temp', 167 ]; 168 final ProcessResult runResult = processManager.runSync(args, 169 environment: androidSdk?.sdkManagerEnv); 170 171 // Get the list of IDs that match our criteria 172 final List<String> availableIDs = runResult.stderr 173 .split('\n') 174 .where((String l) => androidApiVersion.hasMatch(l)) 175 .where((String l) => l.contains('system-images')) 176 .where((String l) => l.contains('google_apis_playstore')) 177 .toList(); 178 179 final List<int> availableApiVersions = availableIDs 180 .map<String>((String id) => androidApiVersion.firstMatch(id).group(1)) 181 .map<int>((String apiVersion) => int.parse(apiVersion)) 182 .toList(); 183 184 // Get the highest Android API version or whats left 185 final int apiVersion = availableApiVersions.isNotEmpty 186 ? availableApiVersions.reduce(math.max) 187 : -1; // Don't match below 188 189 // We're out of preferences, we just have to return the first one with the high 190 // API version. 191 return availableIDs.firstWhere( 192 (String id) => id.contains(';android-$apiVersion;'), 193 orElse: () => null, 194 ); 195 } 196 197 /// Whether we're capable of listing any emulators given the current environment configuration. 198 bool get canListAnything { 199 return _platformDiscoverers.any((EmulatorDiscovery discoverer) => discoverer.canListAnything); 200 } 201} 202 203/// An abstract class to discover and enumerate a specific type of emulators. 204abstract class EmulatorDiscovery { 205 bool get supportsPlatform; 206 207 /// Whether this emulator discovery is capable of listing any emulators given the 208 /// current environment configuration. 209 bool get canListAnything; 210 211 Future<List<Emulator>> get emulators; 212} 213 214abstract class Emulator { 215 Emulator(this.id, this.hasConfig); 216 217 final String id; 218 final bool hasConfig; 219 String get name; 220 String get manufacturer; 221 Category get category; 222 PlatformType get platformType; 223 224 @override 225 int get hashCode => id.hashCode; 226 227 @override 228 bool operator ==(dynamic other) { 229 if (identical(this, other)) 230 return true; 231 if (other is! Emulator) 232 return false; 233 return id == other.id; 234 } 235 236 Future<void> launch(); 237 238 @override 239 String toString() => name; 240 241 static List<String> descriptions(List<Emulator> emulators) { 242 if (emulators.isEmpty) 243 return <String>[]; 244 245 // Extract emulators information 246 final List<List<String>> table = <List<String>>[]; 247 for (Emulator emulator in emulators) { 248 table.add(<String>[ 249 emulator.id ?? '', 250 emulator.name ?? '', 251 emulator.manufacturer ?? '', 252 emulator.platformType?.toString() ?? '', 253 ]); 254 } 255 256 // Calculate column widths 257 final List<int> indices = List<int>.generate(table[0].length - 1, (int i) => i); 258 List<int> widths = indices.map<int>((int i) => 0).toList(); 259 for (List<String> row in table) { 260 widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList(); 261 } 262 263 // Join columns into lines of text 264 final RegExp whiteSpaceAndDots = RegExp(r'[•\s]+$'); 265 return table 266 .map<String>((List<String> row) { 267 return indices 268 .map<String>((int i) => row[i].padRight(widths[i])) 269 .join(' • ') + 270 ' • ${row.last}'; 271 }) 272 .map<String>((String line) => line.replaceAll(whiteSpaceAndDots, '')) 273 .toList(); 274 } 275 276 static void printEmulators(List<Emulator> emulators) { 277 descriptions(emulators).forEach(printStatus); 278 } 279} 280 281class CreateEmulatorResult { 282 CreateEmulatorResult(this.emulatorName, {this.success, this.output, this.error}); 283 284 final bool success; 285 final String emulatorName; 286 final String output; 287 final String error; 288} 289