Initial commit of Distro example

This commit is based on the (obsolete) hello_android example (now
hello_services). It uses V23 for RPC and mount table globbing for
discovery (since global discovery is not exposed in Java and local
discovery works only through mDNS right now). It does not actually cast
components yet, but presents a list of (globally) available cast
targets.

This commit is subject to v.io/i/1356, and the VDL Java generation is
commented out.

Change-Id: Iba5116324109c48d7ab009d3d3759f6bc469d342
diff --git a/.gitignore b/.gitignore
index 543c46e..42d86f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,7 @@
 build/
 **/ios/.generated/
 packages
+generated-src
+*.iml
+.gradle
+local.properties
\ No newline at end of file
diff --git a/examples/distro/README.md b/examples/distro/README.md
new file mode 100644
index 0000000..61308bc
--- /dev/null
+++ b/examples/distro/README.md
@@ -0,0 +1,40 @@
+# Example for UI distribution (casting)
+
+This project demonstrates simple UI distribution across multiple
+devices. At this time, only unidirectional unicast is supported.
+
+## Building
+
+To build this project:
+
+* Create a `local.properties` file with these entries:
+  * `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]
+(https://vanadium.github.io/concepts/rpc.html) for communication and
+[global mount table globbing]
+(https://vanadium.github.io/concepts/naming.html) for discovery.
+
+Vanadium has a global discovery facility, but that is not currently
+surfaced in Java or Flutter/Dart. At present, all Vanadium usage is
+surfaced through Java, to Flutter as `HostMessages`.
+
+This is based on the [hello_services]
+(https://github.com/flutter/flutter/tree/master/examples/hello_services)
+Flutter example.
+
+## 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
diff --git a/examples/distro/app/build.gradle b/examples/distro/app/build.gradle
new file mode 100644
index 0000000..03221db
--- /dev/null
+++ b/examples/distro/app/build.gradle
@@ -0,0 +1,63 @@
+buildscript {
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath 'io.v:gradle-plugin:1.7'
+    }
+}
+
+apply plugin: 'android-sdk-manager'
+apply plugin: 'com.android.application'
+
+/*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).
+For detailed instructions, see https://github.com/evant/gradle-retrolambda
+ */
+apply plugin: 'me.tatarka.retrolambda'
+apply plugin: 'flutter'
+
+android {
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    compileSdkVersion 23
+    buildToolsVersion '23.0.1'
+
+    defaultConfig {
+        applicationId "io.baku.examples.distro"
+        minSdkVersion 21
+        multiDexEnabled true
+        targetSdkVersion 23
+        versionCode 1
+        versionName "1.0"
+    }
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+}
+
+flutter {
+    source 'src/flutter'
+}
+
+repositories {
+    jcenter()
+}
+
+dependencies {
+    provided 'org.projectlombok:lombok:1.16.8'
+    compile (
+            'com.jaredrummler:android-device-names:1.0.9',
+            'io.reactivex:rxjava:1.1.5',
+            'io.v:vanadium-android:2.1.9'
+    )
+}
\ 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
new file mode 100644
index 0000000..6f366ca
--- /dev/null
+++ b/examples/distro/app/src/flutter/lib/main.dart
@@ -0,0 +1,76 @@
+// 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.
+
+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();
+
+void main() {
+  runApp(new BakuDistro());
+}
+
+class BakuDistro extends StatefulWidget {
+  @override
+  _BakuDistroState createState() => new _BakuDistroState();
+}
+
+class _Device {
+  String name, description;
+
+  _Device(this.name, this.description);
+}
+
+class _BakuDistroState extends State<BakuDistro> {
+  Map<String, String> devices = {};
+
+  _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));
+
+    return new Scaffold(
+      appBar: new AppBar(
+        title: new Text('Baku Distro Example')
+      ),
+      body: new MaterialList(
+        type: MaterialListType.oneLine,
+        children: sortedDevices.map((d) => new ListItem(
+          title: new Text(d.description)
+        ))
+      )
+    );
+  }
+
+  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);
+    });
+    return null;
+  }
+}
diff --git a/examples/distro/app/src/flutter/pubspec.lock b/examples/distro/app/src/flutter/pubspec.lock
new file mode 100644
index 0000000..82ee346
--- /dev/null
+++ b/examples/distro/app/src/flutter/pubspec.lock
@@ -0,0 +1,298 @@
+# Generated by pub
+# See http://pub.dartlang.org/doc/glossary.html#lockfile
+packages:
+  analyzer:
+    description:
+      name: analyzer
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.27.4-alpha.6"
+  args:
+    description:
+      name: args
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.13.4+2"
+  async:
+    description:
+      name: async
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.11.0"
+  barback:
+    description:
+      name: barback
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.15.2+8"
+  boolean_selector:
+    description:
+      name: boolean_selector
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
+  charcode:
+    description:
+      name: charcode
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
+  collection:
+    description:
+      name: collection
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.8.0"
+  convert:
+    description:
+      name: convert
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.1"
+  crypto:
+    description:
+      name: crypto
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.9.2"
+  csslib:
+    description:
+      name: csslib
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.13.2"
+  flutter:
+    description:
+      path: "../../../../../deps/flutter/packages/flutter"
+      relative: true
+    source: path
+    version: "0.0.21"
+  flutter_test:
+    description:
+      path: "../../../../../deps/flutter/packages/flutter_test"
+      relative: true
+    source: path
+    version: "0.0.0"
+  glob:
+    description:
+      name: glob
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.2"
+  html:
+    description:
+      name: html
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.2+2"
+  http_multi_server:
+    description:
+      name: http_multi_server
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  http_parser:
+    description:
+      name: http_parser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  intl:
+    description:
+      name: intl
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.7+1"
+  logging:
+    description:
+      name: logging
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.11.3"
+  matcher:
+    description:
+      name: matcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.0+2"
+  meta:
+    description:
+      name: meta
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.1"
+  mime:
+    description:
+      name: mime
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.9.3"
+  mojo:
+    description:
+      name: mojo
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.4.20"
+  mojo_sdk:
+    description:
+      name: mojo_sdk
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.24"
+  mojo_services:
+    description:
+      name: mojo_services
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.4.27"
+  package_config:
+    description:
+      name: package_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.3"
+  path:
+    description:
+      name: path
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.9"
+  petitparser:
+    description:
+      name: petitparser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.5.3"
+  plugin:
+    description:
+      name: plugin
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.0"
+  pool:
+    description:
+      name: pool
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.4"
+  pub_semver:
+    description:
+      name: pub_semver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.4"
+  quiver:
+    description:
+      name: quiver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.21.4"
+  shelf:
+    description:
+      name: shelf
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.6.5+2"
+  shelf_static:
+    description:
+      name: shelf_static
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.3+4"
+  shelf_web_socket:
+    description:
+      name: shelf_web_socket
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.1"
+  sky_engine:
+    description:
+      path: "../../../../../deps/flutter/bin/cache/pkg/sky_engine"
+      relative: true
+    source: path
+    version: "0.0.99"
+  sky_services:
+    description:
+      path: "../../../../../deps/flutter/bin/cache/pkg/sky_services"
+      relative: true
+    source: path
+    version: "0.0.99"
+  source_map_stack_trace:
+    description:
+      name: source_map_stack_trace
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.4"
+  source_maps:
+    description:
+      name: source_maps
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.10.1+1"
+  source_span:
+    description:
+      name: source_span
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.2"
+  stack_trace:
+    description:
+      name: stack_trace
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.6.5"
+  stream_channel:
+    description:
+      name: stream_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.4.0"
+  string_scanner:
+    description:
+      name: string_scanner
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.4+1"
+  test:
+    description:
+      name: test
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.13+4"
+  typed_data:
+    description:
+      name: typed_data
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.3"
+  utf:
+    description:
+      name: utf
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.9.0+3"
+  vector_math:
+    description:
+      name: vector_math
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.3"
+  watcher:
+    description:
+      name: watcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.9.7+2"
+  web_socket_channel:
+    description:
+      name: web_socket_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
+  yaml:
+    description:
+      name: yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.9"
+sdk: ">=1.16.0 <1.18.0"
diff --git a/examples/distro/app/src/flutter/pubspec.yaml b/examples/distro/app/src/flutter/pubspec.yaml
new file mode 100644
index 0000000..4f57eb8
--- /dev/null
+++ b/examples/distro/app/src/flutter/pubspec.yaml
@@ -0,0 +1,9 @@
+name: gradle
+
+dependencies:
+  flutter:
+    path: ../../../../../deps/flutter/packages/flutter
+
+dev_dependencies:
+  flutter_test:
+    path: ../../../../../deps/flutter/packages/flutter_test
diff --git a/examples/distro/app/src/main/AndroidManifest.xml b/examples/distro/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8c5f6c2
--- /dev/null
+++ b/examples/distro/app/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="io.baku.examples.distro"
+          xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+
+    <application
+        android:name="org.domokit.sky.shell.SkyApplication"
+        android:label="@string/app_name">
+        <activity
+            android:name=".DistroActivity"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+            android:hardwareAccelerated="true"
+            android:launchMode="singleTop"
+            android:theme="@android:style/Theme.Black.NoTitleBar"
+            android:windowSoftInputMode="adjustResize">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <service
+            android:name=".DistroAndroidService"
+            android:exported="false"/>
+    </application>
+
+</manifest>
diff --git a/examples/distro/app/src/main/java/io/baku/examples/distro/Connection.java b/examples/distro/app/src/main/java/io/baku/examples/distro/Connection.java
new file mode 100644
index 0000000..1cee39b
--- /dev/null
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/Connection.java
@@ -0,0 +1,30 @@
+// 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.
+
+package io.baku.examples.distro;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import io.v.v23.context.VContext;
+
+public class Connection {
+    private final DistroClient client;
+
+    public Connection(final String name) {
+        client = DistroClientFactory.getDistroClient(name);
+    }
+
+    private ListenableFuture<String> opInProgress;
+
+    public ListenableFuture<String> pollDescription(final VContext vContext) {
+        if (opInProgress == null) {
+            opInProgress = client.getDescription(vContext);
+            opInProgress.addListener(() -> opInProgress = null, MoreExecutors.directExecutor());
+            return opInProgress;
+        } else {
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/examples/distro/app/src/main/java/io/baku/examples/distro/DeviceId.java b/examples/distro/app/src/main/java/io/baku/examples/distro/DeviceId.java
new file mode 100644
index 0000000..4f2a420
--- /dev/null
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/DeviceId.java
@@ -0,0 +1,19 @@
+// 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.
+
+package io.baku.examples.distro;
+
+import android.content.Context;
+import android.provider.Settings;
+
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class DeviceId {
+    public static String get(final Context context) {
+        return Settings.Secure.getString(
+                context.getContentResolver(),
+                Settings.Secure.ANDROID_ID);
+    }
+}
diff --git a/examples/distro/app/src/main/java/io/baku/examples/distro/Disco.java b/examples/distro/app/src/main/java/io/baku/examples/distro/Disco.java
new file mode 100644
index 0000000..27b2a86
--- /dev/null
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/Disco.java
@@ -0,0 +1,52 @@
+// 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.
+
+package io.baku.examples.distro;
+
+import android.content.Context;
+import android.util.Log;
+
+import org.joda.time.Duration;
+
+import java.util.concurrent.TimeUnit;
+
+import io.v.android.VAndroidContext;
+import io.v.android.v23.V;
+import io.v.v23.naming.MountEntry;
+import lombok.experimental.UtilityClass;
+import rx.Observable;
+
+@UtilityClass
+public class Disco {
+    private static final String TAG = Disco.class.getSimpleName();
+    private static final Duration GLOB_TIMEOUT = Duration.standardSeconds(2);
+    private static final long SCAN_PERIOD = 750;
+
+    public static String name(final Context context) {
+        return "tmp/baku-disco/" + DeviceId.get(context);
+    }
+
+    public static Observable<String> scanOnce(final VAndroidContext<?> context) {
+        return RxInputChannel.wrap(V.getNamespace(context.getVContext())
+                .glob(context.getVContext().withTimeout(GLOB_TIMEOUT), "tmp/baku-disco/*"))
+                .refCount()
+                .flatMap(g -> {
+                    if (g.getElem() instanceof MountEntry) {
+                        return Observable.just(((MountEntry)g.getElem()).getName());
+                    } else {
+                        Log.e(TAG, "Unsupported glob response " + g);
+                        return Observable.empty();
+                    }
+                })
+                .filter(n -> !name(context.getAndroidContext()).equals(n));
+    }
+
+    public static Observable<String> scanContinuously(final VAndroidContext<?> context) {
+        return Observable.interval(SCAN_PERIOD, TimeUnit.MILLISECONDS)
+                .switchMap(x -> scanOnce(context).onErrorResumeNext(t -> {
+                    Log.e(TAG, t.getMessage(), t);
+                    return Observable.empty();
+                }));
+    }
+}
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
new file mode 100644
index 0000000..c45b373
--- /dev/null
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/DistroActivity.java
@@ -0,0 +1,176 @@
+// 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.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.FragmentActivity;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.chromium.base.PathUtils;
+import org.joda.time.Duration;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+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.security.Blessings;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+import java8.util.Maps;
+import rx.Subscription;
+
+/**
+ * Activity representing the example 'app', a.k.a. the initiator/originator/master.
+ */
+public class DistroActivity extends FragmentActivity implements GoogleApiClient.OnConnectionFailedListener {
+    private static final String TAG = DistroActivity.class.getSimpleName();
+    private static final Duration PING_TIMEOUT = Duration.standardSeconds(2);
+    private static final long DISCO_DEBOUNCE = 250;
+
+    private VAndroidContext context;
+    private FlutterView flutterView;
+    private Subscription subscription;
+
+    @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);
+
+        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);
+                    }
+
+                    @Override
+                    public void onFailure(final Throwable t) {
+                        Log.e(TAG, "Unable to attain blessings", t);
+                    }
+                });
+    }
+
+    @Override
+    public void onConnectionFailed(final @NonNull ConnectionResult connectionResult) {
+        Toast.makeText(this, connectionResult.getErrorMessage(), Toast.LENGTH_LONG).show();
+    }
+
+    private void onBlessingsAvailable(final Blessings blessings) {
+        final Intent castIntent = new Intent(DistroActivity.this,
+                DistroAndroidService.class);
+        try {
+            castIntent.putExtra(DistroAndroidService.BLESSINGS_EXTRA,
+                    VomUtil.encodeToString(blessings, Blessings.class));
+        } catch (final VException e) {
+            Log.e(TAG, "Unable to encode blessings", e);
+        }
+        startService(castIntent);
+
+        subscription = startScanning();
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (subscription != null) {
+            subscription.unsubscribe();
+        }
+
+        if (flutterView != null) {
+            flutterView.destroy();
+        }
+
+        context.close();
+
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        flutterView.onPause();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        flutterView.onResume();
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        // Reload the Flutter Dart code when the activity receives an intent
+        // from the "flutter refresh" command.
+        // This feature should only be enabled during development.  Use the
+        // debuggable flag as an indicator that we are in development mode.
+        if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
+            if (Intent.ACTION_RUN.equals(intent.getAction())) {
+                flutterView.runFromBundle(intent.getDataString(),
+                        intent.getStringExtra("snapshot"));
+            }
+        }
+    }
+
+    private Subscription startScanning() {
+        final Map<String, Connection> clients = new HashMap<>();
+
+        return Disco.scanContinuously(context)
+                .subscribe(name -> {
+                    final Connection conn = Maps.computeIfAbsent(clients, name, Connection::new);
+                    ListenableFuture<String> descFuture = conn
+                            .pollDescription(context.getVContext().withTimeout(PING_TIMEOUT));
+                    if (descFuture != null) {
+                        Futures.addCallback(descFuture, new FutureCallback<String>() {
+                            @Override
+                            public void onSuccess(final String description) {
+                                final JSONObject message = new JSONObject();
+                                try {
+                                    message.put("name", name);
+                                    message.put("description", description);
+                                } catch (final JSONException wtf) {
+                                    throw new RuntimeException(wtf);
+                                }
+                                flutterView.sendToFlutter("deviceOnline", message.toString());
+                            }
+
+                            @Override
+                            public void onFailure(final Throwable t) {
+                                flutterView.sendToFlutter("deviceOffline", name);
+                            }
+                        });
+                    }
+                }, t -> context.getErrorReporter().onError(R.string.err_scan, t));
+    }
+}
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
new file mode 100644
index 0000000..0c464c4
--- /dev/null
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/DistroAndroidService.java
@@ -0,0 +1,132 @@
+// 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.
+
+package io.baku.examples.distro;
+
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.IBinder;
+import android.provider.ContactsContract;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.jaredrummler.android.device.DeviceName;
+
+import io.v.android.v23.V;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.Server;
+import io.v.v23.rpc.ServerCall;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.Blessings;
+import io.v.v23.security.VPrincipal;
+import io.v.v23.security.VSecurity;
+import io.v.v23.vdl.ServerRecvStream;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+
+public class DistroAndroidService extends Service {
+    private static final String TAG = DistroAndroidService.class.getSimpleName();
+
+    public static final String
+            BLESSINGS_EXTRA = "Blessings";
+
+    private VContext vContext;
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        vContext = V.init(this);
+
+        final VPrincipal principal = V.getPrincipal(vContext);
+
+        final Blessings blessings;
+        try {
+            blessings = (Blessings) VomUtil.decodeFromString(
+                    intent.getStringExtra(BLESSINGS_EXTRA), Blessings.class);
+
+            principal.blessingStore().setDefaultBlessings(blessings);
+            principal.blessingStore().set(blessings, new BlessingPattern("..."));
+            VSecurity.addToRoots(principal, blessings);
+        } catch (final VException e) {
+            // TODO(rosswang): handle this better
+            Log.e(TAG, "Unable to assume blessings", e);
+        }
+
+        final DistroServer handlers = new DistroServer() {
+            @Override
+            public ListenableFuture<String> getDescription(final VContext context,
+                                                           final ServerCall call) {
+                final SettableFuture<String> description = SettableFuture.create();
+
+                DeviceName.with(DistroAndroidService.this).request((i, e) -> {
+                    final String owner;
+
+                    final ContentResolver cr = getContentResolver();
+                    try (final Cursor c = cr.query(Uri.withAppendedPath(
+                            ContactsContract.Profile.CONTENT_URI,
+                            ContactsContract.Contacts.Data.CONTENT_DIRECTORY),
+                            null, null, null, null)) {
+
+                        if (c.getCount() > 0) {
+                            c.moveToFirst();
+                            owner = c.getString(c.getColumnIndex(
+                                    ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)) +
+                                    "'s ";
+                        } else {
+                            owner = "";
+                        }
+                    }
+
+                    final String device = e == null ? i.getName() : DeviceName.getDeviceName();
+                    description.set(owner + device);
+                });
+
+                return description;
+            }
+
+            @Override
+            public ListenableFuture<Void> cast(final VContext context, final ServerCall call,
+                                               final ServerRecvStream<State> stream) {
+                Log.i(TAG, "BAD WOLF");
+                return null;
+            }
+        };
+
+        VContext listenContextCandidate;
+        try {
+            listenContextCandidate = V.withListenSpec(vContext,
+                    V.getListenSpec(vContext).withProxy("proxy"));
+        } catch (final VException e) {
+            listenContextCandidate = vContext;
+            Log.e(TAG, "Unable to listen on proxy", e);
+        }
+
+        final VContext listenContext = listenContextCandidate;
+
+        final Server s;
+        try {
+            s = V.getServer(V.withNewServer(listenContext, Disco.name(this), handlers,
+                    VSecurity.newAllowEveryoneAuthorizer()));
+        } catch (final VException e) {
+            throw new RuntimeException(e);
+        }
+
+        return START_REDELIVER_INTENT;
+    }
+
+    @Override
+    public void onDestroy() {
+        vContext.cancel();
+    }
+}
diff --git a/examples/distro/app/src/main/java/io/baku/examples/distro/RxInputChannel.java b/examples/distro/app/src/main/java/io/baku/examples/distro/RxInputChannel.java
new file mode 100644
index 0000000..121db9d
--- /dev/null
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/RxInputChannel.java
@@ -0,0 +1,45 @@
+// Copyright 2015 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.
+
+package io.baku.examples.distro;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+
+import io.v.v23.InputChannel;
+import io.v.v23.verror.EndOfFileException;
+import lombok.experimental.UtilityClass;
+import rx.Observable;
+import rx.Subscriber;
+import rx.observables.ConnectableObservable;
+
+@UtilityClass
+public class RxInputChannel {
+    /**
+     * Wraps an {@link io.v.v23.InputChannel} in a connectable observable that produces the same
+     * elements.
+     */
+    public static <T> ConnectableObservable<T> wrap(final InputChannel<T> i) {
+        return Observable.<T>create(s -> connect(i, s)).publish();
+    }
+
+    private static <T> void connect(final InputChannel<T> i, final Subscriber<? super T> s) {
+        Futures.addCallback(i.recv(), new FutureCallback<T>() {
+            @Override
+            public void onSuccess(final T r) {
+                s.onNext(r);
+                connect(i, s);
+            }
+
+            @Override
+            public void onFailure(final Throwable t) {
+                if (t instanceof EndOfFileException) {
+                    s.onCompleted();
+                } else {
+                    s.onError(t);
+                }
+            }
+        });
+    }
+}
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
new file mode 100644
index 0000000..2c1465d
--- /dev/null
+++ b/examples/distro/app/src/main/java/io/baku/examples/distro/distro.vdl
@@ -0,0 +1,12 @@
+// 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.
+
+package distro
+
+type State string
+
+type Distro interface {
+    Cast() stream<State, _> error
+    GetDescription() (string | error)
+}
\ No newline at end of file
diff --git a/examples/distro/app/src/main/res/layout/flutter_layout.xml b/examples/distro/app/src/main/res/layout/flutter_layout.xml
new file mode 100644
index 0000000..90165c0
--- /dev/null
+++ b/examples/distro/app/src/main/res/layout/flutter_layout.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="fill_parent"
+              android:layout_height="fill_parent"
+              android:orientation="vertical">
+    <io.flutter.view.FlutterView
+        android:id="@+id/flutter_view"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"/>
+</LinearLayout>
diff --git a/examples/distro/app/src/main/res/values/strings.xml b/examples/distro/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..56d0277
--- /dev/null
+++ b/examples/distro/app/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">Baku Distro</string>
+    <string name="err_scan">Error scanning for other devices</string>
+</resources>
diff --git a/examples/distro/build.gradle b/examples/distro/build.gradle
new file mode 100644
index 0000000..456fe0a
--- /dev/null
+++ b/examples/distro/build.gradle
@@ -0,0 +1,27 @@
+buildscript {
+    repositories {
+        jcenter()
+        maven { url "https://jitpack.io" }
+    }
+
+    dependencies {
+        classpath (
+                'com.android.tools.build:gradle:2.1.0',
+                // 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
+                // be compatible with gradle 2.0.0.
+                // https://github.com/JakeWharton/sdk-manager-plugin/issues/99
+                'com.github.JakeWharton:sdk-manager-plugin:master',
+                'me.tatarka:gradle-retrolambda:3.2.4'
+        )
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
+
+task wrapper(type: Wrapper) {
+    gradleVersion = '2.13'
+}
diff --git a/examples/distro/buildSrc/build.gradle b/examples/distro/buildSrc/build.gradle
new file mode 100644
index 0000000..e78d924
--- /dev/null
+++ b/examples/distro/buildSrc/build.gradle
@@ -0,0 +1,7 @@
+repositories {
+  jcenter()
+}
+
+dependencies {
+  compile "com.android.tools.build:gradle:2.1.0"
+}
diff --git a/examples/distro/buildSrc/src/main/groovy/FlutterPlugin.groovy b/examples/distro/buildSrc/src/main/groovy/FlutterPlugin.groovy
new file mode 100644
index 0000000..540ac8d
--- /dev/null
+++ b/examples/distro/buildSrc/src/main/groovy/FlutterPlugin.groovy
@@ -0,0 +1,135 @@
+// 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 org.domokit.sky.gradle
+
+import com.android.builder.model.AndroidProject
+import com.google.common.base.Joiner
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+import org.gradle.api.Plugin
+import org.gradle.api.Task
+import org.gradle.api.file.FileCollection
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+
+class FlutterPlugin implements Plugin<Project> {
+    private File sdkDir
+    private File engineSrcDir
+
+    @Override
+    void apply(Project project) {
+        Properties properties = new Properties()
+        properties.load(project.rootProject.file("local.properties").newDataInputStream())
+
+        String sdkPath = properties.getProperty("flutter.sdk")
+        if (sdkPath == null) {
+            throw new GradleException("flutter.sdk must be defined in local.properties")
+        }
+        sdkDir = project.file(sdkPath)
+        if (!sdkDir.isDirectory()) {
+            throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
+        }
+
+        File flutterJar
+        String flutterJarPath = properties.getProperty("flutter.jar")
+        if (flutterJarPath != null) {
+            flutterJar = project.file(flutterJarPath)
+            if (!flutterJar.isFile()) {
+                throw new GradleException("flutter.jar must point to a Flutter engine JAR")
+            }
+        } else {
+            flutterJar = new File(sdkDir, Joiner.on(File.separatorChar).join(
+                "bin", "cache", "artifacts", "engine", "android-arm", "flutter.jar"))
+            if (!flutterJar.isFile()) {
+                project.exec {
+                    executable "${sdkDir}/bin/flutter"
+                    args "precache"
+                }
+                if (!flutterJar.isFile()) {
+                    throw new GradleException("Unable to find flutter.jar in SDK: ${flutterJar}")
+                }
+            }
+        }
+
+        String engineSrcPath = properties.getProperty("flutter.engineSrcPath")
+        if (engineSrcPath != null) {
+            engineSrcDir = project.file(engineSrcPath)
+            if (!engineSrcDir.isDirectory()) {
+                throw new GradleException("flutter.engineSrcPath must be a Flutter engine source directory")
+            }
+        }
+
+        project.extensions.create("flutter", FlutterExtension)
+        project.dependencies.add("compile", project.files(flutterJar))
+        project.afterEvaluate this.&addFlutterTask
+    }
+
+    private void addFlutterTask(Project project) {
+        if (project.flutter.source == null) {
+            throw new GradleException("Must provide Flutter source directory")
+        }
+
+        FlutterTask flutterTask = project.tasks.create("flutterBuild", FlutterTask) {
+            sdkDir this.sdkDir
+            sourceDir project.file(project.flutter.source)
+            intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter")
+            engineSrcDir this.engineSrcDir
+        }
+
+        project.android.applicationVariants.all { variant ->
+            Task copyFlxTask = project.tasks.create(name: "copyFlx${variant.name.capitalize()}", type: Copy) {
+                dependsOn flutterTask
+                dependsOn variant.mergeAssets
+                from flutterTask.flxPath
+                into variant.mergeAssets.outputDir
+            }
+            variant.outputs[0].processResources.dependsOn(copyFlxTask)
+        }
+    }
+}
+
+class FlutterExtension {
+    String source
+}
+
+class FlutterTask extends DefaultTask {
+    File sdkDir
+
+    @InputDirectory
+    File sourceDir
+
+    @OutputDirectory
+    File intermediateDir
+
+    File engineSrcDir
+
+    String getFlxPath() {
+        return "${intermediateDir}/app.flx"
+    }
+
+    @TaskAction
+    void build() {
+        if (!sourceDir.isDirectory()) {
+            throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
+        }
+
+        intermediateDir.mkdirs()
+        project.exec {
+            executable "${sdkDir}/bin/flutter"
+            workingDir sourceDir
+            if (engineSrcDir != null) {
+              args "--engine-src-path", engineSrcDir
+            }
+            args "build", "flx"
+            args "-o", flxPath
+            args "--snapshot", "${intermediateDir}/snapshot_blob.bin"
+            args "--depfile", "${intermediateDir}/snapshot_blob.bin.d"
+            args "--working-dir", "${intermediateDir}/flx"
+        }
+    }
+}
diff --git a/examples/distro/buildSrc/src/main/resources/META-INF/gradle-plugins/flutter.properties b/examples/distro/buildSrc/src/main/resources/META-INF/gradle-plugins/flutter.properties
new file mode 100644
index 0000000..f8219be
--- /dev/null
+++ b/examples/distro/buildSrc/src/main/resources/META-INF/gradle-plugins/flutter.properties
@@ -0,0 +1 @@
+implementation-class=org.domokit.sky.gradle.FlutterPlugin
diff --git a/examples/distro/gradle/wrapper/gradle-wrapper.jar b/examples/distro/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..ca78035
--- /dev/null
+++ b/examples/distro/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/examples/distro/gradle/wrapper/gradle-wrapper.properties b/examples/distro/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..57b8891
--- /dev/null
+++ b/examples/distro/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 03 18:36:35 PDT 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-all.zip
diff --git a/examples/distro/gradlew b/examples/distro/gradlew
new file mode 100755
index 0000000..27309d9
--- /dev/null
+++ b/examples/distro/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/examples/distro/gradlew.bat b/examples/distro/gradlew.bat
new file mode 100644
index 0000000..f6d5974
--- /dev/null
+++ b/examples/distro/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windows variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+if "%@eval[2+2]" == "4" goto 4NT_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+goto execute

+

+:4NT_args

+@rem Get arguments from the 4NT Shell from JP Software

+set CMD_LINE_ARGS=%$

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/examples/distro/settings.gradle b/examples/distro/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/examples/distro/settings.gradle
@@ -0,0 +1 @@
+include ':app'