Merge pull request #2482 from HansMuller/dismiss_action

Support undo in the leave-behind demo
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index 35358be..53b7162 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -1605,8 +1605,8 @@
     while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
       Element oldChild = oldChildren[oldChildrenTop];
       Widget newWidget = newWidgets[newChildrenTop];
-      assert(oldChild._debugLifecycleState == _ElementLifecycle.active);
-      if (!Widget.canUpdate(oldChild.widget, newWidget))
+      assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active);
+      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
         break;
       Element newChild = updateChild(oldChild, newWidget, previousChild);
       assert(newChild._debugLifecycleState == _ElementLifecycle.active);
@@ -1620,8 +1620,8 @@
     while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
       Element oldChild = oldChildren[oldChildrenBottom];
       Widget newWidget = newWidgets[newChildrenBottom];
-      assert(oldChild._debugLifecycleState == _ElementLifecycle.active);
-      if (!Widget.canUpdate(oldChild.widget, newWidget))
+      assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active);
+      if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
         break;
       oldChildrenBottom -= 1;
       newChildrenBottom -= 1;
@@ -1634,11 +1634,13 @@
       oldKeyedChildren = new Map<Key, Element>();
       while (oldChildrenTop <= oldChildrenBottom) {
         Element oldChild = oldChildren[oldChildrenTop];
-        assert(oldChild._debugLifecycleState == _ElementLifecycle.active);
-        if (oldChild.widget.key != null)
-          oldKeyedChildren[oldChild.widget.key] = oldChild;
-        else
-          _deactivateChild(oldChild);
+        assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active);
+        if (oldChild != null) {
+          if (oldChild.widget.key != null)
+            oldKeyedChildren[oldChild.widget.key] = oldChild;
+          else
+            _deactivateChild(oldChild);
+        }
         oldChildrenTop += 1;
       }
     }
@@ -1682,6 +1684,7 @@
     // Update the bottom of the list.
     while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
       Element oldChild = oldChildren[oldChildrenTop];
+      assert(oldChild != null);
       assert(oldChild._debugLifecycleState == _ElementLifecycle.active);
       Widget newWidget = newWidgets[newChildrenTop];
       assert(Widget.canUpdate(oldChild.widget, newWidget));
@@ -1826,6 +1829,19 @@
   }
 
   List<Element> _children;
+  // We null out detached children lazily to avoid O(n^2) work walking _children
+  // repeatedly to remove children.
+  Set<Element> _detachedChildren;
+
+  void _replaceDetachedChildrenWithNull() {
+    if (_detachedChildren != null && _detachedChildren.isNotEmpty) {
+      for (int i = 0; i < _children.length; ++i) {
+        if (_detachedChildren.contains(_children[i]))
+          _children[i] = null;
+      }
+      _detachedChildren.clear();
+    }
+  }
 
   void insertChildRenderObject(RenderObject child, Element slot) {
     final ContainerRenderObjectMixin renderObject = this.renderObject;
@@ -1865,8 +1881,18 @@
   }
 
   void visitChildren(ElementVisitor visitor) {
-    for (Element child in _children)
-      visitor(child);
+    _replaceDetachedChildrenWithNull();
+    for (Element child in _children) {
+      if (child != null)
+        visitor(child);
+    }
+  }
+
+  bool detachChild(Element child) {
+    _detachedChildren ??= new Set<Element>();
+    _detachedChildren.add(child);
+    _deactivateChild(child);
+    return true;
   }
 
   void mount(Element parent, dynamic newSlot) {
@@ -1883,6 +1909,7 @@
   void update(T newWidget) {
     super.update(newWidget);
     assert(widget == newWidget);
+    _replaceDetachedChildrenWithNull();
     _children = updateChildren(_children, widget.children);
   }
 }
diff --git a/packages/flutter/test/widget/reparent_state_test.dart b/packages/flutter/test/widget/reparent_state_test.dart
index 024e34c..4577d9a 100644
--- a/packages/flutter/test/widget/reparent_state_test.dart
+++ b/packages/flutter/test/widget/reparent_state_test.dart
@@ -95,6 +95,68 @@
     });
   });
 
