Add --create option to `flutter emulators` command (#18235)

* Add --create option to flutter emulators

* Tweaks to error message

* Simplify emulator search logic

* Make name optional

* Add a note about this option being used with --create

* Tweaks to help information

* Switch to processManager for easier testing

* Don't crash on missing files or missing properties in Android Emulator

* Move name suffixing into emulator manager

This allows it to be tested in the EmulatorManager tests and also used by daemon later if desired.

* Pass the context's android SDK through so it can be mocked by tests

* Misc fixes

* Add tests around emulator creation

Process calls are mocked to avoid needing a real SDK (and to be fast). Full integration tests may be useful, but may require ensuring all build environments etc. are set up correctly.

* Simplify avdManagerPath

Previous changes were to emulatorPath!

* Fix lint errors

* Fix incorrect file exgtension for Windows

* Fix an issue where no system images would crash

reduce throws on an empty collection.

* Fix "null" appearing in error messages

The name we attempted to use will now always be returned, even in the case of failure.

* Add additional info to missing-system-image failure message

On Windows after installing Andriod Studio I didn't have any of these and got this message. Installing with sdkmanager fixed the issue.

* Fix thrown errors

runResult had a toString() but we moved to ProcessResult when switching to ProcessManager to this ended up throwing "Instance of ProcessResult".

* Fix package import

* Fix more package imports

* Move mock implementation into Mock class

There seemed to be issues using Lists in args with Mockito that I couldn't figure out (docs say to use typed() but I couldn't make this compile with these lists still)..

* Rename method that's ambigious now we have create

* Handle where there's no avd path

