Initial casting

Change-Id: I33efb3283fb6897b247c843e07749420db3fea46
diff --git a/examples/distro/app/src/flutter/lib/app.dart b/examples/distro/app/src/flutter/lib/app.dart
new file mode 100644
index 0000000..a379531
--- /dev/null
+++ b/examples/distro/app/src/flutter/lib/app.dart
@@ -0,0 +1,144 @@
+// Copyright 2016 The Vanadium 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 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+
+class BakuDistro extends StatefulWidget {
+  @override
+  _BakuDistroState createState() => new _BakuDistroState();
+}
+
+class _Device {
+  String name, description;
+
+  _Device(this.name, this.description);
+}
+
+class _BakuDistroState extends State<BakuDistro> {
+  final Map<String, String> devices = {};
+  InputValue _data = InputValue.empty;
+  String _castTargetName;
+
+  InputValue get data => _data;
+
+  void set data(final InputValue value) {
+    if (_data != value) {
+      final InputValue oldData = _data;
+      setState(() => _data = value);
+
+      if (castTargetName != null && oldData.text != value.text ) {
+        HostMessages.sendToHost('updateCast', getCastData())
+          .catchError(_handleCastError);
+      }
+    }
+  }
+
+  String getCastData() {
+    return JSON.encode({
+      'name': castTargetName,
+      'data': data.text
+    });
+  }
+
+  String get castTargetName => _castTargetName;
+
+  void set castTargetName(final String value) {
+    if (_castTargetName != value) {
+      initiateCast(value);
+    }
+  }
+
+  void terminateCast() {
+    if (_castTargetName != null) {
+      HostMessages.sendToHost('terminateCast', _castTargetName);
+      setState(() => _castTargetName = null);
+    }
+  }
+
+  void initiateCast(final String target) {
+    terminateCast();
+    if (target != null) {
+      setState(() => _castTargetName = target);
+      HostMessages.sendToHost('initiateCast', getCastData())
+        .catchError(_handleCastError);
+    }
+  }
+
+  _BakuDistroState() {
+    HostMessages.addMessageHandler('deviceOnline', _onDeviceOnline);
+    HostMessages.addMessageHandler('deviceOffline', _onDeviceOffline);
+  }
+
+  @override
+  Widget build(final BuildContext context) {
+    final List<_Device> sortedDevices = devices.keys.map((name) =>
+    new _Device(name, devices[name])).toList(growable: false);
+    sortedDevices.sort((a, b) => a.description.compareTo(b.description));
+
+    final List<Widget> layout = [];
+
+    if (castTargetName == null) {
+      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)
+      ));
+    }
+
+    layout.add(new Input(
+      value: data,
+      labelText: 'Text content',
+      onChanged: (value) => data = value
+    ));
+
+    if (castTargetName == null) {
+      layout.add(new MaterialList(
+        type: MaterialListType.oneLine,
+        children: sortedDevices.map((d) => new ListItem(
+          title: new Text(d.description),
+          onTap: () => castTargetName = d.name
+        ))
+      ));
+    } else {
+      layout.add(new RaisedButton(
+        child: new Text('Recall'),
+        onPressed: terminateCast
+      ));
+    }
+
+    return new Scaffold(
+      appBar: new AppBar(
+        title: new Text('Baku Distro Example')
+      ),
+      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
new file mode 100644
index 0000000..d2a3037
--- /dev/null
+++ b/examples/distro/app/src/flutter/lib/host.dart
@@ -0,0 +1,40 @@
+// Copyright 2016 The Vanadium 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 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+
+class BakuHost extends StatefulWidget {
+  @override
+  _BakuHostState createState() => new _BakuHostState();
+}
+
+class _BakuHostState extends State<BakuHost> {
+  String data;
+
+  Future<String> _updateCast(final String data) async {
+    setState(() => this.data = data);
+    return null;
+  }
+
+  _BakuHostState() {
+    HostMessages.addMessageHandler("updateCast", _updateCast);
+  }
+
+  @override
+  Widget build(final BuildContext context) {
+    return new Material(
+      child: new Padding(
+        padding: new EdgeInsets.all(16.0),
+        child: new Text(data,
+          style: Theme.of(context).textTheme.title
+        )
+      )
+    );
+  }
+}
\ No newline at end of file
diff --git a/examples/distro/app/src/flutter/lib/main.dart b/examples/distro/app/src/flutter/lib/main.dart
index 0fc0fd8..7cdbf19 100644
--- a/examples/distro/app/src/flutter/lib/main.dart
+++ b/examples/distro/app/src/flutter/lib/main.dart
@@ -2,153 +2,38 @@
 // 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.
-
 import 'dart:async';
-import 'dart:convert';
-import 'dart:math';
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'package:flutter/widgets.dart';
 
-final Random random = new Random();
+import 'app.dart';
+import 'host.dart';
 
 void main() {
-  runApp(new BakuDistro());
+  runApp(new MaterialApp(
+    title: 'Baku Distro',
+    routes: {
+      '/app': (_) => new BakuDistro(),
+      '/host': (_) => new BakuHost()
+    },
+    home: new Builder(builder: _buildContainer)
+  ));
 }
 
-class BakuDistro extends StatefulWidget {
-  @override
-  _BakuDistroState createState() => new _BakuDistroState();
-}
-
-class _Device {
-  String name, description;
-
-  _Device(this.name, this.description);
-}
-
-class _BakuDistroState extends State<BakuDistro> {
-  final Map<String, String> devices = {};
-  InputValue _data = InputValue.empty;
-  String _castTargetName;
-
-  InputValue get data => _data;
-
-  void set data(final InputValue value) {
-    _data = value;
-    if (castTargetName != null) {
-      HostMessages.sendToHost('updateCast', getCastData())
-        .catchError(_handleCastError);
-    }
-  }
-
-  String getCastData() {
-    return JSON.encode({
-      'name': castTargetName,
-      'data': data.toString()
-    });
-  }
-
-  String get castTargetName => _castTargetName;
-
-  void set castTargetName(final String value) {
-    if (_castTargetName != value) {
-      initiateCast(value);
-    }
-  }
-
-  void terminateCast() {
-    if (_castTargetName != null) {
-      HostMessages.sendToHost('terminateCast', _castTargetName);
-      _castTargetName = null;
-    }
-  }
-
-  void initiateCast(final String target) {
-    terminateCast();
-    if (target != null) {
-      _castTargetName = target;
-      HostMessages.sendToHost('initiateCast', getCastData())
-        .catchError(_handleCastError);
-    }
-  }
-
-  _BakuDistroState() {
-    HostMessages.addMessageHandler('deviceOnline', _onDeviceOnline);
-    HostMessages.addMessageHandler('deviceOffline', _onDeviceOffline);
-  }
-
-  @override
-  Widget build(final BuildContext context) {
-    final List<_Device> sortedDevices = devices.keys.map((name) =>
-        new _Device(name, devices[name])).toList(growable: false);
-    sortedDevices.sort((a, b) => a.description.compareTo(b.description));
-
-    final List<Widget> layout = [];
-
-    if (castTargetName == null) {
-      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)
-      ));
-    }
-
-    layout.add(new Input(
-      value: data,
-      labelText: 'Text content',
-      onChanged: (value) => setState(() => data = value)
-    ));
-
-    if (castTargetName == null) {
-      layout.add(new MaterialList(
-        type: MaterialListType.oneLine,
-        children: sortedDevices.map((d) => new ListItem(
-          title: new Text(d.description),
-          onTap: () => castTargetName = d.name
-        ))
-      ));
-    } else {
-      layout.add(new RaisedButton(
-        child: new Text('Recall'),
-        onPressed: terminateCast
-      ));
-    }
-
-    return new Scaffold(
-      appBar: new AppBar(
-        title: new Text('Baku Distro Example')
-      ),
-      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']);
-
+Widget _buildContainer(final BuildContext context) {
+  Future<String> _runApp(_) async {
+    Navigator.popAndPushNamed(context, '/app');
     return null;
   }
 
-  Future<String> _onDeviceOffline(final String name) async {
-    setState(() {
-      devices.remove(name);
-      if (castTargetName == name) {
-        terminateCast();
-      }
-    });
+  Future<String> _runHost(_) async {
+    Navigator.popAndPushNamed(context, '/host');
     return null;
   }
 
-  void _handleCastError(final Object e) {
-    Scaffold.of(context).showSnackBar(
-      new SnackBar(content: new Text(e.toString())));
-    terminateCast();
-  }
+  HostMessages.addMessageHandler('runApp', _runApp);
+  HostMessages.addMessageHandler('runHost', _runHost);
+
+  return new Material();
 }
diff --git a/examples/distro/app/src/main/AndroidManifest.xml b/examples/distro/app/src/main/AndroidManifest.xml
index 8c5f6c2..07dd42e 100644
--- a/examples/distro/app/src/main/AndroidManifest.xml
+++ b/examples/distro/app/src/main/AndroidManifest.xml
@@ -21,6 +21,13 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+        <activity
+            android:name=".HostActivity"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+            android:hardwareAccelerated="true"
+            android:launchMode="singleTop"
+            android:theme="@android:style/Theme.Black.NoTitleBar"
+            android:windowSoftInputMode="adjustResize"/>
         <service
             android:name=".DistroAndroidService"
             android:exported="false"/>
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 836f960..b3b0535 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
@@ -8,10 +8,10 @@
 
 package io.baku.examples.distro;
 
+import android.app.Activity;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.os.Bundle;
-import android.support.v4.app.FragmentActivity;
 import android.util.Log;
 
 import com.google.common.util.concurrent.FutureCallback;
@@ -46,7 +46,7 @@
 /**
  * Activity representing the example 'app', a.k.a. the initiator/originator/master.
  */
-public class DistroActivity extends FragmentActivity {
+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 long POLL_INTERVAL = 750;
@@ -61,6 +61,8 @@
     public void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
+        poller = new ScheduledThreadPoolExecutor(1);
+
         FlutterMain.ensureInitializationComplete(getApplicationContext(), null);
         setContentView(R.layout.flutter_layout);
 
@@ -69,23 +71,23 @@
                 FlutterMain.APP_BUNDLE);
         flutterView.runFromBundle(appBundle.getPath(), null);
 
-        poller = new ScheduledThreadPoolExecutor(1);
+        flutterView.sendToFlutter("runApp", "", r -> {
+            context = VAndroidContexts.withDefaults(this, savedInstanceState);
 
-        context = VAndroidContexts.withDefaults(this, savedInstanceState);
+            Futures.addCallback(BlessingsManager
+                            .getBlessings(context.getVContext(), this, "blessings", true),
+                    new FutureCallback<Blessings>() {
+                        @Override
+                        public void onSuccess(final Blessings blessings) {
+                            onBlessingsAvailable(blessings);
+                        }
 
-        Futures.addCallback(BlessingsManager
-                        .getBlessings(context.getVContext(), this, "blessings", true),
-                new FutureCallback<Blessings>() {
-                    @Override
-                    public void onSuccess(final Blessings blessings) {
-                        onBlessingsAvailable(blessings);
-                    }
-
-                    @Override
-                    public void onFailure(final Throwable t) {
-                        Log.e(TAG, "Unable to attain blessings", t);
-                    }
-                });
+                        @Override
+                        public void onFailure(final Throwable t) {
+                            Log.e(TAG, "Unable to attain blessings", t);
+                        }
+                    });
+        });
     }
 
     private void onBlessingsAvailable(final Blessings blessings) {
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 0c464c4..b29ab2a 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
@@ -15,9 +15,14 @@
 import android.util.Log;
 
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.SettableFuture;
 import com.jaredrummler.android.device.DeviceName;
 
+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.rpc.Server;
@@ -29,10 +34,23 @@
 import io.v.v23.vdl.ServerRecvStream;
 import io.v.v23.verror.VException;
 import io.v.v23.vom.VomUtil;
+import lombok.RequiredArgsConstructor;
 
 public class DistroAndroidService extends Service {
     private static final String TAG = DistroAndroidService.class.getSimpleName();
 
+    @RequiredArgsConstructor
+    public static class Session {
+        public final SettableFuture<Void> completion;
+        public final ServerRecvStream<State> stream;
+    }
+
+    private static Map<String, Session> sessions = new HashMap<>();
+
+    public static Session getSession(final String key) {
+        return sessions.get(key);
+    }
+
     public static final String
             BLESSINGS_EXTRA = "Blessings";
 
@@ -98,8 +116,24 @@
             @Override
             public ListenableFuture<Void> cast(final VContext context, final ServerCall call,
                                                final ServerRecvStream<State> stream) {
-                Log.i(TAG, "BAD WOLF");
-                return null;
+                final String key = UUID.randomUUID().toString();
+                final SettableFuture<Void> completion = SettableFuture.create();
+                completion.addListener(() -> sessions.remove(key), MoreExecutors.directExecutor());
+
+                sessions.put(key, new Session(completion, stream));
+
+                final Intent intent = new Intent(DistroAndroidService.this, HostActivity.class);
+                intent.putExtra(HostActivity.SESSION_EXTRA, key);
+                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+                try {
+                    startActivity(intent);
+                } catch (final Exception e) {
+                    completion.setException(e);
+                    Log.e(TAG, e.getMessage(), e);
+                }
+
+                return completion;
             }
         };
 
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
new file mode 100644
index 0000000..766ce7a
--- /dev/null
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/HostActivity.java
@@ -0,0 +1,93 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// 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;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+
+import org.chromium.base.PathUtils;
+
+import java.io.File;
+
+import io.flutter.view.FlutterMain;
+import io.flutter.view.FlutterView;
+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 FlutterView flutterView;
+    private DistroAndroidService.Session session;
+
+    private final FutureCallback<State> render = new FutureCallback<State>() {
+        @Override
+        public void onSuccess(final State state) {
+            flutterView.sendToFlutter("updateCast", state.getValue());
+            Futures.addCallback(session.stream.recv(), render);
+        }
+
+        @Override
+        public void onFailure(final Throwable t) {
+            if (!(t instanceof EndOfFileException)) {
+                Log.e(TAG, "Unexpected error while hosting cast", t);
+            }
+            finish();
+        }
+    };
+
+    @Override
+    public void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        FlutterMain.ensureInitializationComplete(getApplicationContext(), null);
+        setContentView(R.layout.flutter_layout);
+
+        flutterView = (FlutterView) findViewById(R.id.flutter_view);
+        File appBundle = new File(PathUtils.getDataDirectory(this),
+                FlutterMain.APP_BUNDLE);
+        flutterView.runFromBundle(appBundle.getPath(), null);
+
+        flutterView.sendToFlutter("runHost", "", r -> {
+            Futures.addCallback(session.stream.recv(), render);
+        });
+
+        session = DistroAndroidService.getSession(getIntent().getStringExtra(SESSION_EXTRA));
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        flutterView.onPause();
+
+        finish();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        flutterView.onResume();
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (flutterView != null) {
+            flutterView.destroy();
+        }
+
+        session.completion.set(null);
+
+        super.onDestroy();
+    }
+}