SyncSlides: Make targets for deploying the binaries
and creating a shortcut so app can be installed on devices.

Also couple of small fixes:
-Database structure is now created in advance.
-Bug fix where I was aborting a batch before reading
all the data.

Closes https://github.com/vanadium/syncslides/issues/15
Closes https://github.com/vanadium/syncslides/issues/3

Change-Id: I5ef2d897cddcfe69bec83f6876cf5427168d6ba6
diff --git a/.gitignore b/.gitignore
index faa2bc5..c9f8a00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@
 /dart/snapshot_blob.bin
 /dart/app.flx
 /dart/build
+/dart/shortcut_commands
 .atom
diff --git a/dart/MOJO_VERSION b/dart/MOJO_VERSION
index a8fb06d..3d22a47 100644
--- a/dart/MOJO_VERSION
+++ b/dart/MOJO_VERSION
@@ -1 +1 @@
-08b1d8fc1e0296569b628cae2e7611988497b273
\ No newline at end of file
+4503cee3f5f2d3bc3ad46636af0a1029fb22e108
diff --git a/dart/Makefile b/dart/Makefile
index ec6b260..670dcc3 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -7,7 +7,6 @@
 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
-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
@@ -21,6 +20,8 @@
 	VLOG_FLAGS = --v=$(VLOG) --logtostderr=true
 endif
 
+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)
+
 default: run
 
 .PHONY: dartanalyzer
@@ -38,21 +39,59 @@
 upgrade-packages:
 	pub upgrade
 
+.PHONY: build
+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 deploy
+	$(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)"
+
+.PHONY: uninstall
+uninstall:
+	adb -s $(DEVICE_ID) uninstall org.chromium.mojo.shell
+	adb -s $(DEVICE_ID) shell rm -f $(MOJO_SHELL_CMD_PATH)
+# TODO(aghassemi): Is there a way to remove the shortcut via adb?
+
+.PHONY: deploy
+deploy: packages
+	gsutil cp $(APP_FLX_FILE) $(GS_BUCKET_PATH)
+	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)
+
 # Usage example:
 # DEVICE_NUM=1 make run
 # DEVICE_NUM=2 make run
-run: packages
+run: build
 	adb -s $(DEVICE_ID) shell 'echo $(SETTINGS_JSON) > $(SETTINGS_FILE)'
-	pub run flutter_tools build && pub run flutter_tools run_mojo \
+	pub run flutter_tools run_mojo \
 	--mojo-path $(MOJO_DIR)/src \
 	--android --mojo-debug -- --enable-multiprocess \
-	--map-origin="https://syncslides.mojo.v.io/=$(PWD)" \
-	--map-origin="https://discovery.mojo.v.io/=$(JIRI_ROOT)/release/mojo/discovery/gen/mojo/android" \
-	--args-for="https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo \
-	--root-dir=$(SYNCBASE_DATA_DIR) \
-	--v23.namespace.root=$(MOUNTTABLE_ADDR) \
-	$(NAME_FLAG) \
-	$(VLOG_FLAGS)" \
+	--map-origin="https://syncbase.syncslides.mojo.v.io/=$(SYNCBASE_MOJO_DIR)" \
+	--map-origin="https://discovery.syncslides.mojo.v.io/=$(DISCOVERY_MOJO_DIR)" \
+	--args-for="$(SYNCBASE_ARGS)" \
 	$(DEVICE_FLAG) \
 	$(REUSE_FLAG) \
 	--no-config-file
@@ -67,15 +106,11 @@
 run4:
 	DEVICE_NUM=4 make run
 
-.PHONY: uninstall
-uninstall:
-	adb -s $(DEVICE_ID) uninstall org.chromium.mojo.shell
-
 .PHONY: clean
 clean:
 	rm -f app.flx snapshot_blob.bin
 	rm -rf packages
-	adb -s $(DEVICE_ID) shell run-as org.chromium.mojo.shell rm $(SETTINGS_FILE)
+	adb -s $(DEVICE_ID) shell run-as org.chromium.mojo.shell rm $(SETTINGS_FILE) settings_commands
 
 .PHONY: clean-syncbase
 clean-syncbase:
