syncslides: Syncbase initialization.

Initialize syncbase and do a better job of handling errors along the
way.

Change-Id: I359f86afb8838aa7060be8e8dd97cabcde15bea4
diff --git a/android/app/src/main/java/io/v/syncslides/DeckChooserActivity.java b/android/app/src/main/java/io/v/syncslides/DeckChooserActivity.java
index dba60e0..c866ec1 100644
--- a/android/app/src/main/java/io/v/syncslides/DeckChooserActivity.java
+++ b/android/app/src/main/java/io/v/syncslides/DeckChooserActivity.java
@@ -11,6 +11,7 @@
 import android.support.v7.app.AppCompatActivity;
 import android.util.Log;
 import android.view.Menu;
+import android.widget.Toast;
 
 import io.v.syncslides.V23;
 
@@ -29,7 +30,13 @@
         super.onCreate(savedInstanceState);
         // Immediately initialize V23, possibly sending user to the
         // AccountManager to get blessings.
-        V23.Singleton.get().init(getApplicationContext(), this);
+        try {
+            V23.Singleton.get().init(getApplicationContext(), this);
+        } catch (InitException e) {
+            // TODO(kash): Start a to-be-written SettingsActivity that makes it possible
+            // to wipe the state of syncbase and/or blessings.
+            handleError("Failed to init", e);
+        }
 
         setContentView(R.layout.activity_deck_chooser);
 
@@ -79,12 +86,21 @@
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
-        Log.d(TAG, "onActivityResult");
-        if (V23.Singleton.get().onActivityResult(
-                getApplicationContext(), requestCode, resultCode, data)) {
-            Log.d(TAG, "did the v23 result");
-            return;
+        try {
+            if (V23.Singleton.get().onActivityResult(
+                    getApplicationContext(), requestCode, resultCode, data)) {
+                return;
+            }
+        } catch (InitException e) {
+            // TODO(kash): Start a to-be-written SettingsActivity that makes it possible
+            // to wipe the state of syncbase and/or blessings.
+            handleError("Failed onActivityResult initialization", e);
         }
         // Any other activity results would be handled here.
     }
+
+    private void handleError(String msg, Throwable throwable) {
+        Log.e(TAG, msg + ": " + Log.getStackTraceString(throwable));
+        Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
+    }
 }
