syncslides/flutter: Few bug fixes and improvements.

-Splash screen.
-SdCard importer.
-Simplified stop/start/resume presentation flow.
-Using v23 mojo profile
-Updating to latest Flutter

Change-Id: I15b6addbc2542ee6c12e86427ffa9fb4893a083b
diff --git a/.gitignore b/.gitignore
index c9f8a00..0ef0e2d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,10 @@
-/.jiri
-/dart/.pub
+.atom
+/dart/app.flx
+/dart/bin
+/dart/build
 /dart/packages
 /dart/.packages
-/dart/snapshot_blob.bin
-/dart/app.flx
-/dart/build
+/dart/.pub
 /dart/shortcut_commands
-.atom
+/dart/snapshot_blob.bin
+/.jiri
\ No newline at end of file
diff --git a/dart/FLUTTER_VERSION b/dart/FLUTTER_VERSION
index 09f48c6..4142ce8 100644
--- a/dart/FLUTTER_VERSION
+++ b/dart/FLUTTER_VERSION
@@ -1 +1 @@
-519b190c0f2e87f2e03326c0d41566c9d32fc4f6
+e1b16729bfe369bdfd2c21027087f7d0181566e8
diff --git a/dart/MOJO_VERSION b/dart/MOJO_VERSION
deleted file mode 100644
index 3d22a47..0000000
--- a/dart/MOJO_VERSION
+++ /dev/null
@@ -1 +0,0 @@
-4503cee3f5f2d3bc3ad46636af0a1029fb22e108
diff --git a/dart/Makefile b/dart/Makefile
index 9893527..6ae7cd9 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -3,14 +3,14 @@
 endif
 
 SYNCBASE_DATA_DIR=/data/data/org.chromium.mojo.shell/app_home/syncbasedata
+# Mounttable address on SyncSlides-Alpha network
+# TODO(aghassemi): Now that BLE discovery support is added and sync uses neighbourhood,
+# we should no longer need a mounttable after upgrading and testing the latest code.
+MOUNTTABLE_ADDR := /192.168.86.254:8101
+
 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
-
-# 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
@@ -22,6 +22,32 @@
 
 SYNCBASE_ARGS := https://syncbase.syncslides.mojo.v.io/syncbase_server.mojo --root-dir=$(SYNCBASE_DATA_DIR) --v23.namespace.root=$(MOUNTTABLE_ADDR) --name=$(DEVICE_ID) $(VLOG_FLAGS)
 
