syncslides: Port account and blessing initialization.

V23.java is heavily inspired by the old V23Manager.java.  I stripped out
anything related to mounttables, and I simplified some of the code.

SignInActivity and related files are a copy/paste from the other project.
This activity requires a few more permissions and a min sdk of 22.

Also, renamed the app to DevSyncSlides so I can retain the demo-ready
version while I do development on this version.

Change-Id: Ia7a468a76c7df492e8f3d36f8528c48f8299c159
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 4e5b9ac..da166ab 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -18,7 +18,7 @@
 
     defaultConfig {
         applicationId "io.v.syncslides"
-        minSdkVersion 19
+        minSdkVersion 22
         targetSdkVersion 23
         versionCode 1
         versionName "1.0"
@@ -40,4 +40,5 @@
     testCompile 'junit:junit:4.12'
     compile 'com.android.support:appcompat-v7:23.1.0'
     compile 'com.android.support:design:23.1.0'
+    compile project(':android-lib')
 }
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 7d6ac9d..f30168e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,22 +1,36 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="io.v.syncslides" >
+<manifest
+    package="io.v.syncslides"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <uses-sdk android:minSdkVersion="22"/>
+
+    <!-- SignInActivity has the user select an email address. -->
+    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+    <!-- SignInActivity uses this as a simple way to get the user's name -->
+    <!-- without an internet connection. -->
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission android:name="android.permission.READ_PROFILE" />
 
     <application
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
         android:supportsRtl="true"
-        android:theme="@style/AppTheme" >
+        android:theme="@style/AppTheme">
+        <activity
+            android:name=".SignInActivity"
+            android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
         <activity
             android:name=".MainActivity"
             android:label="@string/app_name"
-            android:theme="@style/AppTheme.NoActionBar" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
+            android:theme="@style/AppTheme.NoActionBar">
         </activity>
     </application>
 
diff --git a/android/app/src/main/java/io/v/syncslides/MainActivity.java b/android/app/src/main/java/io/v/syncslides/MainActivity.java
index bcf663e..fc3872d 100644
--- a/android/app/src/main/java/io/v/syncslides/MainActivity.java
+++ b/android/app/src/main/java/io/v/syncslides/MainActivity.java
@@ -4,20 +4,29 @@
 
 package io.v.syncslides;
 
+import android.content.Intent;
 import android.os.Bundle;
 import android.support.design.widget.FloatingActionButton;
 import android.support.design.widget.Snackbar;
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.Toolbar;
+import android.util.Log;
 import android.view.View;
 import android.view.Menu;
 import android.view.MenuItem;
 
 public class MainActivity extends AppCompatActivity {
 
+    private static final String TAG = "MainActivity";
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        // Immediately initialize V23, possibly sending user to the
+        // AccountManager to get blessings.
+        V23.Singleton.get().init(getApplicationContext(), this);
+
         setContentView(R.layout.activity_main);
         Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
         setSupportActionBar(toolbar);
@@ -53,4 +62,17 @@
 
         return super.onOptionsItemSelected(item);
     }
+
+    @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;
+        }
+        // Any other activity results would be handled here.
+    }
+
 }
