croupier: Introduce Settings Manager

The Settings manager describes features of the Croupier player.
This information is stored in Syncbase and in the future, will be synced
across players of the same game.

To assist with that, the LogWriter was partially abstracted out to a
syncbase CroupierClient. A mock is also created for tests.

The CroupierSettings include a user id, avatar, color, and name.
Only the last 3 can be modified by the user.

Due to some issues with Sky, only the user's name can be changed
(in a rudimentary UI). The user's other settings can be viewed
(excluding the user id).

Change-Id: If12383f9f255a95a3648c079c5ef1a66204055b7
diff --git a/Makefile b/Makefile
index 606b2f7..c508531 100644
--- a/Makefile
+++ b/Makefile
@@ -53,11 +53,14 @@
 .PHONY: mock
 mock:
 	mv lib/src/syncbase/log_writer.dart lib/src/syncbase/log_writer.dart.backup
+	mv lib/src/syncbase/settings_manager.dart lib/src/syncbase/settings_manager.dart.backup
 	cp lib/src/mocks/log_writer.dart lib/src/syncbase/
+	cp lib/src/mocks/settings_manager.dart lib/src/syncbase/
 
 .PHONY: unmock
 unmock:
 	mv lib/src/syncbase/log_writer.dart.backup lib/src/syncbase/log_writer.dart
+	mv lib/src/syncbase/settings_manager.dart.backup lib/src/syncbase/settings_manager.dart
 
 .PHONY: env-check
 env-check:
@@ -74,9 +77,12 @@
 test: packages
 	# Protect src/syncbase/log_writer.dart
 	mv lib/src/syncbase/log_writer.dart lib/src/syncbase/log_writer.dart.backup
+	mv lib/src/syncbase/settings_manager.dart lib/src/syncbase/settings_manager.dart.backup
 	cp lib/src/mocks/log_writer.dart lib/src/syncbase/
+	cp lib/src/mocks/settings_manager.dart lib/src/syncbase/
 	pub run test -r expanded $(DART_TEST_FILES) || (mv lib/src/syncbase/log_writer.dart.backup lib/src/syncbase/log_writer.dart && exit 1)
 	mv lib/src/syncbase/log_writer.dart.backup lib/src/syncbase/log_writer.dart
+	mv lib/src/syncbase/settings_manager.dart.backup lib/src/syncbase/settings_manager.dart
 
 .PHONY: clean
 clean:
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index e5d6db2..2d4dbb9 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -4,12 +4,15 @@
 
 import '../logic/croupier.dart' as logic_croupier;
 import '../logic/game/game.dart' as logic_game;
-import 'game.dart' show createGameComponent, NoArgCb;
+import 'game.dart' show createGameComponent;
+import 'croupier_settings.dart' show CroupierSettingsComponent;
 
 import 'package:sky/widgets_next.dart';
 
 import 'dart:sky' as sky;
 
+typedef void NoArgCb();
+
 class CroupierComponent extends StatefulComponent {
   final NavigatorState navigator;
   final logic_croupier.Croupier croupier;
@@ -55,10 +58,17 @@
                   onPressed: makeSetStateCallback(
                       logic_croupier.CroupierState.ChooseGame)),
               new FlatButton(child: new Text('Join Game')),
-              new FlatButton(child: new Text('Settings'))
+              new FlatButton(
+                child: new Text('Settings'),
+                onPressed: makeSetStateCallback(
+                      logic_croupier.CroupierState.Settings))
             ], direction: FlexDirection.vertical));
       case logic_croupier.CroupierState.Settings:
