GoogleAccountLogin.java revision cf1df73719e171915eed887a5f06916158e42f04
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.browser; 18 19import android.accounts.Account; 20import android.accounts.AccountManager; 21import android.accounts.AccountManagerCallback; 22import android.accounts.AccountManagerFuture; 23import android.app.Activity; 24import android.app.ProgressDialog; 25import android.content.Context; 26import android.content.DialogInterface; 27import android.content.DialogInterface.OnCancelListener; 28import android.content.SharedPreferences.Editor; 29import android.net.Uri; 30import android.net.http.AndroidHttpClient; 31import android.os.Bundle; 32import android.util.Log; 33import android.webkit.CookieSyncManager; 34import android.webkit.WebView; 35import android.webkit.WebViewClient; 36 37import org.apache.http.HttpEntity; 38import org.apache.http.HttpResponse; 39import org.apache.http.HttpStatus; 40import org.apache.http.client.methods.HttpPost; 41import org.apache.http.util.EntityUtils; 42 43public class GoogleAccountLogin implements Runnable, 44 AccountManagerCallback<Bundle>, OnCancelListener { 45 46 private static final String LOGTAG = "BrowserLogin"; 47 48 // Url for issuing the uber token. 49 private Uri ISSUE_AUTH_TOKEN_URL = Uri.parse( 50 "https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false"); 51 // Url for signing into a particular service. 52 private static final Uri TOKEN_AUTH_URL = Uri.parse( 53 "https://www.google.com/accounts/TokenAuth"); 54 // Google account type 55 private static final String GOOGLE = "com.google"; 56 // Last auto login time 57 public static final String PREF_AUTOLOGIN_TIME = "last_autologin_time"; 58 59 private final Activity mActivity; 60 private final Account mAccount; 61 private final WebView mWebView; 62 private Runnable mRunnable; 63 private ProgressDialog mProgressDialog; 64 65 // SID and LSID retrieval process. 66 private String mSid; 67 private String mLsid; 68 private int mState; // {NONE(0), SID(1), LSID(2)} 69 private boolean mTokensInvalidated; 70 71 private GoogleAccountLogin(Activity activity, Account account, 72 Runnable runnable) { 73 mActivity = activity; 74 mAccount = account; 75 mWebView = new WebView(mActivity); 76 mRunnable = runnable; 77 78 // XXX: Doing pre-login causes onResume to skip calling 79 // resumeWebViewTimers. So to avoid problems with timers not running, we 80 // duplicate the work here using the off-screen WebView. 81 CookieSyncManager.getInstance().startSync(); 82 WebViewTimersControl.getInstance().onBrowserActivityResume(mWebView); 83 84 mWebView.setWebViewClient(new WebViewClient() { 85 @Override 86 public boolean shouldOverrideUrlLoading(WebView view, String url) { 87 return false; 88 } 89 @Override 90 public void onPageFinished(WebView view, String url) { 91 done(); 92 } 93 }); 94 } 95 96 private void saveLoginTime() { 97 Editor ed = BrowserSettings.getInstance().getPreferences().edit(); 98 ed.putLong(PREF_AUTOLOGIN_TIME, System.currentTimeMillis()); 99 ed.apply(); 100 } 101 102 // Runnable 103 @Override 104 public void run() { 105 String url = ISSUE_AUTH_TOKEN_URL.buildUpon() 106 .appendQueryParameter("SID", mSid) 107 .appendQueryParameter("LSID", mLsid) 108 .build().toString(); 109 // Check mRunnable to see if the request has been canceled. Otherwise 110 // we might access a destroyed WebView. 111 String ua = null; 112 synchronized (this) { 113 if (mRunnable == null) { 114 return; 115 } 116 ua = mWebView.getSettings().getUserAgentString(); 117 } 118 // Intentionally not using Proxy. 119 AndroidHttpClient client = AndroidHttpClient.newInstance(ua); 120 HttpPost request = new HttpPost(url); 121 122 String result = null; 123 try { 124 HttpResponse response = client.execute(request); 125 int status = response.getStatusLine().getStatusCode(); 126 if (status != HttpStatus.SC_OK) { 127 Log.d(LOGTAG, "LOGIN_FAIL: Bad status from auth url " 128 + status + ": " 129 + response.getStatusLine().getReasonPhrase()); 130 // Invalidate the tokens once just in case the 403 was for other 131 // reasons. 132 if (status == HttpStatus.SC_FORBIDDEN && !mTokensInvalidated) { 133 Log.d(LOGTAG, "LOGIN_FAIL: Invalidating tokens..."); 134 // Need to regenerate the auth tokens and try again. 135 invalidateTokens(); 136 // XXX: Do not touch any more member variables from this 137 // thread as a second thread will handle the next login 138 // attempt. 139 return; 140 } 141 done(); 142 return; 143 } 144 HttpEntity entity = response.getEntity(); 145 if (entity == null) { 146 Log.d(LOGTAG, "LOGIN_FAIL: Null entity in response"); 147 done(); 148 return; 149 } 150 result = EntityUtils.toString(entity, "UTF-8"); 151 } catch (Exception e) { 152 Log.d(LOGTAG, "LOGIN_FAIL: Exception acquiring uber token " + e); 153 request.abort(); 154 done(); 155 return; 156 } finally { 157 client.close(); 158 } 159 final String newUrl = TOKEN_AUTH_URL.buildUpon() 160 .appendQueryParameter("source", "android-browser") 161 .appendQueryParameter("auth", result) 162 .appendQueryParameter("continue", 163 BrowserSettings.getFactoryResetHomeUrl(mActivity)) 164 .build().toString(); 165 mActivity.runOnUiThread(new Runnable() { 166 @Override public void run() { 167 // Check mRunnable in case the request has been canceled. This 168 // is most likely not necessary as run() is the only non-UI 169 // thread that calls done() but I am paranoid. 170 synchronized (GoogleAccountLogin.this) { 171 if (mRunnable == null) { 172 return; 173 } 174 mWebView.loadUrl(newUrl); 175 } 176 } 177 }); 178 } 179 180 private void invalidateTokens() { 181 AccountManager am = AccountManager.get(mActivity); 182 am.invalidateAuthToken(GOOGLE, mSid); 183 am.invalidateAuthToken(GOOGLE, mLsid); 184 mTokensInvalidated = true; 185 mState = 1; // SID 186 am.getAuthToken(mAccount, "SID", null, mActivity, this, null); 187 } 188 189 // AccountManager callbacks. 190 @Override 191 public void run(AccountManagerFuture<Bundle> value) { 192 try { 193 String id = value.getResult().getString( 194 AccountManager.KEY_AUTHTOKEN); 195 switch (mState) { 196 default: 197 case 0: 198 throw new IllegalStateException( 199 "Impossible to get into this state"); 200 case 1: 201 mSid = id; 202 mState = 2; // LSID 203 AccountManager.get(mActivity).getAuthToken( 204 mAccount, "LSID", null, mActivity, this, null); 205 break; 206 case 2: 207 mLsid = id; 208 new Thread(this).start(); 209 break; 210 } 211 } catch (Exception e) { 212 Log.d(LOGTAG, "LOGIN_FAIL: Exception in state " + mState + " " + e); 213 // For all exceptions load the original signin page. 214 // TODO: toast login failed? 215 done(); 216 } 217 } 218 219 // Start the login process if auto-login is enabled and the user is not 220 // already logged in. 221 public static void startLoginIfNeeded(Activity activity, 222 Runnable runnable) { 223 // Already logged in? 224 if (isLoggedIn()) { 225 runnable.run(); 226 return; 227 } 228 229 // No account found? 230 Account[] accounts = getAccounts(activity); 231 if (accounts == null || accounts.length == 0) { 232 runnable.run(); 233 return; 234 } 235 236 GoogleAccountLogin login = 237 new GoogleAccountLogin(activity, accounts[0], runnable); 238 login.startLogin(); 239 } 240 241 private void startLogin() { 242 saveLoginTime(); 243 mProgressDialog = ProgressDialog.show(mActivity, 244 mActivity.getString(R.string.pref_autologin_title), 245 mActivity.getString(R.string.pref_autologin_progress, 246 mAccount.name), 247 true /* indeterminate */, 248 true /* cancelable */, 249 this); 250 mState = 1; // SID 251 AccountManager.get(mActivity).getAuthToken( 252 mAccount, "SID", null, mActivity, this, null); 253 } 254 255 private static Account[] getAccounts(Context ctx) { 256 return AccountManager.get(ctx).getAccountsByType(GOOGLE); 257 } 258 259 // Checks if we already did pre-login. 260 private static boolean isLoggedIn() { 261 // See if we last logged in less than a week ago. 262 long lastLogin = BrowserSettings.getInstance().getPreferences() 263 .getLong(PREF_AUTOLOGIN_TIME, -1); 264 if (lastLogin == -1) { 265 return false; 266 } 267 return true; 268 } 269 270 // Used to indicate that the Browser should continue loading the main page. 271 // This can happen on success, error, or timeout. 272 private synchronized void done() { 273 if (mRunnable != null) { 274 Log.d(LOGTAG, "Finished login attempt for " + mAccount.name); 275 mActivity.runOnUiThread(mRunnable); 276 277 try { 278 mProgressDialog.dismiss(); 279 } catch (Exception e) { 280 // TODO: Switch to a managed dialog solution (DialogFragment?) 281 // Also refactor this class, it doesn't 282 // play nice with the activity lifecycle, leading to issues 283 // with the dialog it manages 284 Log.w(LOGTAG, "Failed to dismiss mProgressDialog: " + e.getMessage()); 285 } 286 mRunnable = null; 287 mActivity.runOnUiThread(new Runnable() { 288 @Override 289 public void run() { 290 mWebView.destroy(); 291 } 292 }); 293 } 294 } 295 296 // Called by the progress dialog on startup. 297 public void onCancel(DialogInterface unused) { 298 done(); 299 } 300 301} 302