diff --git a/dart/lib/components/askquestion.dart b/dart/lib/components/askquestion.dart
index e69ac67..0dad214 100644
--- a/dart/lib/components/askquestion.dart
+++ b/dart/lib/components/askquestion.dart
@@ -25,6 +25,8 @@
       return new Text('Not in a presentation.');
     }
 
+    // TODO(aghassemi): Switch to multi-line input when support is added.
+    // https://github.com/flutter/flutter/issues/627
     var input = new Input(placeholder: 'Your question',
         onSubmitted: (String questionText) async {
       await appActions.askQuestion(
diff --git a/dart/lib/discovery/client.dart b/dart/lib/discovery/client.dart
index 5fac314..87b2d47 100644
--- a/dart/lib/discovery/client.dart
+++ b/dart/lib/discovery/client.dart
@@ -13,7 +13,7 @@
 final Logger log = new Logger('discovery/client');
 
 const String v23DiscoveryMojoUrl =
-    'https://syncslides.mojo.v.io/packages/v23discovery/mojo_services/android/discovery.mojo';
+    'https://discovery.syncslides.mojo.v.io/discovery.mojo';
 
 // TODO(aghassemi): We should make this the same between Flutter and Java apps when
 // they can actually talk to each other.
diff --git a/dart/lib/stores/syncbase/actions.dart b/dart/lib/stores/syncbase/actions.dart
index c1cec97..86b1b7d 100644
--- a/dart/lib/stores/syncbase/actions.dart
+++ b/dart/lib/stores/syncbase/actions.dart
@@ -14,18 +14,18 @@
 
   Future addDeck(model.Deck deck) async {
     log.info("Adding deck ${deck.name}...");
-    sb.SyncbaseTable tb = await _getDecksTable();
+    sb.SyncbaseTable tb = _getDecksTable();
     await tb.put(deck.key, UTF8.encode(deck.toJson()));
     log.info("Deck ${deck.name} added.");
   }
 
   Future removeDeck(String deckKey) async {
-    sb.SyncbaseTable tb = await _getDecksTable();
+    sb.SyncbaseTable tb = _getDecksTable();
     tb.deleteRange(new sb.RowRange.prefix(deckKey));
   }
 
   Future setSlides(String deckKey, List<model.Slide> slides) async {
-    sb.SyncbaseTable tb = await _getDecksTable();
+    sb.SyncbaseTable tb = _getDecksTable();
 
     slides.forEach((slide) async {
       // TODO(aghassemi): Use batching.
@@ -51,7 +51,7 @@
       // Is the current user driving the presentation?
       if (deckState.presentation.isDriving(_state.user)) {
         // Update the common slide number for the presentation.
-        sb.SyncbaseTable tb = await _getPresentationsTable();
+        sb.SyncbaseTable tb = _getPresentationsTable();
         await tb.put(
             keyutil.getPresentationCurrSlideNumKey(
                 deckId, deckState.presentation.key),
@@ -123,7 +123,7 @@
 
     setDefaultsAndJoin() async {
       // Set the current slide number to 0.
-      sb.SyncbaseTable tb = await _getPresentationsTable();
+      sb.SyncbaseTable tb = _getPresentationsTable();
       await tb.put(
           keyutil.getPresentationCurrSlideNumKey(deckId, presentation.key),
           [0]);
@@ -229,7 +229,7 @@
           'Cannot ask a question because deck is not part of a presentation');
     }
 
-    sb.SyncbaseTable tb = await _getPresentationsTable();
+    sb.SyncbaseTable tb = _getPresentationsTable();
     String questionId = uuidutil.createUuid();
 
     model.Question question = new model.Question(
@@ -255,12 +255,12 @@
   // Blobs
 
   Future putBlob(String key, List<int> bytes) async {
-    sb.SyncbaseTable tb = await _getBlobsTable();
+    sb.SyncbaseTable tb = _getBlobsTable();
     await tb.put(key, bytes);
   }
 
   Future<List<int>> getBlob(String key) async {
-    sb.SyncbaseTable tb = await _getBlobsTable();
+    sb.SyncbaseTable tb = _getBlobsTable();
     return tb.get(key);
   }
 }
@@ -270,7 +270,7 @@
 
 Future _setPresentationDriver(
     String deckId, String presentationId, model.User driver) async {
-  sb.SyncbaseTable tb = await _getPresentationsTable();
+  sb.SyncbaseTable tb = _getPresentationsTable();
   await tb.put(keyutil.getPresentationDriverKey(deckId, presentationId),
       UTF8.encode(driver.toJson()));
 }
@@ -279,27 +279,14 @@
   return '${settings.mounttable}/${settings.deviceId}/%%sync/$uuid';
 }
 
-Future<sb.SyncbaseTable> _getTable(String tableName) async {
-  sb.SyncbaseDatabase sbDb = await sb.getDatabase();
-  sb.SyncbaseTable tb = sbDb.table(tableName);
-  try {
-    await tb.create(sb.createOpenPerms());
-  } catch (e) {
-    if (!errorsutil.isExistsError(e)) {
-      throw e;
-    }
-  }
-  return tb;
+sb.SyncbaseTable _getDecksTable() {
+  return sb.database.table(decksTableName);
 }
 
-Future<sb.SyncbaseTable> _getDecksTable() {
-  return _getTable(decksTableName);
+sb.SyncbaseTable _getPresentationsTable() {
+  return sb.database.table(presentationsTableName);
 }
 
-Future<sb.SyncbaseTable> _getPresentationsTable() {
-  return _getTable(presentationsTableName);
-}
-
-Future<sb.SyncbaseTable> _getBlobsTable() {
-  return _getTable(blobsTableName);
+sb.SyncbaseTable _getBlobsTable() {
+  return sb.database.table(blobsTableName);
 }
diff --git a/dart/lib/stores/syncbase/store.dart b/dart/lib/stores/syncbase/store.dart
index 3c8913f..64e705f 100644
--- a/dart/lib/stores/syncbase/store.dart
+++ b/dart/lib/stores/syncbase/store.dart
@@ -48,8 +48,9 @@
     _asyncInits();
   }
 
-  // Initializations that we must wait for before considering store initialized.
+  // Initializations that we must wait for before considering store initalized.
   Future _syncInits() async {
+    await _createSyncbaseHierarchy();
     _state._user = await identity.getUser();
     _state._settings = await settings.getSettings();
   }
@@ -59,11 +60,8 @@
   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);
+      _getInitialValuesAndStartWatching(table);
     }
     _startScanningForPresentations();
   }
@@ -103,20 +101,19 @@
     discovery.startScan();
   }
 
-  Future _getInitialValuesAndStartWatching(
-      sb.SyncbaseDatabase sbDb, String table) async {
+  Future _getInitialValuesAndStartWatching(String table) async {
     // TODO(aghassemi): Ideally we wouldn't need an initial query and can configure
     // watch to give both initial values and future changes.
     // See https://github.com/vanadium/issues/issues/917
-    var batchDb =
-        await sbDb.beginBatch(sb.SyncbaseClient.batchOptions(readOnly: true));
+    var batchDb = await sb.database
+        .beginBatch(sb.SyncbaseClient.batchOptions(readOnly: true));
     var resumeMarker = await batchDb.getResumeMarker();
 
     // Get initial values in a batch.
     String query = 'SELECT k, v FROM $table';
     Stream<sb.Result> results = batchDb.exec(query);
     // NOTE(aghassemi): First row is always the name of the columns, so we skip(1).
-    results.skip(1).forEach((sb.Result result) => _onChange(
+    await results.skip(1).forEach((sb.Result result) => _onChange(
         table,
         sb.WatchChangeTypes.put,
         UTF8.decode(result.values[0]),
@@ -125,7 +122,7 @@
     await batchDb.abort();
 
     // Start watching from batch's resume marker.
-    var stream = sbDb.watch(table, '', resumeMarker);
+    var stream = sb.database.watch(table, '', resumeMarker);
     stream.listen((sb.WatchChange change) =>
         _onChange(table, change.changeType, change.rowKey, change.valueBytes));
   }
@@ -245,9 +242,22 @@
     });
   }
 
-  Future _ensureTablesExist() async {
-    await _getDecksTable();
-    await _getPresentationsTable();
-    await _getBlobsTable();
+  Future<sb.SyncbaseTable> _createTable(String tableName) async {
+    sb.SyncbaseTable tb = sb.database.table(tableName);
+    try {
+      await tb.create(sb.createOpenPerms());
+    } catch (e) {
+      if (!errorsutil.isExistsError(e)) {
+        throw e;
+      }
+    }
+    return tb;
+  }
+
+  Future _createSyncbaseHierarchy() async {
+    await sb.init();
+    await _createTable(decksTableName);
+    await _createTable(presentationsTableName);
+    await _createTable(blobsTableName);
   }
 }
diff --git a/dart/lib/syncbase/client.dart b/dart/lib/syncbase/client.dart
index dc95299..c4d0c73 100644
--- a/dart/lib/syncbase/client.dart
+++ b/dart/lib/syncbase/client.dart
@@ -15,41 +15,40 @@
 final Logger log = new Logger('syncbase/client');
 
 const String syncbaseMojoUrl =
-    'https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo';
+    'https://syncbase.syncslides.mojo.v.io/syncbase_server.mojo';
 const appName = 'syncslides';
 const dbName = 'syncslides';
 
-SyncbaseDatabase _db;
-// Returns the database handle for the SyncSlides app.
-Future<SyncbaseDatabase> getDatabase() async {
-  if (_db != null) {
-    return _db;
-  }
+SyncbaseDatabase database;
 
-  // Initialize Syncbase app and database.
+// Initializes Syncbase by creating the app and the database.
+Future init() async {
   SyncbaseClient sbClient =
       new SyncbaseClient(shell.connectToService, syncbaseMojoUrl);
   SyncbaseApp sbApp = await _createApp(sbClient);
-  _db = await _createDb(sbApp);
-
-  return _db;
+  database = await _createDb(sbApp);
 }
 
 Future createSyncgroup(
     String mounttable, String syncgroupName, prefixes) async {
-  SyncbaseDatabase sbDb = await getDatabase();
-  SyncbaseSyncgroup sg = sbDb.syncgroup(syncgroupName);
+  SyncbaseSyncgroup sg = database.syncgroup(syncgroupName);
   var sgSpec = SyncbaseClient.syncgroupSpec(prefixes,
       perms: createOpenPerms(), mountTables: [mounttable]);
   var myInfo = SyncbaseClient.syncgroupMemberInfo(syncPriority: 1);
 
-  await sg.create(sgSpec, myInfo);
+  try {
+    await sg.create(sgSpec, myInfo);
+  } catch (e) {
+    if (!errorsutil.isExistsError(e)) {
+      throw e;
+    }
+  }
+
   log.info('Created syncgroup $syncgroupName');
 }
 
 Future joinSyncgroup(String syncgroupName) async {
-  SyncbaseDatabase sbDb = await getDatabase();
-  SyncbaseSyncgroup sg = sbDb.syncgroup(syncgroupName);
+  SyncbaseSyncgroup sg = database.syncgroup(syncgroupName);
   var myInfo = SyncbaseClient.syncgroupMemberInfo(syncPriority: 1);
 
   await sg.join(myInfo);
diff --git a/dart/pubspec.lock b/dart/pubspec.lock
index f808f5e..979700a 100644
--- a/dart/pubspec.lock
+++ b/dart/pubspec.lock
@@ -214,7 +214,7 @@
   syncbase:
     description: syncbase
     source: hosted
-    version: "0.0.15"
+    version: "0.0.18"
   test:
     description: test
     source: hosted
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
index 9a6892e..efbe82b 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.15 <0.1.0"
+  syncbase: ">=0.0.18 <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
new file mode 100644
index 0000000..5330e32
--- /dev/null
+++ b/dart/shortcut_template
@@ -0,0 +1,6 @@
+--map-origin=http://flutter/=https://storage.googleapis.com/mojo/flutter/e80be08b5794930731171c151a73140b8f75b0f7/android-arm/
+--url-mappings=mojo:flutter=http://flutter/flutter.mojo
+--enable-multiprocess
+--map-origin=https://syncbase.syncslides.mojo.v.io=https://%GS_BUCKET_URL%/
+--map-origin=https://discovery.syncslides.mojo.v.io=https://%GS_BUCKET_URL%/
+--args-for=%SYNCBASE_FLAGS%