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