Making casting sensitive to connectivity issues

Change-Id: I0d4ad778380ea94bb0eadb9ecdd4b10cfc9ef490
diff --git a/FLUTTER_VERSION b/FLUTTER_VERSION
index 4581483..ef96177 100644
--- a/FLUTTER_VERSION
+++ b/FLUTTER_VERSION
@@ -1 +1 @@
-646b5350d1d1c6e39c1c9f1cbb199d958cc6684b
+ffde6777fcded1d8143205e7bca66c3ac66308f5
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 7129b17..5bc32cc 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@
 all: deps/flutter
 	@true # silences watch, do not remove.
 
-deps/flutter:
+deps/flutter: FLUTTER_VERSION | depclean
 	git clone https://github.com/flutter/flutter.git -b alpha $@
 	cd $@ && git checkout $(shell echo -e `cat FLUTTER_VERSION`)
 	flutter doctor
diff --git a/examples/distro/README.md b/examples/distro/README.md
index 61308bc..e674c3b 100644
--- a/examples/distro/README.md
+++ b/examples/distro/README.md
@@ -11,9 +11,6 @@
   * `sdk.dir=[path to the Android SDK]`
   * `flutter.sdk=[path to the Flutter SDK]`
 
-TODO(rosswang): Finalize build instructions after VDL generation works
-properly.
-
 ## Technologies
 
 This example uses [Vanadium RPC]
@@ -31,10 +28,6 @@
 
 ## Known issues
 
