Adding text echo UI

Also fixing up discovery logic.

Change-Id: Ic30e3e0ab6d547850cd3b41504daef5edfa202fe
diff --git a/examples/distro/app/src/flutter/lib/main.dart b/examples/distro/app/src/flutter/lib/main.dart
index 6f366ca..6772b09 100644
--- a/examples/distro/app/src/flutter/lib/main.dart
+++ b/examples/distro/app/src/flutter/lib/main.dart
@@ -32,13 +32,18 @@
 }
 
 class _BakuDistroState extends State<BakuDistro> {
-  Map<String, String> devices = {};
+  final Map<String, String> devices = {};
+  InputValue data = InputValue.empty;
 
   _BakuDistroState() {
     HostMessages.addMessageHandler('deviceOnline', _onDeviceOnline);
     HostMessages.addMessageHandler('deviceOffline', _onDeviceOffline);
   }
 
+  void castTo(final String name) {
+
+  }
+
   @override
   Widget build(final BuildContext context) {
     final List<_Device> sortedDevices = devices.keys.map((name) =>
@@ -49,11 +54,28 @@
       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)
-        ))
+      body: new Column(
+        children: <Widget>[
+          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)
+          ),
+          new Input(
+            value: data,
+            labelText: 'Text content',
+            onChanged: (value) => setState(() => data = value)
+          ),
+          new MaterialList(
+            type: MaterialListType.oneLine,
+            children: sortedDevices.map((d) => new ListItem(
+              title: new Text(d.description),
+              onTap: () => castTo(d.name)
+            ))
+          )
+        ]
       )
     );
   }
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
deleted file mode 100644
index 1cee39b..0000000
--- a/examples/distro/app/src/main/java/io/baku/examples/distro/Connection.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// 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/DistroActivity.java b/examples/distro/app/src/main/java/io/baku/examples/distro/DistroActivity.java
index c45b373..cace0c5 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
@@ -30,6 +30,9 @@
 import java.io.File;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 
 import io.flutter.view.FlutterMain;
 import io.flutter.view.FlutterView;
@@ -48,10 +51,12 @@
 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 static final long POLL_INTERVAL = 750;
 
     private VAndroidContext context;
     private FlutterView flutterView;
+    private final Map<String, ConnectionMonitor> clients = new HashMap<>();
+    private ScheduledExecutorService poller;
     private Subscription subscription;
 
     @Override
@@ -66,6 +71,8 @@
                 FlutterMain.APP_BUNDLE);
         flutterView.runFromBundle(appBundle.getPath(), null);
 
+        poller = new ScheduledThreadPoolExecutor(1);
+
         context = VAndroidContexts.withDefaults(this, savedInstanceState);
 
         Futures.addCallback(BlessingsManager
@@ -108,6 +115,9 @@
             subscription.unsubscribe();
         }
 
+        poller.shutdown();
+        clients.clear();
+
         if (flutterView != null) {
             flutterView.destroy();
         }
@@ -143,34 +153,57 @@
         }
     }
 
+    private boolean isActive() {
+        return subscription != null && !subscription.isUnsubscribed();
+    }
+
+    private class ConnectionMonitor implements FutureCallback<String> {
+        final String name;
+        final DistroClient client;
+
+        private ListenableFuture<String> poll;
+
+        public ConnectionMonitor(final String name) {
+            this.name = name;
+            client = DistroClientFactory.getDistroClient(name);
+
+            poll();
+        }
+
+        public void poll() {
+            poll = client.getDescription(context.getVContext().withTimeout(PING_TIMEOUT));
+            Futures.addCallback(poll, this);
+        }
+
+        @Override
+        public void onSuccess(final String description) {
+            if (isActive()) {
+                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());
+
+                poller.schedule(this::poll, POLL_INTERVAL, TimeUnit.MILLISECONDS);
+            }
+        }
+
+        @Override
+        public void onFailure(final Throwable t) {
+            if (isActive()) {
+                flutterView.sendToFlutter("deviceOffline", name);
+
+                clients.remove(name);
+            }
+        }
+    }
+
     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));
+                .subscribe(name -> Maps.computeIfAbsent(clients, name, ConnectionMonitor::new),
+                        t -> context.getErrorReporter().onError(R.string.err_scan, t));
     }
 }