Beginning implementation of IOSDevice. Implements list and install.

Also update tests to be compatible with the presence of iOS and add tests for list and install.
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index 0739f77..34afb4b 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -42,6 +42,15 @@
       : super(path.join(appDir, 'apks'), appPackageID, appFileName);
 }
 
+class IOSApp extends ApplicationPackage {
+  static const String _appName = 'SkyShell.app';
+  static const String _packageID = 'com.google.SkyShell';
+
+  IOSApp(String appDir,
+      {String appPackageID: _packageID, String appFileName: _appName})
+      : super(appDir, appPackageID, appFileName);
+}
+
 enum BuildType { prebuilt, release, debug, }
 
 enum BuildPlatform { android, iOS, iOSSimulator, mac, linux, }
@@ -62,7 +71,10 @@
   static BuildType defaultBuildType = BuildType.prebuilt;
 
   /// Default BuildPlatforms chosen if no BuildPlatforms are specified.
-  static List<BuildPlatform> defaultBuildPlatforms = [BuildPlatform.android];
+  static List<BuildPlatform> defaultBuildPlatforms = [
+    BuildPlatform.android,
+    BuildPlatform.iOS,
+  ];
 
   static Map<BuildPlatform, ApplicationPackage> getAvailableApplicationPackages(
       {BuildType requestedType, List<BuildPlatform> requestedPlatforms}) {
@@ -80,6 +92,9 @@
         case BuildPlatform.android:
           packages[platform] = new AndroidApk(buildPath);
           break;
+        case BuildPlatform.iOS:
+          packages[platform] = new IOSApp(buildPath);
+          break;
         default:
           // TODO(iansf): Add other platforms
           assert(false);
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 6c15b3b..b692a07 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -24,6 +24,8 @@
     if (id == null) {
       if (className == AndroidDevice.className) {
         id = AndroidDevice.defaultDeviceID;
+      } else if (className == IOSDevice.className) {
+        id = IOSDevice.defaultDeviceID;
       } else {
         throw 'Attempted to create a Device of unknown type $className';
       }
@@ -34,6 +36,10 @@
         final device = new AndroidDevice._(id);
         _deviceCache[id] = device;
         return device;
+      } else if (className == IOSDevice.className) {
+        final device = new IOSDevice._(id);
+        _deviceCache[id] = device;
+        return device;
       } else {
         throw 'Attempted to create a Device of unknown type $className';
       }
@@ -52,13 +58,120 @@
   bool isAppInstalled(ApplicationPackage app);
 }
 
