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>