croupier: Croupier with 2+ devices

The Makefile setup has been upgraded (accompanied by README.md) such that
we can run Croupier on multiple Android devices at the same time.

To further prove this, the devices join a syncgroup together and
share each other's settings. These settings are shown (in an ugly
manner) as CroupierProfileComponents.

There are some caveats though:
- must change Mojo to expose some ports as environment variables
- must start the first device (syncgroup) early enough
- that same first device's app will likely crash after restarting the
  app, so it must be fully uninstalled.
- must connect the devices in a specific order and use ANDROID=1 (or 2, 3, ...)
  to reference them. (We parse adb logcat in order to get the device id.)

These Syncgroups never forget devices (at this point in time), so this is
not necessarily equivalent to Discovery. However, it is close enough that
we can continue onto the Invite Players and Arrange Players screens.

Change-Id: I8f940930fddcbb81fe7df408c3bacc84cd61eb54
diff --git a/Makefile b/Makefile
index b872f84..9fde230 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,15 @@
 SHELL := /bin/bash -euo pipefail
 
 # Flags for Syncbase service running as Mojo service.
-ETHER_FLAGS := --v=1
+ETHER_FLAGS := --v=5
+
+ifdef ANDROID
+	# Parse the adb devices output to obtain the correct device id.
+	# sed takes out the ANDROID_PLUS_ONE'th row of the output
+	# awk takes just the first bit of the line (before whitespace).
+	ANDROID_PLUS_ONE := $(shell echo $(ANDROID) \+ 1 | bc)
+	DEVICE_ID := $(shell adb devices | sed -n $(ANDROID_PLUS_ONE)p | awk '{ print $$1; }')
+endif
 
 ifdef ANDROID
 	MOJO_ANDROID_FLAGS := --android
@@ -18,16 +26,33 @@
 	# Location of mounttable on syncslides-alpha network.
 	MOUNTTABLE := /192.168.86.254:8101
 	# Name to mount under.
-	NAME := croupier/sb1
+	NAME := croupier
 
 	APP_HOME_DIR = /data/data/org.chromium.mojo.shell/app_home
 	ANDROID_CREDS_DIR := /sdcard/v23creds
 
 	ETHER_FLAGS += --logtostderr=true \
-		--name=$(NAME) \
 		--root-dir=$(APP_HOME_DIR)/syncbase_data \
 		--v23.credentials=$(ANDROID_CREDS_DIR) \
 		--v23.namespace.root=$(MOUNTTABLE)
+
+	# Setup the ports. These match the original default ports when ANDROID=1.
+	# ANDROID must be an integer for this to work well.
+	# This helps mojo_run setup the proper ports for HTTP server setup.
+	ENV_LOCAL_ORIGIN_PORT := $(shell echo 31840 \- 10 \+ 10 \* $(ANDROID) | bc)
+	ENV_MAPPINGS_BASE_PORT := $(shell echo 31841 \- 10 \+ 10 \* $(ANDROID) | bc)
+
+ifeq ($(ANDROID), 1)
+	# If ANDROID is set to 1 exactly, then treat it like the first device.
+	# TODO(alexfandrianto): If we can do a better job of this, we won't have to
+	# special-case the first device.
+	ETHER_FLAGS += --name=$(NAME)
+else
+	# It turns out that the other syncbases need to be mounted too.
+	# If not, it looks like they won't sync values to each other.
+	ETHER_FLAGS += --name=foo$(ANDROID)
+endif
+
 else
 	ETHER_BUILD_DIR := $(ETHER_DIR)/gen/mojo/linux_amd64
 	export SYNCBASE_SERVER_URL := file://$(ETHER_BUILD_DIR)/syncbase_server.mojo
@@ -38,19 +63,22 @@
 MOJO_SHELL_FLAGS := --enable-multiprocess --args-for="$(SYNCBASE_SERVER_URL) $(ETHER_FLAGS)"
 
 ifdef ANDROID
