Fix install for iOS simulator, and add ability to uninstall (#4223)
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 1ae3359..b7051e3 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -232,6 +232,22 @@
     return true;
   }
 
+  @override
+  bool uninstallApp(ApplicationPackage app) {
+    if (!_checkForSupportedAdbVersion() || !_checkForSupportedAndroidVersion())
+      return false;
+
+    String uninstallOut = runCheckedSync(adbCommandForDevice(<String>['uninstall', app.id]));
+    RegExp failureExp = new RegExp(r'^Failure.*$', multiLine: true);
+    String failure = failureExp.stringMatch(uninstallOut);
+    if (failure != null) {
+      printError('Package uninstall error: $failure');
+      return false;
+    }
+
+    return true;
+  }
+
   Future<Null> _forwardPort(String service, int devicePort, int port) async {
     try {
       // Set up port forwarding for observatory.
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index b9b2747..e5c754e 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -274,6 +274,8 @@
   printTrace('Installing application package.');
   ApplicationPackage package = command.applicationPackages
       .getPackageForPlatform(command.device.platform);
+  if (command.device.isAppInstalled(package))
+    command.device.uninstallApp(package);
   command.device.installApp(package);
 
   Map<String, dynamic> platformArgs = <String, dynamic>{};
diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart
index c3ae09c..ab2ebb1 100644
--- a/packages/flutter_tools/lib/src/commands/install.dart
+++ b/packages/flutter_tools/lib/src/commands/install.dart
@@ -34,8 +34,11 @@
   if (package == null)
     return false;
 
-  if (device.isAppInstalled(package))
-    return true;
+  if (device.isAppInstalled(package)) {
+    printStatus('Uninstalling old version...');
+    if (!device.uninstallApp(package))
+      printError('Warning: uninstalling old version failed');
+  }
 
   return device.installApp(package);
 }
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index f67c0d3..d0451f0 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -149,9 +149,15 @@
   /// Whether it is an emulated device running on localhost.
   bool get isLocalEmulator;
 
+  /// Check if a version of the given app is already installed
+  bool isAppInstalled(ApplicationPackage app);
+
   /// Install an app package on the current device
   bool installApp(ApplicationPackage app);
 
+  /// Uninstall an app package from the current device
+  bool uninstallApp(ApplicationPackage app);
+
   /// Check if the device is supported by Flutter
   bool isSupported();
 
@@ -159,9 +165,6 @@
   // supported by Flutter, and, if not, why.
   String supportMessage() => isSupported() ? "Supported" : "Unsupported";
 
-  /// Check if the current version of the given app is already installed
-  bool isAppInstalled(ApplicationPackage app);
-
   TargetPlatform get platform;
 
   /// Get the log reader for this device.
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 321ab3a..c86cf80 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -124,21 +124,6 @@
   }
 
   @override
-  bool installApp(ApplicationPackage app) {
-    try {
-      IOSApp iosApp = app;
-      runCheckedSync(<String>[installerPath, '-i', iosApp.deviceBundlePath]);
-      return true;
-    } catch (e) {
-      return false;
-    }
-    return false;
-  }
-
-  @override
-  bool isSupported() => true;
-
-  @override
   bool isAppInstalled(ApplicationPackage app) {
     try {
       String apps = runCheckedSync(<String>[installerPath, '--list-apps']);
@@ -152,6 +137,36 @@
   }
 
   @override
+  bool installApp(ApplicationPackage app) {
+    IOSApp iosApp = app;
+    Directory bundle = new Directory(iosApp.deviceBundlePath);
+    if (!bundle.existsSync()) {
+      printError("Could not find application bundle at ${bundle.path}; have you run 'flutter build ios'?");
+      return false;
+    }
+
+    try {
+      runCheckedSync(<String>[installerPath, '-i', iosApp.deviceBundlePath]);
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }
+
+  @override
+  bool uninstallApp(ApplicationPackage app) {
+    try {
+      runCheckedSync(<String>[installerPath, '-U', app.id]);
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }
+
+  @override
+  bool isSupported() => true;
+
+  @override
   Future<LaunchResult> startApp(
     ApplicationPackage app,
     BuildMode mode, {
@@ -174,8 +189,7 @@
     // Step 2: Check that the application exists at the specified path.
     IOSApp iosApp = app;
     Directory bundle = new Directory(iosApp.deviceBundlePath);
-    bool bundleExists = bundle.existsSync();
-    if (!bundleExists) {
+    if (!bundle.existsSync()) {
       printError('Could not find the built application bundle at ${bundle.path}.');
       return new LaunchResult.failed();
     }
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index f566a14..58ae15b 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -274,10 +274,24 @@
 
   bool _isAnyConnected() => getConnectedDevices().isNotEmpty;
 
+  bool isInstalled(String appId) {
+    return exitsHappy(<String>[
+      _xcrunPath,
+      'simctl',
+      'get_app_container',
+      'booted',
+      appId,
+    ]);
+  }
+
   void install(String deviceId, String appPath) {
     runCheckedSync(<String>[_xcrunPath, 'simctl', 'install', deviceId, appPath]);
   }
 
+  void uninstall(String deviceId, String appId) {
+    runCheckedSync(<String>[_xcrunPath, 'simctl', 'uninstall', deviceId, appId]);
+  }
+
   void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) {
     List<String> args = <String>[_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier];
     if (launchArgs != null)
@@ -362,6 +376,11 @@
   }
 
   @override
+  bool isAppInstalled(ApplicationPackage app) {
+    return SimControl.instance.isInstalled(app.id);
+  }
+
+  @override
   bool installApp(ApplicationPackage app) {
     try {
       IOSApp iosApp = app;
@@ -373,6 +392,16 @@
   }
 
   @override
+  bool uninstallApp(ApplicationPackage app) {
+    try {
+      SimControl.instance.uninstall(id, app.id);
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }
+
+  @override
   bool isSupported() {
     if (!Platform.isMacOS) {
       _supportMessage = "Not supported on a non Mac host";
@@ -424,17 +453,6 @@
   }
 
   @override
-  bool isAppInstalled(ApplicationPackage app) {
-    try {
-      // TODO(tvolkert): This logic is wrong; simulatorHomeDirectory always exists
-      String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
-      return FileSystemEntity.isDirectorySync(simulatorHomeDirectory);
-    } catch (e) {
-      return false;
-    }
-  }
-
-  @override
   Future<LaunchResult> startApp(
     ApplicationPackage app,
     BuildMode mode, {
@@ -505,13 +523,7 @@
   }
 
   bool _applicationIsInstalledAndRunning(ApplicationPackage app) {
-    bool isInstalled = exitsHappy(<String>[
-      'xcrun',
-      'simctl',
-      'get_app_container',
-      'booted',
-      app.id,
-    ]);
+    bool isInstalled = isAppInstalled(app);
 
     bool isRunning = exitsHappy(<String>[
       '/usr/bin/killall',