Add FlutterView and ViewManager and hook them into the ResidentRunner. (#5345)

- [x] Refactor view support into separate classes.
- [x] Make the hot runner grab the main view and call runFromSource on it.
- [x] Remove Device.needsDevFS (because it is always true).
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index b254568..36b374a 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -365,8 +365,12 @@
     if (_dirtyEntries.length > 0) {
       status = logger.startProgress('Updating files...');
       if (_httpWriter != null) {
-        await _httpWriter.write(_dirtyEntries,
-                                progressReporter: progressReporter);
+        try {
+          await _httpWriter.write(_dirtyEntries,
+                                  progressReporter: progressReporter);
+        } catch (e) {
+          printError("Could not update files on device: $e");
+        }
       } else {
         // Make service protocol requests for each.
         for (DevFSEntry entry in _dirtyEntries) {
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index d343e5f..ed53c6e 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -192,9 +192,6 @@
   /// Does this device implement support for hot reloading / restarting?
   bool get supportsHotMode => false;
 
-  /// Does this device need a DevFS to support hot mode?
-  bool get needsDevFS => true;
-
   /// Run from a file. Necessary for hot mode.
   Future<bool> runFromFile(ApplicationPackage package,
                            String scriptUri,
diff --git a/packages/flutter_tools/lib/src/hot.dart b/packages/flutter_tools/lib/src/hot.dart
index 2b1b2d9..0fb35c2 100644
--- a/packages/flutter_tools/lib/src/hot.dart
+++ b/packages/flutter_tools/lib/src/hot.dart
@@ -23,6 +23,7 @@
 import 'observatory.dart';
 import 'resident_runner.dart';
 import 'toolchain.dart';
+import 'view.dart';
 
 String getDevFSLoaderScript() {
   return path.absolute(path.join(Cache.flutterRoot,
@@ -238,17 +239,13 @@
 
     await startEchoingDeviceLog();
 
-    if (device.needsDevFS) {
-      printStatus('Launching loader on ${device.name}...');
-    } else {
-      printStatus('Launching ${getDisplayPath(_mainPath)} on ${device.name}...');
-    }
+    printStatus('Launching loader on ${device.name}...');
 
     // Start the loader.
     Future<LaunchResult> futureResult = device.startApp(
       _package,
       debuggingOptions.buildMode,
-      mainPath: device.needsDevFS ? getDevFSLoaderScript() : _mainPath,
+      mainPath: getDevFSLoaderScript(),
       debuggingOptions: debuggingOptions,
       platformArgs: platformArgs,
       route: route
@@ -266,48 +263,43 @@
     LaunchResult result = await futureResult;
 
     if (!result.started) {
-      if (device.needsDevFS) {
-        printError('Error launching DevFS loader on ${device.name}.');
-      } else {
-        printError('Error launching ${getDisplayPath(_mainPath)} on ${device.name}.');
-      }
+      printError('Error launching DevFS loader on ${device.name}.');
       await stopEchoingDeviceLog();
       return 2;
     }
 
     await connectToServiceProtocol(result.observatoryPort);
 
-    if (device.needsDevFS) {
-      try {
-        Uri baseUri = await _initDevFS();
-        if (connectionInfoCompleter != null) {
-          connectionInfoCompleter.complete(
-            new DebugConnectionInfo(result.observatoryPort, baseUri: baseUri.toString())
-          );
-        }
-      } catch (error) {
-        printError('Error initializing DevFS: $error');
-        return 3;
+    try {
+      Uri baseUri = await _initDevFS();
+      if (connectionInfoCompleter != null) {
+        connectionInfoCompleter.complete(
+          new DebugConnectionInfo(result.observatoryPort, baseUri: baseUri.toString())
+        );
       }
-      _loaderShowMessage('Connecting...', progress: 0);
-      bool devfsResult = await _updateDevFS(
-        progressReporter: (int progress, int max) {
-          if (progress % 10 == 0)
-            _loaderShowMessage('Syncing files to device...', progress: progress, max: max);
-        }
-      );
-      if (!devfsResult) {
-        _loaderShowMessage('Failed.');
-        printError('Could not perform initial file synchronization.');
-        return 3;
-      }
-      printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...');
-      _loaderShowMessage('Launching...');
-      await _launchFromDevFS(_package, _mainPath);
-    } else {
-      if (connectionInfoCompleter != null)
-        connectionInfoCompleter.complete(new DebugConnectionInfo(result.observatoryPort));
+    } catch (error) {
+      printError('Error initializing DevFS: $error');
+      return 3;
     }
+    _loaderShowMessage('Connecting...', progress: 0);
+    bool devfsResult = await _updateDevFS(
+      progressReporter: (int progress, int max) {
+        if (progress % 10 == 0)
+          _loaderShowMessage('Syncing files to device...', progress: progress, max: max);
+      }
+    );
+    if (!devfsResult) {
+      _loaderShowMessage('Failed.');
+      printError('Could not perform initial file synchronization.');
+      return 3;
+    }
+
+    await viewManager.refresh();
+    printStatus('Connected to view: ${viewManager.mainView}');
+
+    printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...');
+    _loaderShowMessage('Launching...');
+    await _launchFromDevFS(_package, _mainPath);
 
     _startReadingFromControlPipe();
 
@@ -383,9 +375,6 @@
   }
 
   Future<Null> _evictDirtyAssets() async {
-    if (_devFS == null) {
-      return;
-    }
     if (_devFS.dirtyAssetEntries.length == 0) {
       return;
     }
@@ -408,26 +397,8 @@
   Future<Null> _launchInView(String entryPath,
                              String packagesPath,
                              String assetsDirectoryPath) async {
-    String viewId = await serviceProtocol.getFirstViewId();
-    // When this completer completes the isolate is running.
-    // TODO(johnmccutchan): Have the framework send an event after the first
-    // frame is rendered and use that instead of 'runnable'.
-    Completer<Null> completer = new Completer<Null>();
-    StreamSubscription<Event> subscription =
-       serviceProtocol.onIsolateEvent.listen((Event event) {
-     if (event.kind == 'IsolateStart') {
-       printTrace('Isolate is spawned.');
-     } else if (event.kind == 'IsolateRunnable') {
-       printTrace('Isolate is runnable.');
-       completer.complete(null);
-     }
-    });
-    await serviceProtocol.runInView(viewId,
-                                   entryPath,
-                                   packagesPath,
-                                   assetsDirectoryPath);
-    await completer.future;
-    await subscription.cancel();
+    FlutterView view = viewManager.mainView;
+    return view.runFromSource(entryPath, packagesPath, assetsDirectoryPath);
   }
 
   Future<Null> _launchFromDevFS(ApplicationPackage package,
@@ -444,27 +415,11 @@
                         deviceAssetsDirectoryPath);
   }
 
-  Future<Null> _launchFromDisk(ApplicationPackage package,
-                               String mainScript) async {
-    Uri baseUri = new Uri.directory(_projectRootPath);
-    String entryPath = path.relative(mainScript, from: _projectRootPath);
-    String diskEntryPath = baseUri.resolve(entryPath).toFilePath();
-    String diskPackagesPath = baseUri.resolve('.packages').toFilePath();
-    String diskAssetsDirectoryPath = baseUri.resolve('build/flx').toFilePath();
-    await _launchInView(diskEntryPath,
-                        diskPackagesPath,
-                        diskAssetsDirectoryPath);
-  }
-
   Future<Null> _restartFromSources() async {
     FirstFrameTimer firstFrameTimer = new FirstFrameTimer(serviceProtocol);
     firstFrameTimer.start();
-    if (_devFS == null) {
-      await _launchFromDisk(_package, _mainPath);
-    } else {
-      await _updateDevFS();
-      await _launchFromDevFS(_package, _mainPath);
-    }
+    await _updateDevFS();
+    await _launchFromDevFS(_package, _mainPath);
     Status restartStatus =
         logger.startProgress('Waiting for application to start...');
     // Wait for the first frame to be rendered.
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 81d5169..8040103 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -362,9 +362,6 @@
   @override
   bool get supportsHotMode => true;
 
-  @override
-  bool get needsDevFS => true;
-
   _IOSSimulatorLogReader _logReader;
   _IOSSimulatorDevicePortForwarder _portForwarder;
 
diff --git a/packages/flutter_tools/lib/src/observatory.dart b/packages/flutter_tools/lib/src/observatory.dart
index bcbb52d..0381ef1 100644
--- a/packages/flutter_tools/lib/src/observatory.dart
+++ b/packages/flutter_tools/lib/src/observatory.dart
@@ -128,9 +128,14 @@
     }
   }
 
-  Future<String> getFirstViewId() async {
+  Future<List<Map<String, String>>> getViewList() async {
     Map<String, dynamic> response = await peer.sendRequest('_flutter.listViews');
     List<Map<String, String>> views = response['views'];
+    return views;
+  }
+
+  Future<String> getFirstViewId() async {
+    List<Map<String, String>> views = await getViewList();
     return views[0]['id'];
   }
 
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 0eb5aea..0aea86f 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -12,6 +12,7 @@
 import 'device.dart';
 import 'globals.dart';
 import 'observatory.dart';
+import 'view.dart';
 
 // Shared code between different resident application runners.
 abstract class ResidentRunner {
@@ -28,6 +29,7 @@
   final Completer<int> _finished = new Completer<int>();
 
   Observatory serviceProtocol;
+  ViewManager viewManager;
   StreamSubscription<String> _loggingSubscription;
 
   /// Start the app and keep the process running during its lifetime.
@@ -96,6 +98,11 @@
     serviceProtocol.onIsolateEvent.listen((Event event) {
       printTrace(event.toString());
     });
+
+    // Setup view manager and refresh the view list.
+    viewManager = new ViewManager(serviceProtocol);
+    await viewManager.refresh();
+
     // Listen for service protocol connection to close.
     serviceProtocol.done.whenComplete(() {
       appFinished();
diff --git a/packages/flutter_tools/lib/src/view.dart b/packages/flutter_tools/lib/src/view.dart
new file mode 100644
index 0000000..fb4085c
--- /dev/null
+++ b/packages/flutter_tools/lib/src/view.dart
@@ -0,0 +1,89 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'globals.dart';
+import 'observatory.dart';
+
+/// Peered to a Android/iOS FlutterView widget on a device.
+class FlutterView {
+  FlutterView(this.viewId, this.viewManager);
+
+  final String viewId;
+  final ViewManager viewManager;
+  String _uiIsolateId;
+  String get uiIsolateId => _uiIsolateId;
+
+  Future<Null> runFromSource(String entryPath,
+                             String packagesPath,
+                             String assetsDirectoryPath) async {
+    return viewManager._runFromSource(this,
+                                      entryPath,
+                                      packagesPath,
+                                      assetsDirectoryPath);
+  }
+
+  @override
+  String toString() => viewId;
+
+  @override
+  bool operator ==(FlutterView other) {
+    return other.viewId == viewId;
+  }
+
+  @override
+  int get hashCode => viewId.hashCode;
+}
+
+/// Manager of FlutterViews.
+class ViewManager {
+  ViewManager(this.serviceProtocol);
+
+  final Observatory serviceProtocol;
+
+  Future<Null> refresh() async {
+    List<Map<String, String>> viewList = await serviceProtocol.getViewList();
+    for (Map<String, String> viewDescription in viewList) {
+      FlutterView view = new FlutterView(viewDescription['id'], this);
+      if (!views.contains(view)) {
+        // Canonicalize views against the view set.
+        views.add(view);
+      }
+    }
+  }
+
+  // TODO(johnmccutchan): Report errors when running failed.
+  Future<Null> _runFromSource(FlutterView view,
+                              String entryPath,
+                              String packagesPath,
+                              String assetsDirectoryPath) async {
+    final String viewId = await serviceProtocol.getFirstViewId();
+    // When this completer completes the isolate is running.
+    final Completer<Null> completer = new Completer<Null>();
+    final StreamSubscription<Event> subscription =
+      serviceProtocol.onIsolateEvent.listen((Event event) {
+      // TODO(johnmccutchan): Listen to the debug stream and catch initial
+      // launch errors.
+      if (event.kind == 'IsolateRunnable') {
+        printTrace('Isolate is runnable.');
+        completer.complete(null);
+      }
+    });
+    await serviceProtocol.runInView(viewId,
+                                   entryPath,
+                                   packagesPath,
+                                   assetsDirectoryPath);
+    await completer.future;
+    await subscription.cancel();
+  }
+
+  // TODO(johnmccutchan): Remove this accessor and make the runner multi-view
+  // aware.
+  FlutterView get mainView {
+    return views.first;
+  }
+
+  final Set<FlutterView> views = new Set<FlutterView>();
+}