[flutter_tools] Adds some support for '-d all' (#9585)

diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index 59eba86..ae9aa09 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -353,11 +353,13 @@
     final Directory cwd = fs.currentDirectory;
     fs.currentDirectory = fs.directory(projectDirectory);
 
+    final FlutterDevice flutterDevice = new FlutterDevice(device);
+
     ResidentRunner runner;
 
     if (enableHotReload) {
       runner = new HotRunner(
-        device,
+        <FlutterDevice>[flutterDevice],
         target: target,
         debuggingOptions: options,
         usesTerminalUI: false,
@@ -368,7 +370,7 @@
       );
     } else {
       runner = new ColdRunner(
-        device,
+        <FlutterDevice>[flutterDevice],
         target: target,
         debuggingOptions: options,
         usesTerminalUI: false,
@@ -448,7 +450,7 @@
     if (app == null)
       throw "app '$appId' not found";
 
-    final Isolate isolate = app.runner.currentView.uiIsolate;
+    final Isolate isolate = app.runner.flutterDevices.first.views.first.uiIsolate;
     final Map<String, dynamic> result = await isolate.invokeFlutterExtensionRpcRaw(methodName, params: params);
     if (result == null)
       return new OperationResult(1, 'method not available: $methodName');
diff --git a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
index 2d6ac06..4af62da 100644
--- a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
+++ b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart
@@ -16,6 +16,7 @@
 import '../flx.dart' as flx;
 import '../fuchsia/fuchsia_device.dart';
 import '../globals.dart';
+import '../resident_runner.dart';
 import '../run_hot.dart';
 import '../runner/flutter_command.dart';
 import '../vmservice.dart';
@@ -112,18 +113,21 @@
     final List<String> fullAddresses = targetPorts.map(
       (int p) => '$_address:$p'
     ).toList();
+    final List<Uri> observatoryUris = fullAddresses.map(
+      (String a) => Uri.parse('http://$a')
+    ).toList();
     final FuchsiaDevice device = new FuchsiaDevice(fullAddresses[0]);
+    final FlutterDevice flutterDevice = new FlutterDevice(device);
+    flutterDevice.observatoryUris = observatoryUris;
     final HotRunner hotRunner = new HotRunner(
-      device,
+      <FlutterDevice>[flutterDevice],
       debuggingOptions: new DebuggingOptions.enabled(getBuildMode()),
       target: _target,
       projectRootPath: _fuchsiaProjectPath,
       packagesFilePath: _dotPackagesPath
     );
-    final List<Uri> observatoryUris =
-      fullAddresses.map((String a) => Uri.parse('http://$a')).toList();
     printStatus('Connecting to $_binaryName');
-    await hotRunner.attach(observatoryUris, isolateFilter: isolateName);
+    await hotRunner.attach(viewFilter: isolateName);
   }
 
   // A cache of VMService connections.
@@ -151,12 +155,12 @@
   }
 
   // Find ports where there is a view isolate with the given name
-  Future<List<int>> _filterPorts(List<int> ports, String isolateFilter) async {
+  Future<List<int>> _filterPorts(List<int> ports, String viewFilter) async {
     final List<int> result = <int>[];
     for (FlutterView v in await _getViews(ports)) {
       final Uri addr = v.owner.vmService.httpAddress;
       printTrace('At $addr, found view: ${v.uiIsolate.name}');
-      if (v.uiIsolate.name.indexOf(isolateFilter) == 0)
+      if (v.uiIsolate.name.indexOf(viewFilter) == 0)
         result.add(addr.port);
     }
     return result;
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index aad16e6..ab90f87 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -150,17 +150,20 @@
     };
   }
 
-  Device device;
+  List<Device> devices;
 
   @override
   Future<String> get usagePath async {
     final String command = shouldUseHotMode() ? 'hotrun' : name;
 
-    if (device == null)
+    if (devices == null)
       return command;
 
     // Return 'run/ios'.
-    return '$command/${getNameForTargetPlatform(await device.targetPlatform)}';
+    if (devices.length > 1)
+      return '$command/all';
+    else
+      return '$command/${getNameForTargetPlatform(await devices[0].targetPlatform)}';
   }
 
   @override
@@ -199,9 +202,11 @@
   @override
   Future<Null> verifyThenRunCommand() async {
     commandValidator();
-    device = await findTargetDevice();
-    if (device == null)
+    devices = await findAllTargetDevices();
+    if (devices == null)
       throwToolExit(null);
+    if (deviceManager.hasSpecifiedAllDevices && runningWithPrebuiltApplication)
+      throwToolExit('Using -d all with --use-application-binary is not supported');
     return super.verifyThenRunCommand();
   }
 
@@ -221,7 +226,6 @@
 
   @override
   Future<Null> runCommand() async {
-
     Cache.releaseLockEarly();
 
     // Enable hot mode by default if `--no-hot` was not passed and we are in
@@ -229,17 +233,20 @@
     final bool hotMode = shouldUseHotMode();
 
     if (argResults['machine']) {
+      if (devices.length > 1)
+        throwToolExit('--machine does not support -d all.');
       final Daemon daemon = new Daemon(stdinCommandStream, stdoutCommandResponse,
           notifyingLogger: new NotifyingLogger(), logToStdout: true);
       AppInstance app;
       try {
         app = await daemon.appDomain.startApp(
-          device, fs.currentDirectory.path, targetFile, route,
+          devices.first, fs.currentDirectory.path, targetFile, route,
           _createDebuggingOptions(), hotMode,
           applicationBinary: argResults['use-application-binary'],
           projectRootPath: argResults['project-root'],
           packagesFilePath: argResults['packages'],
-          projectAssets: argResults['project-assets']);
+          projectAssets: argResults['project-assets']
+        );
       } catch (error) {
         throwToolExit(error.toString());
       }
@@ -249,12 +256,16 @@
       return null;
     }
 
-    if (await device.isLocalEmulator && !isEmulatorBuildMode(getBuildMode()))
-      throwToolExit('${toTitleCase(getModeName(getBuildMode()))} mode is not supported for emulators.');
+    for (Device device in devices) {
+      if (await device.isLocalEmulator && !isEmulatorBuildMode(getBuildMode()))
+        throwToolExit('${toTitleCase(getModeName(getBuildMode()))} mode is not supported for emulators.');
+    }
 
     if (hotMode) {
-      if (!device.supportsHotMode)
-        throwToolExit('Hot mode is not supported by this device. Run with --no-hot.');
+      for (Device device in devices) {
+        if (!device.supportsHotMode)
+          throwToolExit('Hot mode is not supported by ${device.name}. Run with --no-hot.');
+      }
     }
 
     final String pidFile = argResults['pid-file'];
@@ -262,11 +273,15 @@
       // Write our pid to the file.
       fs.file(pidFile).writeAsStringSync(pid.toString());
     }
