syncslides: Emulating BlobRef ( to some extend )

This allows us to sync data structures faster and load
the image bytes on demand after UI loads.

This also allows us to simply sync the thumbnail of advertised
presentations using Syncbase instead of making an Rpc.

-Moving all bytes to another general purpose blobs table.
-All blobs for a deck are prefixed with deckId.
-There is a special syncgroup of the thumbnail of a deck
 that only includes a single blob row. This syncgroup is
 joined by everyone who discovers the presentation to get
 the thumbnail.

Change-Id: Ib821f1e81ec25c4b08e49246bb34f12508870b22
diff --git a/dart/lib/discovery/client.dart b/dart/lib/discovery/client.dart
index 72e0668..5fac314 100644
--- a/dart/lib/discovery/client.dart
+++ b/dart/lib/discovery/client.dart
@@ -9,7 +9,6 @@
 import 'package:v23discovery/discovery.dart' as v23discovery;
 
 import '../models/all.dart' as model;
-import '../utils/asset.dart' as assetutil;
 
 final Logger log = new Logger('discovery/client');
 
@@ -49,12 +48,13 @@
   Map<String, String> serviceAttrs = new Map();
   serviceAttrs['deckid'] = presentation.deck.key;
   serviceAttrs['name'] = presentation.deck.name;
+  serviceAttrs['thumbnailkey'] = presentation.deck.thumbnail.key;
   v23discovery.Service serviceInfo = new v23discovery.Service()
     ..instanceId = presentation.key
     ..interfaceName = presentationInterfaceName
     ..instanceName = presentation.key
     ..attrs = serviceAttrs
-    ..addrs = [presentation.syncgroupName];
+    ..addrs = [presentation.syncgroupName, presentation.thumbnailSyncgroupName];
 
   v23discovery.AdvertiserProxy advertiser =
       new v23discovery.AdvertiserProxy.unbound();
@@ -166,15 +166,13 @@
       return;
     }
 
-    // TODO(aghassemi): For now we use the default thumbnail. We need to find a way
-    // to fetch the actual thumbnail from the other side.
-    var thumbnail =
-        await assetutil.getRawBytes(assetutil.defaultThumbnailAssetKey);
-    model.Deck deck =
-        new model.Deck(s.attrs['deckid'], s.attrs['name'], thumbnail.toList());
+    model.Deck deck = new model.Deck(s.attrs['deckid'], s.attrs['name'],
+        new model.BlobRef(s.attrs['thumbnailkey']));
     var syncgroupName = s.addrs[0];
+    var thumbnailSyncgroupName = s.addrs[1];
     model.PresentationAdvertisement presentation =
-        new model.PresentationAdvertisement(key, deck, syncgroupName);
+        new model.PresentationAdvertisement(
+            key, deck, syncgroupName, thumbnailSyncgroupName);
 
     _onFoundEmitter.add(presentation);
   }
diff --git a/dart/lib/loaders/demo_loader.dart b/dart/lib/loaders/demo_loader.dart
index 781dc6a..4585668 100644
--- a/dart/lib/loaders/demo_loader.dart
+++ b/dart/lib/loaders/demo_loader.dart
@@ -7,6 +7,7 @@
 
 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';
