java/syncbase: Add LoginFragment for Android login
Android login is easier than ever with this wrapper!
The LoginFragment will attach a fragment to the given activity. Then,
- It uses the AccountManager to ask the user to pick an account.
- Once selected, it retrieves the corresponding OAuth token.
- Once that is retrieved, it calls login with the corresponding
callback calls.
Change-Id: I00383b60628e085ff702f76d8342003c3f6b8148
diff --git a/syncbase/src/main/AndroidManifest.xml b/syncbase/src/main/AndroidManifest.xml
index 36846f6..4cf99ec 100644
--- a/syncbase/src/main/AndroidManifest.xml
+++ b/syncbase/src/main/AndroidManifest.xml
@@ -1,3 +1,9 @@
-<manifest package="io.v.syncbase">
-
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="io.v.v23.android" >
+ <!-- Request the USE_CREDENTIALS permission for Android login if targeting version <= 22.
+ See: https://developer.android.com/reference/android/accounts/AccountManager.html -->
+ <uses-permission
+ android:name="android.permission.USE_CREDENTIALS"
+ android:maxSdkVersion="22" />
</manifest>
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncbase.java b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
index 7af2289..ca33935 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncbase.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
@@ -4,6 +4,9 @@
package io.v.syncbase;
+import android.app.Activity;
+import android.app.FragmentTransaction;
+
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -14,7 +17,9 @@
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
+import java.util.UUID;
+import io.v.syncbase.android.LoginFragment;
import io.v.syncbase.core.NeighborhoodPeer;
import io.v.syncbase.core.Permissions;
import io.v.syncbase.core.Service;
@@ -163,6 +168,27 @@
}
/**
+ * Logs in the user on Android.
+ * The user selects an account through an account picker flow and is logged into Syncbase.
+ * Note: This default account flow is currently restricted to Google accounts.
+ *
+ * @param activity The Android activity where login will occur.
+ * @param cb The callback to call when the login was done.
+ */
+ public static void loginAndroid(Activity activity, final LoginCallback cb) {
+ FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
+ LoginFragment fragment = new LoginFragment();
+ fragment.setTokenReceiver(new LoginFragment.TokenReceiver() {
+ @Override
+ public void receiveToken(String token) {
+ Syncbase.login(token, User.PROVIDER_GOOGLE, cb);
+ }
+ });
+ transaction.add(fragment, UUID.randomUUID().toString());
+ transaction.commit(); // This will invoke the fragment's onCreate() immediately.
+ }
+
+ /**
* Logs in the user associated with the given OAuth token and provider and starts Syncbase;
* creates default database if needed; performs create-or-join for "userdata" syncgroup if
* needed. The passed callback is called on the current thread.
diff --git a/syncbase/src/main/java/io/v/syncbase/android/LoginFragment.java b/syncbase/src/main/java/io/v/syncbase/android/LoginFragment.java
new file mode 100644
index 0000000..f327b10
--- /dev/null
+++ b/syncbase/src/main/java/io/v/syncbase/android/LoginFragment.java
@@ -0,0 +1,126 @@
+// Copyright 2016 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.syncbase.android;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+public class LoginFragment extends Fragment {
+ private static final String TAG = "LoginFragment";
+
+ private static final int REQUEST_CODE_PICK_ACCOUNT = 1000;
+ private static final int REQUEST_CODE_USER_APPROVAL = 1001;
+ private static final String OAUTH_SCOPE =
+ "oauth2:https://www.googleapis.com/auth/userinfo.email";
+ private static final String ACCOUNT_TYPE = "com.google";
+
+ private String mGoogleAccount = "";
+ private TokenReceiver tokenReceiver;
+
+ public LoginFragment() {}
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Note: newChooseAccountIntent is deprecated, but the API level we're targeting is < 23,
+ // which is when the replacement is introduced. If we do upgrade to API v23, just remove the
+ // "false" parameter.
+ Intent accountIntent = AccountManager.newChooseAccountIntent(
+ null, null, new String[]{ACCOUNT_TYPE}, false, null, null, null, null);
+ startActivityForResult(accountIntent, REQUEST_CODE_PICK_ACCOUNT);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CODE_PICK_ACCOUNT:
+ if (resultCode != Activity.RESULT_OK) {
+ Log.e(TAG, "User didn't pick account: " + resultCode);
+ return;
+ }
+ mGoogleAccount = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
+ Log.i(TAG, "Account name: " + mGoogleAccount);
+ getAccessToken();
+ break;
+ case REQUEST_CODE_USER_APPROVAL:
+ if (resultCode != Activity.RESULT_OK) {
+ Log.e(TAG, "User didn't give proposed permissions: " + resultCode);
+ return;
+ }
+ getAccessToken();
+ break;
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ // Call this immediately after fragment creation.
+ public void setTokenReceiver(TokenReceiver receiver) {
+ tokenReceiver = receiver;
+ }
+
+ public interface TokenReceiver {
+ void receiveToken(String token);
+ }
+
+ private void getAccessToken() {
+ Account account = new Account(mGoogleAccount, ACCOUNT_TYPE);
+ AccountManager.get(this.getActivity()).getAuthToken(
+ account,
+ OAUTH_SCOPE,
+ new Bundle(),
+ false,
+ new OnTokenAcquired(this),
+ new Handler(new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ Log.e(TAG, "Error getting auth token: " + msg.toString());
+ return true;
+ }
+ }));
+ }
+
+ private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
+ private final Fragment fragment;
+
+ OnTokenAcquired(Fragment fragment) {
+ this.fragment = fragment;
+ }
+
+ @Override
+ public void run(AccountManagerFuture<Bundle> result) {
+ try {
+ Bundle bundle = result.getResult();
+ Intent launch = (Intent) bundle.get(AccountManager.KEY_INTENT);
+ if (launch != null) {
+ launch.setFlags(0);
+ startActivityForResult(launch, REQUEST_CODE_USER_APPROVAL);
+ return;
+ }
+ String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
+ Log.i(TAG, "token: " + token);
+ tokenReceiver.receiveToken(token);
+ } catch (Exception e) {
+ Log.e(TAG, "onTokenAcquired errored", e);
+ }
+
+ // The token has been acquired (or failed). Remove the fragment.
+ FragmentTransaction transaction = getActivity().getFragmentManager().beginTransaction();
+ transaction.remove(fragment);
+ transaction.commit();
+ }
+ }
+}