-        return null; // in which we let them pick an avatar, name, and color. And return to the previous screen after (NOT IMPLEMENTED YET)
+        // in which we let them pick an avatar, name, and color. And return to the previous screen after.
+       return new Container(
+            padding: new EdgeDims.only(top: sky.view.paddingTop),
+            child: new CroupierSettingsComponent(config.croupier, makeSetStateCallback(
+                      logic_croupier.CroupierState.Welcome)));
       case logic_croupier.CroupierState.ChooseGame:
         // in which we let them pick a game out of the many possible games... There aren't that many.
         return new Container(
diff --git a/lib/components/croupier_settings.dart b/lib/components/croupier_settings.dart
new file mode 100644
index 0000000..8f12275
--- /dev/null
+++ b/lib/components/croupier_settings.dart
@@ -0,0 +1,119 @@
+// 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 '../logic/croupier.dart' as logic_croupier;
+
+import 'package:sky/widgets_next.dart';
+
+typedef void NoArgCb();
+typedef void OneStringCb(String data);
+
+Map<String, GlobalKey> globalKeys = {
+  "name": new GlobalKey(),
+  "color": new GlobalKey(),
+  "avatar": new GlobalKey()
+};
+
+class CroupierSettingsComponent extends StatefulComponent {
+  final logic_croupier.Croupier croupier;
+  final NoArgCb backCb;
+
+  CroupierSettingsComponent(this.croupier, this.backCb);
+
+  CroupierSettingsComponentState createState() => new CroupierSettingsComponentState();
+}
+
+class CroupierSettingsComponentState extends State<CroupierSettingsComponent> {
+  String _tempName;
+  String _tempColor; // will be parsed to an int later.
+  String _tempAvatar;
+
+  void initState(_) {
+    super.initState(_);
+
+    _initializeTemp();
+  }
+
+  void _initializeTemp() {
+    _tempName = config.croupier.settings.name;
+    _tempColor = "${config.croupier.settings.color}";
+    _tempAvatar = config.croupier.settings.avatar;
+  }
+
+  Widget build(BuildContext context) {
+    List<Widget> w = new List<Widget>();
+    w.add(_makeInput("name"));
+    // Having multiple Input Widgets on-screen at the same time is bad.
+    // https://github.com/flutter/engine/issues/1387
+    // Using a Dialog instead of a Text widget requires reworking the app.
+    // https://github.com/flutter/engine/issues/243
+    w.add(new Container(
+      decoration: new BoxDecoration(
+            backgroundColor: new Color(config.croupier.settings.color)),
+      child: new Text("color")));
+    w.add(new NetworkImage(src: config.croupier.settings.avatar));
+    //w.add(_makeInput("color"));
+    //w.add(_makeInput("avatar"));
+    w.add(new FlatButton(child: new Text("Return"), onPressed: config.backCb));
+    return new Column(w);
+  }
+
+  void _persist() {
+    setState(() {
+      config.croupier.settings.name = _tempName;
+      int newColor;
+      // https://github.com/domokit/mojo/issues/192
+      // Just calling int.parse will crash SIGSEGV the Dart VM on Android.
+      // Note: if the number is too big. If you do a smaller number, it's fine.
+      /*try {
+        newColor = int.parse(_tempColor);
+      } catch (e) {
+        print(e);
+      }*/
+      if (newColor != null) {
+        config.croupier.settings.color = newColor;
+      }
+      config.croupier.settings.avatar = _tempAvatar;
+      config.croupier.settings_manager.save(config.croupier.settings.userID, config.croupier.settings.toJSONString());
+    });
+  }
+
+  Widget _makeInput(String type) {
+    var capType = _capitalize(type);
+    var keyboardType = type == "color" ? KeyboardType.NUMBER : KeyboardType.TEXT;
+    Input i = new Input(
+      key: globalKeys[type],
+      initialValue: config.croupier.settings.getStringValue(type),
+      placeholder: capType,
+      keyboardType: keyboardType,
+      onChanged: _makeHandleChanged(type)
+    );
+    FlatButton fb = new FlatButton(child: new Text("Save ${capType}"), onPressed: _persist);
+
+    return new Row([i, fb]);
+  }
+
+  String _capitalize(String s) => s[0].toUpperCase() + s.substring(1);
+
+  OneStringCb _makeHandleChanged(String type) {
+    return (String data) {
+      setState(() {
+        print(data);
+        switch (type) {
+          case "name":
+            _tempName = data;
+            break;
+          case "color":
+            _tempColor = data;
+            break;
+          case "avatar":
+            _tempAvatar = data;
+            break;
+          default:
+            break;
+        }
+      });
+    };
+  }
+}
\ No newline at end of file
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index 64eaa76..ac04026 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -4,6 +4,8 @@
 
 import 'game/game.dart' show Game, GameType;
 import 'create_game.dart' as cg;
+import 'croupier_settings.dart' show CroupierSettings;
+import '../src/syncbase/settings_manager.dart' show SettingsManager;
 
 enum CroupierState {
   Welcome,
@@ -16,12 +18,21 @@
 
 class Croupier {
   CroupierState state;
-  Settings settings;
+  SettingsManager settings_manager;
+  CroupierSettings settings; // null, but loaded asynchronously.
   Game game; // null until chosen
 
   Croupier() {
     state = CroupierState.Welcome;
-    // settings = new Settings.load(?); // Give it in the croupier constructor. The app itself should load this info.
+    settings_manager = new SettingsManager();
+    settings_manager.load().then((String csString) {
+      if (csString == null) {
+        settings = new CroupierSettings.random();
+        settings_manager.save(settings.userID, settings.toJSONString());
+      } else {
+        settings = new CroupierSettings.fromJSONString(csString);
+      }
+    });
   }
 
   // Sets the next part of croupier state.
@@ -59,17 +70,13 @@
         assert(false);
     }
 
+    // TODO(alexfandrianto): We may want to have a splash screen or something
+    // when the user first loads the app. It takes a few seconds before the
+    // Syncbase tables are created.
+    if (settings == null && nextState == CroupierState.Settings) {
+      return; // you can't switch till the settings are present.
+    }
+
     state = nextState;
   }
 }