@@ -50,28 +51,36 @@
   static const int maxNumSlides = 20;
 
   Future<model.Deck> _getRandomDeck() async {
-    var thumbnail = await assetutil
-        .getRawBytes(thumbnails[_rand.nextInt(thumbnails.length)]);
     var firstWord = firstWords[_rand.nextInt(firstWords.length)];
     var secondWord = secondWords[_rand.nextInt(secondWords.length)];
-    return new model.Deck(
-        uuidutil.createUuid(), '$firstWord $secondWord', thumbnail);
+    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() async* {
+  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;
-      yield new model.Slide(
-          i,
-          await assetutil.getRawBytes(
-              'assets/images/sample_decks/vanadium/${slideIndex + 1}.jpg'));
+      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().toList();
+    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/models/all.dart b/dart/lib/models/all.dart
index e2dbdbd..ff29f7f 100644
--- a/dart/lib/models/all.dart
+++ b/dart/lib/models/all.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+export 'blobref.dart';
 export 'deck.dart';
 export 'presentation_advertisement.dart';
 export 'question.dart';
diff --git a/dart/lib/models/blobref.dart b/dart/lib/models/blobref.dart
new file mode 100644
index 0000000..bf89028
--- /dev/null
+++ b/dart/lib/models/blobref.dart
@@ -0,0 +1,44 @@
+// 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 '../stores/store.dart';
+
+// TODO(aghassemi): Replace with the true Blob type when supported in Dart.
+const int maxNumTries = 100;
+const Duration interval = const Duration(milliseconds: 100);
+
+class BlobRef {
+  String _key;
+  String get key => _key;
+
+  BlobRef(this._key);
+
+  Future<List<int>> getData() {
+    var store = new Store.singleton();
+    int numTries = 0;
+
+    getBlobFromStore() async {
+      return store.actions.getBlob(key);
+    }
+
+    getBlobWithRetries() async {
+      // Don't fail immediately if blob is not found in store, it might be still syncing.
+      try {
+        numTries++;
+        var data = await getBlobFromStore();
+        return data;
+      } catch (e) {
+        if (numTries <= maxNumTries) {
+          return new Future.delayed(interval, getBlobWithRetries);
+        } else {
+          throw new ArgumentError.value(key, 'Blob not found');
+        }
+      }
+    }
+
+    return getBlobWithRetries();
+  }
+}
diff --git a/dart/lib/models/deck.dart b/dart/lib/models/deck.dart
index 133dc04..861d186 100644
--- a/dart/lib/models/deck.dart
+++ b/dart/lib/models/deck.dart
@@ -4,6 +4,8 @@
 
 import 'dart:convert';
 
+import 'blobref.dart';
+
 // Deck represents a deck of slides.
 class Deck {
   String _key;
@@ -12,8 +14,8 @@
   String _name;
   String get name => _name;
 
-  List<int> _thumbnail;
-  List<int> get thumbnail => _thumbnail;
+  BlobRef _thumbnail;
+  BlobRef get thumbnail => _thumbnail;
 
   Deck(this._key, this._name, this._thumbnail) {}
 
@@ -21,14 +23,14 @@
     Map map = JSON.decode(json);
     _key = key;
     _name = map['name'];
-    _thumbnail = map['thumbnail'];
+    _thumbnail = new BlobRef(map['thumbnailkey']);
   }
 
   String toJson() {
     // NOTE(aghassemi): We never serialize the key with the object.
     Map map = new Map();
     map['name'] = name;
-    map['thumbnail'] = thumbnail;
+    map['thumbnailkey'] = thumbnail.key;
     return JSON.encode(map);
   }
 
diff --git a/dart/lib/models/presentation_advertisement.dart b/dart/lib/models/presentation_advertisement.dart
index 5e19c4e..925fc8a 100644
--- a/dart/lib/models/presentation_advertisement.dart
+++ b/dart/lib/models/presentation_advertisement.dart
@@ -16,5 +16,9 @@
   String _syncgroupName;
   String get syncgroupName => _syncgroupName;
 
-  PresentationAdvertisement(this._key, this._deck, this._syncgroupName) {}
+  String _thumbnailSyncgroupName;
+  String get thumbnailSyncgroupName => _thumbnailSyncgroupName;
+
+  PresentationAdvertisement(
+      this._key, this._deck, this._syncgroupName, this._thumbnailSyncgroupName);
 }
diff --git a/dart/lib/models/slide.dart b/dart/lib/models/slide.dart
index e93b03a..7694ddd 100644
--- a/dart/lib/models/slide.dart
+++ b/dart/lib/models/slide.dart
@@ -4,26 +4,28 @@
 
 import 'dart:convert';
 
+import 'blobref.dart';
+
 // Slide represents a slide within a deck.
 class Slide {
   int _num;
   int get num => _num;
 
-  List<int> _image;
-  List<int> get image => _image;
+  BlobRef _image;
+  BlobRef get image => _image;
 
   Slide(this._num, this._image) {}
 
   Slide.fromJson(String json) {
     Map map = JSON.decode(json);
     _num = map['num'];
-    _image = map['image'];
+    _image = new BlobRef(map['imagekey']);
   }
 
   String toJson() {
     Map map = new Map();
     map['num'] = _num;
-    map['image'] = image;
+    map['imagekey'] = image.key;
     return JSON.encode(map);
   }
 
diff --git a/dart/lib/stores/actions.dart b/dart/lib/stores/actions.dart
index 678d30a..faca6ab 100644
--- a/dart/lib/stores/actions.dart
+++ b/dart/lib/stores/actions.dart
@@ -51,4 +51,13 @@
 
   // Sets the driver of a presentation to the given user.
   Future setDriver(String deckId, model.User driver);
+
+  //////////////////////////////////////
+  // Blobs
+
+  // Stores the given blob bytes under the given key.
+  Future putBlob(String key, List<int> bytes);
+
+  // Gets the blob bytes for the given key.
+  Future<List<int>> getBlob(String key);
 }
diff --git a/dart/lib/stores/syncbase/actions.dart b/dart/lib/stores/syncbase/actions.dart
index 8513b06..c1cec97 100644
--- a/dart/lib/stores/syncbase/actions.dart
+++ b/dart/lib/stores/syncbase/actions.dart
@@ -88,16 +88,28 @@
       throw new ArgumentError.value(deckId, 'Deck no longer exists.');
     }
 
