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