diff --git a/android/app/src/main/java/io/v/syncslides/InitException.java b/android/app/src/main/java/io/v/syncslides/InitException.java
new file mode 100644
index 0000000..3fd2038
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/InitException.java
@@ -0,0 +1,18 @@
+// 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.
+
+package io.v.syncslides;
+
+/**
+ * Thrown when initialization fails.
+ */
+public class InitException extends Exception {
+    public InitException(Throwable throwable) {
+        super(throwable);
+    }
+
+    public InitException(String detailMessage, Throwable throwable) {
+        super(detailMessage, throwable);
+    }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/V23.java b/android/app/src/main/java/io/v/syncslides/V23.java
index 1a8ab37..4c9b5d7 100644
--- a/android/app/src/main/java/io/v/syncslides/V23.java
+++ b/android/app/src/main/java/io/v/syncslides/V23.java
@@ -13,6 +13,7 @@
 import io.v.android.v23.V;
 import io.v.android.v23.services.blessing.BlessingCreationException;
 import io.v.android.v23.services.blessing.BlessingService;
+import io.v.syncslides.db.DB;
 import io.v.v23.context.VContext;
 import io.v.v23.security.BlessingPattern;
 import io.v.v23.security.Blessings;
@@ -53,7 +54,7 @@
     private V23() {
     }
 
-    public void init(Context context, Activity activity) {
+    public void init(Context context, Activity activity) throws InitException {
         if (mBlessings != null) {
             return;
         }
@@ -72,16 +73,18 @@
 
     /**
      * To be called from an Activity's onActivityResult method, e.g.
-     *     public void onActivityResult(
-     *         int requestCode, int resultCode, Intent data) {
-     *         if (V23.Singleton.get().onActivityResult(
-     *             getApplicationContext(), requestCode, resultCode, data)) {
-     *           return;
+     *     public void onActivityResult(int requestCode, int resultCode, Intent data) {
+     *         try {
+     *             if (V23.Singleton.get().onActivityResult(
+     *                     getApplicationContext(), requestCode, resultCode, data)) {
+     *               return;
+     *             }
+     *         } catch (InitException e) {
+     *             // Handle the error, possibly by resetting blessings and syncbase.
      *         }
-     *     }
      */
     public boolean onActivityResult(
-            Context context, int requestCode, int resultCode, Intent data) {
+            Context context, int requestCode, int resultCode, Intent data) throws InitException {
         if (requestCode != BLESSING_REQUEST) {
             return false;
         }
@@ -91,37 +94,65 @@
             Blessings blessings = (Blessings) VomUtil.decode(blessingsVom, Blessings.class);
             BlessingsManager.addBlessings(mContext, blessings);
             configurePrincipal(blessings);
-//            DB.Singleton.get(androidCtx).init();
         } catch (BlessingCreationException e) {
-            throw new IllegalStateException(e);
+            throw new InitException(e);
         } catch (VException e) {
-            throw new IllegalStateException(e);
+            throw new InitException(e);
         }
         return true;
     }
 
-    private Blessings loadBlessings() {
+    private Blessings loadBlessings() throws InitException {
         try {
             // See if there are blessings stored in shared preferences.
             return BlessingsManager.getBlessings(mContext);
         } catch (VException e) {
-            Log.w(TAG, "Cannot get blessings from prefs: " + e.getMessage());
+            throw new InitException(e);
         }
-        return null;
     }
 
-    private void configurePrincipal(Blessings blessings) {
+    private void configurePrincipal(Blessings blessings) throws InitException {
         try {
             VPrincipal p = V.getPrincipal(mVContext);
             p.blessingStore().setDefaultBlessings(blessings);
             p.blessingStore().set(blessings, new BlessingPattern("..."));
             VSecurity.addToRoots(p, blessings);
             mBlessings = blessings;
+            DB.Singleton.get().init(mContext);
         } catch (VException e) {
-            Log.e(TAG, String.format(
-                    "Couldn't set local blessing %s: %s", blessings, e.getMessage()));
+            throw new InitException(
+                    String.format("Couldn't set local blessing %s", blessings), e);
         }
     }
 
+    /**
+     * v23 operations that require a blessing (almost everything) will fail if
+     * attempted before this is true.
+     *
+     * The simplest usage is 1) There are no blessings. 2) An activity starts
+     * and calls V23Manager.init. 2) init notices there are no blessings and
+     * calls startActivityForResult 3) meanwhile, the activity and/or its
+     * components still run, but can test isBlessed before attempting anything
+     * requiring blessings. The activity will soon be re-initialized anyway. 4)
+     * user kicked over into 'account manager', gets a blessing, and the
+     * activity is restarted, this time with isBlessed == true.
+     */
+    public boolean isBlessed() {
+        return mBlessings != null;
+    }
+
+    /**
+     * Returns the blessings for this process.
+     */
+    public Blessings getBlessings() {
+        return mBlessings;
+    }
+
+    /**
+     * Returns the Vanadium context for this process.
+     */
+    public VContext getVContext() {
+        return mVContext;
+    }
 
 }
diff --git a/android/app/src/main/java/io/v/syncslides/db/DB.java b/android/app/src/main/java/io/v/syncslides/db/DB.java
new file mode 100644
index 0000000..8281671
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/DB.java
@@ -0,0 +1,39 @@
+// 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.
+
+package io.v.syncslides.db;
+
+import android.content.Context;
+
+import io.v.syncslides.InitException;
+import io.v.syncslides.model.Deck;
+import io.v.syncslides.model.DynamicList;
+
+/**
+ * Provides high-level methods for getting and setting the state of SyncSlides.
+ * It is an interface instead of a concrete class to make testing easier.
+ */
+public interface DB {
+    class Singleton {
+        private static volatile DB instance;
+
+        public static DB get() {
+            DB result = instance;
+            if (instance == null) {
+                synchronized (Singleton.class) {
+                    result = instance;
+                    if (result == null) {
+                        instance = result = new SyncbaseDB();
+                    }
+                }
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Perform initialization steps.
+     */
+    void init(Context context) throws InitException;
+}
diff --git a/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java b/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java
new file mode 100644
index 0000000..8ad5fce
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/db/SyncbaseDB.java
@@ -0,0 +1,146 @@
+// 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.
+
+package io.v.syncslides.db;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.io.File;
+import java.util.Arrays;
+
+import io.v.android.v23.V;
+import io.v.impl.google.services.syncbase.SyncbaseServer;
+import io.v.syncslides.InitException;
+import io.v.syncslides.V23;
+import io.v.syncslides.model.Deck;
+import io.v.syncslides.model.DynamicList;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.Server;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.Blessings;
+import io.v.v23.security.access.AccessList;
+import io.v.v23.security.access.Constants;
+import io.v.v23.security.access.Permissions;
+import io.v.v23.syncbase.Syncbase;
+import io.v.v23.syncbase.SyncbaseApp;
+import io.v.v23.syncbase.SyncbaseService;
+import io.v.v23.syncbase.nosql.Database;
+import io.v.v23.syncbase.nosql.Table;
+import io.v.v23.verror.VException;
+
+public class SyncbaseDB implements DB {
+    private static final String TAG = "SyncbaseDB";
+    private static final String SYNCBASE_APP = "syncslides";
+    private static final String SYNCBASE_DB = "syncslides";
+    private static final String DECKS_TABLE = "Decks";
+    private static final String NOTES_TABLE = "Notes";
+    static final String PRESENTATIONS_TABLE = "Presentations";
+    static final String CURRENT_SLIDE = "CurrentSlide";
+    static final String QUESTIONS = "questions";
+    private static final String SYNCGROUP_PRESENTATION_DESCRIPTION = "Live Presentation";
+
+    private boolean mInitialized = false;
+    private Handler mHandler;
+    private Permissions mPermissions;
+    private Context mContext;
+    private VContext mVContext;
+    private Server mSyncbaseServer;
+    private Database mDB;
+
+    // Singleton.
+    SyncbaseDB() {
+    }
+
+    @Override
+    public void init(Context context) throws InitException {
+        if (mInitialized) {
+            return;
+        }
+        mContext = context;
+        mHandler = new Handler(Looper.getMainLooper());
+
+        // If blessings aren't in place, the fragment that called this
+        // initialization may continue to load and use DB, but nothing will
+        // work so DB methods should return noop values.  It's assumed that
+        // the calling fragment will send the user to the AccountManager,
+        // accept blessings on return, then re-call this init.
+        if (!V23.Singleton.get().isBlessed()) {
+            Log.d(TAG, "no blessings.");
+            return;
+        }
+        mVContext = V23.Singleton.get().getVContext();
+        setupSyncbase();
+    }
+
+    // TODO(kash): Run this in an AsyncTask so it doesn't block the UI.
+    private void setupSyncbase() throws InitException {
+        Blessings blessings = V23.Singleton.get().getBlessings();
+        AccessList everyoneAcl = new AccessList(
+                ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
+        AccessList justMeAcl = new AccessList(
+                ImmutableList.of(new BlessingPattern(blessings.toString())),
+                ImmutableList.<String>of());
+
+        mPermissions = new Permissions(ImmutableMap.of(
+                Constants.RESOLVE.getValue(), everyoneAcl,
+                Constants.READ.getValue(), justMeAcl,
+                Constants.WRITE.getValue(), justMeAcl,
+                Constants.ADMIN.getValue(), justMeAcl));
+
+        // Prepare the syncbase storage directory.
+        File storageDir = new File(mContext.getFilesDir(), "syncbase");
+        storageDir.mkdirs();
+
+        try {
+            String id = Settings.Secure.getString(
+                    mContext.getContentResolver(), Settings.Secure.ANDROID_ID);
+            mVContext = SyncbaseServer.withNewServer(mVContext, new SyncbaseServer.Params()
+                    .withPermissions(mPermissions)
+                            // TODO(kash): Mount it!
+                            //.withName(V23Manager.syncName(id))
+                    .withStorageRootDir(storageDir.getAbsolutePath()));
+        } catch (SyncbaseServer.StartException e) {
+            throw new InitException("Couldn't start syncbase server", e);
+        }
+        try {
+            mSyncbaseServer = V.getServer(mVContext);
+            Log.i(TAG, "Endpoints: " + Arrays.toString(mSyncbaseServer.getStatus().getEndpoints()));
+            String serverName = "/" + mSyncbaseServer.getStatus().getEndpoints()[0];
+
+            // Now that we've started Syncbase, set up our connections to it.
+            SyncbaseService service = Syncbase.newService(serverName);
+            SyncbaseApp app = service.getApp(SYNCBASE_APP);
+            if (!app.exists(mVContext)) {
+                app.create(mVContext, mPermissions);
+            }
+            mDB = app.getNoSqlDatabase(SYNCBASE_DB, null);
+            if (!mDB.exists(mVContext)) {
+                mDB.create(mVContext, mPermissions);
+            }
+            Table decks = mDB.getTable(DECKS_TABLE);
+            if (!decks.exists(mVContext)) {
+                decks.create(mVContext, mPermissions);
+            }
+            Table notes = mDB.getTable(NOTES_TABLE);
+            if (!notes.exists(mVContext)) {
+                notes.create(mVContext, mPermissions);
+            }
+            Table presentations = mDB.getTable(PRESENTATIONS_TABLE);
+            if (!presentations.exists(mVContext)) {
+                presentations.create(mVContext, mPermissions);
+            }
+            //importDecks();
+        } catch (VException e) {
+            throw new InitException("Couldn't setup syncbase service", e);
+        }
+        mInitialized = true;
+    }
+}