// Copyright (c) 2013 The Chromium 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 org.chromium; import java.io.IOException; import org.apache.cordova.CordovaArgs; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.accounts.AccountManager; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.util.Log; import com.google.android.gms.auth.GoogleAuthException; import com.google.android.gms.auth.GoogleAuthUtil; import com.google.android.gms.auth.GooglePlayServicesAvailabilityException; import com.google.android.gms.auth.UserRecoverableAuthException; import com.google.android.gms.common.AccountPicker; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; public class ChromeIdentity extends CordovaPlugin { private static final String LOG_TAG = "ChromeIdentity"; // These are just unique request codes. They can be anything as long as they don't clash. private static final int AUTH_REQUEST_CODE = 5; private static final int ACCOUNT_CHOOSER_INTENT = 6; private static final int OAUTH_PERMISSIONS_GRANT_INTENT = 7; // Error codes. private static final int GOOGLE_PLAY_SERVICES_UNAVAILABLE = -1; private String accountName = ""; private CordovaArgs savedCordovaArgs; private CallbackContext savedCallbackContext; private boolean savedContent = false; private class TokenDetails { private boolean interactive; } @Override public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException { if ("getAuthToken".equals(action)) { getAuthToken(args, callbackContext); return true; } else if ("removeCachedAuthToken".equals(action)) { removeCachedAuthToken(args, callbackContext); return true; } return false; } private String getScopesString(CordovaArgs args) throws IOException, JSONException { JSONArray scopes = args.getJSONObject(1).getJSONArray("scopes"); StringBuilder ret = new StringBuilder("oauth2:"); for (int i = 0; i < scopes.length(); i++) { if (i != 0) { ret.append(" "); } ret.append(scopes.getString(i)); } return ret.toString(); } private TokenDetails getTokenDetailsFromArgs(CordovaArgs args) throws JSONException { TokenDetails tokenDetails = new TokenDetails(); tokenDetails.interactive = args.getBoolean(0); return tokenDetails; } private boolean haveAccount() { return !(accountName.isEmpty()); } private void launchAccountChooserAndCallback(CordovaArgs cordovaArgsToSave, CallbackContext callbackContextToSave) { // Check if Google Play Services is available. int availabilityCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this.cordova.getActivity()); if (availabilityCode == ConnectionResult.SUCCESS) { this.savedCordovaArgs = cordovaArgsToSave; this.savedCallbackContext = callbackContextToSave; this.savedContent = true; // The "google.com" filter accepts both Google and Gmail accounts. Intent intent = AccountPicker.newChooseAccountIntent(null, null, new String[]{"com.google"}, false, null, null, null, null); this.cordova.startActivityForResult(this, intent, ACCOUNT_CHOOSER_INTENT); } else { callbackContextToSave.error(GOOGLE_PLAY_SERVICES_UNAVAILABLE); } } private void launchPermissionsGrantPageAndCallback(Intent permissionsIntent, CordovaArgs cordovaArgsToSave, CallbackContext callbackContextToSave) { this.savedCallbackContext = callbackContextToSave; this.savedCordovaArgs = cordovaArgsToSave; this.savedContent = true; this.cordova.startActivityForResult(this, permissionsIntent, OAUTH_PERMISSIONS_GRANT_INTENT); } @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { // Enter only if we have requests waiting if(savedContent) { if(requestCode == ACCOUNT_CHOOSER_INTENT) { if(resultCode == Activity.RESULT_OK && intent.hasExtra(AccountManager.KEY_ACCOUNT_NAME)) { accountName = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); getAuthToken(this.savedCordovaArgs, this.savedCallbackContext); } else { this.savedCallbackContext.error("User declined to provide an account"); } this.savedContent = false; this.savedCallbackContext = null; this.savedCordovaArgs = null; } else if(requestCode == OAUTH_PERMISSIONS_GRANT_INTENT) { cordova.getThreadPool().execute(new Runnable() { @Override public void run() { if (resultCode == Activity.RESULT_OK) { String token = null; if (intent.hasExtra("authtoken")) { token = intent.getStringExtra("authtoken"); } else { try { token = GoogleAuthUtil.getToken(cordova.getActivity(), intent.getExtras().getString("authAccount"), intent.getExtras().getString("service")); } catch (UserRecoverableAuthException e) { e.printStackTrace(); savedCallbackContext.error("Auth Error: " + e.getMessage()); return; } catch (IOException e) { e.printStackTrace(); savedCallbackContext.error("Auth Error: " + e.getMessage()); return; } catch (GoogleAuthException e) { e.printStackTrace(); savedCallbackContext.error("Auth Error: " + e.getMessage()); return; } } if (token == null) { savedCallbackContext.error("Unknown auth error."); } else { getAuthTokenCallback(token, savedCallbackContext); } } else { savedCallbackContext.error("User did not approve oAuth permissions request"); } savedContent = false; savedCallbackContext = null; savedCordovaArgs = null; } }); } } } private void getAuthToken(final CordovaArgs args, final CallbackContext callbackContext) { this.cordova.getThreadPool().execute(new Runnable() { public void run() { if(!haveAccount()) { String accountHint = null; try { accountHint = args.getString(2); } catch (JSONException e) { } if (accountHint != null) { accountName = accountHint; getAuthTokenWithAccount(accountName, args, callbackContext); } else { launchAccountChooserAndCallback(args, callbackContext); } } else { getAuthTokenWithAccount(accountName, args, callbackContext); } } }); } private void getAuthTokenWithAccount(String account, CordovaArgs args, CallbackContext callbackContext) { String token = ""; String scope = ""; Context context = null; boolean done = true; TokenDetails tokenDetails = null; try { tokenDetails = getTokenDetailsFromArgs(args); scope = getScopesString(args); context = this.cordova.getActivity(); token = GoogleAuthUtil.getToken(context, account, scope); } catch (GooglePlayServicesAvailabilityException playEx) { // Play is not available if (tokenDetails.interactive) { Activity myActivity = this.cordova.getActivity(); Dialog dialog = GooglePlayServicesUtil.getErrorDialog(playEx.getConnectionStatusCode(), myActivity , AUTH_REQUEST_CODE); dialog.show(); } else { Log.e(LOG_TAG, "Google Play Services is not available", playEx); } } catch (UserRecoverableAuthException recoverableException) { // OAuth Permissions for the app during first run if(tokenDetails.interactive) { Intent permissionsIntent = recoverableException.getIntent(); launchPermissionsGrantPageAndCallback(permissionsIntent, args, callbackContext); // If the user allows it then we need ask for the token again and pass the token to the success callback done = false; } else { Log.e(LOG_TAG, "Recoverable Error occured while getting token. No action was taken as interactive is set to false", recoverableException); } } catch(Exception e) { Log.e(LOG_TAG, "Error occured while getting token", e); } if(done) { getAuthTokenCallback(token, callbackContext); } } private void getAuthTokenCallback(String token, CallbackContext callbackContext) { if(token.trim().equals("")) { callbackContext.error("Could not get auth token"); } else { try { JSONObject jsonObject = new JSONObject(); jsonObject.put("account", accountName); jsonObject.put("token", token); callbackContext.success(jsonObject); } catch (JSONException e) { } } } private void removeCachedAuthToken(final CordovaArgs args, final CallbackContext callbackContext) { this.cordova.getThreadPool().execute(new Runnable() { public void run() { invalidateToken(args, callbackContext); } }); } private void invalidateToken(CordovaArgs args, CallbackContext callbackContext) { try { String token = args.getString(0); Context context = this.cordova.getActivity(); GoogleAuthUtil.invalidateToken(context, token); callbackContext.success(); } catch (SecurityException e) { // This happens when trying to clear a token that doesn't exist. callbackContext.success(); } catch (JSONException e) { callbackContext.error("Could not invalidate token due to JSONException."); } } }