Merge "syncslides: PresentationActivity - get deck from DB"
diff --git a/projects/syncslides/app/build.gradle b/projects/syncslides/app/build.gradle
index 81966f8..c8c0063 100644
--- a/projects/syncslides/app/build.gradle
+++ b/projects/syncslides/app/build.gradle
@@ -48,7 +48,7 @@
 
     defaultConfig {
         applicationId "io.v.android.apps.syncslides"
-        minSdkVersion 21
+        minSdkVersion 22
         targetSdkVersion 23
         versionCode 1
         versionName "1.0"
diff --git a/projects/syncslides/app/src/main/AndroidManifest.xml b/projects/syncslides/app/src/main/AndroidManifest.xml
index 1719535..6030afe 100644
--- a/projects/syncslides/app/src/main/AndroidManifest.xml
+++ b/projects/syncslides/app/src/main/AndroidManifest.xml
@@ -1,29 +1,37 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest package="io.v.android.apps.syncslides"
-          xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.v.android.apps.syncslides" >
+
+    <uses-sdk android:minSdkVersion="22" />
+
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
 
     <application
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
-        android:theme="@style/AppTheme">
+        android:theme="@style/AppTheme" >
         <activity
-            android:name=".DeckChooserActivity"
-            android:label="@string/app_name">
+            android:name=".SignInActivity"
+            android:label="@string/app_name" >
             <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-
-                <category android:name="android.intent.category.LAUNCHER"/>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        <activity android:name=".PresentationActivity"/>
+        <activity
+            android:name=".DeckChooserActivity"
+            android:label="@string/app_name" >
+        </activity>
+        <activity android:name=".PresentationActivity" />
         <service
             android:name=".discovery.ParticipantPeer"
             android:exported="false"
             android:label="Location Service"
-            android:process=":ParticipantPeer">
+            android:process=":ParticipantPeer" >
         </service>
     </application>
 
-    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
 </manifest>
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java
index 61fc9b4..2f5d437 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/NavigationDrawerFragment.java
@@ -15,6 +15,7 @@
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -24,6 +25,10 @@
 import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.ListView;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
 
 /**
  * Fragment used for managing interactions for and presentation of a navigation drawer.
@@ -31,6 +36,7 @@
  * design guidelines</a> for a complete explanation of the behaviors implemented here.
  */
 public class NavigationDrawerFragment extends Fragment {
+    private static final String TAG = "NavigationDrawer";
 
     /**
      * Remember the position of the selected item.
@@ -56,6 +62,7 @@
     private DrawerLayout mDrawerLayout;
     private ListView mDrawerListView;
     private View mFragmentContainerView;
+    private JSONObject mUserProfile;
 
     private int mCurrentSelectedPosition = 0;
     private boolean mFromSavedInstanceState;
@@ -70,8 +77,14 @@
 
         // Read in the flag indicating whether or not the user has demonstrated awareness of the
         // drawer. See PREF_USER_LEARNED_DRAWER for details.
-        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
-        mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false);
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
+        mUserLearnedDrawer = prefs.getBoolean(PREF_USER_LEARNED_DRAWER, false);
+        String userProfileJsonStr = prefs.getString(SignInActivity.PREF_USER_PROFILE_JSON, "");
+        try {
+            mUserProfile = new JSONObject(userProfileJsonStr);
+        } catch (JSONException e) {
+            Log.e(TAG, "Couldn't parse user profile data: " + userProfileJsonStr);
+        }
 
         if (savedInstanceState != null) {
             mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION);
@@ -100,15 +113,33 @@
                 selectItem(position);
             }
         });
-        mDrawerListView.setAdapter(new ArrayAdapter<String>(
+        mDrawerListView.setAdapter(new ArrayAdapter<JSONObject>(
                 getActionBar().getThemedContext(),
-                android.R.layout.simple_list_item_activated_1,
+                android.R.layout.simple_list_item_activated_2,
                 android.R.id.text1,
-                new String[]{
-                        getString(R.string.title_account1),
-                        getString(R.string.title_account2),
-                        getString(R.string.title_account3),
-                }));
+                new JSONObject[]{mUserProfile}) {
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                JSONObject userProfile = getItem(position);
+                String name = "";
+                String email = "";
+                if (userProfile != null) {
+                    try {
+                        name = userProfile.getString("name");
+                        email = userProfile.getString("email");
+                    } catch (JSONException e) {
+                        Log.e(TAG, "Error reading from user profile: " + e.getMessage());
+                    }
+                }
+                if (name.isEmpty() && email.isEmpty()) {
+                    name = "USER1";
+                }
+                View view = super.getView(position, convertView, parent);
+                ((TextView) view.findViewById(android.R.id.text1)).setText(name);
+                ((TextView) view.findViewById(android.R.id.text2)).setText(email);
+                return view;
+            }
+        });
         mDrawerListView.setItemChecked(mCurrentSelectedPosition, true);
         return mDrawerListView;
     }
@@ -269,7 +300,7 @@
     /**
      * Callbacks interface that all activities using this fragment must implement.
      */
-    public static interface NavigationDrawerCallbacks {
+    public interface NavigationDrawerCallbacks {
         /**
          * Called when an item in the navigation drawer is selected.
          */
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java
new file mode 100644
index 0000000..3d225bc
--- /dev/null
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/SignInActivity.java
@@ -0,0 +1,234 @@
+// 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.android.apps.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.os.AsyncTask;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.PreferenceManager;
+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";
+
+    public static final String PREF_USER_ACCOUNT_NAME = "user_account";
+    public 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;
+
+    @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();
+
+        fetchUserProfile();
+    }
+
+    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());
+            editor.commit();
+        }
+
+        finishActivity();
+    }
+
+
+    private void finishActivity() {
+        mProgressDialog.dismiss();
+        startActivity(new Intent(this, DeckChooserActivity.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/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java
index b046769..32341af 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/db/SyncbaseDB.java
@@ -317,8 +317,8 @@
         private final DeckFactory mDeckFactory;
         private final Handler mHandler;
         private ResumeMarker mWatchMarker;
-        private volatile boolean mIsDiscarded;
-        private volatile Listener mListener;
+        private boolean mIsDiscarded;
+        private Listener mListener;
         private List<Deck> mDecks;
 
         public DeckList(VContext vContext, Database db, DeckFactory df) {
@@ -435,18 +435,20 @@
         }
 
         @Override
-        public synchronized void discard() {
+        public void discard() {
             Log.i(TAG, "Discarding deck list.");
+            mIsDiscarded = true;
             mVContext.cancel();  // this will cause the watcher thread to exit
             mHandler.removeCallbacksAndMessages(null);
-            // We've canceled all the pending callbacks, but the handler might be just about
-            // to execute put()/get() and those messages wouldn't get canceled.  So we mark
-            // the list as discarded and count on put()/get() checking for it. (Note that
-            // put()/get() are synchronized along with this method.)
-            mIsDiscarded = true;
         }
 
-        private synchronized void put(Deck deck) {
+        private void put(Deck deck) {
+            // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+            // been called), because that method doesn't prevent future post()s being made on
+            // the handler.  So the following scenario is possible:
+            //    - fetcher thread is about to execute post().
+            //    - discard clears all pending messages from the handler.
+            //    - fetcher executes the post().
             if (mIsDiscarded) {
                 return;
             }
@@ -473,7 +475,13 @@
             }
         }
 
-        private synchronized void delete(String deckId) {
+        private void delete(String deckId) {
+            // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+            // been called), because that method doesn't prevent future post()s being made on
+            // the handler.  So the following scenario is possible:
+            //    - fetcher thread is about to execute post().
+            //    - discard clears all pending messages from the handler.
+            //    - fetcher executes the post().
             if (mIsDiscarded) {
                 return;
             }
@@ -548,7 +556,7 @@
         private final Handler mHandler;
         private final String mDeckId;
         private ResumeMarker mWatchMarker;
-        private volatile boolean mIsDiscarded;
+        private boolean mIsDiscarded;
         // Storage for slides, mirroring the slides in the Syncbase.  Since slide numbers can
         // have "holes" in them (e.g., 1, 2, 4, 6, 8), we maintain a map from slide key
         // to the slide, as well as an ordered list which is returned to the caller.
@@ -680,18 +688,20 @@
         }
 
         @Override
-        public synchronized void discard() {
+        public void discard() {
             Log.i(TAG, "Discarding slides list");
+            mIsDiscarded = true;
             mVContext.cancel();  // this will cause the watcher thread to exit
             mHandler.removeCallbacksAndMessages(null);
-            // We've canceled all the pending callbacks, but the handler might be just about
-            // to execute put()/get() and those messages wouldn't get canceled.  So we mark
-            // the list as discarded and count on put()/get() checking for it. (Note that
-            // put()/get() are synchronized along with this method.)
-            mIsDiscarded = true;
         }
 
-        private synchronized void put(String key, Slide slide) {
+        private void put(String key, Slide slide) {
+            // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+            // been called), because that method doesn't prevent future post()s being made on
+            // the handler.  So the following scenario is possible:
+            //    - fetcher thread is about to execute post().
+            //    - discard clears all pending messages from the handler.
+            //    - fetcher executes the post().
             if (mIsDiscarded) {
                 return;
             }
@@ -711,7 +721,13 @@
             }
         }
 
-        private synchronized void delete(String key) {
+        private void delete(String key) {
+            // We need to check for mIsDiscarded (even though removeCallbacksAndMessages() has
+            // been called), because that method doesn't prevent future post()s being made on
+            // the handler.  So the following scenario is possible:
+            //    - fetcher thread is about to execute post().
+            //    - discard clears all pending messages from the handler.
+            //    - fetcher executes the post().
             if (mIsDiscarded) {
                 return;
             }
diff --git a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
index 5bc8a95..734d79e 100644
--- a/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
+++ b/projects/syncslides/app/src/main/java/io/v/android/apps/syncslides/misc/V23Manager.java
@@ -7,6 +7,8 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
 import android.util.Log;
 import android.widget.Toast;
 
@@ -20,6 +22,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+import io.v.android.apps.syncslides.SignInActivity;
 import io.v.android.libs.security.BlessingsManager;
 import io.v.android.v23.V;
 import io.v.android.v23.services.blessing.BlessingCreationException;
@@ -172,8 +175,11 @@
                 throw new IllegalArgumentException(
                         "Cannot get blessings without an activity to return to.");
             }
+            // Get the signed-in user's email to generate the blessings from.
+            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(androidCtx);
+            String userEmail = prefs.getString(SignInActivity.PREF_USER_ACCOUNT_NAME, "");
             activity.startActivityForResult(
-                    BlessingService.newBlessingIntent(androidCtx),
+                    BlessingService.newBlessingIntent(androidCtx, userEmail),
                     BLESSING_REQUEST);
             return;
         }
diff --git a/projects/syncslides/app/src/main/res/layout/activity_sign_in.xml b/projects/syncslides/app/src/main/res/layout/activity_sign_in.xml
new file mode 100644
index 0000000..7c93107
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/layout/activity_sign_in.xml
@@ -0,0 +1,9 @@
+<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:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    tools:context="io.v.android.apps.syncslides.SignInActivity">
+
+</RelativeLayout>
diff --git a/projects/syncslides/app/src/main/res/menu/menu_sign_in.xml b/projects/syncslides/app/src/main/res/menu/menu_sign_in.xml
new file mode 100644
index 0000000..813fc46
--- /dev/null
+++ b/projects/syncslides/app/src/main/res/menu/menu_sign_in.xml
@@ -0,0 +1,7 @@
+<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.android.apps.syncslides.SignInActivity">
+    <item android:id="@+id/action_settings" android:title="@string/action_settings"
+        android:orderInCategory="100" app:showAsAction="never" />
+</menu>
diff --git a/projects/syncslides/app/src/main/res/values/strings.xml b/projects/syncslides/app/src/main/res/values/strings.xml
index 7ffb985..48013cd 100644
--- a/projects/syncslides/app/src/main/res/values/strings.xml
+++ b/projects/syncslides/app/src/main/res/values/strings.xml
@@ -22,5 +22,4 @@
     <string name="handoff_message">You handed off to</string>
     <string name="end_handoff">RESUME</string>
     <string name="presentation_live">LIVE NOW</string>
-
 </resources>