-    String uuid = uuidutil.createUuid();
-    String syncgroupName = _getPresentationSyncgroupName(_state.settings, uuid);
-
     model.Deck deck = _state._getOrCreateDeckState(deckId)._deck;
-    var presentation =
-        new model.PresentationAdvertisement(uuid, deck, syncgroupName);
+    String presentationId = uuidutil.createUuid();
+    String syncgroupName = _getSyncgroupName(_state.settings, presentationId);
+    String thumbnailSyncgroupName =
+        _getSyncgroupName(_state.settings, deck.thumbnail.key);
 
+    var presentation = new model.PresentationAdvertisement(
+        presentationId, deck, syncgroupName, thumbnailSyncgroupName);
+
+    // Syncgroup for deck and presentation data, including blobs.
     await sb.createSyncgroup(_state.settings.mounttable, syncgroupName, [
       sb.SyncbaseClient.syncgroupPrefix(decksTableName, deckId),
-      sb.SyncbaseClient.syncgroupPrefix(presentationsTableName, deckId)
+      sb.SyncbaseClient.syncgroupPrefix(presentationsTableName, deckId),
+      sb.SyncbaseClient.syncgroupPrefix(blobsTableName, deckId)
+    ]);
+
+    // TODO(aghassemi): Use a simple RPC instead of a syncgroup to get the thumbnail.
+    // See https://github.com/vanadium/syncslides/issues/17
+    // Syncgroup for deck thumbnail.
+    await sb.createSyncgroup(
+        _state.settings.mounttable, thumbnailSyncgroupName, [
+      sb.SyncbaseClient.syncgroupPrefix(blobsTableName, deck.thumbnail.key)
     ]);
 
     await discovery.advertise(presentation);
@@ -238,6 +250,19 @@
     }
     await _setPresentationDriver(deckId, deckState.presentation.key, driver);
   }
+
+  //////////////////////////////////////
+  // Blobs
+
+  Future putBlob(String key, List<int> bytes) async {
+    sb.SyncbaseTable tb = await _getBlobsTable();
+    await tb.put(key, bytes);
+  }
+
+  Future<List<int>> getBlob(String key) async {
+    sb.SyncbaseTable tb = await _getBlobsTable();
+    return tb.get(key);
+  }
 }
 
 //////////////////////////////////////
@@ -250,9 +275,8 @@
       UTF8.encode(driver.toJson()));
 }
 