+  test('can reparent state with multichild widgets', () {
+    testWidgets((WidgetTester tester) {
+      GlobalKey left = new GlobalKey();
+      GlobalKey right = new GlobalKey();
+
+      StateMarker grandchild = new StateMarker();
+      tester.pumpWidget(
+        new Stack(
+          children: <Widget>[
+            new StateMarker(key: left),
+            new StateMarker(
+              key: right,
+              child: grandchild
+            )
+          ]
+        )
+      );
+
+      (left.currentState as StateMarkerState).marker = "left";
+      (right.currentState as StateMarkerState).marker = "right";
+
+      StateMarkerState grandchildState = tester.findStateByConfig(grandchild);
+      expect(grandchildState, isNotNull);
+      grandchildState.marker = "grandchild";
+
+      StateMarker newGrandchild = new StateMarker();
+      tester.pumpWidget(
+        new Stack(
+          children: <Widget>[
+            new StateMarker(
+              key: right,
+              child: newGrandchild
+            ),
+            new StateMarker(key: left)
+          ]
+        )
+      );
+
+      expect((left.currentState as StateMarkerState).marker, equals("left"));
+      expect((right.currentState as StateMarkerState).marker, equals("right"));
+
+      StateMarkerState newGrandchildState = tester.findStateByConfig(newGrandchild);
+      expect(newGrandchildState, isNotNull);
+      expect(newGrandchildState, equals(grandchildState));
+      expect(newGrandchildState.marker, equals("grandchild"));
+
+      tester.pumpWidget(
+        new Center(
+          child: new Container(
+            child: new StateMarker(
+              key: left,
+              child: new Container()
+            )
+          )
+        )
+      );
+
+      expect((left.currentState as StateMarkerState).marker, equals("left"));
+      expect(right.currentState, isNull);
+    });
+  });
+
   test('can with scrollable list', () {
     testWidgets((WidgetTester tester) {
       GlobalKey key = new GlobalKey();
diff --git a/packages/flutter_tools/lib/src/service_protocol.dart b/packages/flutter_tools/lib/src/service_protocol.dart
new file mode 100644
index 0000000..ffbbdfa
--- /dev/null
+++ b/packages/flutter_tools/lib/src/service_protocol.dart
@@ -0,0 +1,52 @@
+// 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 'device.dart';
+
+/// Discover service protocol ports on devices.
+class ServiceProtocolDiscovery {
+  /// [logReader] A [DeviceLogReader] to look for Observatory messages in.
+  ServiceProtocolDiscovery(DeviceLogReader logReader)
+      : _logReader = logReader {
+    assert(_logReader != null);
+    if (!_logReader.isReading)
+      _logReader.start();
+
+    _logReader.lines.listen(_onLine);
+  }
+
+  final DeviceLogReader _logReader;
+  Completer _completer = new Completer();
+
+  /// The [Future] returned by this function will complete when the next
+  /// service protocol port is found.
+  Future<int> nextPort() {
+    return _completer.future;
+  }
+
+  void _onLine(String line) {
+    int portNumber = 0;
+    if (line.startsWith('Observatory listening on http://')) {
+      try {
+        RegExp portExp = new RegExp(r"\d+.\d+.\d+.\d+:(\d+)");
+        var port = portExp.firstMatch(line).group(1);
+        portNumber = int.parse(port);
+      } catch (_) {
+        // Ignore errors.
+      }
+    }
+    if (portNumber != 0) {
+      _located(portNumber);
+    }
+  }
+
+  void _located(int port) {
+    assert(_completer != null);
+    assert(!_completer.isCompleted);
+    _completer.complete(port);
+    _completer = new Completer();
+  }
+}
diff --git a/packages/flutter_tools/test/service_protocol_test.dart b/packages/flutter_tools/test/service_protocol_test.dart
new file mode 100644
index 0000000..7c62e57
--- /dev/null
+++ b/packages/flutter_tools/test/service_protocol_test.dart
@@ -0,0 +1,47 @@
+// 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 'package:test/test.dart';
+
+import 'package:flutter_tools/src/service_protocol.dart';
+
+import 'src/mocks.dart';
+
+main() => defineTests();
+
+defineTests() {
+  group('service_protocol', () {
+    test('Discovery Heartbeat', () async {
+      MockDeviceLogReader logReader = new MockDeviceLogReader();
+      ServiceProtocolDiscovery discoverer =
+          new ServiceProtocolDiscovery(logReader);
+      // Get next port future.
+      Future nextPort = discoverer.nextPort();
+      expect(nextPort, isNotNull);
+      // Inject some lines.
+      logReader.addLine('HELLO WORLD');
+      logReader.addLine(
+          'Observatory listening on http://127.0.0.1:9999');
+      // Await the port.
+      expect(await nextPort, 9999);
+      // Get next port future.
+      nextPort = discoverer.nextPort();
+      logReader.addLine(
+          'Observatory listening on http://127.0.0.1:3333');
+      expect(await nextPort, 3333);
+      // Get next port future.
+      nextPort = discoverer.nextPort();
+      // Inject some bad lines.
+      logReader.addLine('Observatory listening on http://127.0.0.1');
+      logReader.addLine('Observatory listening on http://127.0.0.1:');
+      logReader.addLine(
+      'Observatory listening on http://127.0.0.1:apple');
+      int port = await nextPort.timeout(
+      const Duration(milliseconds: 100), onTimeout: () => 77);
+      // Expect the timeout port.
+      expect(port, 77);
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 38a4610..1d7b819 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
 import 'package:flutter_tools/src/android/android_device.dart';
 import 'package:flutter_tools/src/application_package.dart';
 import 'package:flutter_tools/src/build_configuration.dart';
@@ -51,6 +52,39 @@
     iOSSimulator: new MockIOSSimulator());
 }
 
+class MockDeviceLogReader extends DeviceLogReader {
+  String get name => 'MockLogReader';
+
+  final StreamController<String> _linesStreamController =
+      new StreamController<String>.broadcast();
+
+  final Completer _finishedCompleter = new Completer();
+
+  Stream<String> get lines => _linesStreamController.stream;
+
+  void addLine(String line) {
+    _linesStreamController.add(line);
+  }
+
+  bool _started = false;
+
+  Future start() {
+    assert(!_started);
+    _started = true;
+    return new Future.value(this);
+  }
+
+  bool get isReading => _started;
+
+  Future stop() {
+    assert(_started);
+    _started = false;
+    return new Future.value(this);
+  }
+
+  Future get finished => _finishedCompleter.future;
+}
+
 void applyMocksToCommand(FlutterCommand command) {
   command
     ..applicationPackages = new MockApplicationPackageStore()