+class IOSDevice extends _Device {
+  static const String className = 'IOSDevice';
+  static final String defaultDeviceID = 'default_ios_id';
+
+  static const String _macInstructions =
+      'To work with iOS devices, please install ideviceinstaller. '
+      'If you use homebrew, you can install it with '
+      '"\$ brew install ideviceinstaller".';
+  static const String _linuxInstructions =
+      'To work with iOS devices, please install ideviceinstaller. '
+      'On Ubuntu or Debian, you can install it with '
+      '"\$ apt-get install ideviceinstaller".';
+
+  String _installerPath;
+  String get installerPath => _installerPath;
+
+  String _listerPath;
+  String get listerPath => _listerPath;
+
+  String _informerPath;
+  String get informerPath => _informerPath;
+
+  String _name;
+  String get name => _name;
+
+  factory IOSDevice({String id, String name}) {
+    IOSDevice device = new _Device(className, id);
+    device._name = name;
+    return device;
+  }
+
+  IOSDevice._(String id) : super._(id) {
+    _installerPath = _checkForCommand('ideviceinstaller');
+    _listerPath = _checkForCommand('idevice_id');
+    _informerPath = _checkForCommand('ideviceinfo');
+  }
+
+  static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
+    List<IOSDevice> devices = [];
+    for (String id in _getAttachedDeviceIDs(mockIOS)) {
+      String name = _getDeviceName(id, mockIOS);
+      devices.add(new IOSDevice(id: id, name: name));
+    }
+    return devices;
+  }
+
+  static List<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
+    String listerPath =
+        (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
+    return runSync([listerPath, '-l']).trim().split('\n');
+  }
+
+  static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
+    String informerPath = (mockIOS != null)
+        ? mockIOS.informerPath
+        : _checkForCommand('ideviceinfo');
+    return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]);
+  }
+
+  static final Map<String, String> _commandMap = {};
+  static String _checkForCommand(String command,
+      [String macInstructions = _macInstructions,
+      String linuxInstructions = _linuxInstructions]) {
+    return _commandMap.putIfAbsent(command, () {
+      try {
+        command = runCheckedSync(['which', command]).trim();
+      } catch (e) {
+        if (Platform.isMacOS) {
+          _logging.severe(macInstructions);
+        } else if (Platform.isLinux) {
+          _logging.severe(linuxInstructions);
+        } else {
+          _logging.severe('$command is not available on your platform.');
+        }
+        exit(2);
+      }
+      return command;
+    });
+  }
+
+  @override
+  bool installApp(ApplicationPackage app) {
+    if (id == defaultDeviceID) {
+      runCheckedSync([installerPath, '-i', app.appPath]);
+    } else {
+      runCheckedSync([installerPath, '-u', id, '-i', app.appPath]);
+    }
+    return false;
+  }
+
+  @override
+  bool isConnected() {
+    List<String> ids = _getAttachedDeviceIDs();
+    for (String id in ids) {
+      if (id == this.id || this.id == defaultDeviceID) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @override
+  bool isAppInstalled(ApplicationPackage app) {
+    return false;
+  }
+}
+
 class AndroidDevice extends _Device {
   static const String _ADB_PATH = 'adb';
   static const String _observatoryPort = '8181';
   static const String _serverPort = '9888';
 
   static const String className = 'AndroidDevice';
-  static final String defaultDeviceID = 'default';
+  static final String defaultDeviceID = 'default_android_device';
 
   String productID;
   String modelID;
@@ -85,10 +198,8 @@
   /// we don't have to rely on the test setup having adb available to it.
   static List<AndroidDevice> getAttachedDevices([AndroidDevice mockAndroid]) {
     List<AndroidDevice> devices = [];
-    String adbPath = _getAdbPath();
-    if (mockAndroid != null) {
-      adbPath = mockAndroid.adbPath;
-    }
+    String adbPath =
+        (mockAndroid != null) ? mockAndroid.adbPath : _getAdbPath();
     List<String> output =
         runSync([adbPath, 'devices', '-l']).trim().split('\n');
     RegExp deviceInfo = new RegExp(
diff --git a/packages/flutter_tools/lib/src/install.dart b/packages/flutter_tools/lib/src/install.dart
index f7ee8fc..d29ded1 100644
--- a/packages/flutter_tools/lib/src/install.dart
+++ b/packages/flutter_tools/lib/src/install.dart
@@ -16,15 +16,12 @@
   final description = 'Install your Flutter app on attached devices.';
 
   AndroidDevice android = null;
+  IOSDevice ios;
 
-  InstallCommand([this.android]);
+  InstallCommand({this.android, this.ios});
 
   @override
   Future<int> run() async {
-    if (android == null) {
-      android = new AndroidDevice();
-    }
-
     if (install()) {
       return 0;
     } else {
@@ -33,15 +30,28 @@
   }
 
   bool install() {
+    if (android == null) {
+      android = new AndroidDevice();
+    }
+    if (ios == null) {
+      ios = new IOSDevice();
+    }
+
     bool installedSomewhere = false;
 
     Map<BuildPlatform, ApplicationPackage> packages =
         ApplicationPackageFactory.getAvailableApplicationPackages();
     ApplicationPackage androidApp = packages[BuildPlatform.android];
+    ApplicationPackage iosApp = packages[BuildPlatform.iOS];
+
     if (androidApp != null && android.isConnected()) {
       installedSomewhere = android.installApp(androidApp) || installedSomewhere;
     }
 
+    if (iosApp != null && ios.isConnected()) {
+      installedSomewhere = ios.installApp(iosApp) || installedSomewhere;
+    }
+
     return installedSomewhere;
   }
 }
diff --git a/packages/flutter_tools/lib/src/list.dart b/packages/flutter_tools/lib/src/list.dart
index 2270331..752fe35 100644
--- a/packages/flutter_tools/lib/src/list.dart
+++ b/packages/flutter_tools/lib/src/list.dart
@@ -16,8 +16,9 @@
   final name = 'list';
   final description = 'List all connected devices.';
   AndroidDevice android;
+  IOSDevice ios;
 
-  ListCommand([this.android]) {
+  ListCommand({this.android, this.ios}) {
     argParser.addFlag('details',
         abbr: 'd',
         negatable: false,
@@ -40,6 +41,18 @@
         print(device.id);
       }
     }
+
+    if (details) {
+      print('iOS Devices:');
+    }
+    for (IOSDevice device in IOSDevice.getAttachedDevices(ios)) {
+      if (details) {
+        print('${device.id}\t${device.name}');
+      } else {
+        print(device.id);
+      }
+    }
+
     return 0;
   }
 }
diff --git a/packages/flutter_tools/lib/src/start.dart b/packages/flutter_tools/lib/src/start.dart
index dcc381e..08af1a1 100644
--- a/packages/flutter_tools/lib/src/start.dart
+++ b/packages/flutter_tools/lib/src/start.dart
@@ -20,8 +20,9 @@
   final name = 'start';
   final description = 'Start your Flutter app on attached devices.';
   AndroidDevice android = null;
+  IOSDevice ios = null;
 
-  StartCommand([this.android]) {
+  StartCommand({this.android, this.ios}) {
     argParser.addFlag('poke',
         negatable: false,
         help: 'Restart the connection to the server (Android only).');
@@ -48,7 +49,7 @@
       stopper.stop();
 
       // Only install if the user did not specify a poke
-      InstallCommand installer = new InstallCommand(android);
+      InstallCommand installer = new InstallCommand(android: android, ios: ios);
       startedSomewhere = installer.install();
     }
 
diff --git a/packages/flutter_tools/test/init_test.dart b/packages/flutter_tools/test/init_test.dart
index dc16b7c..12846be 100644
--- a/packages/flutter_tools/test/init_test.dart
+++ b/packages/flutter_tools/test/init_test.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// This test can take a while due to network requests
+@Timeout(const Duration(seconds: 60))
 import 'dart:io';
 
 import 'package:args/command_runner.dart';
@@ -27,7 +29,7 @@
     test('init flutter-simple', () async {
       InitCommand command = new InitCommand();
       CommandRunner runner = new CommandRunner('test_flutter', '')
-          ..addCommand(command);
+        ..addCommand(command);
       await runner.run(['init', '--out', temp.path])
           .then((int code) => expect(code, equals(0)));
 
diff --git a/packages/flutter_tools/test/install_test.dart b/packages/flutter_tools/test/install_test.dart
index f9c1c35..ff9a91f 100644
--- a/packages/flutter_tools/test/install_test.dart
+++ b/packages/flutter_tools/test/install_test.dart
@@ -6,7 +6,6 @@
 
 import 'package:args/command_runner.dart';
 import 'package:mockito/mockito.dart';
-import 'package:sky_tools/src/application_package.dart';
 import 'package:sky_tools/src/install.dart';
 import 'package:test/test.dart';
 
@@ -17,19 +16,39 @@
 defineTests() {
   group('install', () {
     test('returns 0 when Android is connected and ready for an install', () {
-      ApplicationPackageFactory.srcPath = './';
-      ApplicationPackageFactory.setBuildPath(
-          BuildType.prebuilt, BuildPlatform.android, './');
+      applicationPackageSetup();
 
       MockAndroidDevice android = new MockAndroidDevice();
       when(android.isConnected()).thenReturn(true);
       when(android.installApp(any)).thenReturn(true);
-      InstallCommand command = new InstallCommand(android);
+
+      MockIOSDevice ios = new MockIOSDevice();
+      when(ios.isConnected()).thenReturn(false);
+      when(ios.installApp(any)).thenReturn(false);
+
+      InstallCommand command = new InstallCommand(android: android, ios: ios);
 
       CommandRunner runner = new CommandRunner('test_flutter', '')
-          ..addCommand(command);
-      runner.run(['install'])
-          .then((int code) => expect(code, equals(0)));
+        ..addCommand(command);
+      runner.run(['install']).then((int code) => expect(code, equals(0)));
+    });
+
+    test('returns 0 when iOS is connected and ready for an install', () {
+      applicationPackageSetup();
+
+      MockAndroidDevice android = new MockAndroidDevice();
+      when(android.isConnected()).thenReturn(false);
+      when(android.installApp(any)).thenReturn(false);
+
+      MockIOSDevice ios = new MockIOSDevice();
+      when(ios.isConnected()).thenReturn(true);
+      when(ios.installApp(any)).thenReturn(true);
+
+      InstallCommand command = new InstallCommand(android: android, ios: ios);
+
+      CommandRunner runner = new CommandRunner('test_flutter', '')
+        ..addCommand(command);
+      runner.run(['install']).then((int code) => expect(code, equals(0)));
     });
   });
 }
diff --git a/packages/flutter_tools/test/list_test.dart b/packages/flutter_tools/test/list_test.dart
index 19a9d26..2ff1cff 100644
--- a/packages/flutter_tools/test/list_test.dart
+++ b/packages/flutter_tools/test/list_test.dart
@@ -16,12 +16,21 @@
 defineTests() {
   group('list', () {
     test('returns 0 when called', () {
+      applicationPackageSetup();
+
       MockAndroidDevice android = new MockAndroidDevice();
       // Avoid relying on adb being installed on the test system.
       // Instead, cause the test to run the echo command.
       when(android.adbPath).thenReturn('echo');
 
-      ListCommand command = new ListCommand(android);
+      MockIOSDevice ios = new MockIOSDevice();
+      // Avoid relying on idevice* being installed on the test system.
+      // Instead, cause the test to run the echo command.
+      when(ios.informerPath).thenReturn('echo');
+      when(ios.installerPath).thenReturn('echo');
+      when(ios.listerPath).thenReturn('echo');
+
+      ListCommand command = new ListCommand(android: android, ios: ios);
       CommandRunner runner = new CommandRunner('test_flutter', '')
         ..addCommand(command);
       runner.run(['list']).then((int code) => expect(code, equals(0)));
diff --git a/packages/flutter_tools/test/listen_test.dart b/packages/flutter_tools/test/listen_test.dart
index 8951086..75e142d 100644
--- a/packages/flutter_tools/test/listen_test.dart
+++ b/packages/flutter_tools/test/listen_test.dart
@@ -6,7 +6,6 @@
 
 import 'package:args/command_runner.dart';
 import 'package:mockito/mockito.dart';
-import 'package:sky_tools/src/application_package.dart';
 import 'package:sky_tools/src/listen.dart';
 import 'package:test/test.dart';
 
@@ -17,9 +16,7 @@
 defineTests() {
   group('listen', () {
     test('returns 0 when no device is connected', () {
-      ApplicationPackageFactory.srcPath = './';
-      ApplicationPackageFactory.setBuildPath(
-          BuildType.prebuilt, BuildPlatform.android, './');
+      applicationPackageSetup();
 
       MockAndroidDevice android = new MockAndroidDevice();
       when(android.isConnected()).thenReturn(false);
diff --git a/packages/flutter_tools/test/logs_test.dart b/packages/flutter_tools/test/logs_test.dart
index b26523e..534c4fa 100644
--- a/packages/flutter_tools/test/logs_test.dart
+++ b/packages/flutter_tools/test/logs_test.dart
@@ -6,7 +6,6 @@
 
 import 'package:args/command_runner.dart';
 import 'package:mockito/mockito.dart';
-import 'package:sky_tools/src/application_package.dart';
 import 'package:sky_tools/src/logs.dart';
 import 'package:test/test.dart';
 
@@ -17,9 +16,7 @@
 defineTests() {
   group('logs', () {
     test('returns 0 when no device is connected', () {
-      ApplicationPackageFactory.srcPath = './';
-      ApplicationPackageFactory.setBuildPath(
-          BuildType.prebuilt, BuildPlatform.android, './');
+      applicationPackageSetup();
 
       MockAndroidDevice android = new MockAndroidDevice();
       when(android.isConnected()).thenReturn(false);
diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart
index b49f2c3..8903b16 100644
--- a/packages/flutter_tools/test/src/common.dart
+++ b/packages/flutter_tools/test/src/common.dart
@@ -3,9 +3,23 @@
 // found in the LICENSE file.
 
 import 'package:mockito/mockito.dart';
+import 'package:sky_tools/src/application_package.dart';
 import 'package:sky_tools/src/device.dart';
 
 class MockAndroidDevice extends Mock implements AndroidDevice {
   @override
   dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
 }
+
+class MockIOSDevice extends Mock implements IOSDevice {
+  @override
+  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+void applicationPackageSetup() {
+  ApplicationPackageFactory.srcPath = './';
+  ApplicationPackageFactory.setBuildPath(
+      BuildType.prebuilt, BuildPlatform.android, './');
+  ApplicationPackageFactory.setBuildPath(
+      BuildType.prebuilt, BuildPlatform.iOS, './');
+}
diff --git a/packages/flutter_tools/test/start_test.dart b/packages/flutter_tools/test/start_test.dart
index d3cbe5e..732f18b 100644
--- a/packages/flutter_tools/test/start_test.dart
+++ b/packages/flutter_tools/test/start_test.dart
@@ -6,7 +6,6 @@
 
 import 'package:args/command_runner.dart';
 import 'package:mockito/mockito.dart';
-import 'package:sky_tools/src/application_package.dart';
 import 'package:sky_tools/src/start.dart';
 import 'package:test/test.dart';
 
@@ -17,15 +16,18 @@
 defineTests() {
   group('start', () {
     test('returns 0 when Android is connected and ready to be started', () {
-      ApplicationPackageFactory.srcPath = './';
-      ApplicationPackageFactory.setBuildPath(
-          BuildType.prebuilt, BuildPlatform.android, './');
+      applicationPackageSetup();
 
       MockAndroidDevice android = new MockAndroidDevice();
       when(android.isConnected()).thenReturn(true);
       when(android.installApp(any)).thenReturn(true);
       when(android.stop(any)).thenReturn(true);
-      StartCommand command = new StartCommand(android);
+
+      MockIOSDevice ios = new MockIOSDevice();
+      when(ios.isConnected()).thenReturn(false);
+      when(ios.installApp(any)).thenReturn(false);
+
+      StartCommand command = new StartCommand(android: android, ios: ios);
 
       CommandRunner runner = new CommandRunner('test_flutter', '')
         ..addCommand(command);
diff --git a/packages/flutter_tools/test/stop_test.dart b/packages/flutter_tools/test/stop_test.dart
index ff53eda..af6ec19 100644
--- a/packages/flutter_tools/test/stop_test.dart
+++ b/packages/flutter_tools/test/stop_test.dart
@@ -6,7 +6,6 @@
 
 import 'package:args/command_runner.dart';
 import 'package:mockito/mockito.dart';
-import 'package:sky_tools/src/application_package.dart';
 import 'package:sky_tools/src/stop.dart';
 import 'package:test/test.dart';
 
@@ -17,9 +16,7 @@
 defineTests() {
   group('stop', () {
     test('returns 0 when Android is connected and ready to be stopped', () {
-      ApplicationPackageFactory.srcPath = './';
-      ApplicationPackageFactory.setBuildPath(
-          BuildType.prebuilt, BuildPlatform.android, './');
+      applicationPackageSetup();
 
       MockAndroidDevice android = new MockAndroidDevice();
       when(android.isConnected()).thenReturn(true);
diff --git a/packages/flutter_tools/test/trace_test.dart b/packages/flutter_tools/test/trace_test.dart
index 1305981..726c78f 100644
--- a/packages/flutter_tools/test/trace_test.dart
+++ b/packages/flutter_tools/test/trace_test.dart
@@ -6,7 +6,6 @@
 
 import 'package:args/command_runner.dart';
 import 'package:mockito/mockito.dart';
-import 'package:sky_tools/src/application_package.dart';
 import 'package:sky_tools/src/trace.dart';
 import 'package:test/test.dart';
 
@@ -17,9 +16,7 @@
 defineTests() {
   group('trace', () {
     test('returns 1 when no Android device is connected', () {
-      ApplicationPackageFactory.srcPath = './';
-      ApplicationPackageFactory.setBuildPath(
-          BuildType.prebuilt, BuildPlatform.android, './');
+      applicationPackageSetup();
 
       MockAndroidDevice android = new MockAndroidDevice();
       when(android.isConnected()).thenReturn(false);