syncslides: Support for asking questions and
handing off driving of the presentation.

Screenshots: https://goo.gl/photos/7qAokfLCsGwUZZvP7

-Audience can ask a question
-Presenter can see list of questions:
 + Notification number when questions are added
 + See name, question text and slide num/thumbnail
 + Jump to slide of the question
 + Hand off control to the questioner
 + Resume control
-Images are now centered
-slide x of y overlay
-next/previous overlays
-Drawer menu with the name of the logged in user
-We now use the device id as name for mounting syncbase
 so each device now creates the syncgroup on itself.

Change-Id: I65f086b1c5881e5abfcde6a439fcff17b308f47d
diff --git a/dart/Makefile b/dart/Makefile
index 5ae2d85..ec6b260 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -2,24 +2,24 @@
 	DEVICE_NUM := 1
 endif
 
-ifneq ($(DEVICE_NUM), 1)
-	REUSE_FLAG := --reuse-servers
-else
-	# TODO(aghassemi): This needs to become $(DEVICE_ID) when we have a way to
-	# send the $(DEVICE_ID) to the Flutter app.
-	NAME_FLAG := --name=syncslides
-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)
 MOUNTTABLE_ADDR := /192.168.86.254:8101
-ANDROID_CREDS_DIR := /sdcard/v23creds
+NAME_FLAG := --name=$(DEVICE_ID)
+
+# Currently the only way to pass arguments to the app is by using a file.
+SETTINGS_FILE := /sdcard/syncslides_settings.json
+SETTINGS_JSON := {\"deviceid\": \"$(DEVICE_ID)\", \"mounttable\": \"$(MOUNTTABLE_ADDR)\"}
+
+ifneq ($(DEVICE_NUM), 1)
+	REUSE_FLAG := --reuse-servers
+endif
+
+ifdef VLOG
+	VLOG_FLAGS = --v=$(VLOG) --logtostderr=true
+endif
 
 default: run
 
@@ -42,6 +42,7 @@
 # DEVICE_NUM=1 make run
 # DEVICE_NUM=2 make run
 run: packages
+	adb -s $(DEVICE_ID) shell 'echo $(SETTINGS_JSON) > $(SETTINGS_FILE)'
 	pub run flutter_tools build && pub run flutter_tools run_mojo \
 	--mojo-path $(MOJO_DIR)/src \
 	--android --mojo-debug -- --enable-multiprocess \
@@ -74,6 +75,7 @@
 clean:
 	rm -f app.flx snapshot_blob.bin
 	rm -rf packages
+	adb -s $(DEVICE_ID) shell run-as org.chromium.mojo.shell rm $(SETTINGS_FILE)
 
 .PHONY: clean-syncbase
 clean-syncbase:
diff --git a/dart/flutter.yaml b/dart/flutter.yaml
index 00ecac9..05af4c9 100644
--- a/dart/flutter.yaml
+++ b/dart/flutter.yaml
@@ -1,8 +1,15 @@
 name: syncslides
 material-design-icons:
+  - name: action/account_circle
+  - name: action/perm_device_information
+  - name: av/loop
+  - name: av/play_arrow
+  - name: communication/live_help
+  - name: content/add
+  - name: maps/layers
   - name: navigation/arrow_back
   - name: navigation/arrow_forward
-  - name: content/add
+  - name: navigation/menu
   - name: notification/sync
   - name: maps/layers
 assets:
diff --git a/dart/lib/components/askquestion.dart b/dart/lib/components/askquestion.dart
new file mode 100644
index 0000000..e69ac67
--- /dev/null
+++ b/dart/lib/components/askquestion.dart
@@ -0,0 +1,48 @@
+// 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:flutter/material.dart';
+
+import '../stores/store.dart';
+import 'syncslides_page.dart';
+
+class AskQuestionPage extends SyncSlidesPage {
+  final String _deckId;
+  final int _currSlideNum;
+
+  AskQuestionPage(this._deckId, this._currSlideNum);
+
+  Widget build(BuildContext context, AppState appState, AppActions appActions) {
+    if (!appState.decks.containsKey(_deckId)) {
+      // TODO(aghassemi): Proper error page with navigation back to main view.
+      return new Text('Deck no longer exists.');
+    }
+    var deckState = appState.decks[_deckId];
+    var presentationState = deckState.presentation;
+    if (presentationState == null) {
+      // TODO(aghassemi): Proper error page with navigation back to main view.
+      return new Text('Not in a presentation.');
+    }
+
+    var input = new Input(placeholder: 'Your question',
+        onSubmitted: (String questionText) async {
+      await appActions.askQuestion(
+          deckState.deck.key, _currSlideNum, questionText);
+
+      // TODO(aghassemi): Add a 'Question submitted.' toast on the parent page.
+      // Blocked on https://github.com/flutter/flutter/issues/608
+      Navigator.of(context).pop();
+    });
+
+    var view = new Row([input], alignItems: FlexAlignItems.stretch);
+
+    return new Scaffold(
+        toolBar: new ToolBar(
+            left: new IconButton(
+                icon: 'navigation/arrow_back',
+                onPressed: () => Navigator.of(context).pop()),
+            center: new Text('Ask a question')),
+        body: new Material(child: view));
+  }
+}
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
index f3dbe30..9893e12 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -3,7 +3,6 @@
 // license that can be found in the LICENSE file.
 
 import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
 
 import '../models/all.dart' as model;
 import '../stores/store.dart';
@@ -13,6 +12,7 @@
 import 'slideshow.dart';
 import 'syncslides_page.dart';
 import 'toast.dart' as toast;
+import 'utils/stop_wrapping.dart';
 
 final GlobalKey _scaffoldKey = new GlobalKey();
 
@@ -32,9 +32,25 @@
     List<model.PresentationAdvertisement> presentations =
         appState.presentationAdvertisements.values;
 
+    Widget title = new Text('SyncSlides');
+    Widget drawer = new IconButton(icon: "navigation/menu", onPressed: () {
+      showDrawer(
+          context: context,
+          child: new Block([
+            new DrawerItem(
+                icon: 'action/account_circle',
+                child: stopWrapping(new Text(appState.user.name,
+                    style: style.Text.titleStyle))),
+            new DrawerItem(
+                icon: 'action/perm_device_information',
+                child: stopWrapping(new Text(appState.settings.deviceId,
+                    style: style.Text.titleStyle)))
+          ]));
+    });
+
     return new Scaffold(
         key: _scaffoldKey,
-        toolBar: new ToolBar(center: new Text('SyncSlides')),
+        toolBar: new ToolBar(left: drawer, center: title),
         floatingActionButton: new FloatingActionButton(
             child: new Icon(icon: 'content/add'), onPressed: () {
           appActions.loadDemoDeck();
@@ -67,7 +83,7 @@
     // TODO(aghassemi): Add "Opened on" data.
     var subtitleWidget =
         new Text("Opened on Sep 12, 2015", style: style.Text.subtitleStyle);
-    subtitleWidget = _stopWrapping(subtitleWidget);
+    subtitleWidget = stopWrapping(subtitleWidget);
     var footer = _buildBoxFooter(deckData.name, subtitleWidget);
     var box = _buildCard(deckData.key, [thumbnail, footer], () {
       Navigator.of(context).push(new MaterialPageRoute(
@@ -116,27 +132,18 @@
 
   Widget _buildBoxFooter(String title, Widget subtitle) {
     var titleWidget = new Text(title, style: style.Text.titleStyle);
-    titleWidget = _stopWrapping(titleWidget);
+    titleWidget = stopWrapping(titleWidget);
 
-    var titleAndSubtitle = new Block([titleWidget, subtitle]);
+    var titleAndSubtitle = new BlockBody([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)),
+        child: new Card(child: new BlockBody(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/questionlist.dart b/dart/lib/components/questionlist.dart
new file mode 100644
index 0000000..7ddcabc
--- /dev/null
+++ b/dart/lib/components/questionlist.dart
@@ -0,0 +1,121 @@
+// 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:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+import '../models/all.dart' as model;
+import '../stores/store.dart';
+import '../styles/common.dart' as style;
+import '../utils/image_provider.dart' as imageProvider;
+import 'syncslides_page.dart';
+import 'toast.dart' as toast;
+
+final GlobalKey _scaffoldKey = new GlobalKey();
+
+class QuestionListPage extends SyncSlidesPage {
+  final String _deckId;
+
+  QuestionListPage(this._deckId);
+
+  Widget build(BuildContext context, AppState appState, AppActions appActions) {
+    if (!appState.decks.containsKey(_deckId)) {
+      // TODO(aghassemi): Proper error page with navigation back to main view.
+      return new Text('Deck no longer exists.');
+    }
+    var deckState = appState.decks[_deckId];
+    var presentationState = deckState.presentation;
+    if (presentationState == null) {
+      // TODO(aghassemi): Proper error page with navigation back to main view.
+      return new Text('Not in a presentation.');
+    }
+    if (!presentationState.isOwner) {
+      // TODO(aghassemi): Proper error page with navigation back to main view.
+      return new Text('Only presentation owner can see the list of questions.');
+    }
+
+    return new Scaffold(
+        key: _scaffoldKey,
+        toolBar: new ToolBar(
+            left: new IconButton(
+                icon: 'navigation/arrow_back',
+                onPressed: () => Navigator.of(context).pop()),
+            center: new Text('Answer questions')),
+        body: new Material(
+            child: new QuestionList(_deckId, presentationState,
+                deckState.slides, appActions, appState)));
+  }
+}
+
+class QuestionList extends StatelessComponent {
+  String _deckId;
+  PresentationState _presentationState;
+  List<model.Slide> _slides;
+  AppActions _appActions;
+  AppState _appState;
+  QuestionList(this._deckId, this._presentationState, this._slides,
+      this._appActions, this._appState);
+
+  Widget build(BuildContext context) {
+    List<Widget> questionCards = _presentationState.questions
+        .map((model.Question q) => _buildQuestionCard(context, q))
+        .toList();
+    return new ScrollableViewport(child: new Block(questionCards));
+  }
+
+  Widget _buildQuestionCard(BuildContext context, model.Question q) {
+    List<Widget> titleChildren = [
+      new Text('Slide ${q.slideNum + 1}', style: style.Text.titleStyle)
+    ];
+
+    Widget jumpToSlide;
+    if (_presentationState.isDriving(_appState.user)) {
+      jumpToSlide = new IconButton(icon: 'av/loop', onPressed: () async {
+        await _appActions.setCurrSlideNum(_deckId, q.slideNum);
+        toast.info(_scaffoldKey, 'Jumped to slide ${q.slideNum + 1}');
+      });
+      titleChildren.add(jumpToSlide);
+    }
+
+    Widget title = new Column([
+      new Text('${q.questioner.name} asked about',
+          style: style.Text.subtitleStyle),
+      new Row(titleChildren)
+    ], alignItems: FlexAlignItems.start);
+
+    Widget thumbnail = new Container(
+        child: new AsyncImage(
+            width: style.Size.questionListThumbnailWidth,
+            provider:
+                imageProvider.getSlideImage(_deckId, _slides[q.slideNum])));
+
+    Widget titleAndThumbnail = new Row([
+      new Flexible(child: title, flex: 2),
+      new Flexible(child: thumbnail, flex: 1)
+    ], alignItems: FlexAlignItems.start);
+
+    Widget question = new Container(
+        child: new BlockBody([titleAndThumbnail, new Text(q.text)]),
+        padding: style.Spacing.normalPadding);
+
+    Widget handoff = new GestureDetector(onTap: () async {
+      await _appActions.setDriver(_deckId, q.questioner);
+      Navigator.of(context).pop();
+    }, child: new Container(child: new Text('HAND OFF')));
+
+    Widget actions = new Container(
+        padding: style.Spacing.normalPadding,
+        decoration: new BoxDecoration(
+            border: new Border(
+                top: new BorderSide(color: style.theme.dividerColor))),
+        child: new DefaultTextStyle(
+            style: new TextStyle(color: style.theme.accentColor),
+            child: new Row([handoff], justifyContent: FlexJustifyContent.end)));
+
+    return new Card(
+        child: new Container(
+            child: new BlockBody([question, actions]),
+            margin: style.Spacing.listItemMargin));
+  }
+}
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
index 9de7fea..b030704 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -24,7 +24,6 @@
       // TODO(aghassemi): Proper error page with navigation back to main view.
       return new Text('Deck no longer exists.');
     }
-
     var deckState = appState.decks[_deckId];
     var slides = deckState.slides;
     return new Scaffold(
@@ -46,8 +45,8 @@
       return null;
     }
 
-    return new FloatingActionButton(
-        child: new Icon(icon: 'navigation/arrow_forward'), onPressed: () async {
+    return new FloatingActionButton(child: new Icon(icon: 'av/play_arrow'),
+        onPressed: () async {
       toast.info(_scaffoldKey, 'Starting presentation...',
           duration: toast.Durations.permanent);
 
@@ -90,12 +89,12 @@
     {Function onTap}) {
   var thumbnail = new AsyncImage(
       provider: imageProvider.getSlideImage(deckId, slideData),
-      height: style.Size.listHeight,
-      fit: ImageFit.cover);
+      fit: ImageFit.scaleDown);
 
-  thumbnail = new Flexible(child: thumbnail);
+  thumbnail = new Flexible(child: new Container(child: thumbnail), flex: 0);
 
-  var title = new Text('Slide $slideIndex', style: style.Text.subtitleStyle);
+  var title =
+      new Text('Slide ${slideIndex + 1}', 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/components/slideshow.dart b/dart/lib/components/slideshow.dart
index 8fb94a6..ce06c28 100644
--- a/dart/lib/components/slideshow.dart
+++ b/dart/lib/components/slideshow.dart
@@ -3,15 +3,18 @@
 // license that can be found in the LICENSE file.
 
 import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
 import 'package:logging/logging.dart';
 
 import '../stores/store.dart';
 import '../styles/common.dart' as style;
 import '../utils/image_provider.dart' as imageProvider;
+import 'askquestion.dart';
+import 'questionlist.dart';
 import 'slideshow_immersive.dart';
 import 'syncslides_page.dart';
 
+final GlobalKey _scaffoldKey = new GlobalKey();
+
 final Logger log = new Logger('components/slideshow');
 
 class SlideshowPage extends SyncSlidesPage {
@@ -26,18 +29,21 @@
     }
 
     return new Scaffold(
+        key: _scaffoldKey,
         body: new Material(
-            child: new SlideShow(appActions, appState.decks[_deckId])));
+            child:
+                new SlideShow(appActions, appState, appState.decks[_deckId])));
   }
 }
 
 class SlideShow extends StatelessComponent {
   AppActions _appActions;
+  AppState _appState;
   DeckState _deckState;
   NavigatorState _navigator;
   int _currSlideNum;
 
-  SlideShow(this._appActions, this._deckState);
+  SlideShow(this._appActions, this._appState, this._deckState);
 
   Widget build(BuildContext context) {
     _navigator = Navigator.of(context);
@@ -76,24 +82,26 @@
   }
 
   Widget _buildPortraitLayout(BuildContext context) {
-    // Portrait mode is a column layout divided as 5 parts image, 1 part actionbar
-    // 3 parts notes and 3 parts next/previous navigation thumbnails.
     var image = new Flexible(child: _buildImage(), flex: 5);
     var actions = new Flexible(child: _buildActions(), flex: 1);
     var notes = new Flexible(child: _buildNotes(), flex: 3);
-    var nav = new Flexible(child: _buildPortraitNav(), flex: 3);
-    var layout = new Column([image, actions, notes, nav],
-        alignItems: FlexAlignItems.stretch);
+    var nav = new Flexible(child: new Row(_buildThumbnailNavs()), flex: 3);
+
+    var items = [image, actions, notes, nav];
+
+    var footer = _buildFooter();
+    if (footer != null) {
+      items.add(footer);
+    }
+
+    var layout = new Column(items, alignItems: FlexAlignItems.stretch);
 
     return layout;
   }
 
   Widget _buildLandscapeLayout(BuildContext context) {
-    // Landscape mode is a two column layout.
-    // First column is divided as 5 parts notes, 8 parts parts next/previous navigation thumbnails.
-    // Second column is divided as 11 parts image, 2 parts actionbar.
     var notes = new Flexible(child: _buildNotes(), flex: 5);
-    var nav = new Flexible(child: _buildLandscapeNav(), flex: 8);
+    var nav = new Flexible(child: new Column(_buildThumbnailNavs()), flex: 8);
 
     var image = new Flexible(child: _buildImage(), flex: 11);
     var actions = new Flexible(child: _buildActions(), flex: 2);
@@ -108,31 +116,31 @@
     var layout = new Row([notesAndNavColumn, imageAndActionsColumn],
         alignItems: FlexAlignItems.stretch);
 
+    var footer = _buildFooter();
+    if (footer != null) {
+      layout = new Column([new Flexible(child: layout, flex: 8), footer],
+          alignItems: FlexAlignItems.stretch);
+    }
+
     return layout;
   }
 
-  Widget _buildPortraitNav() {
-    return new Row([
-      _buildThumbnailNav(_currSlideNum - 1),
-      _buildThumbnailNav(_currSlideNum + 1)
-    ]);
-  }
-
-  Widget _buildLandscapeNav() {
-    return new Column([
-      _buildThumbnailNav(_currSlideNum - 1),
-      _buildThumbnailNav(_currSlideNum + 1)
-    ]);
+  List<Widget> _buildThumbnailNavs() {
+    return <Widget>[
+      _buildThumbnailNav(_currSlideNum - 1, 'Previous'),
+      _buildThumbnailNav(_currSlideNum + 1, 'Next')
+    ];
   }
 
   Widget _buildImage() {
     var provider = imageProvider.getSlideImage(
         _deckState.deck.key, _deckState.slides[_currSlideNum]);
 
-    var image = new AsyncImage(provider: provider);
+    var image = new AsyncImage(provider: provider, fit: ImageFit.scaleDown);
 
     // If not driving the presentation, tapping the image navigates to the immersive mode.
-    if (_deckState.presentation == null || !_deckState.presentation.isDriving) {
+    if (_deckState.presentation == null ||
+        !_deckState.presentation.isDriving(_appState.user)) {
       image = new InkWell(child: image, onTap: () {
         _navigator.push(new MaterialPageRoute(
             builder: (context) =>
@@ -140,13 +148,14 @@
       });
     }
 
-    return new Row([image],
-        justifyContent: FlexJustifyContent.center,
-        alignItems: FlexAlignItems.stretch);
+    var counter = _buildBubbleOverlay(
+        '${_currSlideNum + 1} of ${_deckState.slides.length}', 0.5, 0.98);
+    image = new Stack([image, counter]);
+
+    return new ClipRect(child: image);
   }
 
   Widget _buildNotes() {
-    // TODO(aghassemi): Notes data.
     var notes =
         new Text('Notes (only you see these)', style: style.Text.subtitleStyle);
     var container = new Container(
@@ -158,17 +167,17 @@
     return container;
   }
 
-  Widget _buildThumbnailNav(int slideNum) {
+  Widget _buildThumbnailNav(int slideNum, String label) {
     var container;
 
     if (slideNum >= 0 && slideNum < _deckState.slides.length) {
       var thumbnail = new AsyncImage(
           provider: imageProvider.getSlideImage(
               _deckState.deck.key, _deckState.slides[slideNum]),
+          height: style.Size.thumbnailNavHeight,
           fit: ImageFit.scaleDown);
 
-      container = new Row([thumbnail]);
-      container = new InkWell(child: container, onTap: () {
+      container = new InkWell(child: thumbnail, onTap: () {
         _appActions.setCurrSlideNum(_deckState.deck.key, slideNum);
       });
     } else {
@@ -178,6 +187,10 @@
               backgroundColor: style.theme.primarySwatch[100]));
     }
 
+    var nextPreviousBubble = _buildBubbleOverlay(label, 0.5, 0.05);
+    container = new Stack([container, nextPreviousBubble]);
+    container = new ClipRect(child: container);
+
     return new Flexible(child: container, flex: 1);
   }
 
@@ -190,6 +203,7 @@
 
     _buildActions_prev(left, right);
     _buildActions_slidelist(left, right);
+    _buildActions_question(left, right);
     _buildActions_next(left, right);
     _buildActions_followPresentation(left, right);
 
@@ -216,6 +230,45 @@
     left.add(slideList);
   }
 
+  void _buildActions_question(List<Widget> left, List<Widget> right) {
+    if (_deckState.presentation == null) {
+      return;
+    }
+
+    // Presentation over is taken to a list of questions view.
+    if (_deckState.presentation.isOwner) {
+      var numQuestions = new FloatingActionButton(
+          child: new Text(_deckState.presentation.questions.length.toString(),
+              style: style.theme.primaryTextTheme.title));
+      // TODO(aghassemi): Find a better way. Scaling down a FAB and
+      // using transform to position it does not seem to be the best approach.
+      final Matrix4 moveUp = new Matrix4.identity().translate(-95.0, 25.0);
+      final Matrix4 scaleDown = new Matrix4.identity().scale(0.3);
+      numQuestions = new Transform(child: numQuestions, transform: moveUp);
+      numQuestions = new Transform(child: numQuestions, transform: scaleDown);
+
+      var questions = new InkWell(
+          child: new Icon(icon: 'communication/live_help'), onTap: () {
+        _navigator.push(new MaterialPageRoute(
+            builder: (context) => new QuestionListPage(_deckState.deck.key)));
+      });
+
+      left.add(questions);
+      left.add(numQuestions);
+    } else {
+      // Audience is taken to ask a question view.
+      var route = new MaterialPageRoute(
+          builder: (context) =>
+              new AskQuestionPage(_deckState.deck.key, _currSlideNum));
+
+      var askQuestion = new InkWell(
+          child: new Icon(icon: 'communication/live_help'), onTap: () {
+        _navigator.push(route);
+      });
+      left.add(askQuestion);
+    }
+  }
+
   final Matrix4 moveUpFabTransform =
       new Matrix4.identity().translate(0.0, -27.5);
 
@@ -230,7 +283,8 @@
 
     // If driving the presentation, show a bigger FAB next button on the right side,
     // otherwise a regular next button on the left side.
-    if (_deckState.presentation != null && _deckState.presentation.isDriving) {
+    if (_deckState.presentation != null &&
+        _deckState.presentation.isDriving(_appState.user)) {
       var next = new FloatingActionButton(
           child: new Icon(icon: 'navigation/arrow_forward'),
           onPressed: nextOnTap);
@@ -265,10 +319,77 @@
     right.add(syncNav);
   }
 
+  Widget _buildFooter() {
+    if (_deckState.presentation == null) {
+      return null;
+    }
+
+    // Owner and not driving?
+    if (_deckState.presentation.isOwner &&
+        !_deckState.presentation.isDriving(_appState.user)) {
+      SnackBarAction resume =
+          new SnackBarAction(label: 'RESUME', onPressed: () {
+        _appActions.setDriver(_deckState.deck.key, _appState.user);
+      });
+
+      return _buildSnackbarFooter('You have handed off control.',
+          action: resume);
+    }
+
+    // Driving but not the owner?
+    if (!_deckState.presentation.isOwner &&
+        _deckState.presentation.isDriving(_appState.user)) {
+      return _buildSnackbarFooter('You are now driving the presentation.');
+    }
+
+    return null;
+  }
+
   _buildActions_addMargin(List<Widget> actions) {
     return actions
         .map(
             (w) => new Container(child: w, margin: style.Spacing.actionsMargin))
         .toList();
   }
+
+  Widget _buildBubbleOverlay(String text, double xOffset, double yOffset) {
+    return new Align(
+        child: new Container(
+            child: new DefaultTextStyle(
+                child: new Text(text), style: Typography.white.body1),
+            decoration: new BoxDecoration(
+                borderRadius: 50.0, // Make the bubble round.
+                backgroundColor:
+                    style.Box.bubbleOverlayBackground), // Transparent gray.
+            padding: new EdgeDims.symmetric(horizontal: 5.0, vertical: 2.0)),
+        alignment: new FractionalOffset(xOffset, yOffset));
+  }
+
+  _buildSnackbarFooter(String lable, {SnackBarAction action}) {
+    var text = new Text(lable);
+    text = new DefaultTextStyle(style: Typography.white.subhead, child: text);
+    List<Widget> children = <Widget>[
+      new Flexible(
+          child: new Container(
+              margin: style.Spacing.footerVerticalMargin,
+              child: new DefaultTextStyle(
+                  style: Typography.white.subhead, child: text)))
+    ];
+
+    if (action != null) {
+      children.add(action);
+    }
+
+    var clipper = new ClipRect(
+        child: new Material(
+            elevation: 6,
+            color: style.Box.footerBackground,
+            child: new Container(
+                margin: style.Spacing.footerHorizontalMargin,
+                child: new DefaultTextStyle(
+                    style: new TextStyle(color: style.theme.accentColor),
+                    child: new Row(children)))));
+
+    return new Flexible(child: clipper, flex: 1);
+  }
 }
diff --git a/dart/lib/components/toast.dart b/dart/lib/components/toast.dart
index 488601d..841324f 100644
--- a/dart/lib/components/toast.dart
+++ b/dart/lib/components/toast.dart
@@ -5,9 +5,12 @@
 import 'dart:core';
 
 import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
 
 import '../styles/common.dart' as style;
 
+final Logger log = new Logger('components/toast');
+
 class Durations {
   static const Duration permanent = const Duration(days: 100);
   static const Duration long = const Duration(seconds: 5);
@@ -31,6 +34,8 @@
       // TODO(aghassemi): Add "Details" action to error toasts and move error text there.
       content: new Text(text + ' - ERROR: $err', style: style.Text.error),
       duration: duration));
+
+  log.severe(err);
 }
 
 void _closePrevious() {
diff --git a/dart/lib/components/utils/stop_wrapping.dart b/dart/lib/components/utils/stop_wrapping.dart
new file mode 100644
index 0000000..7c4bd17
--- /dev/null
+++ b/dart/lib/components/utils/stop_wrapping.dart
@@ -0,0 +1,14 @@
+// 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:flutter/material.dart';
+
+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/config.dart b/dart/lib/config.dart
deleted file mode 100644
index 1ba0489..0000000
--- a/dart/lib/config.dart
+++ /dev/null
@@ -1,8 +0,0 @@
-// 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.
-
-// TODO(aghassemi): Make these configurable from command line and/or UI.
-bool DemoEnabled = true;
-
-String mounttableAddr = '/192.168.86.254:8101';
diff --git a/dart/lib/identity/client.dart b/dart/lib/identity/client.dart
new file mode 100644
index 0000000..91de45b
--- /dev/null
+++ b/dart/lib/identity/client.dart
@@ -0,0 +1,36 @@
+// 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 'package:flutter/services.dart' show shell;
+import 'package:logging/logging.dart';
+import 'package:mojo_services/authentication/authentication.mojom.dart' as auth;
+
+import '../models/all.dart' as model;
+import '../settings/client.dart' as settings;
+
+final Logger log = new Logger('identity/client');
+
+// TODO(aghassemi): Switch to using the principal service.
+// See https://github.com/vanadium/issues/issues/955
+const String authenticationUrl = 'mojo:authentication';
+
+Future<model.User> getUser() async {
+  model.Settings s = await settings.getSettings();
+
+  auth.AuthenticationServiceProxy authenticator =
+      new auth.AuthenticationServiceProxy.unbound();
+
+  shell.connectToService(authenticationUrl, authenticator);
+  var account = await authenticator.ptr.selectAccount(true);
+
+  // TODO(aghassemi): How do I get the blessing name from the username?
+  // I don't think the following is correct as it seems the actual blessing
+  // has an app specific prefix.
+  // See https://github.com/vanadium/issues/issues/955
+  // See https://github.com/vanadium/issues/issues/956
+  var blessing = 'dev.v.io:u:${account.username}';
+  return new model.User(account.username, blessing, s.deviceId);
+}
diff --git a/dart/lib/main.dart b/dart/lib/main.dart
index 6ef619e..ab0ed02 100644
--- a/dart/lib/main.dart
+++ b/dart/lib/main.dart
@@ -5,20 +5,23 @@
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 
-import 'styles/common.dart' as style;
 import 'components/deckgrid.dart';
+import 'stores/store.dart';
+import 'styles/common.dart' as style;
 import 'utils/back_button.dart' as backButtonUtil;
 
 NavigatorState _navigator;
 
 void main() {
+  var store = new Store.singleton();
   _initLogging();
   _initBackButtonHandler();
 
-  runApp(new MaterialApp(
+  // TODO(aghassemi): Splash screen while store is initializing.
+  store.init().then((_) => runApp(new MaterialApp(
       theme: style.theme,
       title: 'SyncSlides',
-      routes: {'/': (RouteArguments args) => new LandingPage()}));
+      routes: {'/': (RouteArguments args) => new LandingPage()})));
 }
 
 class LandingPage extends StatelessComponent {
diff --git a/dart/lib/models/all.dart b/dart/lib/models/all.dart
index c802c0b..e2dbdbd 100644
--- a/dart/lib/models/all.dart
+++ b/dart/lib/models/all.dart
@@ -3,5 +3,8 @@
 // license that can be found in the LICENSE file.
 
 export 'deck.dart';
-export 'slide.dart';
 export 'presentation_advertisement.dart';
+export 'question.dart';
+export 'settings.dart';
+export 'slide.dart';
+export 'user.dart';
diff --git a/dart/lib/models/question.dart b/dart/lib/models/question.dart
new file mode 100644
index 0000000..cc7fd73
--- /dev/null
+++ b/dart/lib/models/question.dart
@@ -0,0 +1,46 @@
+// 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:convert';
+
+import 'user.dart';
+
+class Question {
+  // TODO(aghassemi): Fix inconsistencies between key and id everywhere.
+  String _id;
+  String get id => _id;
+
+  String _text;
+  String get text => _text;
+
+  int _slideNum;
+  int get slideNum => _slideNum;
+
+  User _questioner;
+  User get questioner => _questioner;
+
+  DateTime _timestamp;
+  DateTime get timestamp => _timestamp;
+
+  Question(
+      this._id, this._text, this._slideNum, this._questioner, this._timestamp);
+
+  Question.fromJson(String id, String json) {
+    Map map = JSON.decode(json);
+    _id = id;
+    _text = map['text'];
+    _slideNum = map['slidenum'];
+    _questioner = new User.fromJson(map['questioner']);
+    _timestamp = new DateTime.fromMillisecondsSinceEpoch(map['timestamp']);
+  }
+
+  String toJson() {
+    Map map = new Map();
+    map['text'] = text;
+    map['slidenum'] = slideNum;
+    map['questioner'] = questioner.toJson();
+    map['timestamp'] = timestamp.millisecondsSinceEpoch;
+    return JSON.encode(map);
+  }
+}
diff --git a/dart/lib/models/settings.dart b/dart/lib/models/settings.dart
new file mode 100644
index 0000000..cfb1638
--- /dev/null
+++ b/dart/lib/models/settings.dart
@@ -0,0 +1,28 @@
+// 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:convert';
+
+class Settings {
+  String _deviceId;
+  String get deviceId => _deviceId;
+
+  String _mounttable;
+  String get mounttable => _mounttable;
+
+  Settings(this._deviceId, this._mounttable);
+
+  Settings.fromJson(String json) {
+    Map map = JSON.decode(json);
+    _deviceId = map['deviceid'];
+    _mounttable = map['mounttable'];
+  }
+
+  String toJson() {
+    Map map = new Map();
+    map['deviceid'] = deviceId;
+    map['mounttable'] = mounttable;
+    return JSON.encode(map);
+  }
+}
diff --git a/dart/lib/models/user.dart b/dart/lib/models/user.dart
new file mode 100644
index 0000000..15e9607
--- /dev/null
+++ b/dart/lib/models/user.dart
@@ -0,0 +1,33 @@
+// 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:convert';
+
+class User {
+  String _name;
+  String get name => _name;
+
+  String _blessing;
+  String get blessing => _blessing;
+
+  String _deviceId;
+  String get deviceId => _deviceId;
+
+  User(this._name, this._blessing, this._deviceId);
+
+  User.fromJson(String json) {
+    Map map = JSON.decode(json);
+    _name = map['name'];
+    _blessing = map['blessing'];
+    _deviceId = map['deviceid'];
+  }
+
+  String toJson() {
+    Map map = new Map();
+    map['name'] = name;
+    map['blessing'] = blessing;
+    map['deviceid'] = deviceId;
+    return JSON.encode(map);
+  }
+}
diff --git a/dart/lib/settings/client.dart b/dart/lib/settings/client.dart
new file mode 100644
index 0000000..e662b0b
--- /dev/null
+++ b/dart/lib/settings/client.dart
@@ -0,0 +1,15 @@
+// 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:io';
+
+import '../models/all.dart' as model;
+
+const String settingsFilePath = '/sdcard/syncslides_settings.json';
+
+Future<model.Settings> getSettings() async {
+  String settingsJson = await new File(settingsFilePath).readAsString();
+  return new model.Settings.fromJson(settingsJson);
+}
diff --git a/dart/lib/stores/actions.dart b/dart/lib/stores/actions.dart
index 2c6c085..678d30a 100644
--- a/dart/lib/stores/actions.dart
+++ b/dart/lib/stores/actions.dart
@@ -45,4 +45,10 @@
   // If viewer has started navigating on their own, this will align the navigation
   // back up with the presentation.
   Future followPresentation(String deckId);
+
+  // Adds a question to a specific slide within a presentation.
+  Future askQuestion(String deckId, int slideNum, String questionText);
+
+  // Sets the driver of a presentation to the given user.
+  Future setDriver(String deckId, model.User driver);
 }
diff --git a/dart/lib/stores/state.dart b/dart/lib/stores/state.dart
index c9e099c..34599c5 100644
--- a/dart/lib/stores/state.dart
+++ b/dart/lib/stores/state.dart
@@ -8,23 +8,69 @@
 // Application is rendered purely based on this state.
 // State is deeply-immutable outside of store code.
 abstract class AppState {
+  // Current user.
+  model.User get user;
+
+  // Current settings of the app.
+  model.Settings get settings;
+
+  // List of decks.
   UnmodifiableMapView<String, DeckState> get decks;
+
+  // List of presentations advertised by this instance of the app.
   UnmodifiableListView<
       model.PresentationAdvertisement> get advertisedPresentations;
+
+  // List of presentations advertised by others.
   UnmodifiableMapView<String,
       model.PresentationAdvertisement> get presentationAdvertisements;
 }
 
 abstract class DeckState {
+  // The deck.
   model.Deck get deck;
+
+  // List of slides.
   UnmodifiableListView<model.Slide> get slides;
+
+  // State of the presentation for a deck.
+  // null if deck is not in a presentation.
   PresentationState get presentation;
+
+  // Local current slide number for the deck.
   int get currSlideNum;
 }
 
 abstract class PresentationState {
+  // Presentation id.
   String get key;
+
+  // Shared slide number for the presentation.
   int get currSlideNum;
-  bool get isDriving;
+
+  // User who is driving the presentation.
+  model.User get driver;
+
+  // Whether current user owns the presentation.
+  bool get isOwner;
+
+  // Whether current user is following the presentation or has started
+  // navigating on their own.
   bool get isFollowingPresentation;
+
+  // Questions asked in the presentation.
+  UnmodifiableListView<model.Question> get questions;
+
+  // Returns true if the given user is driving the presentation.
+  bool isDriving(model.User currUser) {
+    // TODO(aghassemi): We currently check the deviceId in addition to the blessing
+    // to decide if current user is the driver.
+    // This can change in the future when we have the concept of user sessions and sync
+    // all of a user's data across their own devices, but currently, same user using a different
+    // device is treated like another user everywhere else in the UI so there is no point in
+    // deviating from that behaviour for driving a presentation.
+    return this.driver != null &&
+        this.driver.blessing == currUser.blessing &&
+        this.driver.deviceId == currUser.deviceId;
+  }
 }
diff --git a/dart/lib/stores/store.dart b/dart/lib/stores/store.dart
index da994d5..3c980dc 100644
--- a/dart/lib/stores/store.dart
+++ b/dart/lib/stores/store.dart
@@ -24,4 +24,8 @@
   AppActions get actions;
   AppState get state;
   Stream get onStateChange;
+
+  // Initializes the store and loads parts of the state that are required before
+  // the application can start, such as the user information and settings.
+  Future init();
 }
diff --git a/dart/lib/stores/syncbase/actions.dart b/dart/lib/stores/syncbase/actions.dart
index 978a924..8513b06 100644
--- a/dart/lib/stores/syncbase/actions.dart
+++ b/dart/lib/stores/syncbase/actions.dart
@@ -49,7 +49,7 @@
     // Is slide number change happening within a presentation?
     if (deckState.presentation != null) {
       // Is the current user driving the presentation?
-      if (deckState.presentation.isDriving) {
+      if (deckState.presentation.isDriving(_state.user)) {
         // Update the common slide number for the presentation.
         sb.SyncbaseTable tb = await _getPresentationsTable();
         await tb.put(
@@ -89,13 +89,13 @@
     }
 
     String uuid = uuidutil.createUuid();
-    String syncgroupName = keyutil.getPresentationSyncgroupName(uuid);
+    String syncgroupName = _getPresentationSyncgroupName(_state.settings, uuid);
 
     model.Deck deck = _state._getOrCreateDeckState(deckId)._deck;
     var presentation =
         new model.PresentationAdvertisement(uuid, deck, syncgroupName);
 
-    await sb.createSyncgroup(syncgroupName, [
+    await sb.createSyncgroup(_state.settings.mounttable, syncgroupName, [
       sb.SyncbaseClient.syncgroupPrefix(decksTableName, deckId),
       sb.SyncbaseClient.syncgroupPrefix(presentationsTableName, deckId)
     ]);
@@ -103,53 +103,77 @@
     await discovery.advertise(presentation);
     _state._advertisedPresentations.add(presentation);
 
-    await joinPresentation(presentation);
+    // Set the presentation state for the deck.
+    _DeckState deckState = _state._getOrCreateDeckState(deckId);
+    _PresentationState presentationstate =
+        deckState._getOrCreatePresentationState(presentation.key);
+    presentationstate._isOwner = true;
+
+    setDefaultsAndJoin() async {
+      // Set the current slide number to 0.
+      sb.SyncbaseTable tb = await _getPresentationsTable();
+      await tb.put(
+          keyutil.getPresentationCurrSlideNumKey(deckId, presentation.key),
+          [0]);
+
+      // Set the current user as the driver.
+      await _setPresentationDriver(deckId, presentation.key, _state.user);
+
+      // Also join the presentation.
+      await joinPresentation(presentation);
+    }
+
+    try {
+      // Wait for join. If it fails, remove the presentation state from the deck.
+      await setDefaultsAndJoin();
+    } catch (e) {
+      deckState._presentation = null;
+      throw e;
+    }
 
     return presentation;
   }
 
   Future joinPresentation(model.PresentationAdvertisement presentation) async {
-    bool isMyOwnPresentation =
-        _state._advertisedPresentations.any((p) => p.key == presentation.key);
     String deckId = presentation.deck.key;
 
     // Set the presentation state for the deck.
     _DeckState deckState = _state._getOrCreateDeckState(deckId);
-    _PresentationState presentationState =
-        deckState._getOrCreatePresentationState(presentation.key);
+    deckState._getOrCreatePresentationState(presentation.key);
 
-    // TODO(aghassemi): For now, only the presenter can drive. Later when we have
-    // identity and delegation support, this will change to: if "driver == me".
-    presentationState._isDriving = isMyOwnPresentation;
-
-    if (!isMyOwnPresentation) {
-      // Wait until at least the slide for current page number is synced.
-      join() async {
+    // Wait until at least the current slide number, driver and the slide for current slide number is synced.
+    join() async {
+      bool isMyOwnPresentation =
+          _state._advertisedPresentations.any((p) => p.key == presentation.key);
+      if (!isMyOwnPresentation) {
         await sb.joinSyncgroup(presentation.syncgroupName);
-        Completer completer = new Completer();
-        new Timer.periodic(new Duration(milliseconds: 30), (Timer timer) {
-          if (_state._decks.containsKey(deckId) &&
-              _state._decks[deckId].deck != null &&
-              _state._decks[deckId].slides.length >
-                  _state._decks[deckId].currSlideNum &&
-              !completer.isCompleted) {
-            timer.cancel();
-            completer.complete();
-          }
-        });
-        await completer.future.timeout(new Duration(seconds: 20));
       }
 
-      try {
-        // For for join. If it fails, remove the presentation state from the deck.
-        await join();
-      } catch (e) {
-        deckState._presentation = null;
-        throw e;
-      }
-
-      log.info('Joined presentation ${presentation.key}');
+      Completer completer = new Completer();
+      new Timer.periodic(new Duration(milliseconds: 30), (Timer timer) {
+        if (_state._decks.containsKey(deckId) &&
+            _state._decks[deckId].deck != null &&
+            _state._decks[deckId].slides.length >
+                _state._decks[deckId].currSlideNum &&
+            _state._decks[deckId].presentation != null &&
+            _state._decks[deckId].presentation.driver != null &&
+            !completer.isCompleted) {
+          timer.cancel();
+          completer.complete();
+        }
+      });
+      await completer.future.timeout(new Duration(seconds: 20));
     }
+
+    try {
+      // Wait for join. If it fails, remove the presentation state from the deck.
+      await join();
+    } catch (e) {
+      deckState._presentation = null;
+      throw e;
+    }
+
+    log.info('Joined presentation ${presentation.key}');
   }
 
   Future stopPresentation(String presentationId) async {
@@ -184,11 +208,53 @@
 
     deckState.presentation._isFollowingPresentation = true;
   }
+
+  Future askQuestion(String deckId, int slideNum, String questionText) async {
+    var deckState = _state._getOrCreateDeckState(deckId);
+
+    if (deckState.presentation == null) {
+      throw new ArgumentError.value(deckId,
+          'Cannot ask a question because deck is not part of a presentation');
+    }
+
+    sb.SyncbaseTable tb = await _getPresentationsTable();
+    String questionId = uuidutil.createUuid();
+
+    model.Question question = new model.Question(
+        questionId, questionText, slideNum, _state.user, new DateTime.now());
+
+    var key = keyutil.getPresentationQuestionKey(
+        deckId, deckState.presentation.key, questionId);
+
+    await tb.put(key, UTF8.encode(question.toJson()));
+  }
+
+  Future setDriver(String deckId, model.User driver) async {
+    var deckState = _state._getOrCreateDeckState(deckId);
+
+    if (deckState.presentation == null) {
+      throw new ArgumentError.value(deckId,
+          'Cannot set the driver because deck is not part of a presentation');
+    }
+    await _setPresentationDriver(deckId, deckState.presentation.key, driver);
+  }
 }
 
 //////////////////////////////////////
 // Utilities
 
+Future _setPresentationDriver(
+    String deckId, String presentationId, model.User driver) async {
+  sb.SyncbaseTable tb = await _getPresentationsTable();
+  await tb.put(keyutil.getPresentationDriverKey(deckId, presentationId),
+      UTF8.encode(driver.toJson()));
+}
+
+String _getPresentationSyncgroupName(
+    model.Settings settings, String presentationId) {
+  return '${settings.mounttable}/${settings.deviceId}/%%sync/$presentationId';
+}
+
 Future<sb.SyncbaseTable> _getTable(String tableName) async {
   sb.SyncbaseDatabase sbDb = await sb.getDatabase();
   sb.SyncbaseTable tb = sbDb.table(tableName);
diff --git a/dart/lib/stores/syncbase/state.dart b/dart/lib/stores/syncbase/state.dart
index 060b284..f859fd0 100644
--- a/dart/lib/stores/syncbase/state.dart
+++ b/dart/lib/stores/syncbase/state.dart
@@ -4,24 +4,30 @@
 
 part of syncbase_store;
 
-class _AppState implements AppState {
-  List<model.PresentationAdvertisement> advertisedPresentations;
-  Map<String, model.PresentationAdvertisement> presentationAdvertisements;
-  Map<String, DeckState> decks;
-  Map<String, PresentationState> presentations;
+class _AppState extends AppState {
+  model.User get user => _user;
+  model.Settings get settings => _settings;
+  UnmodifiableMapView<String, DeckState> decks;
+  UnmodifiableListView<model.PresentationAdvertisement> advertisedPresentations;
+  UnmodifiableMapView<String,
+      model.PresentationAdvertisement> presentationAdvertisements;
 
   _AppState() {
-    presentationAdvertisements =
-        new UnmodifiableMapView(_presentationsAdvertisements);
+    _user = null;
+    _settings = null;
     decks = new UnmodifiableMapView(_decks);
     advertisedPresentations =
         new UnmodifiableListView(_advertisedPresentations);
+    presentationAdvertisements =
+        new UnmodifiableMapView(_presentationsAdvertisements);
   }
 
-  Map<String, model.PresentationAdvertisement> _presentationsAdvertisements =
-      new Map();
+  model.User _user;
+  model.Settings _settings;
   Map<String, _DeckState> _decks = new Map();
   List<model.PresentationAdvertisement> _advertisedPresentations = new List();
+  Map<String, model.PresentationAdvertisement> _presentationsAdvertisements =
+      new Map();
 
   _DeckState _getOrCreateDeckState(String deckId) {
     return _decks.putIfAbsent(deckId, () {
@@ -30,12 +36,12 @@
   }
 }
 
-class _DeckState implements DeckState {
+class _DeckState extends DeckState {
   model.Deck _deck;
   model.Deck get deck => _deck;
 
   List<model.Slide> _slides = new List();
-  List<model.Slide> slides;
+  UnmodifiableListView<model.Slide> slides;
 
   PresentationState _presentation = null;
   PresentationState get presentation => _presentation;
@@ -55,17 +61,25 @@
   }
 }
 
-class _PresentationState implements PresentationState {
+class _PresentationState extends PresentationState {
   final String key;
 
   int _currSlideNum = 0;
   int get currSlideNum => _currSlideNum;
 
-  bool _isDriving = false;
-  bool get isDriving => _isDriving;
+  model.User _driver;
+  model.User get driver => _driver;
+
+  bool _isOwner = false;
+  bool get isOwner => _isOwner;
 
   bool _isFollowingPresentation = true;
   bool get isFollowingPresentation => _isFollowingPresentation;
 
-  _PresentationState(this.key);
+  List<model.Question> _questions = new List();
+  UnmodifiableListView<model.Question> questions;
+
+  _PresentationState(this.key) {
+    questions = new UnmodifiableListView(_questions);
+  }
 }
diff --git a/dart/lib/stores/syncbase/store.dart b/dart/lib/stores/syncbase/store.dart
index c4105f9..252f6d7 100644
--- a/dart/lib/stores/syncbase/store.dart
+++ b/dart/lib/stores/syncbase/store.dart
@@ -11,8 +11,10 @@
 import 'package:logging/logging.dart';
 
 import '../../discovery/client.dart' as discovery;
+import '../../identity/client.dart' as identity;
 import '../../loaders/loader.dart';
 import '../../models/all.dart' as model;
+import '../../settings/client.dart' as settings;
 import '../../syncbase/client.dart' as sb;
 import '../../utils/errors.dart' as errorsutil;
 import '../../utils/uuid.dart' as uuidutil;
@@ -20,8 +22,8 @@
 import '../utils/key.dart' as keyutil;
 
 part 'actions.dart';
-part 'state.dart';
 part 'consts.dart';
+part 'state.dart';
 
 // Implementation of Store using Syncbase (http://v.io/syncbase) storage system.
 class SyncbaseStore implements Store {
@@ -36,18 +38,34 @@
   SyncbaseStore() {
     _state = new _AppState();
     _actions = new _AppActions(_state, _triggerStateChange);
+  }
 
-    sb.getDatabase().then((sb.SyncbaseDatabase db) async {
-      // Make sure all table exists.
-      await _ensureTablesExist();
+  Future init() async {
+    // Wait for synchronous initializers.
+    await _syncInits();
 
-      // TODO(aghassemi): Use the multi-table scan and watch API when ready.
-      // See https://github.com/vanadium/issues/issues/923
-      for (String table in [decksTableName, presentationsTableName]) {
-        _getInitialValuesAndStartWatching(db, table);
-      }
-      _startScanningForPresentations();
-    });
+    // Don't wait for async ones.
+    _asyncInits();
+  }
+
+  // Initializations that we must wait for before considering store initialized.
+  Future _syncInits() async {
+    _state._user = await identity.getUser();
+    _state._settings = await settings.getSettings();
+  }
+
+  // Initializations that can be done asynchronously. We do not need to wait for
+  // these before considering store initalized.
+  Future _asyncInits() async {
+    // TODO(aghassemi): Use the multi-table scan and watch API when ready.
+    // See https://github.com/vanadium/issues/issues/923
+    sb.SyncbaseDatabase db = await sb.getDatabase();
+    // Make sure all tables exist.
+    await _ensureTablesExist();
+    for (String table in [decksTableName, presentationsTableName]) {
+      _getInitialValuesAndStartWatching(db, table);
+    }
+    _startScanningForPresentations();
   }
 
   // Note(aghassemi): We could have copied the state to provide a snapshot at the time of
@@ -120,6 +138,12 @@
       case keyutil.KeyType.PresentationCurrSlideNum:
         _onPresentationSlideNumChange(changeType, rowKey, value);
         break;
+      case keyutil.KeyType.PresentationDriver:
+        _onPresentationDriverChange(changeType, rowKey, value);
+        break;
+      case keyutil.KeyType.PresentationQuestion:
+        _onPresentationQuestionChange(changeType, rowKey, value);
+        break;
       case keyutil.KeyType.Unknown:
         log.severe('Got change for $rowKey with an unknown key type.');
     }
@@ -170,6 +194,51 @@
     }
   }
 
+  _onPresentationDriverChange(int changeType, String rowKey, List<int> value) {
+    String deckId = keyutil.presentationDriverKeyToDeckId(rowKey);
+
+    _DeckState deckState = _state._getOrCreateDeckState(deckId);
+    _PresentationState presentationState = deckState.presentation;
+    if (presentationState == null) {
+      return;
+    }
+
+    if (changeType == sb.WatchChangeTypes.put) {
+      model.User driver = new model.User.fromJson(UTF8.decode(value));
+      presentationState._driver = driver;
+      log.info('${driver.name} is now driving the presentation.');
+    } else {
+      presentationState._driver = _state.user;
+    }
+  }
+
+  _onPresentationQuestionChange(
+      int changeType, String rowKey, List<int> value) {
+    String deckId = keyutil.presentationQuestionKeyToDeckId(rowKey);
+
+    _DeckState deckState = _state._getOrCreateDeckState(deckId);
+    _PresentationState presentationState = deckState.presentation;
+    if (presentationState == null) {
+      return;
+    }
+
+    String questionId = keyutil.presentationQuestionKeyToQuestionId(rowKey);
+
+    if (changeType == sb.WatchChangeTypes.put) {
+      model.Question question =
+          new model.Question.fromJson(questionId, UTF8.decode(value));
+      presentationState._questions.add(question);
+    } else {
+      presentationState._questions
+          .removeWhere((model.Question q) => q.id == questionId);
+    }
+
+    // Keep questions sorted by timestamp.
+    presentationState._questions.sort((model.Question a, model.Question b) {
+      return a.timestamp.compareTo(b.timestamp);
+    });
+  }
+
   Future _ensureTablesExist() async {
     await _getDecksTable();
     await _getPresentationsTable();
diff --git a/dart/lib/stores/utils/key.dart b/dart/lib/stores/utils/key.dart
index a8943ad..83b8e1a 100644
--- a/dart/lib/stores/utils/key.dart
+++ b/dart/lib/stores/utils/key.dart
@@ -2,9 +2,14 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-import '../../config.dart' as config;
-
-enum KeyType { Deck, Slide, PresentationCurrSlideNum, Unknown }
+enum KeyType {
+  Deck,
+  Slide,
+  PresentationCurrSlideNum,
+  PresentationDriver,
+  PresentationQuestion,
+  Unknown
+}
 
 KeyType getKeyType(String key) {
   if (isDeckKey(key)) {
@@ -13,6 +18,10 @@
     return KeyType.Slide;
   } else if (isPresentationCurrSlideNumKey(key)) {
     return KeyType.PresentationCurrSlideNum;
+  } else if (isPresentationDriverKey(key)) {
+    return KeyType.PresentationDriver;
+  } else if (isPresentationQuestionKey(key)) {
+    return KeyType.PresentationQuestion;
   } else {
     return KeyType.Unknown;
   }
@@ -46,7 +55,7 @@
 // Gets the deck id given a slide key.
 String currSlideKeyToDeckId(String key) {
   if ((!isSlideKey(key))) {
-    throw new ArgumentError("$key is not a valid slide key.");
+    throw new ArgumentError('$key is not a valid slide key.');
   }
   return key.substring(0, key.indexOf('/slides/'));
 }
@@ -54,7 +63,7 @@
 // Gets the slide index given a slide key.
 int currSlideKeyToIndex(String key) {
   if ((!isSlideKey(key))) {
-    throw new ArgumentError("$key is not a valid slide key.");
+    throw new ArgumentError('$key is not a valid slide key.');
   }
   var indexStr = key.substring(key.lastIndexOf('/') + 1);
   return int.parse(indexStr);
@@ -74,7 +83,7 @@
 String presentationCurrSlideNumKeyToDeckId(String currSlideNumKey) {
   if ((!isPresentationCurrSlideNumKey(currSlideNumKey))) {
     throw new ArgumentError(
-        "$currSlideNumKey is not a valid presentation current slide number key.");
+        '$currSlideNumKey is not a valid presentation current slide number key.');
   }
   return _currPresentationSlideNumPattern.firstMatch(currSlideNumKey).group(1);
 }
@@ -84,12 +93,51 @@
   return _currPresentationSlideNumPattern.hasMatch(key);
 }
 
-// Constructs the Syncgroup name for a presentation.
-String getPresentationSyncgroupName(String presentationId) {
-  // TODO(aghassemi): Currently we are assuming the first device
-  // mounts itself under the name 'syncslides' and then every other device
-  // creates the Syncgroup on the first device.
-  // We should have each device mount itself under a unique name and
-  // to create the Syncgroup on their own instance.
-  return '${config.mounttableAddr}/syncslides/%%sync/$presentationId';
+// TODO(aghassemi): Don't use regex, just regular split should be fine.
+final RegExp _presentationDriverPattern =
+    new RegExp('($_uuidPattern)(?:/$_uuidPattern)(?:/driver)');
+// Constructs a presentation driver key.
+String getPresentationDriverKey(String deckId, String presentationId) {
+  return '$deckId/$presentationId/driver';
+}
+
+// Gets the deck id given a presentation driver key.
+String presentationDriverKeyToDeckId(String driverKey) {
+  if ((!isPresentationDriverKey(driverKey))) {
+    throw new ArgumentError(
+        '$driverKey is not a valid presentation driver key.');
+  }
+  return _presentationDriverPattern.firstMatch(driverKey).group(1);
+}
+
+// Returns true if a key is a presentation driver key.
+bool isPresentationDriverKey(String key) {
+  return _presentationDriverPattern.hasMatch(key);
+}
+
+// TODO(aghassemi): Don't use regex, just regular split should be fine.
+final RegExp _presentationQuestionPattern = new RegExp(
+    '($_uuidPattern)(?:/$_uuidPattern)(?:/questions/)($_uuidPattern)');
+String getPresentationQuestionKey(
+    String deckId, String presentationId, String questionId) {
+  return '$deckId/$presentationId/questions/$questionId';
+}
+
+String presentationQuestionKeyToDeckId(String key) {
+  if ((!isPresentationQuestionKey(key))) {
+    throw new ArgumentError('$key is not a valid presentation question key.');
+  }
+  return _presentationQuestionPattern.firstMatch(key).group(1);
+}
+
+String presentationQuestionKeyToQuestionId(String key) {
+  if ((!isPresentationQuestionKey(key))) {
+    throw new ArgumentError('$key is not a valid presentation question key.');
+  }
+  return _presentationQuestionPattern.firstMatch(key).group(2);
+}
+
+// Returns true if a key is a presentation question key.
+bool isPresentationQuestionKey(String key) {
+  return _presentationQuestionPattern.hasMatch(key);
 }
diff --git a/dart/lib/styles/common.dart b/dart/lib/styles/common.dart
index 17962e8..01bd5ff 100644
--- a/dart/lib/styles/common.dart
+++ b/dart/lib/styles/common.dart
@@ -17,9 +17,9 @@
 
 class Size {
   static const double thumbnailWidth = 250.0;
-  static const double listHeight = 150.0;
-  static const double thumbnailNavHeight = 150.0;
-  static const double thumbnailNavWidth = 267.0;
+  static const double listHeight = 120.0;
+  static const double thumbnailNavHeight = 250.0;
+  static const double questionListThumbnailWidth = 100.0;
 }
 
 class Spacing {
@@ -28,11 +28,18 @@
   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 actionsMargin = new EdgeDims.only(right: 20.0);
+  static final EdgeDims actionsMargin =
+      new EdgeDims.symmetric(horizontal: 10.0);
   static final EdgeDims fabMargin = new EdgeDims.only(right: 7.0);
+  static final EdgeDims footerVerticalMargin =
+      const EdgeDims.symmetric(vertical: 14.0);
+  static final EdgeDims footerHorizontalMargin =
+      const EdgeDims.symmetric(horizontal: 24.0);
 }
 
 class Box {
+  static final Color bubbleOverlayBackground = new Color.fromARGB(80, 0, 0, 0);
+  static final Color footerBackground = new Color(0xFF323232);
   static final BoxDecoration liveNow = new BoxDecoration(
       border: new Border.all(color: theme.accentColor), borderRadius: 2.0);
 }
diff --git a/dart/lib/syncbase/client.dart b/dart/lib/syncbase/client.dart
index 9424099..dc95299 100644
--- a/dart/lib/syncbase/client.dart
+++ b/dart/lib/syncbase/client.dart
@@ -8,7 +8,6 @@
 import 'package:logging/logging.dart';
 import 'package:syncbase/syncbase_client.dart';
 
-import '../config.dart' as config;
 import '../utils/errors.dart' as errorsutil;
 
 export 'package:syncbase/syncbase_client.dart';
@@ -36,11 +35,12 @@
   return _db;
 }
 
-Future createSyncgroup(String syncgroupName, prefixes) async {
+Future createSyncgroup(
+    String mounttable, String syncgroupName, prefixes) async {
   SyncbaseDatabase sbDb = await getDatabase();
   SyncbaseSyncgroup sg = sbDb.syncgroup(syncgroupName);
   var sgSpec = SyncbaseClient.syncgroupSpec(prefixes,
-      perms: createOpenPerms(), mountTables: [config.mounttableAddr]);
+      perms: createOpenPerms(), mountTables: [mounttable]);
   var myInfo = SyncbaseClient.syncgroupMemberInfo(syncPriority: 1);
 
   await sg.create(sgSpec, myInfo);
diff --git a/dart/lib/utils/back_button.dart b/dart/lib/utils/back_button.dart
index 9df68c2..c1b9164 100644
--- a/dart/lib/utils/back_button.dart
+++ b/dart/lib/utils/back_button.dart
@@ -30,11 +30,10 @@
 
 class _InputHandler extends InputClient {
   dynamic onBackButton([Function responseFactory]) {
-    bool exit = !_handler();
-    if (exit) {
-      return null;
-    } else {
-      return responseFactory();
-    }
+    // TODO(aghassemi): Currently there is no way to tell mojo:input service
+    // to use the boolean returned by the handler to exit the app.
+    // See https://github.com/domokit/mojo/issues/544
+    _handler();
+    return responseFactory();
   }
 }
diff --git a/dart/pubspec.lock b/dart/pubspec.lock
index cfed1e8..f808f5e 100644
--- a/dart/pubspec.lock
+++ b/dart/pubspec.lock
@@ -218,7 +218,7 @@
   test:
     description: test
     source: hosted
-    version: "0.12.5+1"
+    version: "0.12.5+2"
   typed_data:
     description: typed_data
     source: hosted