-* [v.io/i/1356](https://github.com/vanadium/issues/issues/1356) - To
-generate VDL, you must uncomment the pertinent sections of the app
-`build.gradle`. The build will fail, but it will generate the needed
-VDL. Then, recomment them and the normal build will succeed.
 * The app crashes after a while; seems to be in the Flutter internals.
 * The app will not start properly after first seeking blessings; restart
-the app.
\ No newline at end of file
+the app. ([flutter 4506](https://github.com/flutter/flutter/issues/4506))
diff --git a/examples/distro/app/build.gradle b/examples/distro/app/build.gradle
index 03221db..67298e0 100644
--- a/examples/distro/app/build.gradle
+++ b/examples/distro/app/build.gradle
@@ -1,20 +1,20 @@
 buildscript {
     repositories {
+        mavenLocal()
         jcenter()
     }
     dependencies {
-        classpath 'io.v:gradle-plugin:1.7'
+        classpath 'io.v:gradle-plugin:1.9'
     }
 }
 
 apply plugin: 'android-sdk-manager'
 apply plugin: 'com.android.application'
 
-/*apply plugin: 'io.v.vdl'
+apply plugin: 'io.v.vdl'
 vdl {
     inputPaths += 'src/main/java'
-}*/
-android.sourceSets.main.java.srcDirs += 'generated-src/vdl'
+}
 
 /*
 You might have to download JDK8 and set JAVA8_HOME (or set the jdk to Java 8 via Project Structure).
@@ -50,6 +50,7 @@
 }
 
 repositories {
+    mavenLocal()
     jcenter()
 }
 
@@ -58,6 +59,6 @@
     compile (
             'com.jaredrummler:android-device-names:1.0.9',
             'io.reactivex:rxjava:1.1.5',
-            'io.v:vanadium-android:2.1.9'
+            'io.v:vanadium-android:2.2.3'
     )
 }
\ No newline at end of file
diff --git a/examples/distro/app/src/flutter/lib/app.dart b/examples/distro/app/src/flutter/lib/app.dart
index a379531..526c753 100644
--- a/examples/distro/app/src/flutter/lib/app.dart
+++ b/examples/distro/app/src/flutter/lib/app.dart
@@ -21,9 +21,29 @@
 }
 
 class _BakuDistroState extends State<BakuDistro> {
+  _BakuDistroState() {
+    HostMessages.addMessageHandler('deviceOnline', _onDeviceOnline);
+    HostMessages.addMessageHandler('deviceOffline', _onDeviceOffline);
+    HostMessages.addMessageHandler('castTerminated', _onCastTerminated);
+  }
+
   final Map<String, String> devices = {};
+
+  Future<String> _onDeviceOnline(final String json) async {
+    final Map<String, dynamic> message = JSON.decode(json);
+    setState(() => devices[message['name']] = message['description']);
+    return null;
+  }
+
+  Future<String> _onDeviceOffline(final String name) async {
+    setState(() => devices.remove(name));
+    if (castTargetName == name) {
+      terminateCast();
+    }
+    return null;
+  }
+
   InputValue _data = InputValue.empty;
-  String _castTargetName;
 
   InputValue get data => _data;
 
@@ -34,7 +54,7 @@
 
       if (castTargetName != null && oldData.text != value.text ) {
         HostMessages.sendToHost('updateCast', getCastData())
-          .catchError(_handleCastError);
+            .catchError(_handleCastError);
       }
     }
   }
@@ -46,6 +66,8 @@
     });
   }
 
+  String _castTargetName;
+
   String get castTargetName => _castTargetName;
 
   void set castTargetName(final String value) {
@@ -54,6 +76,15 @@
     }
   }
 
+  void initiateCast(final String target) {
+    terminateCast();
+    if (target != null) {
+      setState(() => _castTargetName = target);
+      HostMessages.sendToHost('initiateCast', getCastData())
+          .catchError(_handleCastError);
+    }
+  }
+
   void terminateCast() {
     if (_castTargetName != null) {
       HostMessages.sendToHost('terminateCast', _castTargetName);
@@ -61,18 +92,17 @@
     }
   }
 
-  void initiateCast(final String target) {
-    terminateCast();
-    if (target != null) {
-      setState(() => _castTargetName = target);
-      HostMessages.sendToHost('initiateCast', getCastData())
-        .catchError(_handleCastError);
+  Future<String> _onCastTerminated(final String name) async {
+    if (castTargetName == name) {
+      terminateCast();
     }
+    return null;
   }
 
-  _BakuDistroState() {
-    HostMessages.addMessageHandler('deviceOnline', _onDeviceOnline);
-    HostMessages.addMessageHandler('deviceOffline', _onDeviceOffline);
+  void _handleCastError(final Object e) {
+    terminateCast();
+    Scaffold.of(context).showSnackBar(
+        new SnackBar(content: new Text(e.toString())));
   }
 
   @override
@@ -84,12 +114,14 @@
     final List<Widget> layout = [];
 
     if (castTargetName == null) {
+      final TextStyle style = Theme.of(context).textTheme.display1;
+
       layout.add(new Padding(
         padding: new EdgeInsets.all(8.0),
         child: data.text.isEmpty?
         new Text('No content', style:
-        new TextStyle(color: Theme.of(context).disabledColor)) :
-        new Text(data.text)
+        style.copyWith(color: Theme.of(context).disabledColor)) :
+        new Text(data.text, style: style)
       ));
     }
 
@@ -121,24 +153,4 @@
       body: new Column(children: layout)
     );
   }
-
-  Future<String> _onDeviceOnline(final String json) async {
-    final Map<String, dynamic> message = JSON.decode(json);
-    setState(() => devices[message['name']] = message['description']);
-    return null;
-  }
-
-  Future<String> _onDeviceOffline(final String name) async {
-    setState(() => devices.remove(name));
-    if (castTargetName == name) {
-      terminateCast();
-    }
-    return null;
-  }
-
-  void _handleCastError(final Object e) {
-    terminateCast();
-    Scaffold.of(context).showSnackBar(
-      new SnackBar(content: new Text(e.toString())));
-  }
 }
\ No newline at end of file
diff --git a/examples/distro/app/src/flutter/lib/host.dart b/examples/distro/app/src/flutter/lib/host.dart
index d2a3037..304fed7 100644
--- a/examples/distro/app/src/flutter/lib/host.dart
+++ b/examples/distro/app/src/flutter/lib/host.dart
@@ -3,7 +3,6 @@
 // license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:convert';
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -32,7 +31,7 @@
       child: new Padding(
         padding: new EdgeInsets.all(16.0),
         child: new Text(data,
-          style: Theme.of(context).textTheme.title
+          style: Theme.of(context).textTheme.display3
         )
       )
     );
diff --git a/examples/distro/app/src/flutter/pubspec.lock b/examples/distro/app/src/flutter/pubspec.lock
index 82ee346..55012ee 100644
--- a/examples/distro/app/src/flutter/pubspec.lock
+++ b/examples/distro/app/src/flutter/pubspec.lock
@@ -6,7 +6,7 @@
       name: analyzer
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.27.4-alpha.6"
+    version: "0.27.4-alpha.9"
   args:
     description:
       name: args
@@ -132,19 +132,19 @@
       name: mojo
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.20"
+    version: "0.4.23"
   mojo_sdk:
     description:
       name: mojo_sdk
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.2.24"
+    version: "0.2.27"
   mojo_services:
     description:
       name: mojo_services
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.27"
+    version: "0.4.30"
   package_config:
     description:
       name: package_config
diff --git a/examples/distro/app/src/main/java/io/baku/examples/distro/DistroActivity.java b/examples/distro/app/src/main/java/io/baku/examples/distro/DistroActivity.java
index b3b0535..109300f 100644
--- a/examples/distro/app/src/main/java/io/baku/examples/distro/DistroActivity.java
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/DistroActivity.java
@@ -2,10 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// 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.
-
 package io.baku.examples.distro;
 
 import android.app.Activity;
@@ -30,13 +26,17 @@
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
+import javax.annotation.Nullable;
+
 import io.flutter.view.FlutterMain;
 import io.flutter.view.FlutterView;
 import io.v.android.VAndroidContext;
 import io.v.android.VAndroidContexts;
 import io.v.android.security.BlessingsManager;
+import io.v.v23.options.RpcOptions;
 import io.v.v23.security.Blessings;
-import io.v.v23.vdl.ClientSendStream;
+import io.v.v23.vdl.ClientStream;
+import io.v.v23.vdl.VdlAny;
 import io.v.v23.verror.VException;
 import io.v.v23.vom.VomUtil;
 import java8.util.Maps;
@@ -48,7 +48,7 @@
  */
 public class DistroActivity extends Activity {
     private static final String TAG = DistroActivity.class.getSimpleName();
-    private static final Duration PING_TIMEOUT = Duration.standardSeconds(2);
+    private static final Duration PING_TIMEOUT = Duration.standardSeconds(3);
     private static final long POLL_INTERVAL = 750;
 
     private VAndroidContext context;
@@ -121,7 +121,12 @@
             flutterView.destroy();
         }
 
+        // TODO(rosswang): proactively signal cast termination; right now doing that intelligently
+        // would necessitate keeping some extra state in the Java layer that's already in the
+        // Flutter layer, so let's wait until the Flutter plug-in system is better fleshed out. For
+        // now, we'll just wait for the (short) timeout.
         context.close();
+        Log.i(TAG, "Closed Vanadium context");
 
         super.onDestroy();
     }
@@ -156,30 +161,58 @@
         return subscription != null && !subscription.isUnsubscribed();
     }
 