-	MOJO_SHELL_FLAGS += --map-origin="https://mojo.v.io/=$(ETHER_BUILD_DIR)"
+	MOJO_SHELL_FLAGS += --map-origin="https://mojo.v.io/=$(ETHER_BUILD_DIR)" --target-device $(DEVICE_ID)
 endif
 
 # Runs a sky app.
 # $1 is location of flx file.
 define RUN_SKY_APP
+	ENV_LOCAL_ORIGIN_PORT=$(ENV_LOCAL_ORIGIN_PORT) \
+	ENV_MAPPINGS_BASE_PORT=$(ENV_MAPPINGS_BASE_PORT) \
 	pub run sky_tools -v --very-verbose run_mojo \
 	--app $1 \
 	$(MOJO_ANDROID_FLAGS) \
 	--mojo-path $(MOJO_DIR)/src \
 	--checked \
 	--mojo-debug \
-	-- $(MOJO_SHELL_FLAGS)
+	-- $(MOJO_SHELL_FLAGS) \
+	--no-config-file
 endef
 
 .DELETE_ON_ERROR:
@@ -92,7 +120,7 @@
 ifdef ANDROID
 	# Make creds dir if it does not exist.
 	mkdir -p creds
-	adb push -p $(PWD)/creds $(ANDROID_CREDS_DIR)
+	adb -s $(DEVICE_ID) push -p $(PWD)/creds $(ANDROID_CREDS_DIR)
 endif
 	$(call RUN_SKY_APP,$<)
 
@@ -133,12 +161,20 @@
 .PHONY: clean
 clean:
 ifdef ANDROID
-	# Clean syncbase creds and data dir.
-	adb shell rm -rf $(ANDROID_CREDS_DIR) $(APP_HOME_DIR)/syncbase_data
+	# Clean syncbase data dir.
+	adb -s $(DEVICE_ID) shell rm -rf $(APP_HOME_DIR)/syncbase_data
 endif
 	rm -f croupier.flx snapshot_blob.bin
-	rm -rf bin creds tmp
+	rm -rf bin tmp
+
+.PHONY: clean-creds
+clean-creds:
+ifdef ANDROID
+	# Clean syncbase creds dir.
+	adb -s $(DEVICE_ID) shell rm -rf $(ANDROID_CREDS_DIR)
+endif
+	rm -rf creds
 
 .PHONY: veryclean
-veryclean: clean
+veryclean: clean clean-creds
 	rm -rf .packages .pub packages pubspec.lock
diff --git a/README.md b/README.md
index b01c72b..7a1d8bd 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,136 @@
-This is the basic Getting Started with Sky + some Widgets.
+# Croupier
 
-To run this, just do 
-./packages/sky/sky_tool start
+Croupier is a Vanadium demo app of a general card playing game for multiple
+devices. The app combines Syncbase with Mojo and Flutter and in the near future,
+will also demonstrate P2P discovery and Syncgroup formation.
 
-when your phone is connected.
-Add --install to the end if this is the first run.
+Croupier's primary card game is Hearts, but it is only available in single-device
+form. More games will be added in the future.
 