-
-class Settings {
-  String avatar;
-  String name;
-  String color; // in hex?
-
-  Settings(this.avatar, this.name, this.color);
-
-  // Settings.load(String data) {}
-  // String save() { return null; }
-}
diff --git a/lib/logic/croupier_settings.dart b/lib/logic/croupier_settings.dart
new file mode 100644
index 0000000..ffba9de
--- /dev/null
+++ b/lib/logic/croupier_settings.dart
@@ -0,0 +1,90 @@
+// 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:math' as math;
+import 'dart:convert' show JSON;
+
+/// CroupierSettings is a simple struct that contains player-specific settings.
+/// Players can modify a subset of their settings via the UI.
+class CroupierSettings {
+  int userID; // This is a value the user cannot set on their own.
+  String avatar;
+  String name;
+  int color;
+
+  CroupierSettings.random() {
+    _randomInitialization();
+  }
+
+  CroupierSettings.fromJSONString(String json) {
+    var data = JSON.decode(json);
+    userID = data["userID"];
+    avatar = data["avatar"];
+    name = data["name"];
+    color = data["color"];
+  }
+
+  String getStringValue(String key) {
+    switch(key) {
+      case "name":
+        return name;
+      case "avatar":
+        return avatar;
+      case "color":
+        return "${color}";
+      default:
+        return null;
+    }
+  }
+
+  String toJSONString() {
+    return JSON.encode({"userID": userID, "avatar": avatar, "name": name, "color": color});
+  }
+
+  void _randomInitialization() {
+    userID = RandomSettings.userID;
+    avatar = RandomSettings.avatar;
+    name = RandomSettings.name;
+    color = RandomSettings.color;
+  }
+}
+
+class RandomSettings {
+  static final List avatars = [
+    'images/suits/Club.png',
+    'images/suits/Diamond.png',
+    'images/suits/Heart.png',
+    'images/suits/Spade.png'
+  ];
+  static final List names = [
+    'Anne', 'Mary', 'Jack', 'Morgan', 'Roger',
+    'Bill', 'Ragnar', 'Ed', 'John', 'Jane' ];
+  static final List appellations = [
+    'Jackal', 'King', 'Red', 'Stalwart', 'Axe',
+    'Young', 'Brave', 'Eager', 'Wily', 'Zesty'];
+
+  // Return a random user id.
+  static int get userID {
+    return new math.Random().nextInt(0xffffffff);
+  }
+
+  // Return a random image name.
+  static String get avatar {
+    return avatars[new math.Random().nextInt(avatars.length)];
+  }
+
+  // Return a random pirate name
+  static String get name {
+    var rng = new math.Random();
+    int nameIndex = rng.nextInt(names.length);
+    int appIndex = rng.nextInt(appellations.length);
+
+    return "${names[nameIndex]} the ${appellations[appIndex]}";
+  }
+
+  // Return something between 0x00000000 and 0xffffffff
+  static int get color {
+    return new math.Random().nextInt(0xffffffff);
+  }
+}
\ No newline at end of file
diff --git a/lib/src/mocks/log_writer.dart b/lib/src/mocks/log_writer.dart
index 8ff554c..ba0bc27 100644
--- a/lib/src/mocks/log_writer.dart
+++ b/lib/src/mocks/log_writer.dart
@@ -13,7 +13,7 @@
 typedef void updateCallbackT(String key, String value);
 
 class LogWriter {
-  final updateCallbackT updateCallback; // Takes in String key, String value
+  final updateCallbackT updateCallback;
   final List<int> users;
 
   bool inProposalMode = false;
diff --git a/lib/src/mocks/settings_manager.dart b/lib/src/mocks/settings_manager.dart
new file mode 100644
index 0000000..abb6168
--- /dev/null
+++ b/lib/src/mocks/settings_manager.dart
@@ -0,0 +1,29 @@
+// 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';
+
+
+typedef void updateCallbackT(String key, String value);
+
+class SettingsManager {
+  final updateCallbackT updateCallback;
+
+  SettingsManager(this.updateCallback);
+
+  Map<String, String> _data = new Map<String, String>();
+
+  Future<String> load([int userID]) {
+    if (userID == null) {
+      return new Future<String>(() => _data["settings"]);
+    }
+    return new Future<String>(() => _data["${userID}"]);
+  }
+
+  Future save(int userID, String data) {
+    _data["settings"] = data;
+    _data["${userID}"] = data;
+    return new Future(() => null);
+  }
+}
diff --git a/lib/src/syncbase/croupier_client.dart b/lib/src/syncbase/croupier_client.dart
new file mode 100644
index 0000000..4b5615b
--- /dev/null
+++ b/lib/src/syncbase/croupier_client.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 'util.dart' as util;
+
+import 'dart:async';
+
+import 'package:sky/services.dart' show embedder;
+import 'package:ether/syncbase_client.dart'
+    show Perms, SyncbaseClient, SyncbaseNoSqlDatabase, SyncbaseTable;
+
+Perms emptyPerms() => new Perms()..json = '{}';
+
+class CroupierClient {
+  final SyncbaseClient _syncbaseClient;
+
+  CroupierClient() :
+    _syncbaseClient = new SyncbaseClient(embedder.connectToService,
+      'https://mojo.v.io/syncbase_server.mojo');
+
+  // TODO(alexfandrianto): Try not to call this twice at the same time.
+  // That would lead to very race-y behavior.
+  Future<SyncbaseNoSqlDatabase> createDatabase() async {
+    util.log('CroupierClient.createDatabase');
+    var app = _syncbaseClient.app(util.appName);
+    if (!(await app.exists())) {
+      await app.create(emptyPerms());
+    }
+    var db = app.noSqlDatabase(util.dbName);
+    if (!(await db.exists())) {
+      await db.create(emptyPerms());
+    }
+    return db;
+  }
+
+  // TODO(alexfandrianto): Try not to call this twice at the same time.
+  // That would lead to very race-y behavior.
+  Future<SyncbaseTable> createTable(SyncbaseNoSqlDatabase db, String tableName) async {
+    var table = db.table(tableName);
+    if (!(await table.exists())) {
+      await table.create(emptyPerms());
+    }
+    util.log('CroupierClient: ${tableName} is ready');
+    return table;
+  }
+
+}
\ No newline at end of file
diff --git a/lib/src/syncbase/log_writer.dart b/lib/src/syncbase/log_writer.dart
index ca0abfb..ad6bf4e 100644
--- a/lib/src/syncbase/log_writer.dart
+++ b/lib/src/syncbase/log_writer.dart
@@ -2,34 +2,29 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-/// The goal of log writer is to generically manage game logs.
-/// Syncbase will produce values that combine to form a List<GameCommand> while
-/// the in-memory GameLog will also hold such a list.
-///
-/// Updating the GameLog from the Store/Syncbase:
-/// GameLog will update to whatever Store data says.
-/// If it merges, the game log, then it will write that information off.
-/// Case A: Store is farther along than current state.
-/// Continue.
-/// Case B: Store is somehow behind the current state.
-/// Update with the current state of the GameLog (if not sent yet).
-/// Case C: Store's log branches off from the curernt GameLog.
-/// Depending on phase, resolve the conflict differently and write the resolution.
-///
-/// Updating the Store:
-/// When a new GameCommand is received (that doesn't contradict the existing log),
-/// it is added to a list of pending changes and written to the local store.
-
 /// Since this file includes Sky/Mojo, it will need to be mocked out for unit tests.
 /// Unfortunately, imports can't be replaced, so the best thing to do is to swap out the whole file.
+///
+/// The goal of the LogWriter is to allow clients to write to the log in a
+/// consistent, conflict-free manner. Depending on the Simultaneity level, the
+/// values written will be done immediately or enter a proposal phase.
+///
+/// In proposal mode, all other clients must agree on the proposal via a simple
+/// consensus strategy. Once all clients agree, all clients follow through with
+/// the proposal (writing into their log).
+///
+/// Watch is used to inform clients of proposal agreements and changes made
+/// by this and other clients. When a value is confirmed via watch to be written
+/// to the log, the caller is informed via callback.
+
+import 'croupier_client.dart' show CroupierClient;
+import 'util.dart' as util;
 
 import 'dart:async';
 import 'dart:convert' show UTF8, JSON;
 
-import 'package:sky/services.dart' show embedder;
-
 import 'package:ether/syncbase_client.dart'
-    show Perms, SyncbaseClient, SyncbaseTable, WatchChange, WatchChangeTypes;
+    show SyncbaseNoSqlDatabase, SyncbaseTable, WatchChange, WatchChangeTypes;
 
 enum SimulLevel{
   TURN_BASED,
@@ -39,17 +34,11 @@
 
 typedef void updateCallbackT(String key, String value);
 
-log(String msg) {
-  DateTime now = new DateTime.now();
-  print('$now $msg');
-}
-
-Perms emptyPerms() => new Perms()..json = '{}';
 
 class LogWriter {
-  final updateCallbackT updateCallback; // Takes in String key, String value
+  final updateCallbackT updateCallback;
   final List<int> users;
-  final SyncbaseClient _syncbaseClient;
+  final CroupierClient _cc;
 
   bool inProposalMode = false;
   Map<String, String> proposalsKnown; // Only updated via watch.
@@ -61,47 +50,34 @@
     _associatedUser = other;
   }
 
-  LogWriter(this.updateCallback, this.users)
-      : _syncbaseClient = new SyncbaseClient(embedder.connectToService,
-            'https://mojo.v.io/syncbase_server.mojo');
+  LogWriter(this.updateCallback, this.users) : _cc = new CroupierClient() {
+    _prepareLog();
+  }
 
   int seq = 0;
   SyncbaseTable tb;
   String sendMsg, recvMsg, putStr, getStr;
 
-  Future _doSyncbaseInit() async {
-    log('LogWriter.doSyncbaseInit');
+  Future _prepareLog() async {
     if (tb != null) {
-      log('syncbase already initialized');
-      return;
+      return; // Then we're already prepared.
     }
-    var app = _syncbaseClient.app('app');
-    if (!(await app.exists())) {
-      await app.create(emptyPerms());
-    }
-    var db = app.noSqlDatabase('db');
-    if (!(await db.exists())) {
-      await db.create(emptyPerms());
-    }
-    var table = db.table('table');
-    if (!(await table.exists())) {
-      await table.create(emptyPerms());
-    }
-    tb = table;
-    log('syncbase is now initialized');
+
+    SyncbaseNoSqlDatabase db = await _cc.createDatabase();
+    tb = await _cc.createTable(db, util.tableNameLog);
 
     // Start to watch the stream.
-    Stream<WatchChange> watchStream = db.watch('table', '', await db.getResumeMarker());
+    Stream<WatchChange> watchStream = db.watch(util.tableNameLog, '', await db.getResumeMarker());
     _startWatch(watchStream); // Don't wait for this future.
   }
 
   Future _startWatch(Stream<WatchChange> watchStream) async {
-    log('watching for changes...');
+    util.log('watching for changes...');
     // This stream never really ends, so I guess we'll watch forever.
     await for (WatchChange wc in watchStream) {
-      assert(wc.tableName == 'table');
-      log('Watch Key: ${wc.rowName}');
-      log('Watch Value ${UTF8.decode(wc.valueBytes)}');
+      assert(wc.tableName == util.tableNameLog);
+      util.log('Watch Key: ${wc.rowName}');
+      util.log('Watch Value ${UTF8.decode(wc.valueBytes)}');
       String key = wc.rowName;
       String value;
       switch (wc.changeType) {
@@ -127,8 +103,8 @@
   }
 
   Future write(SimulLevel s, String value) async {
-    log('LogWriter.write start');
-    await _doSyncbaseInit();
+    util.log('LogWriter.write start');
+    await _prepareLog();
 
     assert(!inProposalMode);
     String key = _logKey(associatedUser);
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
new file mode 100644
index 0000000..fba7a8b
--- /dev/null
+++ b/lib/src/syncbase/settings_manager.dart
@@ -0,0 +1,104 @@
+// 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.
+
+/// Since this file includes Sky/Mojo, it will need to be mocked out for unit tests.
+/// Unfortunately, imports can't be replaced, so the best thing to do is to swap out the whole file.
+///
+/// The goal of the SettingsManager is to handle viewing and editing of the
+/// Croupier Settings.
+/// loadSettings: Get the settings of the current player or specified userID.
+/// saveSettings: For the current player and their userID, save settings.
+/// In the background, these values will be synced.
+/// When setting up a sync group, the userIDs are very important.
+
+import '../../logic/croupier_settings.dart' as util;
+import 'croupier_client.dart' show CroupierClient;
+import 'util.dart' as util;
+
+import 'dart:async';
+import 'dart:convert' show UTF8, JSON;
+
+import 'package:ether/syncbase_client.dart'
+    show SyncbaseNoSqlDatabase, SyncbaseTable, WatchChange, WatchChangeTypes;
+
+typedef void updateCallbackT(String key, String value);
+
+class SettingsManager {
+  final updateCallbackT updateCallback;
+  final CroupierClient _cc;
+
+  SyncbaseTable tb;
+  SyncbaseTable tbUser;
+
+  SettingsManager([this.updateCallback]) : _cc = new CroupierClient();
+
+  Future _prepareSettingsTable() async {
+    if (tb != null && tbUser != null) {
+      return; // Then we're already prepared.
+    }
+
+    SyncbaseNoSqlDatabase db = await _cc.createDatabase();
+    tb = await _cc.createTable(db, util.tableNameSettings);
+    tbUser = await _cc.createTable(db, util.tableNameSettingsUser);
+
+    // Start to watch the stream for the shared settings table.
+    Stream<WatchChange> watchStream = db.watch(util.tableNameSettings, '', await db.getResumeMarker());
+    _startWatch(watchStream); // Don't wait for this future.
+  }
+
+  Future<String> load([int userID]) async {
+    util.log('SettingsManager.load');
+    await _prepareSettingsTable();
+
+    if (userID == null) {
+      return _tryReadData(tbUser, "settings");
+    }
+    return _tryReadData(tb, "${userID}");
+  }
+
+  Future<String> _tryReadData(SyncbaseTable st, String rowkey) async {
+    var row = st.row(rowkey);
+    if (!(await row.exists())) {
+      print("${rowkey} did not exist");
+      return null;
+    }
+    return UTF8.decode(await row.get());
+  }
+
+  // Since only the current user is allowed to save, we should also save to the
+  // user's personal settings as well.
+  Future save(int userID, String jsonString) async {
+    util.log('SettingsManager.save');
+    await _prepareSettingsTable();
+
+    await tbUser.row("settings").put(UTF8.encode(jsonString));
+    await tb.row("${userID}").put(UTF8.encode(jsonString));
+  }
+
+  Future _startWatch(Stream<WatchChange> watchStream) async {
+    util.log('Settings watching for changes...');
+    // This stream never really ends, so I guess we'll watch forever.
+    await for (WatchChange wc in watchStream) {
+      assert(wc.tableName == util.tableNameSettings);
+      util.log('Watch Key: ${wc.rowName}');
+      util.log('Watch Value ${UTF8.decode(wc.valueBytes)}');
+      String key = wc.rowName;
+      String value;
+      switch (wc.changeType) {
+        case WatchChangeTypes.put:
+          value = UTF8.decode(wc.valueBytes);
+          break;
+        case WatchChangeTypes.delete:
+          value = null;
+          break;
+        default:
+          assert(false);
+      }
+
+      if (this.updateCallback != null) {
+        this.updateCallback(key, value);
+      }
+    }
+  }
+}
diff --git a/lib/src/syncbase/util.dart b/lib/src/syncbase/util.dart
new file mode 100644
index 0000000..ce7501c
--- /dev/null
+++ b/lib/src/syncbase/util.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.
+
+const appName = 'app';
+const dbName = 'db';
+const tableNameLog = 'table';
+const tableNameSettings = 'table_settings';
+const tableNameSettingsUser = 'table_settings_personal';
+
+log(String msg) {
+  DateTime now = new DateTime.now();
+  print('$now $msg');
+}
\ No newline at end of file