syncslides: Discovery and some UI polish

screenshots: https://goo.gl/photos/SpCoP5JRDVKgcX5F8

-Devices can now discover each other and display
live presentations from other devices.
-Deck and List view Material design polish.
-Demo loader no longer periodically adds/removes decks,
instead the "Add" button loads a random deck.

No sycing yet, next CL will focus on driving and
joining a presentaion.

Change-Id: I58ca3241ddf49215dc2ab002ea9918d44ef62c5c
diff --git a/dart/Makefile b/dart/Makefile
index 88eef82..922d478 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -1,16 +1,20 @@
-ifdef DEVICE_NUM
-
-ifneq ($(DEVICE_NUM), 1)
-	REUSE_FLAG := --reuse-server
+ifndef DEVICE_NUM
+	DEVICE_NUM := 1
 endif
 
+ifneq ($(DEVICE_NUM), 1)
+	REUSE_FLAG := --reuse-servers
+endif
+
+ifdef VLOG
+	VLOG_FLAGS = --v=$(VLOG) --logtostderr=true
+endif
+
+SYNCBASE_DATA_DIR=/data/data/org.chromium.mojo.shell/app_home/syncbasedata
 DEVICE_NUM_PLUS_ONE := $(shell echo $(DEVICE_NUM) \+ 1 | bc)
 DEVICE_ID := $(shell adb devices | sed -n $(DEVICE_NUM_PLUS_ONE)p | awk '{ print $$1; }')
 DEVICE_FLAG := --target-device $(DEVICE_ID)
-
-endif
-
-
+MOUNTTABLE_ADDR := /192.168.86.254:8101
 
 default: run
 
@@ -33,7 +37,18 @@
 # DEVICE_NUM=1 make run
 # DEVICE_NUM=2 make run
 run: packages
-	pub run sky_tools build && pub run sky_tools run_mojo --mojo-path $(MOJO_DIR)/src/mojo/devtools/common/mojo_run --android --mojo-debug -- --enable-multiprocess --map-origin="https://syncslides.mojo.v.io/=$(PWD)" --args-for="https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo --v=1 --v23.namespace.root=/ns.dev.v.io:8101 $(NAME_FLAG) --logtostderr=true --root-dir=/data/data/org.chromium.mojo.shell/app_home/syncbasedata" $(DEVICE_FLAG) --no-config-file $(REUSE_FLAG)
+	pub run sky_tools build && pub run sky_tools run_mojo \
+	--mojo-path $(MOJO_DIR)/src/mojo/devtools/common/mojo_run \
+	--android --mojo-debug -- --enable-multiprocess \
+	--map-origin="https://syncslides.mojo.v.io/=$(PWD)" \
+	--map-origin="https://discovery.mojo.v.io/=$(JIRI_ROOT)/release/mojo/discovery/gen/mojo/android" \
+	--args-for="https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo \
+	--root-dir=$(SYNCBASE_DATA_DIR) \
+	--v23.namespace.root=$(MOUNTTABLE_ADDR) \
+	$(VLOG_FLAGS)" \
+	$(DEVICE_FLAG) \
+	$(REUSE_FLAG) \
+	--no-config-file
 
 # Helper targets
 run1:
@@ -45,7 +60,18 @@
 run4:
 	DEVICE_NUM=4 make run
 
+.PHONY: uninstall
+uninstall:
+	adb -s $(DEVICE_ID) uninstall org.chromium.mojo.shell
+
 .PHONY: clean
 clean:
 	rm -f app.flx snapshot_blob.bin
 	rm -rf packages
+
+.PHONY: clean-syncbase
+clean-syncbase:
+	adb -s $(DEVICE_ID) shell run-as org.chromium.mojo.shell rm -rf $(SYNCBASE_DATA_DIR)
+
+.PHONY: very-clean
+.very-clean: clean clean-syncbase
diff --git a/dart/assets/images/defaults/thumbnail.png b/dart/assets/images/defaults/thumbnail.png
new file mode 100644
index 0000000..aaf9a77
--- /dev/null
+++ b/dart/assets/images/defaults/thumbnail.png
Binary files differ
diff --git a/dart/flutter.yaml b/dart/flutter.yaml
index 13c17d3..2da469b 100644
--- a/dart/flutter.yaml
+++ b/dart/flutter.yaml
@@ -1,3 +1,5 @@
 name: syncslides
 material-design-icons:
   - name: navigation/arrow_back
+  - name: navigation/arrow_forward
+  - name: content/add
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
index 410dad5..479e995 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -7,6 +7,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 
+import '../loaders/loader.dart';
 import '../models/all.dart' as model;
 import '../stores/store.dart';
 import '../styles/common.dart' as style;
@@ -15,9 +16,14 @@
 
 // DeckGridPage is the full page view of the list of decks.
 class DeckGridPage extends StatelessComponent {
+  Loader _loader = new Loader.singleton();
   Widget build(BuildContext context) {
     return new Scaffold(
         toolBar: new ToolBar(center: new Text('SyncSlides')),
+        floatingActionButton: new FloatingActionButton(
+            child: new Icon(icon: 'content/add'), onPressed: () {
+          _loader.addDeck();
+        }),
         body: new Material(child: new DeckGrid()));
   }
 }
