GoogleAccountLogin.java revision 71b1713b9706b068e38202ac1ed7d87c7badfae4
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 extends Thread implements 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 81 private GoogleAccountLogin(Activity activity, String name, 82 Runnable runnable) { 83 mActivity = activity; 84 mAccount = new Account(name, GOOGLE); 85 mWebView = new WebView(mActivity); 86 mRunnable = runnable; 87 88 mWebView.setWebViewClient(new WebViewClient() { 89 @Override 90 public boolean shouldOverrideUrlLoading(WebView view, String url) { 91 return false; 92 } 93 @Override 94 public void onPageFinished(WebView view, String url) { 95 saveLoginTime(); 96 done(); 97 } 98 }); 99 } 100 101 private void saveLoginTime() { 102 Editor ed = PreferenceManager. 103 getDefaultSharedPreferences(mActivity).edit(); 104 ed.putLong(PREF_AUTOLOGIN_TIME, System.currentTimeMillis()); 105 ed.apply(); 106 } 107 108 // Thread 109 @Override 110 public void run() { 111 String url = ISSUE_AUTH_TOKEN_URL.buildUpon() 112 .appendQueryParameter("SID", mSid) 113 .appendQueryParameter("LSID", mLsid) 114 .build().toString(); 115 // Check mRunnable to see if the request has been canceled. Otherwise 116 // we might access a destroyed WebView. 117 String ua = null; 118 synchronized (this) { 119 if (mRunnable == null) { 120 return; 121 } 122 ua = mWebView.getSettings().getUserAgentString(); 123 } 124 // Intentionally not using Proxy. 125 AndroidHttpClient client = AndroidHttpClient.newInstance(ua); 126 HttpPost request = new HttpPost(url); 127 128 String result = null; 129 try { 130 HttpResponse response = client.execute(request); 131 if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { 132 Log.d(LOGTAG, "LOGIN_FAIL: Bad status from auth url " 133 + response.getStatusLine().getStatusCode() + ": " 134 + response.getStatusLine().getReasonPhrase()); 135 done(); 136 return; 137 } 138 HttpEntity entity = response.getEntity(); 139 if (entity == null) { 140 Log.d(LOGTAG, "LOGIN_FAIL: Null entity in response"); 141 done(); 142 return; 143 } 144 result = EntityUtils.toString(entity, "UTF-8"); 145 } catch (Exception e) { 146 Log.d(LOGTAG, "LOGIN_FAIL: Exception acquiring uber token " + e); 147 request.abort(); 148 done(); 149 return; 150 } finally { 151 client.close(); 152 } 153 final String newUrl = TOKEN_AUTH_URL.buildUpon() 154 .appendQueryParameter("source", "android-browser") 155 .appendQueryParameter("auth", result) 156 .appendQueryParameter("continue", 157 BrowserSettings.getFactoryResetHomeUrl(mActivity)) 158 .build().toString(); 159 mActivity.runOnUiThread(new Runnable() { 160 @Override public void run() { 161 // Check mRunnable in case the request has been canceled. This 162 // is most likely not necessary as run() is the only non-UI 163 // thread that calls done() but I am paranoid. 164 synchronized (GoogleAccountLogin.this) { 165 if (mRunnable == null) { 166 return; 167 } 168 mWebView.loadUrl(newUrl); 169 } 170 } 171 }); 172 } 173 174 // AccountManager callbacks. 175 @Override 176 public void run(AccountManagerFuture<Bundle> value) { 177 try { 178 String id = value.getResult().getString( 179 AccountManager.KEY_AUTHTOKEN); 180 switch (mState) { 181 default: 182 case 0: 183 throw new IllegalStateException( 184 "Impossible to get into this state"); 185 case 1: 186 mSid = id; 187 mState = 2; // LSID 188 AccountManager.get(mActivity).getAuthToken( 189 mAccount, "LSID", null, mActivity, this, null); 190 break; 191 case 2: 192 mLsid = id; 193 this.start(); 194 break; 195 } 196 } catch (Exception e) { 197 Log.d(LOGTAG, "LOGIN_FAIL: Exception in state " + mState + " " + e); 198 // For all exceptions load the original signin page. 199 // TODO: toast login failed? 200 done(); 201 } 202 } 203 204 // Start the login process if auto-login is enabled and the user is not 205 // already logged in. 206 public static void startLoginIfNeeded(Activity activity, 207 BrowserSettings settings, Runnable runnable) { 208 // Auto login not enabled? 209 if (!settings.isAutoLoginEnabled()) { 210 runnable.run(); 211 return; 212 } 213 214 // No account found? 215 String account = settings.getAutoLoginAccount(activity); 216 if (account == null) { 217 runnable.run(); 218 return; 219 } 220 221 // Already logged in? 222 if (isLoggedIn(activity)) { 223 runnable.run(); 224 return; 225 } 226 227 GoogleAccountLogin login = 228 new GoogleAccountLogin(activity, account, runnable); 229 login.startLogin(); 230 } 231 232 private void startLogin() { 233 mProgressDialog = ProgressDialog.show(mActivity, 234 mActivity.getString(R.string.pref_autologin_title), 235 mActivity.getString(R.string.pref_autologin_progress, 236 mAccount.name), 237 true /* indeterminate */, 238 true /* cancelable */, 239 this); 240 mState = 1; // SID 241 AccountManager.get(mActivity).getAuthToken( 242 mAccount, "SID", null, mActivity, this, null); 243 } 244 245 // Returns the account name passed in if the account exists, otherwise 246 // returns the default account. 247 public static String validateAccount(Context ctx, String name) { 248 Account[] accounts = getAccounts(ctx); 249 if (accounts.length == 0) { 250 return null; 251 } 252 if (name != null) { 253 // Make sure the account still exists. 254 for (Account a : accounts) { 255 if (a.name.equals(name)) { 256 return name; 257 } 258 } 259 } 260 // Return the first entry. 261 return accounts[0].name; 262 } 263 264 public static Account[] getAccounts(Context ctx) { 265 return AccountManager.get(ctx).getAccountsByType(GOOGLE); 266 } 267 268 // Checks for the presence of the SID cookie on google.com. 269 public static boolean isLoggedIn(Context ctx) { 270 // See if we last logged in less than a week ago. 271 long lastLogin = PreferenceManager. 272 getDefaultSharedPreferences(ctx). 273 getLong(PREF_AUTOLOGIN_TIME, -1); 274 if (lastLogin == -1) { 275 return false; 276 } 277 long diff = System.currentTimeMillis() - lastLogin; 278 if (diff > WEEK_IN_MILLIS) { 279 Log.d(LOGTAG, "Forcing login after " + diff + "ms"); 280 return false; 281 } 282 283 // Use /a/ to grab hosted cookies as well as the base set of google.com 284 // cookies. 285 String cookies = CookieManager.getInstance().getCookie( 286 "http://www.google.com/a/"); 287 if (cookies != null) { 288 StringTokenizer tokenizer = new StringTokenizer(cookies, ";"); 289 while (tokenizer.hasMoreTokens()) { 290 String cookie = tokenizer.nextToken().trim(); 291 if (cookie.startsWith("SID=") || cookie.startsWith("ASIDAP=")) { 292 return true; 293 } 294 } 295 } 296 return false; 297 } 298 299 // Used to indicate that the Browser should continue loading the main page. 300 // This can happen on success, error, or timeout. 301 private synchronized void done() { 302 if (mRunnable != null) { 303 Log.d(LOGTAG, "Finished login attempt for " + mAccount.name); 304 mActivity.runOnUiThread(mRunnable); 305 306 // Post a delayed message to dismiss the dialog in order to avoid a 307 // flash of the progress dialog. 308 mHandler.postDelayed(new Runnable() { 309 @Override public void run() { 310 mProgressDialog.dismiss(); 311 } 312 }, 1000); 313 314 mRunnable = null; 315 mWebView.destroy(); 316 } 317 } 318 319 // Called by the progress dialog on startup. 320 public void onCancel(DialogInterface unused) { 321 done(); 322 } 323} 324