-String _getPresentationSyncgroupName(
-    model.Settings settings, String presentationId) {
-  return '${settings.mounttable}/${settings.deviceId}/%%sync/$presentationId';
+String _getSyncgroupName(model.Settings settings, String uuid) {
+  return '${settings.mounttable}/${settings.deviceId}/%%sync/$uuid';
 }
 
 Future<sb.SyncbaseTable> _getTable(String tableName) async {
@@ -275,3 +299,7 @@
 Future<sb.SyncbaseTable> _getPresentationsTable() {
   return _getTable(presentationsTableName);
 }
+
+Future<sb.SyncbaseTable> _getBlobsTable() {
+  return _getTable(blobsTableName);
+}
diff --git a/dart/lib/stores/syncbase/consts.dart b/dart/lib/stores/syncbase/consts.dart
index 0e4955a..0c93249 100644
--- a/dart/lib/stores/syncbase/consts.dart
+++ b/dart/lib/stores/syncbase/consts.dart
@@ -6,4 +6,5 @@
 
 const String decksTableName = 'decks';
 const String presentationsTableName = 'presentations';
+const String blobsTableName = 'blobs';
 final Logger log = new Logger('store/syncbase_store');
diff --git a/dart/lib/stores/syncbase/store.dart b/dart/lib/stores/syncbase/store.dart
index 252f6d7..3c8913f 100644
--- a/dart/lib/stores/syncbase/store.dart
+++ b/dart/lib/stores/syncbase/store.dart
@@ -81,6 +81,12 @@
     discovery.onFound.listen((model.PresentationAdvertisement newP) {
       _state._presentationsAdvertisements[newP.key] = newP;
       _triggerStateChange();
+
+      // TODO(aghassemi): Use a simple RPC instead of a syncgroup to get the thumbnail.
+      // See https://github.com/vanadium/syncslides/issues/17
+      // Join the thumbnail syncgroup to get the thumbnail blob.
+      String sgName = newP.thumbnailSyncgroupName;
+      sb.joinSyncgroup(sgName);
     });
 
     discovery.onLost.listen((String presentationId) {
@@ -242,5 +248,6 @@
   Future _ensureTablesExist() async {
     await _getDecksTable();
     await _getPresentationsTable();
+    await _getBlobsTable();
   }
 }
diff --git a/dart/lib/stores/utils/key.dart b/dart/lib/stores/utils/key.dart
index 83b8e1a..367dee9 100644
--- a/dart/lib/stores/utils/key.dart
+++ b/dart/lib/stores/utils/key.dart
@@ -141,3 +141,8 @@
 bool isPresentationQuestionKey(String key) {
   return _presentationQuestionPattern.hasMatch(key);
 }
+
+// Constructs a blob key specific to a deck.
+String getDeckBlobKey(String deckId, String blobId) {
+  return '$deckId/$blobId';
+}
diff --git a/dart/lib/utils/image_provider.dart b/dart/lib/utils/image_provider.dart
index d4029c1..78490a5 100644
--- a/dart/lib/utils/image_provider.dart
+++ b/dart/lib/utils/image_provider.dart
@@ -7,14 +7,23 @@
 import 'dart:ui' as ui;
 
 import 'package:flutter/services.dart';
+import 'package:logging/logging.dart';
 
 import '../models/all.dart' as model;
+import '../utils/asset.dart' as assetutil;
+
+final Logger log = new Logger('utils/image_provider');
+
+final ImageProvider defaultImageProvider = new _RawImageProvider(
+    'default_image',
+    () => assetutil.getRawBytes(assetutil.defaultThumbnailAssetKey));
 
 ImageProvider getDeckThumbnailImage(model.Deck deck) {
   if (deck == null) {
     throw new ArgumentError.notNull('deck');
   }
-  return new _RawImageProvider('thumbnail_${deck.key}', deck.thumbnail);
+
+  return new _RawImageProvider('thumbnail_${deck.key}', deck.thumbnail.getData);
 }
 
 ImageProvider getSlideImage(String deckId, model.Slide slide) {
@@ -24,15 +33,29 @@
   if (slide == null) {
     throw new ArgumentError.notNull('slide');
   }
-  return new _RawImageProvider('slide_${deckId}_${slide.num}', slide.image);
+
+  return new _RawImageProvider(
+      'slide_${deckId}_${slide.num}', slide.image.getData);
 }
 
+typedef Future<List<int>> BlobFetcher();
+
 class _RawImageProvider implements ImageProvider {
   final String imageKey;
-  final List<int> imageData;
+  final BlobFetcher blobFetcher;
 
-  _RawImageProvider(this.imageKey, this.imageData);
+  _RawImageProvider(this.imageKey, this.blobFetcher);
+
   Future<ui.Image> loadImage() async {
+    List<int> imageData;
+    try {
+      imageData = await blobFetcher();
+    } catch (e) {
+      log.warning('Blob for ${imageKey} not found.');
+      imageData =
+          await assetutil.getRawBytes(assetutil.defaultThumbnailAssetKey);
+    }
+
     return await decodeImageFromList(new Uint8List.fromList(imageData));
   }