* Add another toList() :(

* Remove comment that was rewritten

* Fix forbidden import

* Make optional arg more obviously optional

* Reformat doc

* Note that we create a pixel device in help text

* Make this a named arg
diff --git a/packages/flutter_tools/lib/src/android/android_emulator.dart b/packages/flutter_tools/lib/src/android/android_emulator.dart
index adb1ee7..582bf0f 100644
--- a/packages/flutter_tools/lib/src/android/android_emulator.dart
+++ b/packages/flutter_tools/lib/src/android/android_emulator.dart
@@ -9,7 +9,8 @@
 import '../android/android_sdk.dart';
 import '../android/android_workflow.dart';
 import '../base/file_system.dart';
-import '../base/process.dart';
+import '../base/io.dart';
+import '../base/process_manager.dart';
 import '../emulator.dart';
 import 'android_sdk.dart';
 
@@ -31,21 +32,23 @@
   Map<String, String> _properties;
 
   @override
-  String get name => _properties['hw.device.name'];
+  String get name => _prop('hw.device.name');
 
   @override
-  String get manufacturer => _properties['hw.device.manufacturer'];
+  String get manufacturer => _prop('hw.device.manufacturer');
 
   @override
   String get label => _properties['avd.ini.displayname'];
 
+  String _prop(String name) => _properties != null ? _properties[name] : null;
+
   @override
   Future<void> launch() async {
     final Future<void> launchResult =
-        runAsync(<String>[getEmulatorPath(), '-avd', id])
-            .then((RunResult runResult) {
+        processManager.run(<String>[getEmulatorPath(), '-avd', id])
+            .then((ProcessResult runResult) {
               if (runResult.exitCode != 0) {
-                throw '$runResult';
+                throw '${runResult.stdout}\n${runResult.stderr}'.trimRight();
               }
             });
     // emulator continues running on a successful launch so if we
@@ -65,10 +68,12 @@
     return <AndroidEmulator>[];
   }
 
-  final String listAvdsOutput = runSync(<String>[emulatorPath, '-list-avds']);
+  final String listAvdsOutput = processManager.runSync(<String>[emulatorPath, '-list-avds']).stdout;
 
   final List<AndroidEmulator> emulators = <AndroidEmulator>[];
-  extractEmulatorAvdInfo(listAvdsOutput, emulators);
+  if (listAvdsOutput != null) {
+    extractEmulatorAvdInfo(listAvdsOutput, emulators);
+  }
   return emulators;
 }
 
@@ -76,21 +81,26 @@
 /// of emulators by reading information from the relevant ini files.
 void extractEmulatorAvdInfo(String text, List<AndroidEmulator> emulators) {
   for (String id in text.trim().split('\n').where((String l) => l != '')) {
-    emulators.add(_createEmulator(id));
+    emulators.add(_loadEmulatorInfo(id));
   }
 }
 
-AndroidEmulator _createEmulator(String id) {
+AndroidEmulator _loadEmulatorInfo(String id) {
   id = id.trim();
-  final File iniFile = fs.file(fs.path.join(getAvdPath(), '$id.ini'));
-  final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync());
-
-  if (ini['path'] != null) {
-    final File configFile = fs.file(fs.path.join(ini['path'], 'config.ini'));
-    if (configFile.existsSync()) {
-      final Map<String, String> properties =
-          parseIniLines(configFile.readAsLinesSync());
-      return new AndroidEmulator(id, properties);
+  final String avdPath = getAvdPath();
+  if (avdPath != null) {
+    final File iniFile = fs.file(fs.path.join(avdPath, '$id.ini'));
+    if (iniFile.existsSync()) {
+      final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync());
+      if (ini['path'] != null) {
+        final File configFile =
+            fs.file(fs.path.join(ini['path'], 'config.ini'));
+        if (configFile.existsSync()) {
+          final Map<String, String> properties =
+              parseIniLines(configFile.readAsLinesSync());
+          return new AndroidEmulator(id, properties);
+        }
+      }
     }
   }
 
diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart
index 79a5d67..2dcc5bf 100644
--- a/packages/flutter_tools/lib/src/android/android_sdk.dart
+++ b/packages/flutter_tools/lib/src/android/android_sdk.dart
@@ -64,16 +64,8 @@
 /// will work for those users who have Android Tools installed but
 /// not the full SDK.
 String getEmulatorPath([AndroidSdk existingSdk]) {
-  if (existingSdk?.emulatorPath != null)
-    return existingSdk.emulatorPath;
-
-  final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
-
-  if (sdk?.latestVersion == null) {
-    return os.which('emulator')?.path;
-  } else {
-    return sdk.emulatorPath;
-  }
+  return existingSdk?.emulatorPath ??
+    AndroidSdk.locateAndroidSdk()?.emulatorPath;
 }
 
 /// Locate the path for storing AVD emulator images. Returns null if none found.
@@ -104,6 +96,15 @@
   );
 }
 
+/// Locate 'avdmanager'. Prefer to use one from an Android SDK, if we can locate that.
+/// This should be used over accessing androidSdk.avdManagerPath directly because it
+/// will work for those users who have Android Tools installed but
+/// not the full SDK.
+String getAvdManagerPath([AndroidSdk existingSdk]) {
+  return existingSdk?.avdManagerPath ??
+    AndroidSdk.locateAndroidSdk()?.avdManagerPath;
+}
+
 class AndroidNdkSearchError {
   AndroidNdkSearchError(this.reason);
 
@@ -314,6 +315,8 @@
 
   String get emulatorPath => getEmulatorPath();
 
+  String get avdManagerPath => getAvdManagerPath();
+
   /// Validate the Android SDK. This returns an empty list if there are no
   /// issues; otherwise, it returns a list of issues found.
   List<String> validateSdkWellFormed() {
@@ -343,6 +346,14 @@
     return null;
   }
 
+  String getAvdManagerPath() {
+    final String binaryName = platform.isWindows ? 'avdmanager.bat' : 'avdmanager';
+    final String path = fs.path.join(directory, 'tools', 'bin', binaryName);
+    if (fs.file(path).existsSync())
+      return path;
+    return null;
+  }
+
   void _init() {
     Iterable<Directory> platforms = <Directory>[]; // android-22, ...
 
diff --git a/packages/flutter_tools/lib/src/commands/emulators.dart b/packages/flutter_tools/lib/src/commands/emulators.dart
index 78b1284..0904bc4 100644
--- a/packages/flutter_tools/lib/src/commands/emulators.dart
+++ b/packages/flutter_tools/lib/src/commands/emulators.dart
@@ -16,13 +16,18 @@
   EmulatorsCommand() {
     argParser.addOption('launch',
         help: 'The full or partial ID of the emulator to launch.');
+    argParser.addFlag('create',
+        help: 'Creates a new Android emulator based on a Pixel device.',
+        negatable: false);
+    argParser.addOption('name',
+        help: 'Used with flag --create. Specifies a name for the emulator being created.');
   }
 
   @override
   final String name = 'emulators';
 
   @override
-  final String description = 'List and launch available emulators.';
+  final String description = 'List, launch and create emulators.';
 
   @override
   final List<String> aliases = <String>['emulator'];
@@ -40,6 +45,8 @@
 
     if (argResults.wasParsed('launch')) {
       await _launchEmulator(argResults['launch']);
+    } else if (argResults.wasParsed('create')) {
+      await _createEmulator(name: argResults['name']);
     } else {
       final String searchText =
           argResults.rest != null && argResults.rest.isNotEmpty
@@ -70,17 +77,27 @@
     }
   }
 
+  Future<Null> _createEmulator({String name}) async {
+    final CreateEmulatorResult createResult =
+        await emulatorManager.createEmulator(name: name);
+
+    if (createResult.success) {
+      printStatus("Emulator '${createResult.emulatorName}' created successfully.");
+    } else {
+      printStatus("Failed to create emulator '${createResult.emulatorName}'.\n");
+      printStatus(createResult.error.trim());
+      _printAdditionalInfo();
+    }
+  }
+
   Future<void> _listEmulators(String searchText) async {
     final List<Emulator> emulators = searchText == null
         ? await emulatorManager.getAllAvailableEmulators()
         : await emulatorManager.getEmulatorsMatching(searchText);
 
     if (emulators.isEmpty) {
-      printStatus('No emulators available.\n\n'
-          // TODO(dantup): Change these when we support creation
-          // 'You may need to create images using "flutter emulators --create"\n'
-          'You may need to create one using Android Studio '
-          'or visit https://flutter.io/setup/ for troubleshooting tips.');
+      printStatus('No emulators available.');
+      _printAdditionalInfo(showCreateInstruction: true);
     } else {
       _printEmulatorList(
         emulators,
@@ -92,7 +109,28 @@
   void _printEmulatorList(List<Emulator> emulators, String message) {
     printStatus('$message\n');
     Emulator.printEmulators(emulators);
-    printStatus(
-        "\nTo run an emulator, run 'flutter emulators --launch <emulator id>'.");
+    _printAdditionalInfo(showCreateInstruction: true, showRunInstruction: true);
+  }
+
+  void _printAdditionalInfo({ bool showRunInstruction = false,
+      bool showCreateInstruction = false }) {
+    printStatus('');
+    if (showRunInstruction) {
+      printStatus(
+          "To run an emulator, run 'flutter emulators --launch <emulator id>'.");
+    }
+    if (showCreateInstruction) {
+      printStatus(
+          "To create a new emulator, run 'flutter emulators --create [--name xyz]'.");
+    }
+
+    if (showRunInstruction || showCreateInstruction) {
+      printStatus('');
+    }
+    // TODO(dantup): Update this link to flutter.io if/when we have a better page.
+    // That page can then link out to these places if required.
+    printStatus('You can find more information on managing emulators at the links below:\n'
+        '  https://developer.android.com/studio/run/managing-avds\n'
+        '  https://developer.android.com/studio/command-line/avdmanager');
   }
 }
diff --git a/packages/flutter_tools/lib/src/emulator.dart b/packages/flutter_tools/lib/src/emulator.dart
index 81bbe2a..5fa6b2a 100644
--- a/packages/flutter_tools/lib/src/emulator.dart
+++ b/packages/flutter_tools/lib/src/emulator.dart
@@ -6,7 +6,10 @@
 import 'dart:math' as math;
 
 import 'android/android_emulator.dart';
+import 'android/android_sdk.dart';
 import 'base/context.dart';
+import 'base/io.dart' show ProcessResult;
+import 'base/process_manager.dart';
 import 'globals.dart';
 import 'ios/ios_emulators.dart';
 
@@ -34,8 +37,8 @@
         emulator.id?.toLowerCase()?.startsWith(searchText) == true ||
         emulator.name?.toLowerCase()?.startsWith(searchText) == true;
 
-    final Emulator exactMatch = emulators.firstWhere(
-        exactlyMatchesEmulatorId, orElse: () => null);
+    final Emulator exactMatch =
+        emulators.firstWhere(exactlyMatchesEmulatorId, orElse: () => null);
     if (exactMatch != null) {
       return <Emulator>[exactMatch];
     }
@@ -57,6 +60,133 @@
     return emulators;
   }
 
+  /// Return the list of all available emulators.
+  Future<CreateEmulatorResult> createEmulator({String name}) async {
+    if (name == null || name == '') {
+      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
+          .map((Emulator e) => e.id)
+          .where((String id) => id.startsWith(autoName))
+          .toSet();
+      int suffix = 1;
+      name = autoName;
+      while (takenNames.contains(name)) {
+        name = '${autoName}_${++suffix}';
+      }
+    }
+
+    final String device = await _getPreferredAvailableDevice();
+    if (device == null)
+      return new CreateEmulatorResult(name,
+          success: false, error: 'No device definitions are available');
+
+    final String sdkId = await _getPreferredSdkId();
+    if (sdkId == null)
+      return new CreateEmulatorResult(name,
+          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"');
+
+    // 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
+    String cleanError(String error) {
+      return (error ?? '')
+          .split('\n')
+          .where((String l) => l.trim() != 'null')
+          .where((String l) =>
+              l.trim() != 'Use --force if you want to replace it.')
+          .join('\n');
+    }
+
+    final List<String> args = <String>[
+      getAvdManagerPath(androidSdk),
+      'create',
+      'avd',
+      '-n', name,
+      '-k', sdkId,
+      '-d', device
+    ];
+    final ProcessResult runResult = processManager.runSync(args);
+    return new CreateEmulatorResult(
+      name,
+      success: runResult.exitCode == 0,
+      output: runResult.stdout,
+      error: cleanError(runResult.stderr),
+    );
+  }
+
+  static const List<String> preferredDevices = const <String>[
+    'pixel',
+    'pixel_xl',
+  ];
+  Future<String> _getPreferredAvailableDevice() async {
+    final List<String> args = <String>[
+      getAvdManagerPath(androidSdk),
+      'list',
+      'device',
+      '-c'
+    ];
+    final ProcessResult runResult = processManager.runSync(args);
+    if (runResult.exitCode != 0)
+      return null;
+
+    final List<String> availableDevices = runResult.stdout
+        .split('\n')
+        .where((String l) => preferredDevices.contains(l.trim()))
+        .toList();
+
+    return preferredDevices.firstWhere(
+      (String d) => availableDevices.contains(d),
+      orElse: () => null,
+    );
+  }
+
+  RegExp androidApiVersion = new RegExp(r';android-(\d+);');
+  Future<String> _getPreferredSdkId() async {
+    // 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>[
+      getAvdManagerPath(androidSdk),
+      'create',
+      'avd',
+      '-n', 'temp',
+    ];
+    final ProcessResult runResult = processManager.runSync(args);
+
+    // Get the list of IDs that match our criteria
+    final List<String> availableIDs = runResult.stderr
+        .split('\n')
+        .where((String l) => androidApiVersion.hasMatch(l))
+        .where((String l) => l.contains('system-images'))
+        .where((String l) => l.contains('google_apis_playstore'))
+        .toList();
+
+    final List<int> availableApiVersions = availableIDs
+        .map((String id) => androidApiVersion.firstMatch(id).group(1))
+        .map((String apiVersion) => int.parse(apiVersion))
+        .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.
+    return availableIDs.firstWhere(
+      (String id) => id.contains(';android-$apiVersion;'),
+      orElse: () => null,
+    );
+  }
+
   /// Whether we're capable of listing any emulators given the current environment configuration.
   bool get canListAnything {
     return _platformDiscoverers.any((EmulatorDiscovery discoverer) => discoverer.canListAnything);
@@ -124,11 +254,13 @@
 
     // Join columns into lines of text
     final RegExp whiteSpaceAndDots = new RegExp(r'[•\s]+$');
-    return table.map((List<String> row) {
-      return indices
-        .map((int i) => row[i].padRight(widths[i]))
-        .join(' • ') + ' • ${row.last}';
-    })
+    return table
+        .map((List<String> row) {
+          return indices
+                  .map((int i) => row[i].padRight(widths[i]))
+                  .join(' • ') +
+              ' • ${row.last}';
+        })
         .map((String line) => line.replaceAll(whiteSpaceAndDots, ''))
         .toList();
   }
@@ -137,3 +269,12 @@
     descriptions(emulators).forEach(printStatus);
   }
 }
+
+class CreateEmulatorResult {
+  final bool success;
+  final String emulatorName;
+  final String output;
+  final String error;
+
+  CreateEmulatorResult(this.emulatorName, {this.success, this.output, this.error});
+}
diff --git a/packages/flutter_tools/lib/src/ios/ios_emulators.dart b/packages/flutter_tools/lib/src/ios/ios_emulators.dart
index cd85c2f..3608bad 100644
--- a/packages/flutter_tools/lib/src/ios/ios_emulators.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_emulators.dart
@@ -71,6 +71,8 @@
 }
 
 String getSimulatorPath() {
+  if (xcode.xcodeSelectPath == null)
+    return null;
   final List<String> searchPaths = <String>[
     fs.path.join(xcode.xcodeSelectPath, 'Applications', 'Simulator.app'),
   ];
diff --git a/packages/flutter_tools/test/emulator_test.dart b/packages/flutter_tools/test/emulator_test.dart
index be36d5f..9aad551 100644
--- a/packages/flutter_tools/test/emulator_test.dart
+++ b/packages/flutter_tools/test/emulator_test.dart
@@ -3,31 +3,61 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 
+import 'package:collection/collection.dart' show ListEquality;
+import 'package:flutter_tools/src/android/android_sdk.dart';
+import 'package:flutter_tools/src/base/config.dart';
+import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/emulator.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
 import 'package:test/test.dart';
 
 import 'src/context.dart';
+import 'src/mocks.dart';
 
 void main() {
+  MockProcessManager mockProcessManager;
+  MockConfig mockConfig;
+  MockAndroidSdk mockSdk;
+
+  setUp(() {
+    mockProcessManager = new MockProcessManager();
+    mockConfig = new MockConfig();
+    mockSdk = new MockAndroidSdk();
+
+    when(mockSdk.avdManagerPath).thenReturn('avdmanager');
+    when(mockSdk.emulatorPath).thenReturn('emulator');
+  });
+
   group('EmulatorManager', () {
     testUsingContext('getEmulators', () async {
       // Test that EmulatorManager.getEmulators() doesn't throw.
-      final EmulatorManager emulatorManager = new EmulatorManager();
-      final List<Emulator> emulators = await emulatorManager.getAllAvailableEmulators();
+      final List<Emulator> emulators =
+          await emulatorManager.getAllAvailableEmulators();
       expect(emulators, isList);
     });
 
     testUsingContext('getEmulatorsById', () async {
-      final _MockEmulator emulator1 = new _MockEmulator('Nexus_5', 'Nexus 5', 'Google', '');
-      final _MockEmulator emulator2 = new _MockEmulator('Nexus_5X_API_27_x86', 'Nexus 5X', 'Google', '');
-      final _MockEmulator emulator3 = new _MockEmulator('iOS Simulator', 'iOS Simulator', 'Apple', '');
-      final List<Emulator> emulators = <Emulator>[emulator1, emulator2, emulator3];
-      final EmulatorManager emulatorManager = new TestEmulatorManager(emulators);
+      final _MockEmulator emulator1 =
+          new _MockEmulator('Nexus_5', 'Nexus 5', 'Google', '');
+      final _MockEmulator emulator2 =
+          new _MockEmulator('Nexus_5X_API_27_x86', 'Nexus 5X', 'Google', '');
+      final _MockEmulator emulator3 =
+          new _MockEmulator('iOS Simulator', 'iOS Simulator', 'Apple', '');
+      final List<Emulator> emulators = <Emulator>[
+        emulator1,
+        emulator2,
+        emulator3
+      ];
+      final TestEmulatorManager testEmulatorManager =
+          new TestEmulatorManager(emulators);
 
       Future<Null> expectEmulator(String id, List<Emulator> expected) async {
-        expect(await emulatorManager.getEmulatorsMatching(id), expected);
+        expect(await testEmulatorManager.getEmulatorsMatching(id), expected);
       }
+
       expectEmulator('Nexus_5', <Emulator>[emulator1]);
       expectEmulator('Nexus_5X', <Emulator>[emulator2]);
       expectEmulator('Nexus_5X_API_27_x86', <Emulator>[emulator2]);
@@ -35,6 +65,61 @@
       expectEmulator('iOS Simulator', <Emulator>[emulator3]);
       expectEmulator('ios', <Emulator>[emulator3]);
     });
+
+    testUsingContext('create emulator with an empty name does not fail',
+        () async {
+      final CreateEmulatorResult res = await emulatorManager.createEmulator();
+      expect(res.success, equals(true));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AndroidSdk: () => mockSdk,
+    });
+
+    testUsingContext('create emulator with a unique name does not throw',
+        () async {
+      final CreateEmulatorResult res =
+          await emulatorManager.createEmulator(name: 'test');
+      expect(res.success, equals(true));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AndroidSdk: () => mockSdk,
+    });
+
+    testUsingContext('create emulator with an existing name errors', () async {
+      final CreateEmulatorResult res =
+          await emulatorManager.createEmulator(name: 'existing-avd-1');
+      expect(res.success, equals(false));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AndroidSdk: () => mockSdk,
+    });
+
+    testUsingContext(
+        'create emulator without a name but when default exists adds a suffix',
+        () async {
+      // First will get default name.
+      CreateEmulatorResult res = await emulatorManager.createEmulator();
+      expect(res.success, equals(true));
+
+      final String defaultName = res.emulatorName;
+
+      // Second...
+      res = await emulatorManager.createEmulator();
+      expect(res.success, equals(true));
+      expect(res.emulatorName, equals('${defaultName}_2'));
+
+      // Third...
+      res = await emulatorManager.createEmulator();
+      expect(res.success, equals(true));
+      expect(res.emulatorName, equals('${defaultName}_3'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AndroidSdk: () => mockSdk,
+    });
   });
 }
 
@@ -50,14 +135,15 @@
 }
 
 class _MockEmulator extends Emulator {
-  _MockEmulator(String id, this.name, this.manufacturer, this.label) : super(id, true);
+  _MockEmulator(String id, this.name, this.manufacturer, this.label)
+      : super(id, true);
 
   @override
   final String name;
- 
+
   @override
   final String manufacturer;
- 
+
   @override
   final String label;
 
@@ -66,3 +152,81 @@
     throw new UnimplementedError('Not implemented in Mock');
   }
 }
+
+class MockConfig extends Mock implements Config {}
+
+class MockProcessManager extends Mock implements ProcessManager {
+  /// We have to send a command that fails in order to get the list of valid
+  /// system images paths. This is an example of the output to use in the mock.
+  static const String mockCreateFailureOutput =
+      'Error: Package path (-k) not specified. Valid system image paths are:\n'
+      'system-images;android-27;google_apis;x86\n'
+      'system-images;android-P;google_apis;x86\n'
+      'system-images;android-27;google_apis_playstore;x86\n'
+      'null\n'; // Yep, these really end with null (on dantup's machine at least)
+
+  static const ListEquality<String> _equality = const ListEquality<String>();
+  final List<String> _existingAvds = <String>['existing-avd-1'];
+
+  @override
+  ProcessResult runSync(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding,
+    Encoding stderrEncoding
+  }) {
+    final String program = command[0];
+    final List<String> args = command.sublist(1);
+    switch (command[0]) {
+      case '/usr/bin/xcode-select':
+        throw new ProcessException(program, args);
+        break;
+      case 'emulator':
+        return _handleEmulator(args);
+      case 'avdmanager':
+        return _handleAvdManager(args);
+    }
+    throw new StateError('Unexpected process call: $command');
+  }
+
+  ProcessResult _handleEmulator(List<String> args) {
+    if (_equality.equals(args, <String>['-list-avds'])) {
+      return new ProcessResult(101, 0, '${_existingAvds.join('\n')}\n', '');
+    }
+    throw new ProcessException('emulator', args);
+  }
+
+  ProcessResult _handleAvdManager(List<String> args) {
+    if (_equality.equals(args, <String>['list', 'device', '-c'])) {
+      return new ProcessResult(101, 0, 'test\ntest2\npixel\npixel-xl\n', '');
+    }
+    if (_equality.equals(args, <String>['create', 'avd', '-n', 'temp'])) {
+      return new ProcessResult(101, 1, '', mockCreateFailureOutput);
+    }
+    if (args.length == 8 &&
+        _equality.equals(args,
+            <String>['create', 'avd', '-n', args[3], '-k', args[5], '-d', args[7]])) {
+      // In order to support testing auto generation of names we need to support
+      // tracking any created emulators and reject when they already exist so this
+      // mock will compare the name of the AVD being created with the fake existing
+      // list and either reject if it exists, or add it to the list and return success.
+      final String name = args[3];
+      // Error if this AVD already existed
+      if (_existingAvds.contains(name)) {
+        return new ProcessResult(
+            101,
+            1,
+            '',
+            "Error: Android Virtual Device '$name' already exists.\n"
+            'Use --force if you want to replace it.');
+      } else {
+        _existingAvds.add(name);
+        return new ProcessResult(101, 0, '', '');
+      }
+    }
+    throw new ProcessException('emulator', args);
+  }
+}