+    private static abstract class TrappingCallback implements FutureCallback<Object> {
+        @Override
+        public void onSuccess(final @Nullable Object result) {
+        }
+    }
+
     private class ConnectionMonitor implements FutureCallback<String> {
-        final String name;
-
+        public final String name;
         private final DistroClient client;
-
-        private ClientSendStream<State, Void> castStream;
-
+        private ClientStream<State, VdlAny, Void> castStream;
         private ListenableFuture<String> poll;
 
-        private final FutureCallback<Object> failureHandler = new FutureCallback<Object>() {
+        private final FutureCallback<Object> castTerminated = new FutureCallback<Object>() {
+            private void notifyFlutter() {
+                if (isActive()) {
+                    flutterView.sendToFlutter("castTerminated", name);
+                }
+            }
+
             @Override
-            public void onSuccess(final Object result) {
+            public void onSuccess(final @Nullable Object result) {
+                Log.i(TAG, "Cast terminated by " + name);
+                notifyFlutter();
             }
 
             @Override
             public void onFailure(final Throwable t) {
-                if (isActive()) {
-                    flutterView.sendToFlutter("deviceOffline", name);
-                    terminateCast();
-                    clients.remove(name);
-                }
+                Log.i(TAG, "Cast terminated by " + name, t);
+                notifyFlutter();
             }
         };
 
+        private final TrappingCallback
+                trapDeviceOffline = new TrappingCallback() {
+                    @Override
+                    public void onFailure(final Throwable t) {
+                        Log.i(TAG, "Lost device " + name, t);
+
+                        if (isActive()) {
+                            flutterView.sendToFlutter("deviceOffline", name);
+                            terminateCast();
+                            clients.remove(name);
+                        }
+                    }
+                },
+                trapCastTerminated = new TrappingCallback() {
+                    @Override
+                    public void onFailure(final Throwable t) {
+                        castTerminated.onFailure(t);
+                    };
+                };
+
         public ConnectionMonitor(final String name) {
             this.name = name;
             client = DistroClientFactory.getDistroClient(name);
@@ -210,21 +243,23 @@
 
         @Override
         public void onFailure(final Throwable t) {
-            failureHandler.onFailure(t);
+            trapDeviceOffline.onFailure(t);
         }
 
         public ConnectionMonitor initiateCast() {
-            castStream = client.cast(context.getVContext());
+            castStream = client.cast(context.getVContext(), new RpcOptions()
+                    .channelTimeout(DistroAndroidService.CHANNEL_TIMEOUT));
+            Futures.addCallback(castStream.recv(), castTerminated);
             return this;
         }
 
         public void updateCast(final String data) {
-            Futures.addCallback(castStream.send(new State(data)), failureHandler);
+            Futures.addCallback(castStream.send(new State(data)), trapCastTerminated);
         }
 
         public void terminateCast() {
             if (castStream != null) {
-                Futures.addCallback(castStream.finish(), failureHandler);
+                Futures.addCallback(castStream.finish(), castTerminated);
                 castStream = null;
             }
         }
diff --git a/examples/distro/app/src/main/java/io/baku/examples/distro/DistroAndroidService.java b/examples/distro/app/src/main/java/io/baku/examples/distro/DistroAndroidService.java
index b29ab2a..77dd1e6 100644
--- a/examples/distro/app/src/main/java/io/baku/examples/distro/DistroAndroidService.java
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/DistroAndroidService.java
@@ -19,12 +19,15 @@
 import com.google.common.util.concurrent.SettableFuture;
 import com.jaredrummler.android.device.DeviceName;
 
+import org.joda.time.Duration;
+
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
 
 import io.v.android.v23.V;
 import io.v.v23.context.VContext;
+import io.v.v23.options.RpcServerOptions;
 import io.v.v23.rpc.Server;
 import io.v.v23.rpc.ServerCall;
 import io.v.v23.security.BlessingPattern;
@@ -32,6 +35,8 @@
 import io.v.v23.security.VPrincipal;
 import io.v.v23.security.VSecurity;
 import io.v.v23.vdl.ServerRecvStream;
+import io.v.v23.vdl.ServerStream;
+import io.v.v23.vdl.VdlAny;
 import io.v.v23.verror.VException;
 import io.v.v23.vom.VomUtil;
 import lombok.RequiredArgsConstructor;
@@ -54,6 +59,8 @@
     public static final String
             BLESSINGS_EXTRA = "Blessings";
 
+    public static final Duration CHANNEL_TIMEOUT = Duration.standardSeconds(3);
+
     private VContext vContext;
 
     @Nullable
@@ -115,10 +122,16 @@
 
             @Override
             public ListenableFuture<Void> cast(final VContext context, final ServerCall call,
-                                               final ServerRecvStream<State> stream) {
+                                               final ServerStream<VdlAny, State> stream) {
                 final String key = UUID.randomUUID().toString();
+
+                Log.i(TAG, "Hosting new casting session " + key);
+
                 final SettableFuture<Void> completion = SettableFuture.create();
-                completion.addListener(() -> sessions.remove(key), MoreExecutors.directExecutor());
+                completion.addListener(() -> {
+                    sessions.remove(key);
+                    Log.i(TAG, "Terminated casting session " + key);
+                }, MoreExecutors.directExecutor());
 
                 sessions.put(key, new Session(completion, stream));
 
@@ -151,7 +164,8 @@
         final Server s;
         try {
             s = V.getServer(V.withNewServer(listenContext, Disco.name(this), handlers,
-                    VSecurity.newAllowEveryoneAuthorizer()));
+                    VSecurity.newAllowEveryoneAuthorizer(), new RpcServerOptions()
+                            .channelTimeout(CHANNEL_TIMEOUT)));
         } catch (final VException e) {
             throw new RuntimeException(e);
         }
diff --git a/examples/distro/app/src/main/java/io/baku/examples/distro/HostActivity.java b/examples/distro/app/src/main/java/io/baku/examples/distro/HostActivity.java
index 766ce7a..4913178 100644
--- a/examples/distro/app/src/main/java/io/baku/examples/distro/HostActivity.java
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/HostActivity.java
@@ -2,10 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// 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.
-
 package io.baku.examples.distro;
 
 import android.app.Activity;
@@ -16,17 +12,20 @@
 import com.google.common.util.concurrent.Futures;
 
 import org.chromium.base.PathUtils;
+import org.json.JSONObject;
 
 import java.io.File;
 
 import io.flutter.view.FlutterMain;
 import io.flutter.view.FlutterView;
+import io.v.v23.verror.CanceledException;
 import io.v.v23.verror.EndOfFileException;
 
 public class HostActivity extends Activity {
     public static final String SESSION_EXTRA = "Session";
 
     private static final String TAG = HostActivity.class.getSimpleName();
+    private static final long POLL_INTERVAL = 750;
 
     private FlutterView flutterView;
     private DistroAndroidService.Session session;
@@ -34,14 +33,18 @@
     private final FutureCallback<State> render = new FutureCallback<State>() {
         @Override
         public void onSuccess(final State state) {
+            Log.i(TAG, "Received state update: " + JSONObject.quote(state.toString()));
             flutterView.sendToFlutter("updateCast", state.getValue());
             Futures.addCallback(session.stream.recv(), render);
         }
 
         @Override
         public void onFailure(final Throwable t) {
-            if (!(t instanceof EndOfFileException)) {
+            if (!(t instanceof EndOfFileException ||
+                    t instanceof CanceledException)) {
                 Log.e(TAG, "Unexpected error while hosting cast", t);
+            } else {
+                Log.i(TAG, "Terminated cast", t);
             }
             finish();
         }
@@ -82,11 +85,14 @@
 
     @Override
     protected void onDestroy() {
+        session.completion.set(null);
+        session = null;
+
         if (flutterView != null) {
             flutterView.destroy();
         }
 
-        session.completion.set(null);
+        Log.i(TAG, "Locally terminated cast");
 
         super.onDestroy();
     }
diff --git a/examples/distro/app/src/main/java/io/baku/examples/distro/distro.vdl b/examples/distro/app/src/main/java/io/baku/examples/distro/distro.vdl
index 2c1465d..39f30a5 100644
--- a/examples/distro/app/src/main/java/io/baku/examples/distro/distro.vdl
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/distro.vdl
@@ -7,6 +7,7 @@
 type State string
 
 type Distro interface {
-    Cast() stream<State, _> error
+    // Right now, the server->client any stream is used as a glorified completion signal.
+    Cast() stream<State, any> error
     GetDescription() (string | error)
 }
\ No newline at end of file
diff --git a/examples/distro/build.gradle b/examples/distro/build.gradle
index 456fe0a..b5b8b56 100644
--- a/examples/distro/build.gradle
+++ b/examples/distro/build.gradle
@@ -6,7 +6,7 @@
 
     dependencies {
         classpath (
-                'com.android.tools.build:gradle:2.1.0',
+                'com.android.tools.build:gradle:2.1.2',
                 // Use the Android SDK manager, which will automatically download the required
                 // Android SDK.
                 // Note: Using jitpack and the master branch of the sdk-manager-plugin in order to