/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.browser; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.SharedPreferences.Editor; import android.net.Uri; import android.net.http.AndroidHttpClient; import android.os.Bundle; import android.util.Log; import android.webkit.CookieSyncManager; import android.webkit.WebView; import android.webkit.WebViewClient; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpPost; import org.apache.http.util.EntityUtils; public class GoogleAccountLogin implements Runnable, AccountManagerCallback<Bundle>, OnCancelListener { private static final String LOGTAG = "BrowserLogin"; // Url for issuing the uber token. private Uri ISSUE_AUTH_TOKEN_URL = Uri.parse( "https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false"); // Url for signing into a particular service. private static final Uri TOKEN_AUTH_URL = Uri.parse( "https://www.google.com/accounts/TokenAuth"); // Google account type private static final String GOOGLE = "com.google"; // Last auto login time public static final String PREF_AUTOLOGIN_TIME = "last_autologin_time"; private final Activity mActivity; private final Account mAccount; private final WebView mWebView; private Runnable mRunnable; private ProgressDialog mProgressDialog; // SID and LSID retrieval process. private String mSid; private String mLsid; private int mState; // {NONE(0), SID(1), LSID(2)} private boolean mTokensInvalidated; private String mUserAgent; private GoogleAccountLogin(Activity activity, Account account, Runnable runnable) { mActivity = activity; mAccount = account; mWebView = new WebView(mActivity); mRunnable = runnable; mUserAgent = mWebView.getSettings().getUserAgentString(); // XXX: Doing pre-login causes onResume to skip calling // resumeWebViewTimers. So to avoid problems with timers not running, we // duplicate the work here using the off-screen WebView. CookieSyncManager.getInstance().startSync(); WebViewTimersControl.getInstance().onBrowserActivityResume(mWebView); mWebView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return false; } @Override public void onPageFinished(WebView view, String url) { done(); } }); } private void saveLoginTime() { Editor ed = BrowserSettings.getInstance().getPreferences().edit(); ed.putLong(PREF_AUTOLOGIN_TIME, System.currentTimeMillis()); ed.apply(); } // Runnable @Override public void run() { String url = ISSUE_AUTH_TOKEN_URL.buildUpon() .appendQueryParameter("SID", mSid) .appendQueryParameter("LSID", mLsid) .build().toString(); // Intentionally not using Proxy. AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent); HttpPost request = new HttpPost(url); String result = null; try { HttpResponse response = client.execute(request); int status = response.getStatusLine().getStatusCode(); if (status != HttpStatus.SC_OK) { Log.d(LOGTAG, "LOGIN_FAIL: Bad status from auth url " + status + ": " + response.getStatusLine().getReasonPhrase()); // Invalidate the tokens once just in case the 403 was for other // reasons. if (status == HttpStatus.SC_FORBIDDEN && !mTokensInvalidated) { Log.d(LOGTAG, "LOGIN_FAIL: Invalidating tokens..."); // Need to regenerate the auth tokens and try again. invalidateTokens(); // XXX: Do not touch any more member variables from this // thread as a second thread will handle the next login // attempt. return; } done(); return; } HttpEntity entity = response.getEntity(); if (entity == null) { Log.d(LOGTAG, "LOGIN_FAIL: Null entity in response"); done(); return; } result = EntityUtils.toString(entity, "UTF-8"); } catch (Exception e) { Log.d(LOGTAG, "LOGIN_FAIL: Exception acquiring uber token " + e); request.abort(); done(); return; } finally { client.close(); } final String newUrl = TOKEN_AUTH_URL.buildUpon() .appendQueryParameter("source", "android-browser") .appendQueryParameter("auth", result) .appendQueryParameter("continue", BrowserSettings.getFactoryResetHomeUrl(mActivity)) .build().toString(); mActivity.runOnUiThread(new Runnable() { @Override public void run() { // Check mRunnable in case the request has been canceled. This // is most likely not necessary as run() is the only non-UI // thread that calls done() but I am paranoid. synchronized (GoogleAccountLogin.this) { if (mRunnable == null) { return; } mWebView.loadUrl(newUrl); } } }); } private void invalidateTokens() { AccountManager am = AccountManager.get(mActivity); am.invalidateAuthToken(GOOGLE, mSid); am.invalidateAuthToken(GOOGLE, mLsid); mTokensInvalidated = true; mState = 1; // SID am.getAuthToken(mAccount, "SID", null, mActivity, this, null); } // AccountManager callbacks. @Override public void run(AccountManagerFuture<Bundle> value) { try { String id = value.getResult().getString( AccountManager.KEY_AUTHTOKEN); switch (mState) { default: case 0: throw new IllegalStateException( "Impossible to get into this state"); case 1: mSid = id; mState = 2; // LSID AccountManager.get(mActivity).getAuthToken( mAccount, "LSID", null, mActivity, this, null); break; case 2: mLsid = id; new Thread(this).start(); break; } } catch (Exception e) { Log.d(LOGTAG, "LOGIN_FAIL: Exception in state " + mState + " " + e); // For all exceptions load the original signin page. // TODO: toast login failed? done(); } } // Start the login process if auto-login is enabled and the user is not // already logged in. public static void startLoginIfNeeded(Activity activity, Runnable runnable) { // Already logged in? if (isLoggedIn()) { runnable.run(); return; } // No account found? Account[] accounts = getAccounts(activity); if (accounts == null || accounts.length == 0) { runnable.run(); return; } GoogleAccountLogin login = new GoogleAccountLogin(activity, accounts[0], runnable); login.startLogin(); } private void startLogin() { saveLoginTime(); mProgressDialog = ProgressDialog.show(mActivity, mActivity.getString(R.string.pref_autologin_title), mActivity.getString(R.string.pref_autologin_progress, mAccount.name), true /* indeterminate */, true /* cancelable */, this); mState = 1; // SID AccountManager.get(mActivity).getAuthToken( mAccount, "SID", null, mActivity, this, null); } private static Account[] getAccounts(Context ctx) { return AccountManager.get(ctx).getAccountsByType(GOOGLE); } // Checks if we already did pre-login. private static boolean isLoggedIn() { // See if we last logged in less than a week ago. long lastLogin = BrowserSettings.getInstance().getPreferences() .getLong(PREF_AUTOLOGIN_TIME, -1); if (lastLogin == -1) { return false; } return true; } // Used to indicate that the Browser should continue loading the main page. // This can happen on success, error, or timeout. private synchronized void done() { if (mRunnable != null) { Log.d(LOGTAG, "Finished login attempt for " + mAccount.name); mActivity.runOnUiThread(mRunnable); try { mProgressDialog.dismiss(); } catch (Exception e) { // TODO: Switch to a managed dialog solution (DialogFragment?) // Also refactor this class, it doesn't // play nice with the activity lifecycle, leading to issues // with the dialog it manages Log.w(LOGTAG, "Failed to dismiss mProgressDialog: " + e.getMessage()); } mRunnable = null; mActivity.runOnUiThread(new Runnable() { @Override public void run() { mWebView.destroy(); } }); } } // Called by the progress dialog on startup. public void onCancel(DialogInterface unused) { done(); } }