+SETTINGS_FILE := /sdcard/syncslides_settings.json
+SETTINGS_JSON := {\"deviceid\": \"$(DEVICE_ID)\", \"mounttable\": \"$(MOUNTTABLE_ADDR)\"}
+
+SAMPLE_DECKS_PATH := $(PWD)/assets/sample_decks
+SD_CARD_DECKS_PATH = '/sdcard/syncslides/decks'
+
+MOJO_DEVTOOLS := $(shell jiri v23-profile env --profiles=mojo --target=arm-android MOJO_DEVTOOLS=)
+MOJO_SHELL := $(shell jiri v23-profile env --profiles=mojo --target=arm-android MOJO_SHELL=)
+
+GS_BUCKET_PATH := gs://mojo_services/syncslides
+GS_BUCKET_URL := storage.googleapis.com/mojo_services/syncslides
+SYNCSLIDES_URL = mojo://$(GS_BUCKET_URL)/app.flx
+
+APP_FLX_FILE := $(PWD)/build/app.flx
+SYNCBASE_MOJO_DIR := $(PWD)/packages/syncbase/mojo_services/android
+DISCOVERY_MOJO_DIR := $(PWD)/packages/v23discovery/mojo_services/android
+MOJO_SHELL_CMD_PATH := /data/local/tmp/org.chromium.mojo.shell.cmd
+
+SYNCSLIDES_SHORTCUT_NAME := SyncSlides
+SYNCSLIDES_ICON := https://avatars0.githubusercontent.com/u/9374332?v=3&s=200
+
+define GENERATE_SHORTCUT_FILE
+	sed -e "s;%GS_BUCKET_URL%;$1;g" -e "s;%SYNCBASE_FLAGS%;$2;g" \
+	shortcut_template > shortcut_commands
+endef
+
 default: run
 
 .PHONY: dartanalyzer
@@ -43,36 +69,25 @@
 build: packages
 	pub run flutter_tools build
 
-GS_BUCKET_PATH := gs://mojo_services/syncslides
-GS_BUCKET_URL := storage.googleapis.com/mojo_services/syncslides
-SYNCSLIDES_URL = mojo://$(GS_BUCKET_URL)/app.flx
-
-APP_FLX_FILE := $(PWD)/build/app.flx
-SYNCBASE_MOJO_DIR := $(PWD)/packages/syncbase/mojo_services/android
-DISCOVERY_MOJO_DIR := $(PWD)/packages/v23discovery/mojo_services/android
-MOJO_SHELL_CMD_PATH := /data/local/tmp/org.chromium.mojo.shell.cmd
-
-SYNCSLIDES_SHORTCUT_NAME := SyncSlides
-SYNCSLIDES_ICON := https://avatars0.githubusercontent.com/u/9374332?v=3&s=200
-
-define GENERATE_SHORTCUT_FILE
-	sed -e "s;%GS_BUCKET_URL%;$1;g" -e "s;%SYNCBASE_FLAGS%;$2;g" \
-	shortcut_template > shortcut_commands
-endef
-
-.PHONY: install
-install: build
+.PHONY: create-shortcut
+create-shortcut: clear-shortcut build install-shell
 	$(call GENERATE_SHORTCUT_FILE,$(GS_BUCKET_URL),$(SYNCBASE_ARGS))
 	adb -s $(DEVICE_ID) push -p shortcut_commands $(MOJO_SHELL_CMD_PATH)
 	adb -s $(DEVICE_ID) shell chmod 555 $(MOJO_SHELL_CMD_PATH)
 	adb -s $(DEVICE_ID) shell 'echo $(SETTINGS_JSON) > $(SETTINGS_FILE)'
-	$(MOJO_DIR)/src/mojo/devtools/common/mojo_run --android $(DEVICE_FLAG) "mojo:shortcut $(SYNCSLIDES_SHORTCUT_NAME) $(SYNCSLIDES_URL) $(SYNCSLIDES_ICON)"
+	$(MOJO_DEVTOOLS)/mojo_run --shell-path $(MOJO_SHELL) --android --target-device=$(DEVICE_ID) "https://$(GS_BUCKET_URL)/shortcut.mojo $(SYNCSLIDES_SHORTCUT_NAME) $(SYNCSLIDES_URL) $(SYNCSLIDES_ICON)"
 
-.PHONY: uninstall
-uninstall:
-	adb -s $(DEVICE_ID) uninstall org.chromium.mojo.shell
+.PHONY: clear-shortcut
+clear-shortcut:
 	adb -s $(DEVICE_ID) shell rm -f $(MOJO_SHELL_CMD_PATH)
-# TODO(aghassemi): Is there a way to remove the shortcut via adb?
+
+.PHONY: install-shell
+install-shell: clear-shortcut
+	adb -s $(DEVICE_ID) install $(MOJO_SHELL)
+
+.PHONY: uninstall-shell
+uninstall-shell: clear-shortcut
+	adb -s $(DEVICE_ID) uninstall org.chromium.mojo.shell
 
 .PHONY: deploy
 deploy: build
@@ -80,14 +95,19 @@
 	gsutil cp $(SYNCBASE_MOJO_DIR)/syncbase_server.mojo $(GS_BUCKET_PATH)
 	gsutil cp $(DISCOVERY_MOJO_DIR)/discovery.mojo $(GS_BUCKET_PATH)
 	gsutil -m acl set -R -a public-read $(GS_BUCKET_PATH)
+	gsutil setmeta -h "Cache-Control:private, max-age=0, no-transform" $(GS_BUCKET_PATH)/*.*
+
+.PHONY: copy-sample-deck
+copy-sample-deck:
+	adb -s $(DEVICE_ID) push -p $(SAMPLE_DECKS_PATH) $(SD_CARD_DECKS_PATH)
 
 # Usage example:
 # DEVICE_NUM=1 make run
 # DEVICE_NUM=2 make run
-run: build
+run: build install-shell copy-sample-deck
 	adb -s $(DEVICE_ID) shell 'echo $(SETTINGS_JSON) > $(SETTINGS_FILE)'
 	pub run flutter_tools run_mojo \
-	--mojo-path $(MOJO_DIR)/src \
+	--devtools-path $(MOJO_DEVTOOLS)/mojo_run \
 	--android --mojo-debug -- --enable-multiprocess \
 	--map-origin="https://syncbase.syncslides.mojo.v.io/=$(SYNCBASE_MOJO_DIR)" \
 	--map-origin="https://discovery.syncslides.mojo.v.io/=$(DISCOVERY_MOJO_DIR)" \
@@ -110,11 +130,11 @@
 clean:
 	rm -f app.flx snapshot_blob.bin shortcut_commands
 	rm -rf packages
-	adb -s $(DEVICE_ID) shell run-as org.chromium.mojo.shell rm $(SETTINGS_FILE) settings_commands
+	-adb -s $(DEVICE_ID) shell run-as org.chromium.mojo.shell rm $(SETTINGS_FILE)
 
 .PHONY: clean-syncbase
 clean-syncbase:
-	adb -s $(DEVICE_ID) shell run-as org.chromium.mojo.shell rm -rf $(SYNCBASE_DATA_DIR)
+	-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/README.md b/dart/README.md
index fc704b7..94ea25e 100644
--- a/dart/README.md
+++ b/dart/README.md
@@ -4,28 +4,18 @@
 
 # Prerequisites
 
-## Mojo
-
-Currently, development is heavily tied to an existing installation of [Mojo](https://github.com/domokit/mojo). Please ensure that your Mojo checkout is located at `$MOJO_DIR` and has been build for Android. Instructions are available [here](https://github.com/domokit/mojo#mojo).
-
-We only guarantee compatibility with the version of Mojo as specified in MOJO_VERSION.
-
 ## Flutter
 
 A clone of https://github.com/flutter/flutter/ at the commit # specified in FLUTTER_VERSION file must be available in a directory
 called `flutter` at the same level as $V23_ROOT directory.
 
+## Mojo
+
+Mojo profile for Android target must be installed. You can run `jiri v23-profile install --target=arm-android mojo` to install it.
+
 ## Dart
 
-Flutter depends on a relatively new version of the Dart SDK. Therefore, please ensure that you have installed the following version or greater:
-
-```
-Dart VM version: 1.13.0-dev.3.1 (Thu Sep 17 10:54:54 2015) on "linux_x64"
-```
-
-If you are unsure what version you are on, use `dart --version`.
-
-To install Dart, visit the [download page](https://www.dartlang.org/downloads/).
+Mojo profile must be installed. You can run `jiri v23-profile install dart` to install it.
 
 ## Android Setup
 
diff --git a/dart/assets/images/sample_decks/baku/thumb.png b/dart/assets/images/sample_decks/baku/thumb.png
deleted file mode 100644
index e8c8a09..0000000
--- a/dart/assets/images/sample_decks/baku/thumb.png
+++ /dev/null
Binary files differ
diff --git a/dart/assets/images/sample_decks/pitch/thumb.png b/dart/assets/images/sample_decks/pitch/thumb.png
deleted file mode 100644
index 85b9ff8..0000000
--- a/dart/assets/images/sample_decks/pitch/thumb.png
+++ /dev/null
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/thumb.png b/dart/assets/images/sample_decks/vanadium/thumb.png
deleted file mode 100644
index 11ecb19..0000000
--- a/dart/assets/images/sample_decks/vanadium/thumb.png
+++ /dev/null
Binary files differ
diff --git a/dart/assets/images/splash/background.png b/dart/assets/images/splash/background.png
new file mode 100644
index 0000000..4812cbb
--- /dev/null
+++ b/dart/assets/images/splash/background.png
Binary files differ
diff --git a/dart/assets/images/splash/flutter.png b/dart/assets/images/splash/flutter.png
new file mode 100644
index 0000000..0f468b6
--- /dev/null
+++ b/dart/assets/images/splash/flutter.png
Binary files differ
diff --git a/dart/assets/images/splash/vanadium.png b/dart/assets/images/splash/vanadium.png
new file mode 100644
index 0000000..6da0b2a
--- /dev/null
+++ b/dart/assets/images/splash/vanadium.png
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/1.jpg b/dart/assets/sample_decks/Vanadium Overview/1.jpg
similarity index 100%
rename from dart/assets/images/sample_decks/vanadium/1.jpg
rename to dart/assets/sample_decks/Vanadium Overview/1.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/2.jpg b/dart/assets/sample_decks/Vanadium Overview/2.jpg
similarity index 100%
rename from dart/assets/images/sample_decks/vanadium/2.jpg
rename to dart/assets/sample_decks/Vanadium Overview/2.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/3.jpg b/dart/assets/sample_decks/Vanadium Overview/3.jpg
similarity index 100%
rename from dart/assets/images/sample_decks/vanadium/3.jpg
rename to dart/assets/sample_decks/Vanadium Overview/3.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/4.jpg b/dart/assets/sample_decks/Vanadium Overview/4.jpg
similarity index 100%
rename from dart/assets/images/sample_decks/vanadium/4.jpg
rename to dart/assets/sample_decks/Vanadium Overview/4.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/5.jpg b/dart/assets/sample_decks/Vanadium Overview/5.jpg
similarity index 100%
rename from dart/assets/images/sample_decks/vanadium/5.jpg
rename to dart/assets/sample_decks/Vanadium Overview/5.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/6.jpg b/dart/assets/sample_decks/Vanadium Overview/6.jpg
similarity index 100%
rename from dart/assets/images/sample_decks/vanadium/6.jpg
rename to dart/assets/sample_decks/Vanadium Overview/6.jpg
Binary files differ
diff --git a/dart/flutter.yaml b/dart/flutter.yaml
index f7279fd..ac78010 100644
--- a/dart/flutter.yaml
+++ b/dart/flutter.yaml
@@ -4,6 +4,7 @@
   - name: action/delete
   - name: action/perm_device_information
   - name: av/loop
+  - name: av/stop
   - name: av/play_arrow
   - name: communication/live_help
   - name: content/add
@@ -15,12 +16,6 @@
   - name: maps/layers
 assets:
   - assets/images/defaults/thumbnail.png
-  - 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
-  - assets/images/sample_decks/baku/thumb.png
-  - assets/images/sample_decks/vanadium/thumb.png
-  - assets/images/sample_decks/pitch/thumb.png
+  - assets/images/splash/background.png
+  - assets/images/splash/vanadium.png
+  - assets/images/splash/flutter.png
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
index b87e395..04199f5 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -18,19 +18,14 @@
 
 // DeckGridPage is the full page view of the list of decks.
 class DeckGridPage extends SyncSlidesPage {
-  initState(AppState appState, AppActions appActions) {
-    appActions.stopAllPresentations();
-  }
-
   Widget build(BuildContext context, AppState appState, AppActions appActions) {
     // Advertised decks.
     List<model.PresentationAdvertisement> presentations =
         appState.presentationAdvertisements.values;
 
-    // Local decks that are not presented or advertised.
+    // Local decks that are not advertised.
     List<model.Deck> decks = appState.decks.values
         .where((DeckState d) => d.deck != null &&
-            d.presentation == null &&
             !presentations.any((model.PresentationAdvertisement p) =>
                 p.deck.key == d.deck.key))
         .map((DeckState d) => d.deck);
@@ -40,11 +35,11 @@
         toolBar: new ToolBar(center: new Text('SyncSlides')),
         floatingActionButton: new FloatingActionButton(
             child: new Icon(icon: 'content/add'), onPressed: () {
-          appActions.loadDemoDeck();
+          appActions.loadDeckFromSdCard();
         }),
         drawer: _buildDrawer(context, appState),
         body: new Material(
-            child: new DeckGrid(decks, presentations, appActions)));
+            child: new DeckGrid(decks, presentations, appState, appActions)));
   }
 
   Widget _buildDrawer(BuildContext context, AppState appState) {
@@ -65,18 +60,22 @@
 // DeckGrid is scrollable grid view of decks.
 class DeckGrid extends StatelessComponent {
   AppActions _appActions;
+  AppState _appState;
   List<model.Deck> _decks;
   List<model.PresentationAdvertisement> _presentations;
 
-  DeckGrid(this._decks, this._presentations, this._appActions);
+  DeckGrid(this._decks, this._presentations, this._appState, this._appActions);
 
   Widget build(BuildContext context) {
     List<Widget> deckBoxes = _decks.map((deck) => _buildDeckBox(context, deck));
     List<Widget> presentationBoxes = _presentations
         .map((presentation) => _buildPresentationBox(context, presentation));
     var allBoxes = new List.from(presentationBoxes)..addAll(deckBoxes);
-    var grid = new Grid(allBoxes, maxChildExtent: style.Size.gridbox);
-    return new ScrollableViewport(child: grid);
+    var grid = new ScrollableGrid(
+        children: allBoxes,
+        delegate:
+            new MaxTileWidthGridDelegate(maxTileWidth: style.Size.gridbox));
+    return grid;
   }
 
   Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
@@ -84,7 +83,18 @@
         provider: imageProvider.getDeckThumbnailImage(deckData),
         fit: ImageFit.scaleDown);
 
-    var footer = _buildBoxFooter(deckData.name);
+    var resumeLiveBox;
+    var presentationState = _appState.decks[deckData.key]?.presentation;
+    if (presentationState != null && presentationState.isOwner) {
+      resumeLiveBox = new Row([
+        new Container(
+            child: new Text("RESUME PRESENTING", style: style.Text.liveNow),
+            decoration: style.Box.liveNow,
+            padding: style.Spacing.extraSmallPadding)
+      ]);
+    }
+
+    var footer = _buildBoxFooter(deckData.name, subtitle: resumeLiveBox);
     var box = _buildCard(deckData.key, thumbnail, footer, () {
       Navigator.push(
           context,
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
index 5fa2422..82758e8 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -39,7 +39,7 @@
                 onPressed: () => Navigator.pop(context)),
             center: new Text(deckState.deck.name),
             right: toolbarActions),
-        floatingActionButton: _buildPresentFab(context, appState, appActions),
+        floatingActionButton: _buildFab(context, appState, appActions),
         body: new Material(child: new SlideList(_deckId, slides, appActions)));
   }
 
@@ -56,31 +56,47 @@
     });
   }
 
-  _buildPresentFab(
-      BuildContext context, AppState appState, AppActions appActions) {
+  _buildFab(BuildContext context, AppState appState, AppActions appActions) {
     var deckState = appState.decks[_deckId];
-    if (deckState.presentation != null) {
-      // Can't present when already in a presentation.
-      return null;
+
+    if (deckState.presentation == null) {
+      return new FloatingActionButton(child: new Icon(icon: 'av/play_arrow'),
+          onPressed: () async {
+        toast.info(_scaffoldKey, 'Starting presentation...',
+            duration: toast.Durations.permanent);
+
+        try {
+          await appActions.startPresentation(_deckId);
+          toast.info(_scaffoldKey, 'Presentation started.');
+
+          Navigator.push(
+              context,
+              new MaterialPageRoute(
+                  builder: (context) => new SlideshowPage(_deckId)));
+        } catch (e) {
+          toast.error(_scaffoldKey, 'Failed to start presentation.', e);
+        }
+      });
     }
 
-    return new FloatingActionButton(child: new Icon(icon: 'av/play_arrow'),
-        onPressed: () async {
-      toast.info(_scaffoldKey, 'Starting presentation...',
-          duration: toast.Durations.permanent);
+    // Already presenting own deck, allow for stopping it.
+    if (deckState.presentation != null && deckState.presentation.isOwner) {
+      var key = deckState.presentation.key;
+      return new FloatingActionButton(child: new Icon(icon: 'av/stop'),
+          onPressed: () async {
+        toast.info(_scaffoldKey, 'Stopping presentation...',
+            duration: toast.Durations.permanent);
 
-      try {
-        await appActions.startPresentation(_deckId);
-        toast.info(_scaffoldKey, 'Presentation started.');
+        try {
+          await appActions.stopPresentation(key);
+          toast.info(_scaffoldKey, 'Presentation stopped.');
+        } catch (e) {
+          toast.error(_scaffoldKey, 'Failed to stop presentation.', e);
+        }
+      });
+    }
 
-        Navigator.push(
-            context,
-            new MaterialPageRoute(
-                builder: (context) => new SlideshowPage(_deckId)));
-      } catch (e) {
-        toast.error(_scaffoldKey, 'Failed to start presentation.', e);
-      }
-    });
+    return null;
   }
 }
 
@@ -91,18 +107,18 @@
   SlideList(this._deckId, this._slides, this._appActions);
 
   Widget build(BuildContext context) {
-    return new ScrollableList(
-        itemExtent: style.Size.listHeight,
-        items: _slides,
-        itemBuilder: (context, value, index) =>
-            _buildSlide(context, _deckId, index, value, onTap: () {
-              _appActions.setCurrSlideNum(_deckId, index);
+    Iterable<Widget> items = _slides.map(
+        (slide) => _buildSlide(context, _deckId, slide.num, slide, onTap: () {
+              _appActions.setCurrSlideNum(_deckId, slide.num);
 
               Navigator.push(
                   context,
                   new MaterialPageRoute(
                       builder: (context) => new SlideshowPage(_deckId)));
             }));
+
+    return new ScrollableList(
+        itemExtent: style.Size.listHeight, children: items);
   }
 }
 
@@ -126,7 +142,10 @@
           padding: style.Spacing.normalPadding));
 
   var card = new Container(
-      child: new Card(child: new Row([thumbnail, titleAndNotes])),
+      child: new Container(
+          margin: style.Spacing.cardMargin,
+          child: new Material(
+              elevation: 2, child: new Row([thumbnail, titleAndNotes]))),
       margin: style.Spacing.listItemMargin);
 
   var listItem = new InkWell(
diff --git a/dart/lib/components/slideshow.dart b/dart/lib/components/slideshow.dart
index a3f3aee..921181d 100644
--- a/dart/lib/components/slideshow.dart
+++ b/dart/lib/components/slideshow.dart
@@ -10,7 +10,7 @@
 import '../utils/image_provider.dart' as imageProvider;
 import 'askquestion.dart';
 import 'questionlist.dart';
-import 'slideshow_immersive.dart';
+import 'slideshow_fullscreen.dart';
 import 'syncslides_page.dart';
 
 final GlobalKey _scaffoldKey = new GlobalKey();
@@ -135,7 +135,7 @@
 
     var image = new AsyncImage(provider: provider, fit: ImageFit.scaleDown);
 
-    // If not driving the presentation, tapping the image navigates to the immersive mode.
+    // If not driving the presentation, tapping the image navigates to fullscreen mode.
     if (_deckState.presentation == null ||
         !_deckState.presentation.isDriving(_appState.user)) {
       image = new InkWell(child: image, onTap: () {
@@ -143,7 +143,7 @@
             context,
             new MaterialPageRoute(
                 builder: (context) =>
-                    new SlideshowImmersivePage(_deckState.deck.key)));
+                    new SlideshowFullscreenPage(_deckState.deck.key)));
       });
     }
 
diff --git a/dart/lib/components/slideshow_immersive.dart b/dart/lib/components/slideshow_fullscreen.dart
similarity index 77%
rename from dart/lib/components/slideshow_immersive.dart
rename to dart/lib/components/slideshow_fullscreen.dart
index b6528c0..009592c 100644
--- a/dart/lib/components/slideshow_immersive.dart
+++ b/dart/lib/components/slideshow_fullscreen.dart
@@ -9,10 +9,10 @@
 import '../utils/image_provider.dart' as imageProvider;
 import 'slideshow.dart';
 
-class SlideshowImmersivePage extends SlideshowPage {
+class SlideshowFullscreenPage extends SlideshowPage {
   String _deckId;
 
-  SlideshowImmersivePage(String deckId) : super(deckId) {
+  SlideshowFullscreenPage(String deckId) : super(deckId) {
     _deckId = deckId;
   }
 
@@ -30,7 +30,8 @@
     }
     var provider = imageProvider.getSlideImage(
         deckState.deck.key, deckState.slides[currSlideNum]);
-
-    return new AsyncImage(provider: provider);
+    return new GestureDetector(
+        child: new AsyncImage(provider: provider, fit: ImageFit.contain),
+        onTap: () => Navigator.pop(context));
   }
 }
diff --git a/dart/lib/loaders/demo_loader.dart b/dart/lib/loaders/demo_loader.dart
deleted file mode 100644
index 4585668..0000000
--- a/dart/lib/loaders/demo_loader.dart
+++ /dev/null
@@ -1,87 +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.
-
-import 'dart:async';
-import 'dart:math';
-
-import '../models/all.dart' as model;
-import '../stores/store.dart';
-import '../stores/utils/key.dart' as keyutil;
-import '../utils/asset.dart' as assetutil;
-import '../utils/uuid.dart' as uuidutil;
-import 'loader.dart';
-
-// DemoLoader loads some sample decks and slides and randomly adds/removes
-// decks based on a timer.
-class DemoLoader implements Loader {
-  final Store _store = new Store.singleton();
-  final Random _rand = new Random();
-
-  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 firstWord = firstWords[_rand.nextInt(firstWords.length)];
-    var secondWord = secondWords[_rand.nextInt(secondWords.length)];
-    var thumbnail = await assetutil
-        .getRawBytes(thumbnails[_rand.nextInt(thumbnails.length)]);
-
-    var deckId = uuidutil.createUuid();
-    var blobRef = new model.BlobRef(
-        keyutil.getDeckBlobKey(deckId, uuidutil.createUuid()));
-
-    await _store.actions.putBlob(blobRef.key, thumbnail);
-
-    return new model.Deck(deckId, '$firstWord $secondWord', blobRef);
-  }
-
-  Stream<model.Slide> _getRandomSlides(model.Deck deck) async* {
-    var numSlides = _rand.nextInt(maxNumSlides);
-    for (var i = 0; i < numSlides; i++) {
-      var slideIndex = i % slides.length;
-      var blobRef = new model.BlobRef(
-          keyutil.getDeckBlobKey(deck.key, uuidutil.createUuid()));
-      var image = await assetutil.getRawBytes(
-          'assets/images/sample_decks/vanadium/${slideIndex + 1}.jpg');
-      await _store.actions.putBlob(blobRef.key, image);
-      yield new model.Slide(i, blobRef);
-    }
-  }
-
-  Future loadDeck() async {
-    var deck = await _getRandomDeck();
-    List<model.Slide> slides = await _getRandomSlides(deck).toList();
-    await _store.actions.addDeck(deck);
-    await _store.actions.setSlides(deck.key, slides);
-  }
-}
diff --git a/dart/lib/loaders/factory.dart b/dart/lib/loaders/factory.dart
deleted file mode 100644
index 354dd93..0000000
--- a/dart/lib/loaders/factory.dart
+++ /dev/null
@@ -1,16 +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.
-
-import 'demo_loader.dart';
-import 'loader.dart';
-import 'sdcard_loader.dart';
-
-// Factory method to create a concrete loader instance.
-Loader createDemoLoader() {
-  return new DemoLoader();
-}
-
-Loader createSdCardLoader() {
-  return new SdCardLoader();
-}
diff --git a/dart/lib/loaders/loader.dart b/dart/lib/loaders/loader.dart
index 8113924..4d6a58e 100644
--- a/dart/lib/loaders/loader.dart
+++ b/dart/lib/loaders/loader.dart
@@ -3,18 +3,105 @@
 // license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:io';
 
-import 'factory.dart' as factory;
+import 'package:logging/logging.dart';
+import 'package:path/path.dart' as pathutil;
+
+import '../models/all.dart' as model;
+import '../stores/store.dart';
+import '../stores/utils/key.dart' as keyutil;
+import '../utils/uuid.dart' as uuidutil;
+
+final Logger log = new Logger('loader');
+
+const String _baseDecksPath = '/sdcard/syncslides/decks';
+Directory _baseDecksDir = new Directory(_baseDecksPath);
 
 // Loader is responsible for importing existing decks and slides into the store.
-abstract class Loader {
-  factory Loader.demo() {
-    return factory.createDemoLoader();
+class Loader {
+  final Store _store = new Store.singleton();
+  static Loader _singletonLoader = new Loader._internal();
+
+  factory Loader.singleton() {
+    return _singletonLoader;
   }
 
-  factory Loader.sdcard() {
-    return factory.createSdCardLoader();
+  Loader._internal() {}
+
+  // Loads all the decks in /sdcard/syncslides/decks
+  // Name of each deck is the name of its directory.
+  // Slide image files must be numbered.
+  // First slide is used as the thumbnail.
+  // Example:
+  // /sdcard/syncslides/decks/Foo
+  //    /sdcard/syncslides/decks/Foo/1.png
+  //    /sdcard/syncslides/decks/Foo/2.png
+  // /sdcard/syncslides/decks/Bar
+  //    /sdcard/syncslides/decks/Bar/1.gif
+  //
+  // TODO(aghassemi): Replace with a Path Selector dialog.
+  Future loadDeck() async {
+    if (!await _baseDecksDir.exists()) {
+      log.warning('Default $_baseDecksPath directory does not exist.');
+      return;
+    }
+
+    Stream allDecksDir = _baseDecksDir.list();
+    await for (FileSystemEntity fsEntity in allDecksDir) {
+      if (!(fsEntity is Directory)) {
+        log.warning(
+            'Ignoring non-directory ${pathutil.basename(fsEntity.path)} in $_baseDecksPath');
+        continue;
+      }
+      _loadDeck(fsEntity.path);
+    }
   }
 
-  Future loadDeck();
+  Future _loadDeck(String path) async {
+    var deckName = pathutil.basename(path);
+    var deckId = uuidutil.createUuid();
+
+    Directory deckDir = new Directory(path);
+    List<model.Slide> slides = new List();
+    await for (FileSystemEntity fsEntity in deckDir.list()) {
+      if (!(fsEntity is File)) {
+        log.warning(
+            'Ignoring non-file ${pathutil.basename(fsEntity.path)} in $path');
+        continue;
+      }
+      File slideFile = fsEntity as File;
+      var slideNum;
+      try {
+        String slideName = pathutil.basenameWithoutExtension(slideFile.path);
+        slideNum = int.parse(slideName);
+        slideNum--; // Zero based index.
+      } catch (e) {
+        throw new ArgumentError(
+            "Filename ${pathutil.basename(slideFile.path)} for a slide must be a number.");
+      }
+
+      // Create the slide object.
+      List<int> slideBytes = await slideFile.readAsBytes();
+      var blobRef = new model.BlobRef(
+          keyutil.getDeckBlobKey(deckId, uuidutil.createUuid()));
+      await _store.actions.putBlob(blobRef.key, slideBytes);
+      model.Slide slide = new model.Slide(slideNum, blobRef);
+      slides.add(slide);
+    }
+
+    if (slides.isEmpty) {
+      log.warning('No image files found in $path.');
+      return;
+    }
+
+    slides.sort((model.Slide s1, model.Slide s2) => s1.num.compareTo(s2.num));
+
+    // Use the first slide as thumbnail.
+    model.BlobRef thumbnailBlobRef = slides.first.image;
+    var deck = new model.Deck(deckId, deckName, thumbnailBlobRef);
+
+    await _store.actions.addDeck(deck);
+    await _store.actions.setSlides(deck.key, slides);
+  }
 }
diff --git a/dart/lib/loaders/sdcard_loader.dart b/dart/lib/loaders/sdcard_loader.dart
deleted file mode 100644
index 60aa269..0000000
--- a/dart/lib/loaders/sdcard_loader.dart
+++ /dev/null
@@ -1,13 +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.
-
-import 'dart:async';
-
-import 'loader.dart';
-
-class SdCardLoader implements Loader {
-  Future loadDeck() {
-    throw new UnimplementedError();
-  }
-}
diff --git a/dart/lib/main.dart b/dart/lib/main.dart
index efcdbe4..29c09f5 100644
--- a/dart/lib/main.dart
+++ b/dart/lib/main.dart
@@ -1,6 +1,7 @@
 // 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/material.dart';
 import 'package:logging/logging.dart';
@@ -9,26 +10,75 @@
 import 'stores/store.dart';
 import 'styles/common.dart' as style;
 import 'utils/back_button.dart' as backButtonUtil;
+import 'utils/image_provider.dart' as imageProvider;
 
 NavigatorState _navigator;
+final Completer storeStatus = new Completer();
 
 void main() {
-  var store = new Store.singleton();
+  Store store = new Store.singleton();
+  store.init().then((_) {
+    storeStatus.complete();
+  });
   _initLogging();
   _initBackButtonHandler();
 
-  // TODO(aghassemi): Display splash screen while store is initializing.
-  store.init().then((_) => runApp(new MaterialApp(
+  runApp(new MaterialApp(
       theme: style.theme,
       title: 'SyncSlides',
-      routes: {'/': (RouteArguments args) => new LandingPage()})));
+      routes: {'/': (RouteArguments args) => new LandingPage()}));
 }
 
-class LandingPage extends StatelessComponent {
+class LandingPage extends StatefulComponent {
+  _LandingPage createState() => new _LandingPage();
+}
+
+class _LandingPage extends State<LandingPage> {
+  bool _initialized = false;
+
+  @override
+  void initState() {
+    if (storeStatus.isCompleted) {
+      _initialized = true;
+    } else {
+      storeStatus.future.then((_) {
+        setState(() {
+          _initialized = true;
+        });
+      });
+    }
+  }
+
   Widget build(BuildContext context) {
+    if (!_initialized) {
+      return _buildSplashScreen();
+    }
     _navigator = context.ancestorStateOfType(NavigatorState);
     return new DeckGridPage();
   }
+
+  Widget _buildSplashScreen() {
+    var stack = new Stack([
+      new AsyncImage(
+          provider: imageProvider.splashBackgroundImageProvider,
+          fit: ImageFit.cover),
+      new Row([
+        new AsyncImage(
+            provider: imageProvider.splashFlutterImageProvider,
+            width: style.Size.splashLogo),
+        new AsyncImage(
+            provider: imageProvider.splashVanadiumImageProvider,
+            width: style.Size.splashLogo)
+      ], justifyContent: FlexJustifyContent.center),
+      new Container(
+          child: new Row(
+              [new Text('Loading SyncSlides...', style: style.Text.splash)],
+              alignItems: FlexAlignItems.end,
+              justifyContent: FlexJustifyContent.center),
+          padding: style.Spacing.normalPadding)
+    ]);
+    return stack;
+  }
 }
 
 void _initLogging() {
diff --git a/dart/lib/stores/actions.dart b/dart/lib/stores/actions.dart
index faca6ab..ab49bbe 100644
--- a/dart/lib/stores/actions.dart
+++ b/dart/lib/stores/actions.dart
@@ -15,9 +15,6 @@
   // Removes a deck given its key.
   Future removeDeck(String key);
 
-  // Loads a demo deck.
-  Future loadDemoDeck();
-
   // Loads a deck from SdCard.
   Future loadDeckFromSdCard();
 
@@ -39,9 +36,6 @@
   // Stops the given presentation.
   Future stopPresentation(String presentationId);
 
-  // Stops all presentations.
-  Future stopAllPresentations();
-
   // If viewer has started navigating on their own, this will align the navigation
   // back up with the presentation.
   Future followPresentation(String deckId);
diff --git a/dart/lib/stores/syncbase/actions.dart b/dart/lib/stores/syncbase/actions.dart
index 2a3f2fe..040700f 100644
--- a/dart/lib/stores/syncbase/actions.dart
+++ b/dart/lib/stores/syncbase/actions.dart
@@ -55,7 +55,7 @@
         await tb.put(
             keyutil.getPresentationCurrSlideNumKey(
                 deckId, deckState.presentation.key),
-            [slideNum]);
+            UTF8.encode(slideNum.toString()));
       } else {
         // User is not driving the presentation so they are navigating on their own.
         deckState.presentation._isFollowingPresentation = false;
@@ -64,12 +64,8 @@
     _emitChange();
   }
 
-  Future loadDemoDeck() {
-    return new Loader.demo().loadDeck();
-  }
-
   Future loadDeckFromSdCard() {
-    return new Loader.demo().loadDeck();
+    return new Loader.singleton().loadDeck();
   }
 
   //////////////////////////////////////
@@ -127,7 +123,7 @@
       sb.SyncbaseTable tb = _getPresentationsTable();
       await tb.put(
           keyutil.getPresentationCurrSlideNumKey(deckId, presentation.key),
-          [0]);
+          UTF8.encode('0'));
 
       // Set the current user as the driver.
       await _setPresentationDriver(deckId, presentation.key, _state.user);
@@ -140,7 +136,7 @@
       // Wait for join. If it fails, remove the presentation state from the deck.
       await setDefaultsAndJoin();
     } catch (e) {
-      deckState._presentation = null;
+      deckState._isPresenting = false;
       throw e;
     }
 
@@ -154,6 +150,8 @@
     _DeckState deckState = _state._getOrCreateDeckState(deckId);
     deckState._getOrCreatePresentationState(presentation.key);
 
+    deckState._isPresenting = true;
+
     // Wait until at least the current slide number, driver and the slide for current slide number is synced.
     join() async {
       bool isMyOwnPresentation =
@@ -166,9 +164,9 @@
       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].slides.length >
+                _state._decks[deckId].presentation.currSlideNum &&
             _state._decks[deckId].presentation.driver != null &&
             !completer.isCompleted) {
           timer.cancel();
@@ -182,7 +180,7 @@
       // Wait for join. If it fails, remove the presentation state from the deck.
       await join();
     } catch (e) {
-      deckState._presentation = null;
+      deckState._isPresenting = false;
       throw e;
     }
 
@@ -195,20 +193,13 @@
     _state._decks.values.forEach((_DeckState deck) {
       if (deck.presentation != null &&
           deck.presentation.key == presentationId) {
-        deck._presentation = null;
+        deck._isPresenting = false;
       }
     });
+    _emitChange();
     log.info('Presentation $presentationId stopped');
   }
 
-  Future stopAllPresentations() async {
-    // Stop all presentations in parallel.
-    return Future.wait(_state._advertisedPresentations
-        .map((model.PresentationAdvertisement p) {
-      return stopPresentation(p.key);
-    }));
-  }
-
   Future followPresentation(String deckId) async {
     var deckState = _state._getOrCreateDeckState(deckId);
 
diff --git a/dart/lib/stores/syncbase/state.dart b/dart/lib/stores/syncbase/state.dart
index f859fd0..484a014 100644
--- a/dart/lib/stores/syncbase/state.dart
+++ b/dart/lib/stores/syncbase/state.dart
@@ -43,18 +43,25 @@
   List<model.Slide> _slides = new List();
   UnmodifiableListView<model.Slide> slides;
 
-  PresentationState _presentation = null;
-  PresentationState get presentation => _presentation;
+  _PresentationState _presentation = null;
+  PresentationState get presentation {
+    if (_isPresenting) {
+      return _presentation;
+    }
+    return null;
+  }
 
   int _currSlideNum = 0;
   int get currSlideNum => _currSlideNum;
 
+  bool _isPresenting = false;
+
   _DeckState() {
     slides = new UnmodifiableListView(_slides);
   }
 
   _PresentationState _getOrCreatePresentationState(String presentationId) {
-    if (_presentation == null) {
+    if (_presentation == null || _presentation.key != presentationId) {
       _presentation = new _PresentationState(presentationId);
     }
     return _presentation;
diff --git a/dart/lib/stores/syncbase/store.dart b/dart/lib/stores/syncbase/store.dart
index 5f27c96..6d08108 100644
--- a/dart/lib/stores/syncbase/store.dart
+++ b/dart/lib/stores/syncbase/store.dart
@@ -92,7 +92,7 @@
       _state._decks.values.forEach((_DeckState deck) {
         if (deck.presentation != null &&
             deck.presentation.key == presentationId) {
-          deck._presentation = null;
+          deck._isPresenting = false;
         }
       });
       _triggerStateChange();
@@ -183,14 +183,15 @@
   _onPresentationSlideNumChange(
       int changeType, String rowKey, List<int> value) {
     String deckId = keyutil.presentationCurrSlideNumKeyToDeckId(rowKey);
+    String presentationId =
+        keyutil.presentationCurrSlideNumKeyToPresentationId(rowKey);
 
     _DeckState deckState = _state._getOrCreateDeckState(deckId);
-    _PresentationState presentationState = deckState.presentation;
-    if (presentationState == null) {
-      return;
-    }
+    _PresentationState presentationState =
+        deckState._getOrCreatePresentationState(presentationId);
+
     if (changeType == sb.WatchChangeTypes.put) {
-      int currSlideNum = value[0];
+      int currSlideNum = int.parse(UTF8.decode(value));
       presentationState._currSlideNum = currSlideNum;
     } else {
       presentationState._currSlideNum = 0;
@@ -199,11 +200,12 @@
 
   _onPresentationDriverChange(int changeType, String rowKey, List<int> value) {
     String deckId = keyutil.presentationDriverKeyToDeckId(rowKey);
+    String presentationId =
+        keyutil.presentationDriverKeyToPresentationId(rowKey);
+
     _DeckState deckState = _state._getOrCreateDeckState(deckId);
-    _PresentationState presentationState = deckState.presentation;
-    if (presentationState == null) {
-      return;
-    }
+    _PresentationState presentationState =
+        deckState._getOrCreatePresentationState(presentationId);
 
     if (changeType == sb.WatchChangeTypes.put) {
       model.User driver = new model.User.fromJson(UTF8.decode(value));
diff --git a/dart/lib/stores/utils/key.dart b/dart/lib/stores/utils/key.dart
index 3c0f154..b666b6f 100644
--- a/dart/lib/stores/utils/key.dart
+++ b/dart/lib/stores/utils/key.dart
@@ -57,19 +57,21 @@
   return key.contains('/slides/');
 }
 
-// Gets the deck id given a slide key.
-String currSlideKeyToDeckId(String key) {
+void checkValidSlideKey(String key) {
   if ((!isSlideKey(key))) {
     throw new ArgumentError('$key is not a valid slide key.');
   }
+}
+
+// Gets the deck id given a slide key.
+String currSlideKeyToDeckId(String key) {
+  checkValidSlideKey(key);
   return key.substring(0, key.indexOf('/slides/'));
 }
 
 // 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.');
-  }
+  checkValidSlideKey(key);
   var indexStr = key.substring(key.lastIndexOf('/') + 1);
   return int.parse(indexStr);
 }
@@ -77,7 +79,7 @@
 // TODO(aghassemi): Don't use regex, just regular split should be fine.
 const String _uuidPattern = '[a-zA-Z0-9-]+';
 final RegExp _currPresentationSlideNumPattern =
-    new RegExp('($_uuidPattern)(?:/$_uuidPattern)(?:/currentslide)');
+    new RegExp('($_uuidPattern)/($_uuidPattern)(?:/currentslide)');
 
 // Constructs a current slide number key.
 String getPresentationCurrSlideNumKey(String deckId, String presentationId) {
@@ -86,21 +88,31 @@
 
 // Gets the deck id given a current slide number key.
 String presentationCurrSlideNumKeyToDeckId(String currSlideNumKey) {
-  if ((!isPresentationCurrSlideNumKey(currSlideNumKey))) {
-    throw new ArgumentError(
-        '$currSlideNumKey is not a valid presentation current slide number key.');
-  }
+  checkValidPresentationCurrSlideNumKey(currSlideNumKey);
   return _currPresentationSlideNumPattern.firstMatch(currSlideNumKey).group(1);
 }
 
+// Gets the presentation id given a current slide number key.
+String presentationCurrSlideNumKeyToPresentationId(String currSlideNumKey) {
+  checkValidPresentationCurrSlideNumKey(currSlideNumKey);
+  return _currPresentationSlideNumPattern.firstMatch(currSlideNumKey).group(2);
+}
+
 // Returns true if a key is a current slide number key.
 bool isPresentationCurrSlideNumKey(String key) {
   return _currPresentationSlideNumPattern.hasMatch(key);
 }
 
+void checkValidPresentationCurrSlideNumKey(String key) {
+  if ((!isPresentationCurrSlideNumKey(key))) {
+    throw new ArgumentError(
+        '$key is not a valid presentation current slide number key.');
+  }
+}
+
 // TODO(aghassemi): Don't use regex, just regular split should be fine.
 final RegExp _presentationDriverPattern =
-    new RegExp('($_uuidPattern)(?:/$_uuidPattern)(?:/driver)');
+    new RegExp('($_uuidPattern)/($_uuidPattern)(?:/driver)');
 // Constructs a presentation driver key.
 String getPresentationDriverKey(String deckId, String presentationId) {
   return '$deckId/$presentationId/driver';
@@ -108,18 +120,27 @@
 
 // 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.');
-  }
+  checkValidPresentationDriverKey(driverKey);
   return _presentationDriverPattern.firstMatch(driverKey).group(1);
 }
 
+// Gets the presentation id given a presentation driver key.
+String presentationDriverKeyToPresentationId(String driverKey) {
+  checkValidPresentationDriverKey(driverKey);
+  return _presentationDriverPattern.firstMatch(driverKey).group(2);
+}
+
 // Returns true if a key is a presentation driver key.
 bool isPresentationDriverKey(String key) {
   return _presentationDriverPattern.hasMatch(key);
 }
 
+void checkValidPresentationDriverKey(String key) {
+  if ((!isPresentationDriverKey(key))) {
+    throw new ArgumentError('$key is not a valid presentation driver key.');
+  }
+}
+
 // TODO(aghassemi): Don't use regex, just regular split should be fine.
 final RegExp _presentationQuestionPattern = new RegExp(
     '($_uuidPattern)(?:/$_uuidPattern)(?:/questions/)($_uuidPattern)');
@@ -129,16 +150,12 @@
 }
 
 String presentationQuestionKeyToDeckId(String key) {
-  if ((!isPresentationQuestionKey(key))) {
-    throw new ArgumentError('$key is not a valid presentation question key.');
-  }
+  checkValidPresentationQuestionKey(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.');
-  }
+  checkValidPresentationQuestionKey(key);
   return _presentationQuestionPattern.firstMatch(key).group(2);
 }
 
@@ -147,6 +164,12 @@
   return _presentationQuestionPattern.hasMatch(key);
 }
 
+void checkValidPresentationQuestionKey(String key) {
+  if ((!isPresentationQuestionKey(key))) {
+    throw new ArgumentError('$key is not a valid presentation question key.');
+  }
+}
+
 // Constructs a blob key specific to a deck.
 String getDeckBlobKey(String deckId, String blobId) {
   return '$deckId/$blobId';
diff --git a/dart/lib/styles/common.dart b/dart/lib/styles/common.dart
index 71668a0..fbd8017 100644
--- a/dart/lib/styles/common.dart
+++ b/dart/lib/styles/common.dart
@@ -8,6 +8,8 @@
   static final Color secondaryTextColor = Colors.grey[500];
   static final Color errorTextColor = Colors.red[500];
   static final TextStyle titleStyle = new TextStyle(fontSize: 18.0);
+  static final TextStyle splash =
+      new TextStyle(fontSize: 16.0, color: Colors.white);
   static final TextStyle subtitleStyle =
       new TextStyle(fontSize: 12.0, color: secondaryTextColor);
   static final TextStyle liveNow =
@@ -17,11 +19,12 @@
 
 class Size {
   static const double gridbox = 250.0;
-  static const double boxFooterHeight = 55.0;
+  static const double boxFooterHeight = 65.0;
   static const double listHeight = 120.0;
   static const double thumbnailNavHeight = 250.0;
   static const double questionListThumbnailWidth = 100.0;
   static const double slideListThumbnailWidth = 200.0;
+  static const double splashLogo = 75.0;
 }
 
 class Spacing {
@@ -29,6 +32,7 @@
   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 cardMargin = new EdgeDims.all(4.0);
   static final EdgeDims listItemMargin = new EdgeDims.TRBL(3.0, 6.0, 0.0, 6.0);
   static final EdgeDims actionsMargin =
       new EdgeDims.symmetric(horizontal: 10.0);
diff --git a/dart/lib/utils/asset.dart b/dart/lib/utils/asset.dart
index fbeaa73..6d94d9e 100644
--- a/dart/lib/utils/asset.dart
+++ b/dart/lib/utils/asset.dart
@@ -17,3 +17,6 @@
 }
 
 String defaultThumbnailAssetKey = 'assets/images/defaults/thumbnail.png';
+String splashBackgroundAssetKey = 'assets/images/splash/background.png';
+String splashVanadiumAssetKey = 'assets/images/splash/vanadium.png';
+String splashFlutterAssetKey = 'assets/images/splash/flutter.png';
diff --git a/dart/lib/utils/image_provider.dart b/dart/lib/utils/image_provider.dart
index 78490a5..50777cf 100644
--- a/dart/lib/utils/image_provider.dart
+++ b/dart/lib/utils/image_provider.dart
@@ -18,6 +18,16 @@
     'default_image',
     () => assetutil.getRawBytes(assetutil.defaultThumbnailAssetKey));
 
+final ImageProvider splashBackgroundImageProvider = new _RawImageProvider(
+    'splash_background',
+    () => assetutil.getRawBytes(assetutil.splashBackgroundAssetKey));
+final ImageProvider splashFlutterImageProvider = new _RawImageProvider(
+    'splash_flutter',
+    () => assetutil.getRawBytes(assetutil.splashFlutterAssetKey));
+final ImageProvider splashVanadiumImageProvider = new _RawImageProvider(
+    'splash_vanadium',
+    () => assetutil.getRawBytes(assetutil.splashVanadiumAssetKey));
+
 ImageProvider getDeckThumbnailImage(model.Deck deck) {
   if (deck == null) {
     throw new ArgumentError.notNull('deck');
diff --git a/dart/pubspec.lock b/dart/pubspec.lock
index d803339..8cef40f 100644
--- a/dart/pubspec.lock
+++ b/dart/pubspec.lock
@@ -4,7 +4,7 @@
   analyzer:
     description: analyzer
     source: hosted
-    version: "0.26.3"
+    version: "0.27.1+2"
   archive:
     description: archive
     source: hosted
@@ -20,7 +20,7 @@
   async:
     description: async
     source: hosted
-    version: "1.4.0"
+    version: "1.5.0"
   barback:
     description: barback
     source: hosted
@@ -47,6 +47,10 @@
     description: collection
     source: hosted
     version: "1.2.0"
+  contrast:
+    description: contrast
+    source: hosted
+    version: "0.1.1"
   convert:
     description: convert
     source: hosted
@@ -59,6 +63,10 @@
     description: csslib
     source: hosted
     version: "0.12.2"
+  den_api:
+    description: den_api
+    source: hosted
+    version: "0.1.0"
   fixnum:
     description: fixnum
     source: hosted
@@ -68,7 +76,7 @@
       path: "../../../../../flutter/packages/flutter"
       relative: true
     source: path
-    version: "0.0.20"
+    version: "0.0.21"
   flutter_tools:
     description:
       path: "../../../../../flutter/packages/flutter_tools"
@@ -81,6 +89,10 @@
       relative: true
     source: path
     version: "0.0.10"
+  github:
+    description: github
+    source: hosted
+    version: "2.3.1"
   glob:
     description: glob
     source: hosted
@@ -88,7 +100,11 @@
   html:
     description: html
     source: hosted
-    version: "0.12.2"
+    version: "0.12.2+1"
+  http:
+    description: http
+    source: hosted
+    version: "0.11.3+3"
   http_multi_server:
     description: http_multi_server
     source: hosted
@@ -100,7 +116,7 @@
   intl:
     description: intl
     source: hosted
-    version: "0.12.4+2"
+    version: "0.12.5"
   logging:
     description: logging
     source: hosted
@@ -120,23 +136,23 @@
   mojo:
     description: mojo
     source: hosted
-    version: "0.4.6"
+    version: "0.4.8"
   mojo_apptest:
     description: mojo_apptest
     source: hosted
-    version: "0.2.9"
+    version: "0.2.12"
   mojo_sdk:
     description: mojo_sdk
     source: hosted
-    version: "0.2.5"
+    version: "0.2.7"
   mojo_services:
     description: mojo_services
     source: hosted
-    version: "0.4.8"
+    version: "0.4.10"
   mojom:
     description: mojom
     source: hosted
-    version: "0.2.10"
+    version: "0.2.12"
   mustache4dart:
     description: mustache4dart
     source: hosted
@@ -167,30 +183,38 @@
     description: pool
     source: hosted
     version: "1.2.1"
+  pub_package_data:
+    description: pub_package_data
+    source: hosted
+    version: "0.0.1"
   pub_semver:
     description: pub_semver
     source: hosted
     version: "1.2.3"
+  quiver:
+    description: quiver
+    source: hosted
+    version: "0.21.4"
   shelf:
     description: shelf
     source: hosted
-    version: "0.6.4+2"
+    version: "0.6.4+3"
   shelf_static:
     description: shelf_static
     source: hosted
-    version: "0.2.3+1"
+    version: "0.2.3+2"
   shelf_web_socket:
     description: shelf_web_socket
     source: hosted
-    version: "0.0.1+4"
+    version: "0.0.1+5"
   sky_engine:
     description: sky_engine
     source: hosted
-    version: "0.0.67"
+    version: "0.0.75"
   sky_services:
     description: sky_services
     source: hosted
-    version: "0.0.67"
+    version: "0.0.75"
   source_map_stack_trace:
     description: source_map_stack_trace
     source: hosted
@@ -214,11 +238,11 @@
   syncbase:
     description: syncbase
     source: hosted
-    version: "0.0.20"
+    version: "0.0.23"
   test:
     description: test
     source: hosted
-    version: "0.12.6"
+    version: "0.12.6+1"
   typed_data:
     description: typed_data
     source: hosted
@@ -234,15 +258,27 @@
   v23discovery:
     description: v23discovery
     source: hosted
-    version: "0.0.6"
+    version: "0.0.7"
   vector_math:
     description: vector_math
     source: hosted
-    version: "1.4.3"
+    version: "1.4.4"
   watcher:
     description: watcher
     source: hosted
     version: "0.9.7"
+  when:
+    description: when
+    source: hosted
+    version: "0.2.0"
+  which:
+    description: which
+    source: hosted
+    version: "0.1.3"
+  xml:
+    description: xml
+    source: hosted
+    version: "2.4.0"
   yaml:
     description: yaml
     source: hosted
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
index a3c99b1..67ad8a3 100644
--- a/dart/pubspec.yaml
+++ b/dart/pubspec.yaml
@@ -5,7 +5,7 @@
     path: "../../../../../flutter/packages/flutter"
   logging: ">=0.11.2 <0.12.0"
   mojo_services: ">=0.4.5 <0.5.0"
-  syncbase: ">=0.0.20 <0.1.0"
+  syncbase: ">=0.0.23 <0.1.0"
   v23discovery: ">=0.0.4 < 0.1.0"
   uuid: ">=0.5.0 <0.6.0"
 dev_dependencies:
diff --git a/dart/shortcut_template b/dart/shortcut_template
index 5330e32..c553beb 100644
--- a/dart/shortcut_template
+++ b/dart/shortcut_template
@@ -1,4 +1,4 @@
---map-origin=http://flutter/=https://storage.googleapis.com/mojo/flutter/e80be08b5794930731171c151a73140b8f75b0f7/android-arm/
+--map-origin=http://flutter/=https://storage.googleapis.com/mojo/flutter/97bf8464d2c342a919a80949b7b43c403db6cf6c/android-arm/
 --url-mappings=mojo:flutter=http://flutter/flutter.mojo
 --enable-multiprocess
 --map-origin=https://syncbase.syncslides.mojo.v.io=https://%GS_BUCKET_URL%/