@@ -30,7 +36,8 @@
 class _DeckGridState extends State<DeckGrid> {
   Store _store = new Store.singleton();
   List<model.Deck> _decks = new List<model.Deck>();
-  StreamSubscription _onChangeSubscription;
+  StreamSubscription _onDecksChangeSubscription;
+  StreamSubscription _onStateChangeSubscription;
 
   void updateDecks(List<model.Deck> decks) {
     setState(() {
@@ -38,54 +45,105 @@
     });
   }
 
+  void _rebuild(_) {
+    setState(() {});
+  }
+
   @override
   void initState() {
+    // Stop all active presentations when coming back to the decks grid page.
+    _store.stopAllPresentations();
     _store.getAllDecks().then(updateDecks);
     // Update the state whenever store tells us decks have changed.
-    _onChangeSubscription = _store.onDecksChange.listen(updateDecks);
+    _onDecksChangeSubscription = _store.onDecksChange.listen(updateDecks);
+    _onStateChangeSubscription = _store.onStateChange.listen(_rebuild);
     super.initState();
   }
 
   @override
   void dispose() {
     // Stop listening to updates from store when component is disposed.
-    _onChangeSubscription.cancel();
+    _onDecksChangeSubscription.cancel();
+    _onStateChangeSubscription.cancel();
     super.dispose();
   }
 
   Widget build(BuildContext context) {
-    var deckBoxes = _decks.map((deck) => _buildDeckBox(context, deck)).toList();
-    var grid = new Grid(deckBoxes, maxChildExtent: style.Size.thumbnailWidth);
+    List<Widget> deckBoxes = _decks.map((deck) => _buildDeckBox(context, deck));
+    List<Widget> presentationBoxes = _store.state.livePresentations
+        .map((presentation) => _buildPresentationBox(context, presentation));
+    var allBoxes = new List.from(presentationBoxes)..addAll(deckBoxes);
+    var grid = new Grid(allBoxes, maxChildExtent: style.Size.thumbnailWidth);
     return new ScrollableViewport(child: grid);
   }
 }
 
-// TODO(aghassemi): Is this approach okay? Check with Flutter team.
-// Building RawImage is expensive, so we cache.
-// Expando is a weak map so this does not effect GC.
-Expando<Widget> _weakDeckItemCache = new Expando<Widget>();
 Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
-  var cachedWidget = _weakDeckItemCache[deckData];
-  if (cachedWidget != null) {
-    return cachedWidget;
-  }
-
-  var thumbnail =
-      new RawImage(bytes: new Uint8List.fromList(deckData.thumbnail));
-
-  var title = new Text(deckData.name, style: style.Text.titleStyle);
-  var titleAndActions =
-      new Container(child: title, padding: style.Spacing.normalPadding);
-
-  var card = new Container(
-      child: new Card(child: new Block([thumbnail, titleAndActions])),
-      margin: style.Spacing.normalMargin);
-
-  var gridItem = new InkWell(child: card, onTap: () {
+  var thumbnail = _buildThumbnail(deckData.thumbnail);
+  // TODO(aghassemi): Add "Opened on" data.
+  var subtitleWidget =
+      new Text("Opened on Sep 12, 2015", style: style.Text.subtitleStyle);
+  subtitleWidget = _stopWrapping(subtitleWidget);
+  var footer = _buildBoxFooter(deckData.name, subtitleWidget);
+  var box = _buildCard(deckData.key, [thumbnail, footer], () {
     Navigator.of(context).push(new PageRoute(
         builder: (context) => new SlideListPage(deckData.key, deckData.name)));
   });
 
-  _weakDeckItemCache[deckData] = gridItem;
-  return gridItem;
+  return box;
+}
+
+Widget _buildPresentationBox(
+    BuildContext context, model.PresentationAdvertisement presentationData) {
+  var thumbnail = _buildThumbnail(presentationData.deck.thumbnail);
+  var liveBox = new Row([
+    new Container(
+        child: new Text("LIVE NOW", style: style.Text.liveNow),
+        decoration: style.Box.liveNow,
+        margin: style.Spacing.normalMargin,
+        padding: style.Spacing.extraSmallPadding)
+  ]);
+  var footer = _buildBoxFooter(presentationData.deck.name, liveBox);
+  var box = _buildCard(presentationData.key, [thumbnail, footer], () {});
+  return box;
+}
+
+// TODO(aghassemi): Cache will be moved to Flutter and will become a map of encoded bytes to
+// decoded bytes. This is just a quick work-around for now.
+Map<int, RawImage> thumbnailCache = new Map();
+Widget _buildThumbnail(List<int> bytes) {
+  var key = bytes.hashCode;
+  if (thumbnailCache.containsKey(key)) {
+    return thumbnailCache[key];
+  }
+  var thumbnail = new RawImage(bytes: new Uint8List.fromList(bytes));
+
+  thumbnailCache[key] = thumbnail;
+  return thumbnail;
+}
+
+Widget _buildBoxFooter(String title, Widget subtitle) {
+  var titleWidget = new Text(title, style: style.Text.titleStyle);
+  titleWidget = _stopWrapping(titleWidget);
+
+  var titleAndSubtitle = new Block([titleWidget, subtitle]);
+  return new Container(
+      child: titleAndSubtitle, padding: style.Spacing.normalPadding);
+}
+
+Widget _buildCard(String key, List<Widget> children, Function onTap) {
+  var content = new Container(
+      child: new Card(child: new Block(children)),
+      margin: style.Spacing.normalMargin);
+
+  return new InkWell(key: new Key(key), child: content, onTap: onTap);
+}
+
+Widget _stopWrapping(Text child) {
+  // TODO(aghassemi): There is no equivalent of CSS's white-space: nowrap,
+  // overflow: hidden or text-overflow: ellipsis in Flutter yet.
+  // This workaround simulates white-space: nowrap and overflow: hidden.
+  // See https://github.com/flutter/flutter/issues/417
+  return new Viewport(
+      child: child, scrollDirection: ScrollDirection.horizontal);
 }
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
index 49cb4c5..cbbb17f 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -17,6 +17,7 @@
 class SlideListPage extends StatelessComponent {
   final String deckId;
   final String title;
+  Store _store = new Store.singleton();
 
   SlideListPage(this.deckId, this.title);
   Widget build(BuildContext context) {
@@ -26,8 +27,20 @@
                 icon: 'navigation/arrow_back',
                 onPressed: () => Navigator.of(context).pop()),
             center: new Text(title)),
+        floatingActionButton: _buildPresentFab(context),
         body: new Material(child: new SlideList(deckId)));
   }
+
+  _buildPresentFab(BuildContext context) {
+    return new FloatingActionButton(
+        child: new Icon(icon: 'navigation/arrow_forward'), onPressed: () async {
+      model.PresentationAdvertisement presentation =
+          await _store.startPresentation(deckId);
+      Navigator
+          .of(context)
+          .push(new PageRoute(builder: (context) => new SlideshowPage(deckId)));
+    });
+  }
 }
 
 // SlideList is scrollable list view of slides for a deck.
