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();
+ }
+}