-    ResidentRunner runner;
 
+    final List<FlutterDevice> flutterDevices = devices.map((Device device) {
+      return new FlutterDevice(device);
+    }).toList();
+
+    ResidentRunner runner;
     if (hotMode) {
       runner = new HotRunner(
-        device,
+        flutterDevices,
         target: targetFile,
         debuggingOptions: _createDebuggingOptions(),
         benchmarkMode: argResults['benchmark'],
@@ -279,7 +294,7 @@
       );
     } else {
       runner = new ColdRunner(
-        device,
+        flutterDevices,
         target: targetFile,
         debuggingOptions: _createDebuggingOptions(),
         traceStartup: traceStartup,
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index b4e0fbc..7ba2299 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -32,11 +32,26 @@
 
   final List<DeviceDiscovery> _deviceDiscoverers = <DeviceDiscovery>[];
 
-  /// A user-specified device ID.
-  String specifiedDeviceId;
+  String _specifiedDeviceId;
 
+  /// A user-specified device ID.
+  String get specifiedDeviceId {
+    if (_specifiedDeviceId == null || _specifiedDeviceId == 'all')
+      return null;
+    return _specifiedDeviceId;
+  }
+
+  set specifiedDeviceId(String id) {
+    _specifiedDeviceId = id;
+  }
+
+  /// True when the user has specified a single specific device.
   bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
 
+  /// True when the user has specified all devices by setting
+  /// specifiedDeviceId = 'all'.
+  bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all';
+
   Stream<Device> getDevicesById(String deviceId) async* {
     final Stream<Device> devices = getAllConnectedDevices();
     deviceId = deviceId.toLowerCase();
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index ae85d7e..e4f07d1 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -18,13 +18,320 @@
 import 'dart/dependencies.dart';
 import 'dart/package_map.dart';
 import 'dependency_checker.dart';
+import 'devfs.dart';
 import 'device.dart';
 import 'globals.dart';
+import 'run_cold.dart';
+import 'run_hot.dart';
 import 'vmservice.dart';
 
+class FlutterDevice {
+  final Device device;
+  List<Uri> observatoryUris;
+  List<VMService> vmServices;
+  DevFS devFS;
+  ApplicationPackage package;
+
+  String _viewFilter;
+  StreamSubscription<String> _loggingSubscription;
+
+  FlutterDevice(this.device);
+
+  String get viewFilter => _viewFilter;
+  set viewFilter(String filter) {
+    _viewFilter = filter;
+    _viewsCache = null;
+  }
+
+  void connect() {
+    if (vmServices != null)
+      return;
+    vmServices = new List<VMService>(observatoryUris.length);
+    for (int i = 0; i < observatoryUris.length; i++) {
+      vmServices[i] = VMService.connect(observatoryUris[i]);
+      printTrace('Connected to service protocol: ${observatoryUris[i]}');
+    }
+  }
+
+  Future<Null> refreshViews() async {
+    if ((vmServices == null) || vmServices.isEmpty)
+      return;
+    for (VMService service in vmServices)
+      await service.vm.refreshViews();
+    _viewsCache = null;
+  }
+
+  List<FlutterView> _viewsCache;
+  List<FlutterView> get views {
+    if (_viewsCache == null) {
+      if ((vmServices == null) || vmServices.isEmpty)
+        return null;
+      final List<FlutterView> result = <FlutterView>[];
+      if (_viewFilter == null) {
+        for (VMService service in vmServices) {
+          if (!service.isClosed)
+            result.addAll(service.vm.views.toList());
+        }
+      } else {
+        for (VMService service in vmServices) {
+          if (!service.isClosed)
+            result.addAll(service.vm.allViewsWithName(_viewFilter));
+        }
+      }
+      _viewsCache = result;
+    }
+    return _viewsCache;
+  }
+
+  Future<Null> getVMs() async {
+    for (VMService service in vmServices)
+      await service.getVM();
+  }
+
+  Future<Null> waitForViews() async {
+    // Refresh the view list, and wait a bit for the list to populate.
+    for (VMService service in vmServices)
+      await service.waitForViews();
+  }
+
+  Future<Null> stopApps() async {
+    final List<FlutterView> flutterViews = views;
+    if (flutterViews == null || flutterViews.isEmpty)
+      return;
+    for (FlutterView view in flutterViews) {
+      if (view != null && view.uiIsolate != null)
+        view.uiIsolate.flutterExit();
+    }
+    await new Future<Null>.delayed(const Duration(milliseconds: 100));
+  }
+
+  Future<Uri> setupDevFS(String fsName,
+                         Directory rootDirectory, {
+                         String packagesFilePath
+                       }) {
+    // One devFS per device. Shared by all running instances.
+    devFS = new DevFS(
+      vmServices[0],
+      fsName,
+      rootDirectory,
+      packagesFilePath: packagesFilePath
+    );
+    return devFS.create();
+  }
+
+  List<Future<Map<String, dynamic>>> reloadSources(
+    String entryPath, {
+    bool pause: false
+  }) {
+    final Uri deviceEntryUri = devFS.baseUri.resolveUri(fs.path.toUri(entryPath));
+    final Uri devicePackagesUri = devFS.baseUri.resolve('.packages');
+    final List<Future<Map<String, dynamic>>> reports = <Future<Map<String, dynamic>>>[];
+    for (FlutterView view in views) {
+      final Future<Map<String, dynamic>> report = view.uiIsolate.reloadSources(
+        pause: pause,
+        rootLibUri: deviceEntryUri,
+        packagesUri: devicePackagesUri
+      );
+      reports.add(report);
+    }
+    return reports;
+  }
+
+  Future<Null> debugDumpApp() async {
+    for (FlutterView view in views)
+      await view.uiIsolate.flutterDebugDumpApp();
+  }
+
+  Future<Null> debugDumpRenderTree() async {
+    for (FlutterView view in views)
+      await view.uiIsolate.flutterDebugDumpRenderTree();
+  }
+
+  Future<Null> toggleDebugPaintSizeEnabled() async {
+    for (FlutterView view in views)
+      await view.uiIsolate.flutterToggleDebugPaintSizeEnabled();
+  }
+
+  Future<String> togglePlatform({String from}) async {
+    String to;
+    switch (from) {
+      case 'iOS':
+        to = 'android';
+        break;
+      case 'android':
+      default:
+        to = 'iOS';
+        break;
+    }
+    for (FlutterView view in views) {
+      await view.uiIsolate.flutterPlatformOverride(to);
+    }
+    return to;
+  }
+
+  void startEchoingDeviceLog() {
+    if (_loggingSubscription != null)
+      return;
+    _loggingSubscription = device.getLogReader(app: package).logLines.listen((String line) {
+      if (!line.contains('Observatory listening on http') &&
+          !line.contains('Diagnostic server listening on http'))
+        printStatus(line);
+    });
+  }
+
+  Future<Null> stopEchoingDeviceLog() async {
+    if (_loggingSubscription == null)
+      return;
+    await _loggingSubscription.cancel();
+    _loggingSubscription = null;
+  }
+
+  void initLogReader() {
+    device.getLogReader(app: package).appPid = vmServices.first.vm.pid;
+  }
+
+  Future<int> runHot({
+    HotRunner hotRunner,
+    String route,
+    bool shouldBuild,
+  }) async {
+    final bool prebuiltMode = hotRunner.applicationBinary != null;
+    final String modeName = getModeName(hotRunner.debuggingOptions.buildMode);
+    printStatus('Launching ${getDisplayPath(hotRunner.mainPath)} on ${device.name} in $modeName mode...');
+
+    final TargetPlatform targetPlatform = await device.targetPlatform;
+    package = getApplicationPackageForPlatform(
+      targetPlatform,
+      applicationBinary: hotRunner.applicationBinary
+    );
+
+    if (package == null) {
+      String message = 'No application found for $targetPlatform.';
+      final String hint = getMissingPackageHintForPlatform(targetPlatform);
+      if (hint != null)
+        message += '\n$hint';
+      printError(message);
+      return 1;
+    }
+
+    final Map<String, dynamic> platformArgs = <String, dynamic>{};
+
+    startEchoingDeviceLog();
+
+    // Start the application.
+    final bool hasDirtyDependencies = hotRunner.hasDirtyDependencies(this);
+    final Future<LaunchResult> futureResult = device.startApp(
+      package,
+      hotRunner.debuggingOptions.buildMode,
+      mainPath: hotRunner.mainPath,
+      debuggingOptions: hotRunner.debuggingOptions,
+      platformArgs: platformArgs,
+      route: route,
+      prebuiltApplication: prebuiltMode,
+      kernelPath: hotRunner.kernelFilePath,
+      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies
+    );
+
+    final LaunchResult result = await futureResult;
+
+    if (!result.started) {
+      printError('Error launching application on ${device.name}.');
+      await stopEchoingDeviceLog();
+      return 2;
+    }
+    observatoryUris = <Uri>[result.observatoryUri];
+    return 0;
+  }
+
+
+  Future<int> runCold({
+    ColdRunner coldRunner,
+    String route,
+    bool shouldBuild: true,
+  }) async {
+    final TargetPlatform targetPlatform = await device.targetPlatform;
+    package = getApplicationPackageForPlatform(
+      targetPlatform,
+      applicationBinary: coldRunner.applicationBinary
+    );
+
+    final String modeName = getModeName(coldRunner.debuggingOptions.buildMode);
+    final bool prebuiltMode = coldRunner.applicationBinary != null;
+    if (coldRunner.mainPath == null) {
+      assert(prebuiltMode);
+      printStatus('Launching ${package.displayName} on ${device.name} in $modeName mode...');
+    } else {
+      printStatus('Launching ${getDisplayPath(coldRunner.mainPath)} on ${device.name} in $modeName mode...');
+    }
+
+    if (package == null) {
+      String message = 'No application found for $targetPlatform.';
+      final String hint = getMissingPackageHintForPlatform(targetPlatform);
+      if (hint != null)
+        message += '\n$hint';
+      printError(message);
+      return 1;
+    }
+
+    Map<String, dynamic> platformArgs;
+    if (coldRunner.traceStartup != null)
+      platformArgs = <String, dynamic>{ 'trace-startup': coldRunner.traceStartup };
+
+    startEchoingDeviceLog();
+
+    final bool hasDirtyDependencies = coldRunner.hasDirtyDependencies(this);
+    final LaunchResult result = await device.startApp(
+      package,
+      coldRunner.debuggingOptions.buildMode,
+      mainPath: coldRunner.mainPath,
+      debuggingOptions: coldRunner.debuggingOptions,
+      platformArgs: platformArgs,
+      route: route,
+      prebuiltApplication: prebuiltMode,
+      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies
+    );
+
+    if (!result.started) {
+      printError('Error running application on ${device.name}.');
+      await stopEchoingDeviceLog();
+      return 2;
+    }
+    if (result.hasObservatory)
+      observatoryUris = <Uri>[result.observatoryUri];
+    return 0;
+  }
+
+  Future<bool> updateDevFS({
+    DevFSProgressReporter progressReporter,
+    AssetBundle bundle,
+    bool bundleDirty: false,
+    Set<String> fileFilter
+  }) async {
+    final Status devFSStatus = logger.startProgress(
+      'Syncing files to device ${device.name}...',
+      expectSlowOperation: true
+    );
+    int bytes = 0;
+    try {
+      bytes = await devFS.update(
+        progressReporter: progressReporter,
+        bundle: bundle,
+        bundleDirty: bundleDirty,
+        fileFilter: fileFilter
+      );
+    } on DevFSException {
+      devFSStatus.cancel();
+      return false;
+    }
+    devFSStatus.stop();
+    printTrace('Synced ${getSizeAsMB(bytes)}.');
+    return true;
+  }
+}
+
 // Shared code between different resident application runners.
 abstract class ResidentRunner {
-  ResidentRunner(this.device, {
+  ResidentRunner(this.flutterDevices, {
     this.target,
     this.debuggingOptions,
     this.usesTerminalUI: true,
@@ -43,7 +350,7 @@
       _assetBundle = new AssetBundle();
   }
 
-  final Device device;
+  final List<FlutterDevice> flutterDevices;
   final String target;
   final DebuggingOptions debuggingOptions;
   final bool usesTerminalUI;
@@ -58,16 +365,12 @@
   String get mainPath => _mainPath;
   AssetBundle _assetBundle;
   AssetBundle get assetBundle => _assetBundle;
-  ApplicationPackage package;
 
   bool get isRunningDebug => debuggingOptions.buildMode == BuildMode.debug;
   bool get isRunningProfile => debuggingOptions.buildMode == BuildMode.profile;
   bool get isRunningRelease => debuggingOptions.buildMode == BuildMode.release;
   bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;
 
-  List<VMService> vmServices;
-  StreamSubscription<String> _loggingSubscription;
-
   /// Start the app and keep the process running during its lifetime.
   Future<int> run({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
@@ -78,25 +381,6 @@
 
   bool get supportsRestart => false;
 
-  String isolateFilter;
-
-  List<FlutterView> _currentViewsCache;
-  List<FlutterView> get currentViews {
-    if (_currentViewsCache == null) {
-      if ((vmServices == null) || vmServices.isEmpty)
-        return null;
-      if (isolateFilter == null)
-        return vmServices[0].vm.views.toList();
-      final List<FlutterView> result = <FlutterView>[];
-      for (VMService service in vmServices)
-        result.addAll(service.vm.allViewsWithName(isolateFilter));
-      _currentViewsCache = result;
-    }
-    return _currentViewsCache;
-  }
-
-  FlutterView get currentView => (currentViews != null) ? currentViews[0] : null;
-
   Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) {
     throw 'unsupported';
   }
@@ -115,47 +399,49 @@
   }
 
   Future<Null> refreshViews() async {
-    if ((vmServices == null) || vmServices.isEmpty)
-      return;
-    for (VMService service in vmServices)
-      await service.vm.refreshViews();
-    _currentViewsCache = null;
+    for (FlutterDevice device in flutterDevices)
+      await device.refreshViews();
   }
 
   Future<Null> _debugDumpApp() async {
     await refreshViews();
-    await currentView.uiIsolate.flutterDebugDumpApp();
+    for (FlutterDevice device in flutterDevices)
+      await device.debugDumpApp();
   }
 
   Future<Null> _debugDumpRenderTree() async {
     await refreshViews();
-    await currentView.uiIsolate.flutterDebugDumpRenderTree();
+    for (FlutterDevice device in flutterDevices)
+      await device.debugDumpRenderTree();
   }
 
   Future<Null> _debugToggleDebugPaintSizeEnabled() async {
     await refreshViews();
-    await currentView.uiIsolate.flutterToggleDebugPaintSizeEnabled();
+    for (FlutterDevice device in flutterDevices)
+      await device.toggleDebugPaintSizeEnabled();
   }
 
-  Future<Null> _screenshot() async {
-    final Status status = logger.startProgress('Taking screenshot...');
+  Future<Null> _screenshot(FlutterDevice device) async {
+    final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...');
     final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
     try {
       if (supportsServiceProtocol && isRunningDebug) {
-        await refreshViews();
+        await device.refreshViews();
         try {
-          await currentView.uiIsolate.flutterDebugAllowBanner(false);
+          for (FlutterView view in device.views)
+            await view.uiIsolate.flutterDebugAllowBanner(false);
         } catch (error) {
           status.stop();
           printError(error);
         }
       }
       try {
-        await device.takeScreenshot(outputFile);
+        await device.device.takeScreenshot(outputFile);
       } finally {
         if (supportsServiceProtocol && isRunningDebug) {
           try {
-            await currentView.uiIsolate.flutterDebugAllowBanner(true);
+            for (FlutterView view in device.views)
+              await view.uiIsolate.flutterDebugAllowBanner(true);
           } catch (error) {
             status.stop();
             printError(error);
@@ -171,15 +457,13 @@
     }
   }
 
-  Future<String> _debugRotatePlatform() async {
+  Future<Null> _debugTogglePlatform() async {
     await refreshViews();
-    switch (await currentView.uiIsolate.flutterPlatformOverride()) {
-      case 'iOS':
-        return await currentView.uiIsolate.flutterPlatformOverride('android');
-      case 'android':
-      default:
-        return await currentView.uiIsolate.flutterPlatformOverride('iOS');
-    }
+    final String from = await flutterDevices[0].views[0].uiIsolate.flutterPlatformOverride();
+    String to;
+    for (FlutterDevice device in flutterDevices)
+      to = await device.togglePlatform(from: from);
+    printStatus('Switched operating system to $to');
   }
 
   void registerSignalHandlers() {
@@ -215,53 +499,38 @@
     }
   }
 
-  Future<Null> startEchoingDeviceLog(ApplicationPackage app) async {
-    if (_loggingSubscription != null)
-      return;
-    _loggingSubscription = device.getLogReader(app: app).logLines.listen((String line) {
-      if (!line.contains('Observatory listening on http') &&
-          !line.contains('Diagnostic server listening on http'))
-        printStatus(line);
-    });
-  }
-
   Future<Null> stopEchoingDeviceLog() async {
-    if (_loggingSubscription != null) {
-      await _loggingSubscription.cancel();
-    }
-    _loggingSubscription = null;
+    for (FlutterDevice device in flutterDevices)
+      device.stopEchoingDeviceLog();
   }
 
-  Future<Null> connectToServiceProtocol(
-    List<Uri> uris, {
-    String isolateFilter
-  }) async {
+  Future<Null> connectToServiceProtocol({String viewFilter}) async {
     if (!debuggingOptions.debuggingEnabled) {
       return new Future<Null>.error('Error the service protocol is not enabled.');
     }
-    final List<VMService> services = new List<VMService>(uris.length);
-    for (int i = 0; i < uris.length; i++) {
-      services[i] = VMService.connect(uris[i]);
-      printTrace('Connected to service protocol: ${uris[i]}');
+
+    bool viewFound = false;
+    for (FlutterDevice device in flutterDevices) {
+      device.viewFilter = viewFilter;
+      device.connect();
+      await device.getVMs();
+      await device.waitForViews();
+      if (device.views == null)
+        printStatus('No Flutter views available on ${device.device.name}');
+      else
+        viewFound = true;
     }
-    vmServices = services;
-    for (VMService service in services)
-      await service.getVM();
-
-    this.isolateFilter = isolateFilter;
-
-    // Refresh the view list, and wait a bit for the list to populate.
-    for (VMService service in services)
-      await service.waitForViews();
-    if (currentView == null)
+    if (!viewFound)
       throwToolExit('No Flutter view is available');
 
     // Listen for service protocol connection to close.
-    for (VMService service in services) {
-      service.done.then<Null>(
-        _serviceProtocolDone,
-        onError: _serviceProtocolError
-      ).whenComplete(_serviceDisconnected);
+    for (FlutterDevice device in flutterDevices) {
+      for (VMService service in device.vmServices) {
+        service.done.then<Null>(
+          _serviceProtocolDone,
+          onError: _serviceProtocolError
+        ).whenComplete(_serviceDisconnected);
+      }
     }
   }
 
@@ -301,14 +570,14 @@
         return true;
       }
     } else if (lower == 's') {
-      if (device.supportsScreenshot) {
-        await _screenshot();
-        return true;
+      for (FlutterDevice device in flutterDevices) {
+        if (device.device.supportsScreenshot)
+          await _screenshot(device);
       }
+      return true;
     } else if (lower == 'o') {
       if (supportsServiceProtocol && isRunningDebug) {
-        final String platform = await _debugRotatePlatform();
-        print('Switched operating system to: $platform');
+        await _debugTogglePlatform();
         return true;
       }
     } else if (lower == 'q') {
@@ -381,12 +650,12 @@
     return exitCode;
   }
 
-  bool hasDirtyDependencies() {
+  bool hasDirtyDependencies(FlutterDevice device) {
     final DartDependencySetBuilder dartDependencySetBuilder =
         new DartDependencySetBuilder(mainPath, packagesFilePath);
     final DependencyChecker dependencyChecker =
         new DependencyChecker(dartDependencySetBuilder, assetBundle);
-    final String path = package.packagePath;
+    final String path = device.package.packagePath;
     if (path == null) {
       return true;
     }
@@ -404,16 +673,8 @@
   Future<Null> preStop() async { }
 
   Future<Null> stopApp() async {
-    if (vmServices != null &&
-        vmServices.isNotEmpty &&
-        !vmServices[0].isClosed) {
-      // TODO(zra): iterate over all the views.
-      if ((currentView != null) && (currentView.uiIsolate != null)) {
-        // TODO(johnmccutchan): Wait for the exit command to complete.
-        currentView.uiIsolate.flutterExit();
-        await new Future<Null>.delayed(const Duration(milliseconds: 100));
-      }
-    }
+    for (FlutterDevice device in flutterDevices)
+      await device.stopApps();
     appFinished();
   }
 
@@ -429,7 +690,7 @@
         printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".');
       }
     }
-    if (device.supportsScreenshot)
+    if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot))
       printStatus('To save a screenshot to flutter.png, press "s".');
   }
 
diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart
index 0a7b671..e837799 100644
--- a/packages/flutter_tools/lib/src/run_cold.dart
+++ b/packages/flutter_tools/lib/src/run_cold.dart
@@ -6,10 +6,7 @@
 
 import 'package:meta/meta.dart';
 
-import 'application_package.dart';
 import 'base/file_system.dart';
-import 'base/utils.dart';
-import 'build_info.dart';
 import 'commands/trace.dart';
 import 'device.dart';
 import 'globals.dart';
@@ -17,25 +14,22 @@
 
 class ColdRunner extends ResidentRunner {
   ColdRunner(
-    Device device, {
+    List<FlutterDevice> devices, {
     String target,
     DebuggingOptions debuggingOptions,
     bool usesTerminalUI: true,
     this.traceStartup: false,
     this.applicationBinary,
     bool stayResident: true,
-  }) : super(device,
+  }) : super(devices,
              target: target,
              debuggingOptions: debuggingOptions,
              usesTerminalUI: usesTerminalUI,
              stayResident: stayResident);
 
-  LaunchResult _result;
   final bool traceStartup;
   final String applicationBinary;
 
-  bool get prebuiltMode => applicationBinary != null;
-
   @override
   Future<int> run({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
@@ -43,6 +37,7 @@
     String route,
     bool shouldBuild: true
   }) async {
+    final bool prebuiltMode = applicationBinary != null;
     if (!prebuiltMode) {
       if (!fs.isFileSync(mainPath)) {
         String message = 'Tried to run $mainPath, but that file does not exist.';
@@ -53,79 +48,49 @@
       }
     }
 
-    final String modeName = getModeName(debuggingOptions.buildMode);
-    if (mainPath == null) {
-      assert(prebuiltMode);
-      printStatus('Launching ${package.displayName} on ${device.name} in $modeName mode...');
-    } else {
-      printStatus('Launching ${getDisplayPath(mainPath)} on ${device.name} in $modeName mode...');
+    for (FlutterDevice device in flutterDevices) {
+      final int result = await device.runCold(
+        coldRunner: this,
+        route: route,
+        shouldBuild: shouldBuild,
+      );
+      if (result != 0)
+        return result;
     }
 
-    final TargetPlatform targetPlatform = await device.targetPlatform;
-    package = getApplicationPackageForPlatform(targetPlatform, applicationBinary: applicationBinary);
-
-    if (package == null) {
-      String message = 'No application found for $targetPlatform.';
-      final String hint = getMissingPackageHintForPlatform(targetPlatform);
-      if (hint != null)
-        message += '\n$hint';
-      printError(message);
-      return 1;
-    }
-
-    final Stopwatch startTime = new Stopwatch()..start();
-
-    Map<String, dynamic> platformArgs;
-    if (traceStartup != null)
-      platformArgs = <String, dynamic>{ 'trace-startup': traceStartup };
-
-    await startEchoingDeviceLog(package);
-
-    _result = await device.startApp(
-      package,
-      debuggingOptions.buildMode,
-      mainPath: mainPath,
-      debuggingOptions: debuggingOptions,
-      platformArgs: platformArgs,
-      route: route,
-      prebuiltApplication: prebuiltMode,
-      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies()
-    );
-
-    if (!_result.started) {
-      printError('Error running application on ${device.name}.');
-      await stopEchoingDeviceLog();
-      return 2;
-    }
-
-    startTime.stop();
-
     // Connect to observatory.
     if (debuggingOptions.debuggingEnabled)
-      await connectToServiceProtocol(<Uri>[_result.observatoryUri]);
+      await connectToServiceProtocol();
 
-    if (_result.hasObservatory) {
+    if (flutterDevices.first.observatoryUris != null) {
+      // For now, only support one debugger connection.
       connectionInfoCompleter?.complete(new DebugConnectionInfo(
-        httpUri: _result.observatoryUri,
-        wsUri: vmServices[0].wsAddress,
+        httpUri: flutterDevices.first.observatoryUris.first,
+        wsUri: flutterDevices.first.vmServices.first.wsAddress,
       ));
     }
 
     printTrace('Application running.');
 
-    if (vmServices != null && vmServices.isNotEmpty) {
-      device.getLogReader(app: package).appPid = vmServices[0].vm.pid;
-      await refreshViews();
-      printTrace('Connected to $currentView.');
+    for (FlutterDevice device in flutterDevices) {
+      if (device.vmServices == null)
+        continue;
+      device.initLogReader();
+      await device.refreshViews();
+      printTrace('Connected to ${device.device.name}');
     }
 
-    if (vmServices != null && vmServices.isNotEmpty && traceStartup) {
-      printStatus('Downloading startup trace info...');
-      try {
-        await downloadStartupTrace(vmServices[0]);
-      } catch(error) {
-        printError(error);
-        return 2;
+    if (traceStartup) {
+      // Only trace startup for the first device.
+      final FlutterDevice device = flutterDevices.first;
+      if (device.vmServices != null && device.vmServices.isNotEmpty) {
+        printStatus('Downloading startup trace info for ${device.device.name}');
+        try {
+          await downloadStartupTrace(device.vmServices.first);
+        } catch (error) {
+          printError(error);
+          return 2;
+        }
       }
       appFinished();
     } else if (stayResident) {
@@ -158,8 +123,13 @@
   @override
   void printHelp({ @required bool details }) {
     bool haveDetails = false;
-    if (_result.hasObservatory)
-      printStatus('The Observatory debugger and profiler is available at: ${_result.observatoryUri}');
+    for (FlutterDevice device in flutterDevices) {
+      final String dname = device.device.name;
+      if (device.observatoryUris != null) {
+        for (Uri uri in device.observatoryUris)
+          printStatus('An Observatory debugger and profiler on $dname is available at $uri');
+      }
+    }
     if (supportsServiceProtocol) {
       haveDetails = true;
       if (details)
@@ -174,8 +144,10 @@
 
   @override
   Future<Null> preStop() async {
-    // If we're running in release mode, stop the app using the device logic.
-    if (vmServices == null || vmServices.isEmpty)
-      await device.stopApp(package);
+    for (FlutterDevice device in flutterDevices) {
+      // If we're running in release mode, stop the app using the device logic.
+      if (device.vmServices == null || device.vmServices.isEmpty)
+        await device.device.stopApp(device.package);
+    }
   }
 }
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 3b05d16..0d9eae6 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -6,7 +6,6 @@
 
 import 'package:meta/meta.dart';
 
-import 'application_package.dart';
 import 'base/context.dart';
 import 'base/file_system.dart';
 import 'base/logger.dart';
@@ -33,7 +32,7 @@
 
 class HotRunner extends ResidentRunner {
   HotRunner(
-    Device device, {
+    List<FlutterDevice> devices, {
     String target,
     DebuggingOptions debuggingOptions,
     bool usesTerminalUI: true,
@@ -44,7 +43,7 @@
     String packagesFilePath,
     String projectAssets,
     bool stayResident: true,
-  }) : super(device,
+  }) : super(devices,
              target: target,
              debuggingOptions: debuggingOptions,
              usesTerminalUI: usesTerminalUI,
@@ -54,9 +53,7 @@
              stayResident: stayResident);
 
   final String applicationBinary;
-  bool get prebuiltMode => applicationBinary != null;
   Set<String> _dartDependencies;
-  Uri _observatoryUri;
 
   final bool benchmarkMode;
   final Map<String, int> benchmarkData = <String, int>{};
@@ -87,30 +84,30 @@
     return true;
   }
 
-  Future<int> attach(List<Uri> observatoryUris, {
+  Future<int> attach({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
     Completer<Null> appStartedCompleter,
-    String isolateFilter,
+    String viewFilter,
   }) async {
-    _observatoryUri = observatoryUris[0];
     try {
-      await connectToServiceProtocol(
-          observatoryUris, isolateFilter: isolateFilter);
+      await connectToServiceProtocol(viewFilter: viewFilter);
     } catch (error) {
       printError('Error connecting to the service protocol: $error');
       return 2;
     }
 
-    device.getLogReader(app: package).appPid = vmServices[0].vm.pid;
+    for (FlutterDevice device in flutterDevices)
+      device.initLogReader();
 
     try {
-      final Uri baseUri = await _initDevFS();
+      final List<Uri> baseUris = await _initDevFS();
       if (connectionInfoCompleter != null) {
+        // Only handle one debugger connection.
         connectionInfoCompleter.complete(
           new DebugConnectionInfo(
-            httpUri: _observatoryUri,
-            wsUri: vmServices[0].wsAddress,
-            baseUri: baseUri.toString()
+            httpUri: flutterDevices.first.observatoryUris.first,
+            wsUri: flutterDevices.first.vmServices.first.wsAddress,
+            baseUri: baseUris.first.toString()
           )
         );
       }
@@ -124,8 +121,10 @@
     }
 
     await refreshViews();
-    for (FlutterView view in currentViews)
-      printTrace('Connected to $view.');
+    for (FlutterDevice device in flutterDevices) {
+      for (FlutterView view in device.views)
+        printTrace('Connected to $view.');
+    }
 
     if (stayResident) {
       setupTerminal();
@@ -174,55 +173,27 @@
       return 1;
     }
 
-    final String modeName = getModeName(debuggingOptions.buildMode);
-    printStatus('Launching ${getDisplayPath(mainPath)} on ${device.name} in $modeName mode...');
-
-    final TargetPlatform targetPlatform = await device.targetPlatform;
-    package = getApplicationPackageForPlatform(targetPlatform, applicationBinary: applicationBinary);
-
-    if (package == null) {
-      String message = 'No application found for $targetPlatform.';
-      final String hint = getMissingPackageHintForPlatform(targetPlatform);
-      if (hint != null)
-        message += '\n$hint';
-      printError(message);
-      return 1;
-    }
-
     // Determine the Dart dependencies eagerly.
     if (!_refreshDartDependencies()) {
       // Some kind of source level error or missing file in the Dart code.
       return 1;
     }
 
-    final Map<String, dynamic> platformArgs = <String, dynamic>{};
-
-    await startEchoingDeviceLog(package);
-
-    // Start the application.
-    final Future<LaunchResult> futureResult = device.startApp(
-      package,
-      debuggingOptions.buildMode,
-      mainPath: mainPath,
-      debuggingOptions: debuggingOptions,
-      platformArgs: platformArgs,
-      route: route,
-      prebuiltApplication: prebuiltMode,
-      kernelPath: kernelFilePath,
-      applicationNeedsRebuild: shouldBuild || hasDirtyDependencies()
-    );
-
-    final LaunchResult result = await futureResult;
-
-    if (!result.started) {
-      printError('Error launching application on ${device.name}.');
-      await stopEchoingDeviceLog();
-      return 2;
+    for (FlutterDevice device in flutterDevices) {
+      final int result = await device.runHot(
+        hotRunner: this,
+        route: route,
+        shouldBuild: shouldBuild,
+      );
+      if (result != 0) {
+        return result;
+      }
     }
 
-    return attach(<Uri>[result.observatoryUri],
-                  connectionInfoCompleter: connectionInfoCompleter,
-                  appStartedCompleter: appStartedCompleter);
+    return attach(
+      connectionInfoCompleter: connectionInfoCompleter,
+      appStartedCompleter: appStartedCompleter
+    );
   }
 
   @override
@@ -238,15 +209,18 @@
     }
   }
 
-  DevFS _devFS;
-
-  Future<Uri> _initDevFS() {
+  Future<List<Uri>> _initDevFS() async {
     final String fsName = fs.path.basename(projectRootPath);
-    _devFS = new DevFS(vmServices[0],
-                       fsName,
-                       fs.directory(projectRootPath),
-                       packagesFilePath: packagesFilePath);
-    return _devFS.create();
+    final List<Uri> devFSUris = <Uri>[];
+    for (FlutterDevice device in flutterDevices) {
+      final Uri uri = await device.setupDevFS(
+        fsName,
+        fs.directory(projectRootPath),
+        packagesFilePath: packagesFilePath
+      );
+      devFSUris.add(uri);
+    }
+    return devFSUris;
   }
 
   Future<bool> _updateDevFS({ DevFSProgressReporter progressReporter }) async {
@@ -261,67 +235,72 @@
       if (result != 0)
         return false;
     }
-    final Status devFSStatus = logger.startProgress('Syncing files to device...',
-        expectSlowOperation: true);
-    int bytes = 0;
-    try {
-      bytes = await _devFS.update(progressReporter: progressReporter,
-          bundle: assetBundle,
-          bundleDirty: rebuildBundle,
-          fileFilter: _dartDependencies);
-    } on DevFSException {
-      devFSStatus.cancel();
-      return false;
+
+    for (FlutterDevice device in flutterDevices) {
+      final bool result = await device.updateDevFS(
+        progressReporter: progressReporter,
+        bundle: assetBundle,
+        bundleDirty: rebuildBundle,
+        fileFilter: _dartDependencies,
+      );
+      if (!result)
+        return false;
     }
-    devFSStatus.stop();
+
     if (!hotRunnerConfig.stableDartDependencies) {
       // Clear the set after the sync so they are recomputed next time.
       _dartDependencies = null;
     }
-    printTrace('Synced ${getSizeAsMB(bytes)}.');
     return true;
   }
 
   Future<Null> _evictDirtyAssets() async {
-    if (_devFS.assetPathsToEvict.isEmpty)
-      return;
-    if (currentView.uiIsolate == null)
-      throw 'Application isolate not found';
-    for (String assetPath in _devFS.assetPathsToEvict) {
-      await currentView.uiIsolate.flutterEvictAsset(assetPath);
+    for (FlutterDevice device in flutterDevices) {
+      if (device.devFS.assetPathsToEvict.isEmpty)
+        return;
+      if (device.views.first.uiIsolate == null)
+        throw 'Application isolate not found';
+      for (String assetPath in device.devFS.assetPathsToEvict)
+        await device.views.first.uiIsolate.flutterEvictAsset(assetPath);
+      device.devFS.assetPathsToEvict.clear();
     }
-    _devFS.assetPathsToEvict.clear();
   }
 
   Future<Null> _cleanupDevFS() async {
-    if (_devFS != null) {
-      // Cleanup the devFS; don't wait indefinitely, and ignore any errors.
-      await _devFS.destroy()
-        .timeout(const Duration(milliseconds: 250))
-        .catchError((dynamic error) {
-          printTrace('$error');
-        });
+    for (FlutterDevice device in flutterDevices) {
+      if (device.devFS != null) {
+        // Cleanup the devFS; don't wait indefinitely, and ignore any errors.
+        await device.devFS.destroy()
+          .timeout(const Duration(milliseconds: 250))
+          .catchError((dynamic error) {
+            printTrace('$error');
+          });
+      }
+      device.devFS = null;
     }
-    _devFS = null;
   }
 
-  Future<Null> _launchInView(Uri entryUri,
+  Future<Null> _launchInView(FlutterDevice device,
+                             Uri entryUri,
                              Uri packagesUri,
                              Uri assetsDirectoryUri) async {
-    final FlutterView view = currentView;
-    return view.runFromSource(entryUri, packagesUri, assetsDirectoryUri);
+    for (FlutterView view in device.views)
+      await view.runFromSource(entryUri, packagesUri, assetsDirectoryUri);
   }
 
-  Future<Null> _launchFromDevFS(ApplicationPackage package,
-                                String mainScript) async {
+  Future<Null> _launchFromDevFS(String mainScript) async {
     final String entryUri = fs.path.relative(mainScript, from: projectRootPath);
-    final Uri deviceEntryUri = _devFS.baseUri.resolveUri(fs.path.toUri(entryUri));
-    final Uri devicePackagesUri = _devFS.baseUri.resolve('.packages');
-    final Uri deviceAssetsDirectoryUri =
-        _devFS.baseUri.resolveUri(fs.path.toUri(getAssetBuildDirectory()));
-    await _launchInView(deviceEntryUri,
-                        devicePackagesUri,
-                        deviceAssetsDirectoryUri);
+    for (FlutterDevice device in flutterDevices) {
+      final Uri deviceEntryUri = device.devFS.baseUri.resolveUri(
+        fs.path.toUri(entryUri));
+      final Uri devicePackagesUri = device.devFS.baseUri.resolve('.packages');
+      final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri(
+        fs.path.toUri(getAssetBuildDirectory()));
+      await _launchInView(device,
+                          deviceEntryUri,
+                          devicePackagesUri,
+                          deviceAssetsDirectoryUri);
+    }
   }
 
   Future<OperationResult> _restartFromSources() async {
@@ -333,18 +312,22 @@
     if (!updatedDevFS)
       return new OperationResult(1, 'DevFS Synchronization Failed');
     // Check if the isolate is paused and resume it.
-    if (currentView?.uiIsolate != null) {
-      // Reload the isolate.
-      await currentView.uiIsolate.reload();
-      final ServiceEvent pauseEvent = currentView.uiIsolate.pauseEvent;
-      if ((pauseEvent != null) && pauseEvent.isPauseEvent) {
-        // Resume the isolate so that it can be killed by the embedder.
-        await currentView.uiIsolate.resume();
+    for (FlutterDevice device in flutterDevices) {
+      for (FlutterView view in device.views) {
+        if (view.uiIsolate != null) {
+          // Reload the isolate.
+          await view.uiIsolate.reload();
+          final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
+          if ((pauseEvent != null) && pauseEvent.isPauseEvent) {
+            // Resume the isolate so that it can be killed by the embedder.
+            await view.uiIsolate.resume();
+          }
+        }
       }
     }
     // We are now running from source.
     _runningFromSnapshot = false;
-    await _launchFromDevFS(package, mainPath);
+    await _launchFromDevFS(mainPath);
     restartTimer.stop();
     printTrace('Restart performed in '
         '${getElapsedAsMilliseconds(restartTimer.elapsed)}.');
@@ -380,7 +363,10 @@
   @override
   Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) async {
     if (fullRestart) {
-      final Status status = logger.startProgress('Performing full restart...', progressId: 'hot.restart');
+      final Status status = logger.startProgress(
+        'Performing full restart...',
+        progressId: 'hot.restart'
+      );
       try {
         final Stopwatch timer = new Stopwatch()..start();
         await _restartFromSources();
@@ -395,7 +381,10 @@
     } else {
       final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
       final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
-      final Status status =  logger.startProgress('$progressPrefix hot reload...', progressId: 'hot.reload');
+      final Status status =  logger.startProgress(
+        '$progressPrefix hot reload...',
+        progressId: 'hot.reload'
+      );
       try {
         final Stopwatch timer = new Stopwatch()..start();
         final OperationResult result = await _reloadSources(pause: pauseAfterRestart);
@@ -414,8 +403,12 @@
   Future<OperationResult> _reloadSources({ bool pause: false }) async {
     printTrace('Refreshing active FlutterViews before reloading.');
     await refreshViews();
-    if (currentView.uiIsolate == null)
-      throw 'Application isolate not found';
+    for (FlutterDevice device in flutterDevices) {
+      for (FlutterView view in device.views) {
+        if (view.uiIsolate == null)
+          throw 'Application isolate not found';
+      }
+    }
     // The initial launch is from a script snapshot. When we reload from source
     // on top of a script snapshot, the first reload will be a worst case reload
     // because all of the sources will end up being dirty (library paths will
@@ -448,22 +441,19 @@
     String reloadMessage;
     try {
       final String entryPath = fs.path.relative(mainPath, from: projectRootPath);
-      final Uri deviceEntryUri = _devFS.baseUri.resolveUri(fs.path.toUri(entryPath));
-      final Uri devicePackagesUri = _devFS.baseUri.resolve('.packages');
       if (benchmarkMode)
         vmReloadTimer.start();
-
       Map<String, dynamic> reloadReport;
-      for (FlutterView view in currentViews) {
-        final Map<String, dynamic> report = await view.uiIsolate.reloadSources(
-          pause: pause,
-          rootLibUri: deviceEntryUri,
-          packagesUri: devicePackagesUri
+      final List<Future<Map<String, dynamic>>> reloadReports = <Future<Map<String, dynamic>>>[];
+      for (FlutterDevice device in flutterDevices) {
+        final List<Future<Map<String, dynamic>>> reports = device.reloadSources(
+          entryPath,
+          pause: pause
         );
-        // Just take the first one until we think of something smart to do.
-        if (reloadReport == null)
-          reloadReport = report;
+        reloadReports.addAll(reports);
       }
+      reloadReport = (await Future.wait(reloadReports)).first;
+
       if (!validateReloadReport(reloadReport)) {
         // Reload failed.
         flutterUsage.sendEvent('hot', 'reload-reject');
@@ -476,6 +466,7 @@
         reloadMessage = '$loadedLibraryCount of $finalLibraryCount libraries';
       }
     } catch (error, st) {
+      printError("Hot reload failed: $error\n$st");
       final int errorCode = error['code'];
       final String errorMessage = error['message'];
       if (errorCode == Isolate.kIsolateReloadBarred) {
@@ -498,20 +489,24 @@
     if (benchmarkMode)
       reassembleTimer.start();
     // Reload the isolate.
-    for (FlutterView view in currentViews)
-      await view.uiIsolate.reload();
+    for (FlutterDevice device in flutterDevices) {
+      for (FlutterView view in device.views)
+        await view.uiIsolate.reload();
+    }
     // We are now running from source.
     _runningFromSnapshot = false;
     // Check if the isolate is paused.
 
     final List<FlutterView> reassembleViews = <FlutterView>[];
-    for (FlutterView view in currentViews) {
-      final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
-      if ((pauseEvent != null) && (pauseEvent.isPauseEvent)) {
-        // Isolate is paused. Don't reassemble.
-        continue;
+    for (FlutterDevice device in flutterDevices) {
+      for (FlutterView view in device.views) {
+        final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
+        if ((pauseEvent != null) && (pauseEvent.isPauseEvent)) {
+          // Isolate is paused. Don't reassemble.
+          continue;
+        }
+        reassembleViews.add(view);
       }
-      reassembleViews.add(view);
     }
     if (reassembleViews.isEmpty) {
       printTrace('Skipping reassemble because all isolates are paused.');
@@ -579,7 +574,11 @@
       ansiAlternative: '$red$fire$bold  To hot reload your app on the fly, '
                        'press "r". To restart the app entirely, press "R".$reset'
     );
-    printStatus('The Observatory debugger and profiler is available at: $_observatoryUri');
+    for (FlutterDevice device in flutterDevices) {
+      final String dname = device.device.name;
+      for (Uri uri in device.observatoryUris)
+        printStatus('An Observatory debugger and profiler on $dname is available at: $uri');
+    }
     if (details) {
       printHelpDetails();
       printStatus('To repeat this help message, press "h". To quit, press "q".');
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 3fd1e0f..8e5d101 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -154,11 +154,11 @@
   /// Subclasses must implement this to execute the command.
   Future<Null> runCommand();
 
-  /// Find and return the target [Device] based upon currently connected
+  /// Find and return all target [Device]s based upon currently connected
   /// devices and criteria entered by the user on the command line.
-  /// If a device cannot be found that meets specified criteria,
+  /// If no device can be found that meets specified criteria,
   /// then print an error message and return `null`.
-  Future<Device> findTargetDevice() async {
+  Future<List<Device>> findAllTargetDevices() async {
     if (!doctor.canLaunchAnything) {
       printError("Unable to locate a development device; please run 'flutter doctor' "
           "for information about installing additional components.");
@@ -171,6 +171,9 @@
       printStatus("No devices found with name or id "
           "matching '${deviceManager.specifiedDeviceId}'");
       return null;
+    } else if (devices.isEmpty && deviceManager.hasSpecifiedAllDevices) {
+      printStatus("No devices found");
+      return null;
     } else if (devices.isEmpty) {
       printNoConnectedDevices();
       return null;
@@ -181,20 +184,39 @@
     if (devices.isEmpty) {
       printStatus('No supported devices connected.');
       return null;
-    } else if (devices.length > 1) {
+    } else if (devices.length > 1 && !deviceManager.hasSpecifiedAllDevices) {
       if (deviceManager.hasSpecifiedDeviceId) {
         printStatus("Found ${devices.length} devices with name or id matching "
             "'${deviceManager.specifiedDeviceId}':");
       } else {
         printStatus("More than one device connected; please specify a device with "
-            "the '-d <deviceId>' flag.");
+            "the '-d <deviceId>' flag, or use '-d all' to act on all devices.");
         devices = await deviceManager.getAllConnectedDevices().toList();
       }
       printStatus('');
       await Device.printDevices(devices);
       return null;
     }
-    return devices.single;
+    return devices;
+  }
+
+  /// Find and return the target [Device] based upon currently connected
+  /// devices and criteria entered by the user on the command line.
+  /// If a device cannot be found that meets specified criteria,
+  /// then print an error message and return `null`.
+  Future<Device> findTargetDevice() async {
+    List<Device> deviceList = await findAllTargetDevices();
+    if (deviceList == null)
+      return null;
+    if (deviceList.length > 1) {
+      printStatus("More than one device connected; please specify a device with "
+        "the '-d <deviceId>' flag.");
+      deviceList = await deviceManager.getAllConnectedDevices().toList();
+      printStatus('');
+      await Device.printDevices(deviceList);
+      return null;
+    }
+    return deviceList.single;
   }
 
   void printNoConnectedDevices() {