@@ -92,7 +105,7 @@
 
   thumbnail = new Flexible(child: thumbnail);
 
-  var title = new Text('Slide $key', style: style.Text.subTitleStyle);
+  var title = new Text('Slide $key', style: style.Text.subtitleStyle);
   var notes = new Text(
       'This is the teaser slide. It should be memorable and descriptive');
   var titleAndNotes = new Flexible(
diff --git a/dart/lib/discovery/client.dart b/dart/lib/discovery/client.dart
new file mode 100644
index 0000000..e2a766f
--- /dev/null
+++ b/dart/lib/discovery/client.dart
@@ -0,0 +1,185 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/services.dart' show shell;
+import 'package:v23discovery/discovery.dart' as v23discovery;
+
+import '../models/all.dart' as model;
+import '../utils/asset.dart' as assetutil;
+
+const String v23DiscoveryMojoUrl =
+    'https://syncslides.mojo.v.io/packages/v23discovery/mojo_services/android/discovery.mojo';
+
+// TODO(aghassemi): We should make this the same between Flutter and Java apps when
+// they can actually talk to each other.
+const String presentationInterfaceName =
+    'v.io/release/projects/syncslides/dart/presentation';
+
+StreamController<model.PresentationAdvertisement> _onFoundEmitter =
+    new StreamController.broadcast();
+StreamController<String> _onLostEmitter = new StreamController.broadcast();
+
+Stream onFound = _onFoundEmitter.stream;
+Stream onLost = _onLostEmitter.stream;
+
+// TODO(aghassemi): v23discovery could really use a Dart client library.
+// Keep proxy, handle pairs so we can cancel calls later.
+ProxyResponseFuturePair<v23discovery.ScannerProxy,
+    v23discovery.ScannerScanResponseParams> _scanCall;
+
+Map<
+        String,
+        ProxyResponseFuturePair<v23discovery.AdvertiserProxy,
+            v23discovery.AdvertiserAdvertiseResponseParams>> _advertiseCalls =
+    new Map();
+
+Future advertise(model.PresentationAdvertisement presentation) async {
+  if (_advertiseCalls.containsKey(presentation.key)) {
+    // We are already advertising for this presentation.
+    return _advertiseCalls[presentation.key].responseFuture;
+  }
+
+  Map<String, String> serviceAttrs = new Map();
+  serviceAttrs['deckid'] = presentation.deck.key;
+  serviceAttrs['name'] = presentation.deck.name;
+  v23discovery.Service serviceInfo = new v23discovery.Service()
+    ..instanceUuid = UTF8.encode(presentation.key)
+    ..interfaceName = presentationInterfaceName
+    ..instanceName = ''
+    ..attrs = serviceAttrs
+    ..addrs = [presentation.syncgroupName];
+
+  v23discovery.AdvertiserProxy advertiser =
+      new v23discovery.AdvertiserProxy.unbound();
+  shell.connectToService(v23DiscoveryMojoUrl, advertiser);
+  Future advertiseResponseFuture =
+      advertiser.ptr.advertise(serviceInfo, <String>[]);
+  _advertiseCalls[presentation.key] =
+      new ProxyResponseFuturePair(advertiser, advertiseResponseFuture);
+
+  await advertiseResponseFuture;
+}
+
+// Tracks advertisements that are in the middle of being stopped.
+Map<String, Future> _stoppingAdvertisingCalls = new Map<String, Future>();
+Future stopAdvertising(String presentationId) async {
+  if (!_advertiseCalls.containsKey(presentationId)) {
+    // Not advertised, nothing to stop.
+    return new Future.value();
+  }
+
+  if (_stoppingAdvertisingCalls.containsKey(presentationId)) {
+    // Already stopping, return the exiting call future.
+    return _stoppingAdvertisingCalls[presentationId];
+  }
+
+  stop() async {
+    v23discovery.AdvertiserAdvertiseResponseParams advertiserResponse =
+        await _advertiseCalls[presentationId].responseFuture;
+
+    await _advertiseCalls[presentationId]
+        .proxy
+        .ptr
+        .stop(advertiserResponse.handle);
+    await _advertiseCalls[presentationId].proxy.close();
+  }
+
+  Future stoppingCall = stop();
+  _stoppingAdvertisingCalls[presentationId] = stoppingCall;
+
+  stoppingCall.then((_) {
+    _advertiseCalls.remove(presentationId);
+    _stoppingAdvertisingCalls.remove(presentationId);
+  }).catchError((e) {
+    _stoppingAdvertisingCalls.remove(presentationId);
+    throw e;
+  });
+}
+
+Future startScan() async {
+  if (_scanCall != null) {
+    // We are already scanning.
+    return _scanCall.responseFuture;
+  }
+
+  var scanner = new v23discovery.ScannerProxy.unbound();
+  shell.connectToService(v23DiscoveryMojoUrl, scanner);
+  v23discovery.ScanHandlerStub handlerStub =
+      new v23discovery.ScanHandlerStub.unbound();
+  handlerStub.impl = new ScanHandler();
+
+  var query = 'v.InterfaceName = "$presentationInterfaceName"';
+  var scannerResponseFuture = scanner.ptr.scan(query, handlerStub);
+  _scanCall = new ProxyResponseFuturePair(scanner, scannerResponseFuture);
+
+  await scannerResponseFuture;
+}
+
+// Tracks whether we are already in the middle of stopping scan.
+Future _stoppingScanCall;
+Future stopScan() async {
+  if (_scanCall == null) {
+    // No scan call has been made before or scan is already being stopped.
+    return new Future.value();
+  }
+
+  if (_stoppingScanCall != null) {
+    // Already stopping, return the exiting call future.
+    return _stoppingScanCall;
+  }
+
+  stop() async {
+    v23discovery.ScannerScanResponseParams scannerResponse =
+        await _scanCall.responseFuture;
+
+    await _scanCall.proxy.ptr.stop(scannerResponse.handle);
+    await _scanCall.proxy.close();
+  }
+
+  _stoppingScanCall = stop();
+
+  _stoppingScanCall.then((_) {
+    _scanCall = null;
+    _stoppingScanCall = null;
+  }).catchError((e) {
+    _stoppingScanCall = null;
+    throw e;
+  });
+}
+
+class ScanHandler extends v23discovery.ScanHandler {
+  found(v23discovery.Service s) async {
+    String key = UTF8.decode(s.instanceUuid);
+    // Ignore our own advertised services.
+    if (_advertiseCalls.containsKey(key)) {
+      return;
+    }
+
+    // TODO(aghassemi): For now we use the default thumbnail. We need to find a way
+    // to fetch the actual thumbnail from the other side.
+    var thumbnail = await assetutil.getRawBytes(assetutil.defaultThumbnailUrl);
+    model.Deck deck =
+        new model.Deck(s.attrs['deckid'], s.attrs['name'], thumbnail.toList());
+    var syncgroupName = s.addrs[0];
+    model.PresentationAdvertisement presentation =
+        new model.PresentationAdvertisement(key, deck, syncgroupName);
+
+    _onFoundEmitter.add(presentation);
+  }
+
+  lost(List<int> instanceId) {
+    String presentationId = UTF8.decode(instanceId);
+    // Ignore our own advertised services.
+    _onLostEmitter.add(presentationId);
+  }
+}
+
+class ProxyResponseFuturePair<T1, T2> {
+  final T1 proxy;
+  final Future<T2> responseFuture;
+  ProxyResponseFuturePair(this.proxy, this.responseFuture);
+}
diff --git a/dart/lib/loaders/demo_loader.dart b/dart/lib/loaders/demo_loader.dart
index 94d0243..52b4257 100644
--- a/dart/lib/loaders/demo_loader.dart
+++ b/dart/lib/loaders/demo_loader.dart
@@ -3,14 +3,12 @@
 // license that can be found in the LICENSE file.
 
 import 'dart:async';
-
 import 'dart:math';
-import 'dart:typed_data';
-
-import 'package:flutter/services.dart' as services;
 
 import '../models/all.dart' as model;
 import '../stores/store.dart';
+import '../utils/uuid.dart' as uuidutil;
+import '../utils/asset.dart' as assetutil;
 
 import 'loader.dart';
 
@@ -24,70 +22,60 @@
       : _store = new Store.singleton(),
         _rand = new Random();
 
-  static const int numDeckSets = 2;
-  Stream<model.Deck> _getSampleDecks() async* {
-    for (var i = 1; i <= numDeckSets; i++) {
-      yield new model.Deck('baku$i', 'Baku Discovery Discussion #$i',
-          await _getRawBytes('assets/images/sample_decks/baku/thumb.png'));
-      yield new model.Deck('pitch$i', 'Pitch Deck #$i',
-          await _getRawBytes('assets/images/sample_decks/pitch/thumb.png'));
-      yield new model.Deck('vanadium$i', 'Vanadium #$i',
-          await _getRawBytes('assets/images/sample_decks/vanadium/thumb.png'));
+  static final List<String> thumbnails = [
+    'assets/images/sample_decks/baku/thumb.png',
+    'assets/images/sample_decks/vanadium/thumb.png',
+    'assets/images/sample_decks/pitch/thumb.png'
+  ];
+  static final List<String> slides = [
+    'assets/images/sample_decks/vanadium/1.jpg',
+    'assets/images/sample_decks/vanadium/2.jpg',
+    'assets/images/sample_decks/vanadium/3.jpg',
+    'assets/images/sample_decks/vanadium/4.jpg',
+    'assets/images/sample_decks/vanadium/5.jpg',
+    'assets/images/sample_decks/vanadium/6.jpg'
+  ];
+  static final List<String> firstWords = [
+    'Today\'s',
+    'Yesterday\'s',
+    'Ali\'s',
+    'Adam\'s',
+    'Misha\'s'
+  ];
+  static final List<String> secondWords = [
+    'Presentation',
+    'Slideshow',
+    'Meeting',
+    'Pitch',
+    'Discussion',
+    'Demo',
+    'All Hands'
+  ];
+
+  static const int maxNumSlides = 20;
+
+  Future<model.Deck> _getRandomDeck() async {
+    var thumbnail = await assetutil
+        .getRawBytes(thumbnails[_rand.nextInt(thumbnails.length)]);
+    var firstWord = firstWords[_rand.nextInt(firstWords.length)];
+    var secondWord = secondWords[_rand.nextInt(secondWords.length)];
+    return new model.Deck(
+        uuidutil.createUuid(), '$firstWord $secondWord', thumbnail);
+  }
+
+  Stream<model.Slide> _getRandomSlides() async* {
+    var numSlides = _rand.nextInt(maxNumSlides);
+    for (var i = 0; i < numSlides; i++) {
+      var slideIndex = i % slides.length;
+      yield new model.Slide(await assetutil.getRawBytes(
+          'assets/images/sample_decks/vanadium/${slideIndex + 1}.jpg'));
     }
   }
 
-  Stream<model.Slide> _getSampleSlides() async* {
-    // TODO(aghassemi): We need different slides for different decks.
-    // For now use Vanadium slides for all.
-    for (var i = 1; i <= 6; i++) {
-      yield new model.Slide(
-          await _getRawBytes('assets/images/sample_decks/vanadium/$i.jpg'));
-    }
-  }
-
-  Future loadDecks() async {
-    // Add some initial decks.
-    await for (var deck in _getSampleDecks()) {
-      await _addDeck(deck);
-    }
-
-    // Periodically add or remove random decks.
-    new Timer.periodic(new Duration(seconds: 2), (_) async {
-      var decks = await _store.getAllDecks();
-      var deckKeys = decks.map((d) => d.key);
-      var removeDeck = _rand.nextBool();
-
-      if (removeDeck && decks.length > 0) {
-        var rIndex = _rand.nextInt(decks.length);
-        // Never delete the first deck so we can safely use it for slideshow
-        if (rIndex >= 1) {
-          _store.removeDeck(decks[rIndex].key);
-        }
-      } else {
-        await for (var deck in _getSampleDecks()) {
-          if (!deckKeys.contains(deck.key)) {
-            await _addDeck(deck);
-            break;
-          }
-        }
-      }
-    });
-  }
-
-  Future _addDeck(model.Deck deck) async {
+  Future addDeck() async {
+    var deck = await _getRandomDeck();
+    List<model.Slide> slides = await _getRandomSlides().toList();
     await _store.addDeck(deck);
-    List<model.Slide> slides = await _getSampleSlides().toList();
     await _store.setSlides(deck.key, slides);
   }
-
-  Map<String, Uint8List> _assetCache = new Map<String, Uint8List>();
-  Future<Uint8List> _getRawBytes(String url) async {
-    if (_assetCache.containsKey(url)) {
-      return _assetCache[url];
-    }
-    services.Response response = await services.fetchBody(url);
-    var bytes = new Uint8List.fromList(response.body.buffer.asUint8List());
-    _assetCache[url] = bytes;
-    return bytes;
-  }
 }
diff --git a/dart/lib/loaders/loader.dart b/dart/lib/loaders/loader.dart
index ec97da1..c206b36 100644
--- a/dart/lib/loaders/loader.dart
+++ b/dart/lib/loaders/loader.dart
@@ -8,14 +8,11 @@
 
 // Loader is responsible for importing existing decks and slides into the store.
 abstract class Loader {
-  static Loader _singletonLoader;
+  static Loader _singletonLoader = loaderFactory.create();
 
   factory Loader.singleton() {
-    if (_singletonLoader == null) {
-      _singletonLoader = loaderFactory.create();
-    }
     return _singletonLoader;
   }
 
-  Future loadDecks();
+  Future addDeck();
 }
diff --git a/dart/lib/loaders/sdcard_loader.dart b/dart/lib/loaders/sdcard_loader.dart
index 72071c2..a2b0102 100644
--- a/dart/lib/loaders/sdcard_loader.dart
+++ b/dart/lib/loaders/sdcard_loader.dart
@@ -7,7 +7,7 @@
 import 'loader.dart';
 
 class SdCardLoader implements Loader {
-  Future loadDecks() {
+  Future addDeck() {
     throw new UnimplementedError();
   }
 }
diff --git a/dart/lib/main.dart b/dart/lib/main.dart
index 8cc1ab6..0503e05 100644
--- a/dart/lib/main.dart
+++ b/dart/lib/main.dart
@@ -4,14 +4,12 @@
 
 import 'package:flutter/material.dart';
 
+import 'styles/common.dart' as style;
 import 'components/deckgrid.dart';
-import 'loaders/loader.dart';
 
 void main() {
-  // Start loading data.
-  new Loader.singleton().loadDecks();
-
   runApp(new MaterialApp(
+      theme: style.theme,
       title: 'SyncSlides',
       routes: {'/': (RouteArguments args) => new DeckGridPage()}));
 }
diff --git a/dart/lib/models/all.dart b/dart/lib/models/all.dart
index 61a7d0f..c802c0b 100644
--- a/dart/lib/models/all.dart
+++ b/dart/lib/models/all.dart
@@ -4,3 +4,4 @@
 
 export 'deck.dart';
 export 'slide.dart';
+export 'presentation_advertisement.dart';
diff --git a/dart/lib/models/presentation_advertisement.dart b/dart/lib/models/presentation_advertisement.dart
new file mode 100644
index 0000000..5e19c4e
--- /dev/null
+++ b/dart/lib/models/presentation_advertisement.dart
@@ -0,0 +1,20 @@
+// 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.
+
+import '../models/all.dart' as model;
+
+// Represents an advertised presentation of a deck.
+class PresentationAdvertisement {
+  // TODO(aghassemi): Fix inconsistencies between key and id everywhere.
+  String _key;
+  String get key => _key;
+
+  model.Deck _deck;
+  model.Deck get deck => _deck;
+
+  String _syncgroupName;
+  String get syncgroupName => _syncgroupName;
+
+  PresentationAdvertisement(this._key, this._deck, this._syncgroupName) {}
+}
diff --git a/dart/lib/stores/memory_store.dart b/dart/lib/stores/memory_store.dart
index aa80f74..bfee15f 100644
--- a/dart/lib/stores/memory_store.dart
+++ b/dart/lib/stores/memory_store.dart
@@ -7,6 +7,7 @@
 import '../models/all.dart' as model;
 
 import 'keyutil.dart' as keyutil;
+import 'state.dart';
 import 'store.dart';
 
 // A memory-based implementation of Store.
@@ -16,16 +17,26 @@
   Map<String, String> _slidesMap;
   Map<String, int> _currSlideNumMap;
   Map<String, StreamController> _currSlideNumChangeEmitterMap;
+  State _state = new State();
+  StreamController _stateChangeEmitter = new StreamController.broadcast();
 
   MemoryStore()
       : _onDecksChangeEmitter = new StreamController.broadcast(),
         _decksMap = new Map(),
         _slidesMap = new Map(),
         _currSlideNumMap = new Map(),
-        _currSlideNumChangeEmitterMap = new Map();
+        _currSlideNumChangeEmitterMap = new Map(),
+        _state = new State(),
+        _stateChangeEmitter = new StreamController.broadcast();
 
   //////////////////////////////////////
-  /// Decks
+  // State
+
+  State get state => _state;
+  Stream get onStateChange => _stateChangeEmitter.stream;
+
+  //////////////////////////////////////
+  // Decks
 
   Future<List<model.Deck>> getAllDecks() async {
     var decks = [];
@@ -36,6 +47,10 @@
     return decks;
   }
 
+  Future<model.Deck> getDeck(String key) async {
+    return new model.Deck.fromJson(key, _decksMap[key]);
+  }
+
   Future addDeck(model.Deck deck) async {
     var json = deck.toJson();
     _decksMap[deck.key] = json;
@@ -55,7 +70,7 @@
   Stream<List<model.Deck>> get onDecksChange => _onDecksChangeEmitter.stream;
 
   //////////////////////////////////////
-  /// Slides
+  // Slides
 
   Future<List<model.Slide>> getAllSlides(String deckKey) async {
     var slides = [];
@@ -99,4 +114,22 @@
         deckId, () => new StreamController.broadcast());
     return _currSlideNumChangeEmitterMap[deckId];
   }
+
+  //////////////////////////////////////
+  // Presentation
+
+  static final Error noPresentationSupportError =
+      new UnsupportedError('MemoryStore does not support presentation.');
+
+  Future<model.PresentationAdvertisement> startPresentation(String deckId) {
+    throw noPresentationSupportError;
+  }
+
+  Future stopPresentation(String presentationId) {
+    throw noPresentationSupportError;
+  }
+
+  Future stopAllPresentations() {
+    throw noPresentationSupportError;
+  }
 }
diff --git a/dart/lib/stores/state.dart b/dart/lib/stores/state.dart
new file mode 100644
index 0000000..3ecf6bd
--- /dev/null
+++ b/dart/lib/stores/state.dart
@@ -0,0 +1,19 @@
+// 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.
+
+import '../models/all.dart' as model;
+
+// Represents current state of the application.
+class State {
+  // TODO(aghassemi): The new store model is to have one state object
+  // and a change event instead of async getters.
+  // This model has not been implemented for decks and slides yet but
+  // we are using the new model for presentation advertisements.
+
+  // TODO(aghassemi): State needs to be deeply immutable.
+  // Maybe https://github.com/google/built_value.dart can help?
+  List<model.PresentationAdvertisement> livePresentations;
+
+  State() : livePresentations = new List();
+}
diff --git a/dart/lib/stores/store.dart b/dart/lib/stores/store.dart
index 06eadfd..6c817c8 100644
--- a/dart/lib/stores/store.dart
+++ b/dart/lib/stores/store.dart
@@ -6,36 +6,36 @@
 
 import '../models/all.dart' as model;
 
+import 'state.dart';
 import 'store_factory.dart' as storeFactory;
 
-// TODO(aghassemi): Make all store operation synchronous.
-// Current pattern of components needing to call async methods and keep and
-// update their own state is already becoming messy. When store becomes
-// synchronous, then these components can simply use _store.getSlides(),
-// _store.getCurrSlide(), etc.. directly in their renderer and do not need to
-// keep any state of their own.
-
 // Provides APIs for reading and writing app-related data.
 abstract class Store {
-  static Store _singletonStore;
+  static Store _singletonStore = storeFactory.create();
 
   factory Store.singleton() {
-    if (_singletonStore == null) {
-      _singletonStore = storeFactory.create();
-    }
     return _singletonStore;
   }
 
   //////////////////////////////////////
-  /// Decks
+  // State
+
+  State get state;
+  Stream get onStateChange;
+
+  //////////////////////////////////////
+  // Decks
 
   // Returns all the existing decks.
   Future<List<model.Deck>> getAllDecks();
 
+  // Returns the deck for the given key.
+  Future<model.Deck> getDeck(String key);
+
   // Adds a new deck.
   Future addDeck(model.Deck deck);
 
-  // Removed a deck given its key.
+  // Removes a deck given its key.
   Future removeDeck(String key);
 
   // Event that fires when deck are added or removed.
@@ -43,7 +43,7 @@
   Stream<List<model.Deck>> get onDecksChange;
 
   //////////////////////////////////////
-  /// Slides
+  // Slides
 
   // Returns the list of all slides for a deck.
   Future<List<model.Slide>> getAllSlides(String deckKey);
@@ -59,4 +59,13 @@
   Future setCurrSlideNum(String deckId, int slideNum);
 
   Stream<int> onCurrSlideNumChange(String deckId);
+
+  //////////////////////////////////////
+  // Presentation
+
+  Future<model.PresentationAdvertisement> startPresentation(String deckId);
+
+  Future stopPresentation(String presentationId);
+
+  Future stopAllPresentations();
 }
diff --git a/dart/lib/stores/syncbase_store.dart b/dart/lib/stores/syncbase_store.dart
index 2bfd564..2a98a33 100644
--- a/dart/lib/stores/syncbase_store.dart
+++ b/dart/lib/stores/syncbase_store.dart
@@ -5,11 +5,14 @@
 import 'dart:async';
 import 'dart:convert';
 
+import '../discovery/client.dart' as discovery;
 import '../models/all.dart' as model;
 import '../syncbase/client.dart' as sb;
 import '../utils/errors.dart' as errorsutil;
+import '../utils/uuid.dart' as uuidutil;
 
 import 'keyutil.dart' as keyutil;
+import 'state.dart';
 import 'store.dart';
 
 const String decksTableName = 'Decks';
@@ -18,18 +21,31 @@
 class SyncbaseStore implements Store {
   StreamController _deckChangeEmitter;
   Map<String, StreamController> _currSlideNumChangeEmitterMap;
+  List<model.PresentationAdvertisement> _advertisedPresentations;
+  State _state = new State();
+  StreamController _stateChangeEmitter = new StreamController.broadcast();
 
   SyncbaseStore() {
     _deckChangeEmitter = new StreamController.broadcast();
     _currSlideNumChangeEmitterMap = new Map();
+    _advertisedPresentations = new List();
+    _state = new State();
+    _stateChangeEmitter = new StreamController.broadcast();
     sb.getDatabase().then((db) {
       _startDecksWatch(db);
-      _startSync(db);
+      _startScanningForPresentations();
     });
   }
 
   //////////////////////////////////////
-  /// Decks
+  // State
+
+  State get state => _state;
+  Stream get onStateChange => _stateChangeEmitter.stream;
+  void _triggerStateChange() => _stateChangeEmitter.add(_state);
+
+  //////////////////////////////////////
+  // Decks
 
   Future<List<model.Deck>> getAllDecks() async {
     // Key schema is:
@@ -42,9 +58,17 @@
     String query = 'SELECT k, v FROM $decksTableName WHERE k NOT LIKE "%/%"';
     Stream<sb.Result> results = sbDb.exec(query);
     // NOTE(aghassemi): First row is always the name of the columns, so we skip(1).
+
     return results.skip(1).map((result) => _toDeck(result.values)).toList();
   }
 
+  // Return the deck for the given key or null if it does not exist.
+  Future<model.Deck> getDeck(String key) async {
+    sb.SyncbaseTable tb = await _getDecksTable();
+    var value = await tb.get(key);
+    return new model.Deck.fromJson(key, UTF8.decode(value));
+  }
+
   Future addDeck(model.Deck deck) async {
     sb.SyncbaseTable tb = await _getDecksTable();
     tb.put(deck.key, UTF8.encode(deck.toJson()));
@@ -59,16 +83,53 @@
   Stream<List<model.Deck>> get onDecksChange => _deckChangeEmitter.stream;
 
   model.Deck _toDeck(List<List<int>> row) {
-    // TODO(aghassemi): Keys return from queries seems to have double quotes
-    // around them.
-    // See https://github.com/vanadium/issues/issues/860
-    var key = UTF8.decode(row[0]).replaceAll('"', '');
+    var key = UTF8.decode(row[0]);
     var value = UTF8.decode(row[1]);
     return new model.Deck.fromJson(key, value);
   }
 
+  Future<sb.SyncbaseTable> _getDecksTable() async {
+    sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
+    sb.SyncbaseTable tb = sbDb.table(decksTableName);
+    try {
+      await tb.create(sb.createOpenPerms());
+    } catch (e) {
+      if (!errorsutil.isExistsError(e)) {
+        throw e;
+      }
+    }
+    return tb;
+  }
+
+  Future _startDecksWatch(sb.SyncbaseNoSqlDatabase sbDb) async {
+    var resumeMarker = await sbDb.getResumeMarker();
+    var stream = sbDb.watch(decksTableName, '', resumeMarker);
+
+    stream.listen((sb.WatchChange change) async {
+      if (keyutil.isDeckKey(change.rowKey)) {
+        // TODO(aghassemi): Maybe manipulate an in-memory list based on watch
+        // changes instead of getting the decks again from Syncbase.
+        if (!_deckChangeEmitter.isPaused || !_deckChangeEmitter.isClosed) {
+          var decks = await getAllDecks();
+          _deckChangeEmitter.add(decks);
+        }
+      } else if (keyutil.isCurrSlideNumKey(change.rowKey)) {
+        var deckId = keyutil.currSlideNumKeyToDeckId(change.rowKey);
+        var emitter = _getCurrSlideNumChangeEmitter(deckId);
+        if (!emitter.isPaused || !emitter.isClosed) {
+          if (change.changeType == sb.WatchChangeTypes.put) {
+            int currSlideNum = change.valueBytes[0];
+            emitter.add(currSlideNum);
+          } else {
+            emitter.add(0);
+          }
+        }
+      }
+    });
+  }
+
   //////////////////////////////////////
-  /// Slides
+  // Slides
 
   Future<List<model.Slide>> getAllSlides(String deckKey) async {
     // Key schema is:
@@ -131,45 +192,54 @@
     return _currSlideNumChangeEmitterMap[deckId];
   }
 
-  Future<sb.SyncbaseTable> _getDecksTable() async {
-    sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
-    sb.SyncbaseTable tb = sbDb.table(decksTableName);
-    try {
-      await tb.create(sb.createOpenPerms());
-    } catch (e) {
-      if (!errorsutil.isExistsError(e)) {
-        throw e;
-      }
+  //////////////////////////////////////
+  // Presentation
+
+  Future<model.PresentationAdvertisement> startPresentation(
+      String deckId) async {
+    var alreadyAdvertised =
+        _advertisedPresentations.any((p) => p.deck.key == deckId);
+    if (alreadyAdvertised) {
+      throw new ArgumentError(
+          'Cannot simultaneously present the same deck. Presentation already in progress for $deckId.');
     }
-    return tb;
+
+    model.Deck deck = await this.getDeck(deckId);
+    String uuid = uuidutil.createUuid();
+    String syncgroupName = '';
+    var presentation =
+        new model.PresentationAdvertisement(uuid, deck, syncgroupName);
+
+    await discovery.advertise(presentation);
+    _advertisedPresentations.add(presentation);
+
+    return presentation;
   }
 
-  Future _startDecksWatch(sb.SyncbaseNoSqlDatabase sbDb) async {
-    var resumeMarker = await sbDb.getResumeMarker();
-    var stream = sbDb.watch(decksTableName, '', resumeMarker);
+  Future stopPresentation(String presentationId) async {
+    await discovery.stopAdvertising(presentationId);
+    _advertisedPresentations.removeWhere((p) => p.key == presentationId);
+  }
 
-    stream.listen((sb.WatchChange change) async {
-      if (keyutil.isDeckKey(change.rowKey)) {
-        // TODO(aghassemi): Maybe manipulate an in-memory list based on watch
-        // changes instead of getting the decks again from Syncbase.
-        if (!_deckChangeEmitter.isPaused || !_deckChangeEmitter.isClosed) {
-          var decks = await getAllDecks();
-          _deckChangeEmitter.add(decks);
-        }
-      } else if (keyutil.isCurrSlideNumKey(change.rowKey)) {
-        var deckId = keyutil.currSlideNumKeyToDeckId(change.rowKey);
-        var emitter = _getCurrSlideNumChangeEmitter(deckId);
-        if (!emitter.isPaused || !emitter.isClosed) {
-          if (change.changeType == sb.WatchChangeTypes.put) {
-            int currSlideNum = change.valueBytes[0];
-            emitter.add(currSlideNum);
-          } else {
-            emitter.add(0);
-          }
-        }
-      }
+  Future stopAllPresentations() async {
+    // Stop all presentations in parallel.
+    return Future.wait(
+        _advertisedPresentations.map((model.PresentationAdvertisement p) {
+      return stopPresentation(p.key);
+    }));
+  }
+
+  Future _startScanningForPresentations() async {
+    discovery.onFound.listen((model.PresentationAdvertisement newP) {
+      state.livePresentations.add(newP);
+      _triggerStateChange();
     });
-  }
 
-  Future _startSync(sb.SyncbaseNoSqlDatabase sbDb) async {}
+    discovery.onLost.listen((String pId) {
+      state.livePresentations.removeWhere((p) => p.key == pId);
+      _triggerStateChange();
+    });
+
+    discovery.startScan();
+  }
 }
diff --git a/dart/lib/styles/common.dart b/dart/lib/styles/common.dart
index 105af9a..d275af0 100644
--- a/dart/lib/styles/common.dart
+++ b/dart/lib/styles/common.dart
@@ -2,13 +2,15 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-import 'package:flutter/widgets.dart';
+import 'package:flutter/material.dart';
 
 class Text {
-  static final Color secondaryTextColor = new Color.fromARGB(70, 0, 0, 0);
+  static final Color secondaryTextColor = Colors.grey[500];
   static final TextStyle titleStyle = new TextStyle(fontSize: 18.0);
-  static final TextStyle subTitleStyle =
+  static final TextStyle subtitleStyle =
       new TextStyle(fontSize: 12.0, color: secondaryTextColor);
+  static final TextStyle liveNow =
+      new TextStyle(fontSize: 12.0, color: theme.accentColor);
 }
 
 class Size {
@@ -19,8 +21,18 @@
 }
 
 class Spacing {
+  static final EdgeDims extraSmallPadding = new EdgeDims.all(2.0);
+  static final EdgeDims smallPadding = new EdgeDims.all(5.0);
   static final EdgeDims normalPadding = new EdgeDims.all(10.0);
   static final EdgeDims normalMargin = new EdgeDims.all(2.0);
   static final EdgeDims listItemMargin = new EdgeDims.TRBL(3.0, 6.0, 0.0, 6.0);
   static final EdgeDims thumbnailNavMargin = new EdgeDims.all(3.0);
 }
+
+class Box {
+  static final BoxDecoration liveNow = new BoxDecoration(
+      border: new Border.all(color: theme.accentColor), borderRadius: 2.0);
+}
+
+ThemeData theme = new ThemeData(
+    primarySwatch: Colors.blueGrey, accentColor: Colors.orangeAccent[700]);
diff --git a/dart/lib/utils/asset.dart b/dart/lib/utils/asset.dart
new file mode 100644
index 0000000..bb001a3
--- /dev/null
+++ b/dart/lib/utils/asset.dart
@@ -0,0 +1,21 @@
+// 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.
+
+import 'dart:async';
+import 'dart:typed_data';
+
+import 'package:flutter/services.dart' as services;
+
+Map<String, Uint8List> _assetCache = new Map<String, Uint8List>();
+Future<Uint8List> getRawBytes(String url) async {
+  if (_assetCache.containsKey(url)) {
+    return _assetCache[url];
+  }
+  services.Response response = await services.fetchBody(url);
+  var bytes = new Uint8List.fromList(response.body.buffer.asUint8List());
+  _assetCache[url] = bytes;
+  return bytes;
+}
+
+String defaultThumbnailUrl = 'assets/images/defaults/thumbnail.png';
diff --git a/dart/lib/utils/uuid.dart b/dart/lib/utils/uuid.dart
new file mode 100644
index 0000000..90e6d12
--- /dev/null
+++ b/dart/lib/utils/uuid.dart
@@ -0,0 +1,12 @@
+// 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.
+
+import 'package:uuid/uuid.dart';
+
+Uuid _uuid = new Uuid();
+
+// Creates a universally unique identifier.
+String createUuid() {
+  return _uuid.v4();
+}
diff --git a/dart/pubspec.lock b/dart/pubspec.lock
index 7eaa90a..a8af949 100644
--- a/dart/pubspec.lock
+++ b/dart/pubspec.lock
@@ -72,11 +72,11 @@
   flutter:
     description: flutter
     source: hosted
-    version: "0.0.17"
+    version: "0.0.18"
   flx:
     description: flx
     source: hosted
-    version: "0.0.9"
+    version: "0.0.10"
   glob:
     description: glob
     source: hosted
@@ -116,23 +116,23 @@
   mojo:
     description: mojo
     source: hosted
-    version: "0.3.0"
+    version: "0.4.2"
   mojo_apptest:
     description: mojo_apptest
     source: hosted
-    version: "0.2.3"
+    version: "0.2.7"
   mojo_sdk:
     description: mojo_sdk
     source: hosted
-    version: "0.1.0"
+    version: "0.2.1"
   mojo_services:
     description: mojo_services
     source: hosted
-    version: "0.4.0"
+    version: "0.4.4"
   mojom:
     description: mojom
     source: hosted
-    version: "0.2.3"
+    version: "0.2.7"
   mustache4dart:
     description: mustache4dart
     source: hosted
@@ -196,11 +196,11 @@
   sky_engine:
     description: sky_engine
     source: hosted
-    version: "0.0.48"
+    version: "0.0.49"
   sky_services:
     description: sky_services
     source: hosted
-    version: "0.0.48"
+    version: "0.0.50"
   sky_tools:
     description: sky_tools
     source: hosted
@@ -228,7 +228,7 @@
   syncbase:
     description: syncbase
     source: hosted
-    version: "0.0.11"
+    version: "0.0.14"
   test:
     description: test
     source: hosted
@@ -245,6 +245,14 @@
     description: utf
     source: hosted
     version: "0.9.0+2"
+  uuid:
+    description: uuid
+    source: hosted
+    version: "0.5.0"
+  v23discovery:
+    description: v23discovery
+    source: hosted
+    version: "0.0.2"
   vector_math:
     description: vector_math
     source: hosted
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
index 0d49473..b44d99c 100644
--- a/dart/pubspec.yaml
+++ b/dart/pubspec.yaml
@@ -3,5 +3,7 @@
 dependencies:
   flutter: ">=0.0.16 <0.1.0"
   syncbase: ">=0.0.9 <0.1.0"
+  v23discovery: ">=0.0.1 < 0.1.0"
+  uuid: ">=0.5.0 <0.6.0"
 dev_dependencies:
   sky_tools: ">=0.0.27 <0.1.0"