-You may also want to debug any problems. Use dartanalyzer.
-dartanalyzer lib/main.dart
\ No newline at end of file
+In order to run the program, it is recommended to use Android devices. (Support
+for desktop is deprecated and will be removed soon.)
+
+# Prerequisites
+
+## Mojo
+
+Currently, development is heavily tied to an existing installation of Mojo.
+Please ensure that your Mojo checkout is located at $MOJO_DIR and has built
+out/android_Debug. Instructions are available [here](https://github.com/domokit/mojo).
+
+__Note__: Currently, in order to run on multiple devices at once, we modified a file
+in Mojo: `mojo/devtools/common/devtoolslib/shell_arguments.py`
+Related issue for multiple Android support: https://github.com/domokit/mojo/issues/470
+
+Use the os library to read environment variables and use defaults otherwise.
+
+```
++import os
+ import os.path
+ import sys
+ import urlparse
+@@ -18,8 +19,8 @@ from devtoolslib.shell_config import ShellConfigurationException
+
+ # When spinning up servers for local origins, we want to use predictable ports
+ # so that caching works between subsequent runs with the same command line.
+-_LOCAL_ORIGIN_PORT = 31840
+-_MAPPINGS_BASE_PORT = 31841
++_LOCAL_ORIGIN_PORT = int(os.getenv('ENV_LOCAL_ORIGIN_PORT', 31840))
++_MAPPINGS_BASE_PORT = int(os.getenv('ENV_MAPPINGS_BASE_PORT', 31841))
+ ```
+
+## 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 [their download page](https://www.dartlang.org/downloads/).
+You may need to manually download a specific version of Dart. If so, visit their
+[archives](https://www.dartlang.org/downloads/archive/) for exact downloads.
+
+## Vanadium
+
+A Vanadium installation is expected, since Croupier also depends on the
+`release/projects/mojo/syncbase` project.
+
+# Running Croupier
+
+## Credentials
+
+Begin by creating your credentials. These are used to determine who can access
+your Syncbase instance. Note that running the following command will pop-up the
+standard `principal seekblessings` tab in order to obtain your approval to use
+OAuth.
+
+```
+make creds
+```
+
+__Any time you clean the credentials, you will need to obtain fresh credentials.__
+
+## Note on Multiple Devices
+
+If you have more than 1 device plugged into the computer, you will need to specify
+which device to use. `adb devices` will tell you the order of your devices.
+
+It is highly recommended that you mark/remember the order of the devices; it is
+the same as the order they were plugged into the computer/workstation.
+
+For later devices, instead of `ANDROID=1` use `2`, `3`, `4`, etc.
+
+__Note:__ Running Croupier on multiple Android devices simultaneously is still a work-in-progress.
+The workaround is to launch Croupier on a single device at a time.
+
+__Note:__ This example currently relies on a mount table on the local network at
+`192.168.86.254:8101`. This may be changed to the global mount table at a later time.
+https://github.com/vanadium/issues/issues/782
+
+## Start
+
+Start Croupier on your USB-debugging enabled Android device.
+```
+ANDROID=1 make start
+```
+
+Alternatively, use a different integer. Since the first device creates a syncgroup,
+it is recommended that you wait a short duration before starting up any other devices.
+
+## Deleting Mojo Shell
+
+On your Android device, go to the Apps that you downloaded and Uninstall Mojo
+Shell from there. This cleanup step is important for when Syncbase is in a bad
+state.
+
+__Note__: Due to issues with Syncgroup creation, an app that creates a syncgroup
+will not be able to start a second time, so you __must__ delete the mojo shell
+after each run. Fixing this is a high priority.
+
+## Cleaning Up
+
+Due to some issues with mojo_shell, you may occasionally fail to start the
+program due to a used port. Follow the error's instructions and try again.
+
+Between builds of Mojo and Syncbase, you may wish to clean the app up.
+
+```
+ANDROID=1 make clean
+```
+
+You can also clean credentials instead:
+
+```
+ANDROID=1 make clean-creds
+```
+
+Don't forget to do `make creds` to rebuild them.
+
+Lastly, you can also clear out the pub packages:
+
+```
+make veryclean
+```
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index 8db6eb1..00ba77e 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -3,9 +3,11 @@
 // license that can be found in the LICENSE file.
 
 import '../logic/croupier.dart' as logic_croupier;
+import '../logic/croupier_settings.dart' show CroupierSettings;
 import '../logic/game/game.dart' as logic_game;
 import 'game.dart' as component_game;
 import 'croupier_settings.dart' show CroupierSettingsComponent;
+import 'croupier_profile.dart' show CroupierProfileComponent;
 
 import 'package:sky/widgets.dart';
 
@@ -28,6 +30,13 @@
   void initState() {
     super.initState();
     // TODO(alexfandrianto): sky.view.width and sky.view.height?
+
+    // Croupier (logic) needs this in case of syncbase watch updates.
+    config.croupier.informUICb = _informUICb;
+  }
+
+  void _informUICb() {
+    setState(() {});
   }
 
   NoArgCb makeSetStateCallback(logic_croupier.CroupierState s,
@@ -52,9 +61,16 @@
     switch (config.croupier.state) {
       case logic_croupier.CroupierState.Welcome:
         // in which we show them a UI to start a new game, join a game, or change some settings.
+        // TODO(alexfandrianto): Put this somewhere nicer.
+        // It is here to demonstrate users joining the main Croupier syncgroup.
+        List<Widget> profileWidgets = new List<Widget>();
+        config.croupier.settings_everyone.forEach((_, CroupierSettings cs) {
+          profileWidgets.add(new CroupierProfileComponent(cs));
+        });
+
         return new Container(
             padding: new EdgeDims.only(top: sky.view.paddingTop),
-            child: new Flex([
+            child: new Column([
               new FlatButton(
                   child: new Text('Create Game'),
                   onPressed: makeSetStateCallback(
@@ -64,14 +80,15 @@
                   child: new Text('Settings'),
                   onPressed: makeSetStateCallback(
                       logic_croupier.CroupierState.Settings))
-            ], direction: FlexDirection.vertical));
+            ]..addAll(profileWidgets)));
       case logic_croupier.CroupierState.Settings:
         // 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.navigator,
-                config.croupier,
+                config.croupier.settings,
+                config.croupier.settings_manager.save,
                 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.
diff --git a/lib/components/croupier_profile.dart b/lib/components/croupier_profile.dart
new file mode 100644
index 0000000..70bd5ba
--- /dev/null
+++ b/lib/components/croupier_profile.dart
@@ -0,0 +1,23 @@
+// 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_settings.dart' show CroupierSettings;
+
+import 'package:sky/widgets.dart';
+
+class CroupierProfileComponent extends StatelessComponent {
+  final CroupierSettings settings;
+  CroupierProfileComponent(this.settings);
+
+  Widget build(BuildContext context) {
+    return new Container(
+      decoration: new BoxDecoration(
+        backgroundColor: new Color(settings.color)),
+        child: new Column([
+          new NetworkImage(src: settings.avatar),
+          new Text(settings.name)
+      ])
+    );
+  }
+}
diff --git a/lib/components/croupier_settings.dart b/lib/components/croupier_settings.dart
index 3c301ff..b15022f 100644
--- a/lib/components/croupier_settings.dart
+++ b/lib/components/croupier_settings.dart
@@ -2,13 +2,13 @@
 // 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 '../logic/croupier_settings.dart' show CroupierSettings, RandomSettings;
 
 import 'package:sky/widgets.dart';
 
 typedef void NoArgCb();
 typedef void OneStringCb(String data);
+typedef void SaveDataCb(int userID, String jsonData);
 
 enum DialogType { Text, ColorPicker, ImagePicker }
 
@@ -30,10 +30,11 @@
 
 class CroupierSettingsComponent extends StatefulComponent {
   final NavigatorState navigator;
-  final logic_croupier.Croupier croupier;
+  final CroupierSettings settings;
+  final SaveDataCb saveDataCb;
   final NoArgCb backCb;
 
-  CroupierSettingsComponent(this.navigator, this.croupier, this.backCb);
+  CroupierSettingsComponent(this.navigator, this.settings, this.saveDataCb, this.backCb);
 
   CroupierSettingsComponentState createState() =>
       new CroupierSettingsComponentState();
@@ -49,9 +50,9 @@
   }
 
   void _initializeTemp() {
-    _tempData[nameKey] = config.croupier.settings.name;
-    _tempData[colorKey] = "${config.croupier.settings.color}";
-    _tempData[avatarKey] = config.croupier.settings.avatar;
+    _tempData[nameKey] = config.settings.name;
+    _tempData[colorKey] = "${config.settings.color}";
+    _tempData[avatarKey] = config.settings.avatar;
   }
 
   Widget _makeColoredRectangle(int colorInfo, String text, NoArgCb cb) {
@@ -68,11 +69,11 @@
 
   Widget build(BuildContext context) {
     List<Widget> w = new List<Widget>();
-    w.add(_makeButtonRow(nameKey, new Text(config.croupier.settings.name)));
+    w.add(_makeButtonRow(nameKey, new Text(config.settings.name)));
     w.add(_makeButtonRow(colorKey,
-        _makeColoredRectangle(config.croupier.settings.color, "", null)));
+        _makeColoredRectangle(config.settings.color, "", null)));
     w.add(_makeButtonRow(
-        avatarKey, new NetworkImage(src: config.croupier.settings.avatar)));
+        avatarKey, new NetworkImage(src: config.settings.avatar)));
 
     w.add(new FlatButton(child: new Text("Return"), onPressed: config.backCb));
     return new Column(w);
@@ -100,7 +101,7 @@
               content: new Input(
                   key: globalKeys[type],
                   placeholder: capType,
-                  initialValue: config.croupier.settings.getStringValue(type),
+                  initialValue: config.settings.getStringValue(type),
                   keyboardType: KeyboardType.TEXT,
                   onChanged: _makeHandleChanged(type)), onDismiss: () {
             navigator.pop();
@@ -174,9 +175,9 @@
       return;
     }
     setState(() {
-      config.croupier.settings.setStringValue(type, data);
-      config.croupier.settings_manager.save(config.croupier.settings.userID,
-          config.croupier.settings.toJSONString());
+      config.settings.setStringValue(type, data);
+      config.saveDataCb(config.settings.userID,
+          config.settings.toJSONString());
     });
   }
 
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index ac04026..a03860c 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -16,15 +16,21 @@
   PlayGame
 }
 