diff --git a/android/app/src/main/java/io/v/syncslides/SignInActivity.java b/android/app/src/main/java/io/v/syncslides/SignInActivity.java
new file mode 100644
index 0000000..0d8408d
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/SignInActivity.java
@@ -0,0 +1,307 @@
+// 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;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharStreams;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Signs in the user into one of his Gmail accounts.
+ */
+public class SignInActivity extends AppCompatActivity {
+    private static final String TAG = "SignInActivity";
+
+    private static final String PREF_USER_ACCOUNT_NAME = "user_account";
+    private static final String PREF_USER_NAME_FROM_CONTACTS = "user_name_from_contacts";
+    private static final String PREF_USER_NAME_FROM_PROFILE = "user_name_from_profile";
+    private static final String PREF_USER_PROFILE_JSON = "user_profile";
+
+    private static final int REQUEST_CODE_PICK_ACCOUNT = 1000;
+    private static final int REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL = 1001;
+
+    private static final String OAUTH_PROFILE = "email";
+    private static final String OAUTH_SCOPE = "oauth2:" + OAUTH_PROFILE;
+    private static final String OAUTH_USERINFO_URL =
+            "https://www.googleapis.com/oauth2/v2/userinfo";
+
+    private SharedPreferences mPrefs;
+    private String mAccountName;
+    private ProgressDialog mProgressDialog;
+
+    /**
+     * Returns the best-effort email of the signed-in user.
+     */
+    public static String getUserEmail(Context ctx) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+        return prefs.getString(PREF_USER_ACCOUNT_NAME, "");
+    }
+
+    /**
+     * Returns the best-effort full name of the signed-in user.
+     */
+    public static String getUserName(Context ctx) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+        // First try to read the user name we obtained from profile, as it's most accurate.
+        if (prefs.contains(PREF_USER_NAME_FROM_PROFILE)) {
+            return prefs.getString(PREF_USER_NAME_FROM_PROFILE, "Anonymous User");
+        }
+        return prefs.getString(PREF_USER_NAME_FROM_CONTACTS, "Anonymous User");
+    }
+
+    /**
+     * Returns the Google profile information of the signed-in user, or {@code null} if the
+     * profile information couldn't be retrieved.
+     */
+    public static JSONObject getUserProfile(Context ctx) {
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+        String userProfileJsonStr = prefs.getString(PREF_USER_PROFILE_JSON, "");
+        if (!userProfileJsonStr.isEmpty()) {
+            try {
+                return new JSONObject(userProfileJsonStr);
+            } catch (JSONException e) {
+                Log.e(TAG, "Couldn't parse user profile data: " + userProfileJsonStr);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_sign_in);
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+        mAccountName = mPrefs.getString(SignInActivity.PREF_USER_ACCOUNT_NAME, "");
+        mProgressDialog = new ProgressDialog(this);
+        if (mAccountName.isEmpty()) {
+            mProgressDialog.setMessage("Signing in...");
+            mProgressDialog.show();
+            pickAccount();
+        } else {
+            finishActivity();
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_CODE_PICK_ACCOUNT: {
+                if (resultCode != RESULT_OK) {
+                    Toast.makeText(this, "Must pick account", Toast.LENGTH_LONG).show();
+                    pickAccount();
+                    break;
+                }
+                pickAccountDone(data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME));
+                break;
+            }
+            case REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL:
+                if (resultCode != RESULT_OK) {
+                    Log.e(TAG, "User didn't approve oauth2 request");
+                    break;
+                }
+                fetchUserProfile();
+                break;
+        }
+    }
+
+    private void pickAccount() {
+        Intent chooseIntent = AccountManager.newChooseAccountIntent(
+                null, null, new String[]{"com.google"}, false, null, null, null, null);
+        startActivityForResult(chooseIntent, REQUEST_CODE_PICK_ACCOUNT);
+    }
+
+    private void pickAccountDone(String accountName) {
+        mAccountName = accountName;
+        SharedPreferences.Editor editor = mPrefs.edit();
+        editor.putString(PREF_USER_ACCOUNT_NAME, accountName);
+        editor.commit();
+
+        fetchUserNameFromContacts();
+
+        // NOTE(spetrovic): For demo purposes, fetching user profile is too risky as it requires
+        // internet access.  So we disable it for now.
+        //fetchUserProfile();
+        finishActivity();
+    }
+
+    private void fetchUserNameFromContacts() {
+        // Get the user's full name from Contacts.
+        Cursor c = getContentResolver().query(ContactsContract.Profile.CONTENT_URI,
+                null, null, null, null);
+        String[] columnNames = c.getColumnNames();
+        String userName = "Anonymous User";
+        while (c.moveToNext()) {
+            for (int j = 0; j < columnNames.length; j++) {
+                String columnName = columnNames[j];
+                if (!columnName.equals(ContactsContract.Contacts.DISPLAY_NAME)) {
+                    continue;
+                }
+                userName = c.getString(c.getColumnIndex(columnName));
+            }
+        }
+        c.close();
+        SharedPreferences.Editor editor = mPrefs.edit();
+        editor.putString(PREF_USER_NAME_FROM_CONTACTS, userName);
+        editor.commit();
+    }
+
+    private void fetchUserProfile() {
+        AccountManager manager = (AccountManager) getSystemService(Context.ACCOUNT_SERVICE);
+        Account[] accounts = manager.getAccountsByType("com.google");
+        Account account = null;
+        for (int i = 0; i < accounts.length; i++) {
+            if (accounts[i].name.equals(mAccountName)) {
+                account = accounts[i];
+                break;
+            }
+        }
+        if (account == null) {
+            Log.e(TAG, "Couldn't find Google account with name: " + mAccountName);
+            pickAccount();
+            return;
+        }
+        manager.getAuthToken(account,
+                OAUTH_SCOPE,
+                new Bundle(),
+                false,
+                new OnTokenAcquired(),
+                new Handler(new Handler.Callback() {
+                    @Override
+                    public boolean handleMessage(Message msg) {
+                        Log.e(TAG, "Error getting auth token: " + msg.toString());
+                        fetchUserProfileDone(null);
+                        return true;
+                    }
+                }));
+    }
+
+    private void fetchUserProfileDone(JSONObject userProfile) {
+        if (userProfile != null) {
+            SharedPreferences.Editor editor = mPrefs.edit();
+            editor.putString(PREF_USER_PROFILE_JSON, userProfile.toString());
+            try {
+                if (userProfile.has("name") && !userProfile.getString("name").isEmpty()) {
+                    editor.putString(PREF_USER_NAME_FROM_PROFILE, userProfile.getString("name"));
+                }
+            } catch (JSONException e) {
+                Log.e(TAG, "Couldn't read user name from user profile: " + e.getMessage());
+            }
+            editor.commit();
+        }
+        finishActivity();
+    }
+
+
+    private void finishActivity() {
+        mProgressDialog.dismiss();
+        startActivity(new Intent(this, MainActivity.class));
+        finish();
+    }
+
+    private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
+        @Override
+        public void run(AccountManagerFuture<Bundle> result) {
+            try {
+                Bundle bundle = result.getResult();
+                Intent launch = (Intent) bundle.get(AccountManager.KEY_INTENT);
+                if (launch != null) {  // Needs user approval.
+                    // NOTE(spetrovic): The returned intent has the wrong flag value
+                    // FLAG_ACTIVITY_NEW_TASK set, which results in the launched intent replying
+                    // immediately with RESULT_CANCELED.  Hence, we clear the flag here.
+                    launch.setFlags(0);
+                    startActivityForResult(launch, REQUEST_CODE_FETCH_USER_PROFILE_APPROVAL);
+                    return;
+                }
+                String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
+                new ProfileInfoFetcher().execute(token);
+            } catch (AuthenticatorException e) {
+                Log.e(TAG, "Couldn't authorize: " + e.getMessage());
+                fetchUserProfileDone(null);
+            } catch (OperationCanceledException e) {
+                Log.e(TAG, "Authorization cancelled: " + e.getMessage());
+                fetchUserProfileDone(null);
+            } catch (IOException e) {
+                Log.e(TAG, "Unexpected error: " + e.getMessage());
+                fetchUserProfileDone(null);
+            }
+        }
+    }
+
+    private class ProfileInfoFetcher extends AsyncTask<String, Void, JSONObject> {
+        @Override
+        protected JSONObject doInBackground(String... params) {
+            try {
+                URL url = new URL(OAUTH_USERINFO_URL + "?access_token=" + params[0]);
+                return new JSONObject(CharStreams.toString(
+                        new InputStreamReader(url.openConnection().getInputStream(),
+                                Charsets.US_ASCII)));
+            } catch (MalformedURLException e) {
+                Log.e(TAG, "Error fetching user's profile info" + e.getMessage());
+            } catch (JSONException e) {
+                Log.e(TAG, "Error fetching user's profile info" + e.getMessage());
+            } catch (IOException e) {
+                Log.e(TAG, "Error fetching user's profile info" + e.getMessage());
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(JSONObject userProfile) {
+            fetchUserProfileDone(userProfile);
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // Inflate the menu; this adds items to the action bar if it is present.
+        getMenuInflater().inflate(R.menu.menu_sign_in, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle action bar item clicks here. The action bar will
+        // automatically handle clicks on the Home/Up button, so long
+        // as you specify a parent activity in AndroidManifest.xml.
+        int id = item.getItemId();
+
+        //noinspection SimplifiableIfStatement
+        if (id == R.id.action_settings) {
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/android/app/src/main/java/io/v/syncslides/V23.java b/android/app/src/main/java/io/v/syncslides/V23.java
new file mode 100644
index 0000000..1a8ab37
--- /dev/null
+++ b/android/app/src/main/java/io/v/syncslides/V23.java
@@ -0,0 +1,127 @@
+// 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;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import io.v.android.libs.security.BlessingsManager;
+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.v23.context.VContext;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.Blessings;
+import io.v.v23.security.VPrincipal;
+import io.v.v23.security.VSecurity;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+
+/**
+ * Handles Vanadium initialization.
+ */
+public class V23 {
+
+    private static final String TAG = "V23";
+    private static final int BLESSING_REQUEST = 201;
+    private Context mContext;
+    private VContext mVContext;
+    private Blessings mBlessings = null;
+
+    public static class Singleton {
+        private static volatile V23 instance;
+
+        public static V23 get() {
+            V23 result = instance;
+            if (instance == null) {
+                synchronized (Singleton.class) {
+                    result = instance;
+                    if (result == null) {
+                        instance = result = new V23();
+                    }
+                }
+            }
+            return result;
+        }
+    }
+
+    // Singleton.
+    private V23() {
+    }
+
+    public void init(Context context, Activity activity) {
+        if (mBlessings != null) {
+            return;
+        }
+        mContext = context;
+        mVContext = V.init(mContext);
+        Blessings blessings = loadBlessings();
+        if (blessings == null) {
+            // Get the signed-in user's email to generate the blessings from.
+            String userEmail = SignInActivity.getUserEmail(mContext);
+            activity.startActivityForResult(
+                    BlessingService.newBlessingIntent(mContext, userEmail), BLESSING_REQUEST);
+            return;
+        }
+        configurePrincipal(blessings);
+    }
+
+    /**
+     * 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 boolean onActivityResult(
+            Context context, int requestCode, int resultCode, Intent data) {
+        if (requestCode != BLESSING_REQUEST) {
+            return false;
+        }
+        try {
+            Log.d(TAG, "unpacking blessing");
+            byte[] blessingsVom = BlessingService.extractBlessingReply(resultCode, data);
+            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);
+        } catch (VException e) {
+            throw new IllegalStateException(e);
+        }
+        return true;
+    }
+
+    private Blessings loadBlessings() {
+        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());
+        }
+        return null;
+    }
+
+    private void configurePrincipal(Blessings blessings) {
+        try {
+            VPrincipal p = V.getPrincipal(mVContext);
+            p.blessingStore().setDefaultBlessings(blessings);
+            p.blessingStore().set(blessings, new BlessingPattern("..."));
+            VSecurity.addToRoots(p, blessings);
+            mBlessings = blessings;
+        } catch (VException e) {
+            Log.e(TAG, String.format(
+                    "Couldn't set local blessing %s: %s", blessings, e.getMessage()));
+        }
+    }
+
+
+}
diff --git a/android/app/src/main/res/layout/activity_sign_in.xml b/android/app/src/main/res/layout/activity_sign_in.xml
new file mode 100644
index 0000000..b3a7afd
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_sign_in.xml
@@ -0,0 +1,11 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:paddingBottom="@dimen/activity_vertical_margin"
+                android:paddingLeft="@dimen/activity_horizontal_margin"
+                android:paddingRight="@dimen/activity_horizontal_margin"
+                android:paddingTop="@dimen/activity_vertical_margin"
+                tools:context="io.v.syncslides.SignInActivity">
+
+</RelativeLayout>
diff --git a/android/app/src/main/res/menu/menu_sign_in.xml b/android/app/src/main/res/menu/menu_sign_in.xml
new file mode 100644
index 0000000..6543eac
--- /dev/null
+++ b/android/app/src/main/res/menu/menu_sign_in.xml
@@ -0,0 +1,10 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto"
+      xmlns:tools="http://schemas.android.com/tools"
+      tools:context="io.v.syncslides.SignInActivity">
+    <item
+        android:id="@+id/action_settings"
+        android:orderInCategory="100"
+        android:title="@string/action_settings"
+        app:showAsAction="never"/>
+</menu>
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 8ac7d35..619268c 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,4 +1,4 @@
 <resources>
-    <string name="app_name">SyncSlides</string>
+    <string name="app_name">DevSyncSlides</string>
     <string name="action_settings">Settings</string>
 </resources>