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();
+        }
+    }
+}