GoogleAccountLogin.java revision ef18de60e02b4b2a7227d9e9751487cc74baec36
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 org.apache.http.Header; 20import org.apache.http.HeaderIterator; 21import org.apache.http.HttpEntity; 22import org.apache.http.HttpResponse; 23import org.apache.http.HttpStatus; 24import org.apache.http.client.methods.HttpPost; 25import org.apache.http.util.EntityUtils; 26 27import android.accounts.Account; 28import android.accounts.AccountManager; 29import android.accounts.AccountManagerCallback; 30import android.accounts.AccountManagerFuture; 31import android.app.Activity; 32import android.app.ProgressDialog; 33import android.content.Context; 34import android.content.DialogInterface; 35import android.content.DialogInterface.OnCancelListener; 36import android.content.SharedPreferences.Editor; 37import android.net.http.AndroidHttpClient; 38import android.net.Uri; 39import android.os.Bundle; 40import android.os.Handler; 41import android.preference.PreferenceManager; 42import android.util.Log; 43import android.webkit.CookieManager; 44import android.webkit.CookieSyncManager; 45import android.webkit.WebView; 46import android.webkit.WebViewClient; 47 48import java.util.StringTokenizer; 49 50public class GoogleAccountLogin implements Runnable, 51 AccountManagerCallback<Bundle>, OnCancelListener { 52 53 private static final String LOGTAG = "BrowserLogin"; 54 55 // Url for issuing the uber token. 56 private Uri ISSUE_AUTH_TOKEN_URL = Uri.parse( 57 "https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false"); 58 // Url for signing into a particular service. 59 private static final Uri TOKEN_AUTH_URL = Uri.parse( 60 "https://www.google.com/accounts/TokenAuth"); 61 // Google account type 62 private static final String GOOGLE = "com.google"; 63 // Last auto login time 64 private static final String PREF_AUTOLOGIN_TIME = "last_autologin_time"; 65 // A week in milliseconds (7*24*60*60*1000) 66 private static final long WEEK_IN_MILLIS = 604800000L; 67 68 private final Activity mActivity; 69 private final Account mAccount; 70 private final WebView mWebView; 71 // Does not matter if this is initialized in a non-ui thread. 72 // Dialog.dismiss() will post to the right handler. 73 private final Handler mHandler = new Handler(); 74 private Runnable mRunnable; 75 private ProgressDialog mProgressDialog; 76 77 // SID and LSID retrieval process. 78 private String mSid; 79 private String mLsid; 80 private int mState; // {NONE(0), SID(1), LSID(2)} 81 private boolean mTokensInvalidated; 82 83 private GoogleAccountLogin(Activity activity, String name, 84 Runnable runnable) { 85 mActivity = activity; 86 mAccount = new Account(name, GOOGLE); 87 mWebView = new WebView(mActivity); 88 mRunnable = runnable; 89 90 // XXX: Doing pre-login causes onResume to skip calling 91 // resumeWebViewTimers. So to avoid problems with timers not running, we 92 // duplicate the work here using the off-screen WebView. 93 CookieSyncManager.getInstance().startSync(); 94 mWebView.resumeTimers(); 95 96 mWebView.setWebViewClient(new WebViewClient() { 97 @Override 98 public boolean shouldOverrideUrlLoading(WebView view, String url) { 99 return false; 100 } 101 @Override 102 public void onPageFinished(WebView view, String url) { 103 saveLoginTime(); 104 done(); 105 } 106 }); 107 } 108 109 private void saveLoginTime() { 110 Editor ed = PreferenceManager. 111 getDefaultSharedPreferences(mActivity).edit(); 112 ed.putLong(PREF_AUTOLOGIN_TIME, System.currentTimeMillis()); 113 ed.apply(); 114 } 115 116 // Runnable 117 @Override 118 public void run() { 119 String url = ISSUE_AUTH_TOKEN_URL.buildUpon() 120 .appendQueryParameter("SID", mSid) 121 .appendQueryParameter("LSID", mLsid) 122 .build().toString(); 123 // Check mRunnable to see if the request has been canceled. Otherwise 124 // we might access a destroyed WebView. 125 String ua = null; 126 synchronized (this) { 127 if (mRunnable == null) { 128 return; 129 } 130 ua = mWebView.getSettings().getUserAgentString(); 131 } 132 // Intentionally not using Proxy. 133 AndroidHttpClient client = AndroidHttpClient.newInstance(ua); 134 HttpPost request = new HttpPost(url); 135 136 String result = null; 137 try { 138 HttpResponse response = client.execute(request); 139 int status = response.getStatusLine().getStatusCode(); 140 if (status != HttpStatus.SC_OK) { 141 Log.d(LOGTAG, "LOGIN_FAIL: Bad status from auth url " 142 + status + ": " 143 + response.getStatusLine().getReasonPhrase()); 144 // Invalidate the tokens once just in case the 403 was for other 145 // reasons. 146 if (status == HttpStatus.SC_FORBIDDEN && !mTokensInvalidated) { 147 Log.d(LOGTAG, "LOGIN_FAIL: Invalidating tokens..."); 148 // Need to regenerate the auth tokens and try again. 149 invalidateTokens(); 150 // XXX: Do not touch any more member variables from this 151 // thread as a second thread will handle the next login 152 // attempt. 153 return; 154 } 155 done(); 156 return; 157 } 158 HttpEntity entity = response.getEntity(); 159 if (entity == null) { 160 Log.d(LOGTAG, "LOGIN_FAIL: Null entity in response"); 161 done(); 162 return; 163 } 164 result = EntityUtils.toString(entity, "UTF-8"); 165 } catch (Exception e) { 166 Log.d(LOGTAG, "LOGIN_FAIL: Exception acquiring uber token " + e); 167 request.abort(); 168 done(); 169 return; 170 } finally { 171 client.close(); 172 } 173 final String newUrl = TOKEN_AUTH_URL.buildUpon() 174 .appendQueryParameter("source", "android-browser") 175 .appendQueryParameter("auth", result) 176 .appendQueryParameter("continue", 177 BrowserSettings.getFactoryResetHomeUrl(mActivity)) 178 .build().toString(); 179 mActivity.runOnUiThread(new Runnable() { 180 @Override public void run() { 181 // Check mRunnable in case the request has been canceled. This 182 // is most likely not necessary as run() is the only non-UI 183 // thread that calls done() but I am paranoid. 184 synchronized (GoogleAccountLogin.this) { 185 if (mRunnable == null) { 186 return; 187 } 188 mWebView.loadUrl(newUrl); 189 } 190 } 191 }); 192 } 193 194 private void invalidateTokens() { 195 AccountManager am = AccountManager.get(mActivity); 196 am.invalidateAuthToken(GOOGLE, mSid); 197 am.invalidateAuthToken(GOOGLE, mLsid); 198 mTokensInvalidated = true; 199 mState = 1; // SID 200 am.getAuthToken(mAccount, "SID", null, mActivity, this, null); 201 } 202 203 // AccountManager callbacks. 204 @Override 205 public void run(AccountManagerFuture<Bundle> value) { 206 try { 207 String id = value.getResult().getString( 208 AccountManager.KEY_AUTHTOKEN); 209 switch (mState) { 210 default: 211 case 0: 212 throw new IllegalStateException( 213 "Impossible to get into this state"); 214 case 1: 215 mSid = id; 216 mState = 2; // LSID 217 AccountManager.get(mActivity).getAuthToken( 218 mAccount, "LSID", null, mActivity, this, null); 219 break; 220 case 2: 221 mLsid = id; 222 new Thread(this).start(); 223 break; 224 } 225 } catch (Exception e) { 226 Log.d(LOGTAG, "LOGIN_FAIL: Exception in state " + mState + " " + e); 227 // For all exceptions load the original signin page. 228 // TODO: toast login failed? 229 done(); 230 } 231 } 232 233 // Start the login process if auto-login is enabled and the user is not 234 // already logged in. 235 public static void startLoginIfNeeded(Activity activity, 236 BrowserSettings settings, Runnable runnable) { 237 // Auto login not enabled? 238 if (!settings.isAutoLoginEnabled()) { 239 runnable.run(); 240 return; 241 } 242 243 // No account found? 244 String account = settings.getAutoLoginAccount(activity); 245 if (account == null) { 246 runnable.run(); 247 return; 248 } 249 250 // Already logged in? 251 if (isLoggedIn(activity)) { 252 runnable.run(); 253 return; 254 } 255 256 GoogleAccountLogin login = 257 new GoogleAccountLogin(activity, account, runnable); 258 login.startLogin(); 259 } 260 261 private void startLogin() { 262 mProgressDialog = ProgressDialog.show(mActivity, 263 mActivity.getString(R.string.pref_autologin_title), 264 mActivity.getString(R.string.pref_autologin_progress, 265 mAccount.name), 266 true /* indeterminate */, 267 true /* cancelable */, 268 this); 269 mState = 1; // SID 270 AccountManager.get(mActivity).getAuthToken( 271 mAccount, "SID", null, mActivity, this, null); 272 } 273 274 // Returns the account name passed in if the account exists, otherwise 275 // returns the default account. 276 public static String validateAccount(Context ctx, String name) { 277 Account[] accounts = getAccounts(ctx); 278 if (accounts.length == 0) { 279 return null; 280 } 281 if (name != null) { 282 // Make sure the account still exists. 283 for (Account a : accounts) { 284 if (a.name.equals(name)) { 285 return name; 286 } 287 } 288 } 289 // Return the first entry. 290 return accounts[0].name; 291 } 292 293 public static Account[] getAccounts(Context ctx) { 294 return AccountManager.get(ctx).getAccountsByType(GOOGLE); 295 } 296 297 // Checks for the presence of the SID cookie on google.com. 298 public static boolean isLoggedIn(Context ctx) { 299 // See if we last logged in less than a week ago. 300 long lastLogin = PreferenceManager. 301 getDefaultSharedPreferences(ctx). 302 getLong(PREF_AUTOLOGIN_TIME, -1); 303 if (lastLogin == -1) { 304 return false; 305 } 306 long diff = System.currentTimeMillis() - lastLogin; 307 if (diff > WEEK_IN_MILLIS) { 308 Log.d(LOGTAG, "Forcing login after " + diff + "ms"); 309 return false; 310 } 311 312 // This will potentially block the UI thread but we have to have the 313 // most updated cookies. 314 // FIXME: Figure out how to avoid waiting to clear session cookies. 315 CookieManager.getInstance().waitForCookieOperationsToComplete(); 316 317 // Use /a/ to grab hosted cookies as well as the base set of google.com 318 // cookies. 319 String cookies = CookieManager.getInstance().getCookie( 320 "http://www.google.com/a/"); 321 if (cookies != null) { 322 StringTokenizer tokenizer = new StringTokenizer(cookies, ";"); 323 while (tokenizer.hasMoreTokens()) { 324 String cookie = tokenizer.nextToken().trim(); 325 if (cookie.startsWith("SID=") || cookie.startsWith("ASIDAP=")) { 326 return true; 327 } 328 } 329 } 330 return false; 331 } 332 333 // Used to indicate that the Browser should continue loading the main page. 334 // This can happen on success, error, or timeout. 335 private synchronized void done() { 336 if (mRunnable != null) { 337 Log.d(LOGTAG, "Finished login attempt for " + mAccount.name); 338 mActivity.runOnUiThread(mRunnable); 339 340 // Post a delayed message to dismiss the dialog in order to avoid a 341 // flash of the progress dialog. 342 mHandler.postDelayed(new Runnable() { 343 @Override public void run() { 344 mProgressDialog.dismiss(); 345 } 346 }, 2000); 347 348 mRunnable = null; 349 mWebView.destroy(); 350 } 351 } 352 353 // Called by the progress dialog on startup. 354 public void onCancel(DialogInterface unused) { 355 done(); 356 } 357} 358