+typedef void NoArgCb();
+
 class Croupier {
   CroupierState state;
   SettingsManager settings_manager;
   CroupierSettings settings; // null, but loaded asynchronously.
+  Map<String, CroupierSettings> settings_everyone; // empty, but loaded asynchronously
   Game game; // null until chosen
+  NoArgCb informUICb;
 
   Croupier() {
     state = CroupierState.Welcome;
-    settings_manager = new SettingsManager();
+    settings_everyone = new Map<String, CroupierSettings>();
+    settings_manager = new SettingsManager(_updateSettingsEveryoneCb);
+
     settings_manager.load().then((String csString) {
       if (csString == null) {
         settings = new CroupierSettings.random();
@@ -35,6 +41,15 @@
     });
   }
 
+  // Updates the settings_everyone map as people join the main Croupier syncgroup
+  // and change their settings.
+  void _updateSettingsEveryoneCb(String key, String json) {
+    settings_everyone[key] = new CroupierSettings.fromJSONString(json);
+    if (this.informUICb != null) {
+      this.informUICb();
+    }
+  }
+
   // Sets the next part of croupier state.
   // Depending on the originating state, data can contain extra information that we need.
   void setState(CroupierState nextState, var data) {
diff --git a/lib/logic/croupier_settings.dart b/lib/logic/croupier_settings.dart
index 81d692e..27966a0 100644
--- a/lib/logic/croupier_settings.dart
+++ b/lib/logic/croupier_settings.dart
@@ -59,7 +59,7 @@
         } catch (e) {
           print(e);
         }
-        print("WARNING: Would have set the color to ${newColor} but not yet.");
+        print("Setting color to 0x${newColor}.");
         color = newColor;
         break;
       default:
diff --git a/lib/src/syncbase/croupier_client.dart b/lib/src/syncbase/croupier_client.dart
index 477df4b..cd266f4 100644
--- a/lib/src/syncbase/croupier_client.dart
+++ b/lib/src/syncbase/croupier_client.dart
@@ -11,8 +11,6 @@
 import 'package:ether/syncbase_client.dart'
     show Perms, SyncbaseClient, SyncbaseNoSqlDatabase, SyncbaseTable;
 
-Perms emptyPerms() => new Perms()..json = '{}';
-
 class CroupierClient {
   final SyncbaseClient _syncbaseClient;
   static final String syncbaseServerUrl = Platform.environment[
@@ -31,11 +29,11 @@
     util.log('CroupierClient.createDatabase');
     var app = _syncbaseClient.app(util.appName);
     if (!(await app.exists())) {
-      await app.create(emptyPerms());
+      await app.create(util.openPerms);
     }
     var db = app.noSqlDatabase(util.dbName);
     if (!(await db.exists())) {
-      await db.create(emptyPerms());
+      await db.create(util.openPerms);
     }
     return db;
   }
@@ -46,7 +44,7 @@
       SyncbaseNoSqlDatabase db, String tableName) async {
     var table = db.table(tableName);
     if (!(await table.exists())) {
-      await table.create(emptyPerms());
+      await table.create(util.openPerms);
     }
     util.log('CroupierClient: ${tableName} is ready');
     return table;
diff --git a/lib/src/syncbase/log_writer.dart b/lib/src/syncbase/log_writer.dart
index cf81e37..34cdce7 100644
--- a/lib/src/syncbase/log_writer.dart
+++ b/lib/src/syncbase/log_writer.dart
@@ -28,10 +28,8 @@
 
 enum SimulLevel { TURN_BASED, INDEPENDENT, DEPENDENT }
 
-typedef void updateCallbackT(String key, String value);
-
 class LogWriter {
-  final updateCallbackT updateCallback;
+  final util.updateCallbackT updateCallback;
   final List<int> users;
   final CroupierClient _cc;
 
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index 599af49..9008f6c 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -21,17 +21,15 @@
 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);
+import 'package:ether/syncbase_client.dart' as sc;
+import 'package:ether/src/naming/util.dart' as naming;
 
 class SettingsManager {
-  final updateCallbackT updateCallback;
+  final util.updateCallbackT updateCallback;
   final CroupierClient _cc;
 
-  SyncbaseTable tb;
-  SyncbaseTable tbUser;
+  sc.SyncbaseTable tb;
+  sc.SyncbaseTable tbUser;
 
   SettingsManager([this.updateCallback]) : _cc = new CroupierClient();
 
@@ -40,14 +38,27 @@
       return; // Then we're already prepared.
     }
 
-    SyncbaseNoSqlDatabase db = await _cc.createDatabase();
+    sc.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 =
+    Stream<sc.WatchChange> watchStream =
         db.watch(util.tableNameSettings, '', await db.getResumeMarker());
     _startWatch(watchStream); // Don't wait for this future.
+    _loadSettings(tb); // Don't wait for this future.
+
+    // Don't wait for this future either.
+    // TODO(alexfandrianto): This is a way to debug who is present in the syncgroup.
+    // This should be removed in the near future, once we are more certain about
+    // the syncgroups we have formed.
+    _joinOrCreateSyncgroup().then((var sg) {
+      new Timer.periodic(const Duration(seconds: 3), (Timer _) async {
+        Map<String, sc.SyncgroupMemberInfo> members = await sg.getMembers();
+        print("There are ${members.length} members.");
+        print(members);
+      });
+    });
   }
 
   Future<String> load([int userID]) async {
@@ -60,7 +71,7 @@
     return _tryReadData(tb, "${userID}");
   }
 
-  Future<String> _tryReadData(SyncbaseTable st, String rowkey) async {
+  Future<String> _tryReadData(sc.SyncbaseTable st, String rowkey) async {
     var row = st.row(rowkey);
     if (!(await row.exists())) {
       print("${rowkey} did not exist");
@@ -79,20 +90,23 @@
     await tb.row("${userID}").put(UTF8.encode(jsonString));
   }
 
-  Future _startWatch(Stream<WatchChange> watchStream) async {
+  // This watch method ensures that any changes are propagated to the caller.
+  // In the case of the settings manager, we're checking for any changes to
+  // any person's Croupier Settings.
+  Future _startWatch(Stream<sc.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) {
+    await for (sc.WatchChange wc in watchStream) {
       assert(wc.tableName == util.tableNameSettings);
       util.log('Watch Key: ${wc.rowKey}');
       util.log('Watch Value ${UTF8.decode(wc.valueBytes)}');
       String key = wc.rowKey;
       String value;
       switch (wc.changeType) {
-        case WatchChangeTypes.put:
+        case sc.WatchChangeTypes.put:
           value = UTF8.decode(wc.valueBytes);
           break;
-        case WatchChangeTypes.delete:
+        case sc.WatchChangeTypes.delete:
           value = null;
           break;
         default:
@@ -104,4 +118,53 @@
       }
     }
   }
+
+  // When starting the settings manager, there may be settings already in the
+  // store. Make sure to load those.
+  Future _loadSettings(sc.SyncbaseTable tb) async {
+    tb.scan(new sc.RowRange.prefix('')).forEach((sc.KeyValue kv) {
+      this.updateCallback(kv.key, UTF8.decode(kv.value));
+    });
+  }
+
+
+  Future<sc.SyncbaseSyncgroup> _joinOrCreateSyncgroup() async {
+
+    sc.SyncbaseNoSqlDatabase db = await _cc.createDatabase();
+    String mtAddr = util.mtAddr;
+    String tableName = util.tableNameSettings;
+
+    var mtName = mtAddr;
+    var sgPrefix = naming.join(mtName, util.sgPrefix);
+    var sgName = naming.join(sgPrefix, util.sgName);
+    var sg = db.syncgroup(sgName);
+
+    print('SGNAME = $sgName');
+
+    var myInfo = sc.SyncbaseClient.syncgroupMemberInfo(syncPriority: 3);
+
+    try {
+      print('trying to join syncgroup');
+      await sg.join(myInfo);
+      print('syncgroup join success');
+    } catch (e) {
+      // Syncgroup does not exist.
+      print('syncgroup does not exist, creating it');
+
+      var sgSpec = sc.SyncbaseClient.syncgroupSpec(
+          // Sync the entire table.
+          [sc.SyncbaseClient.syncgroupPrefix(tableName, '')],
+          description: 'test syncgroup',
+          perms: util.openPerms,
+          mountTables: [mtName]);
+
+      print('SGSPEC = $sgSpec');
+
+      await sg.create(sgSpec, myInfo);
+      print('syncgroup create success');
+    }
+
+    return sg;
+  }
+
 }
diff --git a/lib/src/syncbase/util.dart b/lib/src/syncbase/util.dart
index c54b107..6858029 100644
--- a/lib/src/syncbase/util.dart
+++ b/lib/src/syncbase/util.dart
@@ -2,12 +2,28 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+
+import 'package:ether/syncbase_client.dart' show Perms, SyncbaseClient;
+
 const appName = 'app';
 const dbName = 'db';
 const tableNameLog = 'table';
 const tableNameSettings = 'table_settings';
 const tableNameSettingsUser = 'table_settings_personal';
 
+// TODO(alexfandrianto): This may need to be the global mount table with a
+// proxy. Otherwise, it will be difficult for other users to run.
+// https://github.com/vanadium/issues/issues/782
+const mtAddr = '/192.168.86.254:8101';
+const sgPrefix = 'croupier/%%sync';
+const sgName = 'discovery';
+
+typedef void updateCallbackT(String key, String value);
+
+String openPermsJson =
+    '{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}';
+Perms openPerms = SyncbaseClient.perms(openPermsJson);
+
 log(String msg) {
   DateTime now = new DateTime.now();
   print('$now $msg');