GoogleAccountLogin.java revision 3a2cf8007db4f3258b94fdcb74c147220350aa36
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.os.Bundle; 31import android.util.Log; 32import android.webkit.CookieSyncManager; 33import android.webkit.WebView; 34import android.webkit.WebViewClient; 35 36import java.io.IOException; 37import java.io.InputStream; 38import java.net.HttpURLConnection; 39import java.net.Proxy; 40import java.net.URL; 41import java.nio.charset.Charset; 42import java.nio.charset.IllegalCharsetNameException; 43import java.nio.charset.UnsupportedCharsetException; 44import libcore.io.Streams; 45import libcore.net.http.ResponseUtils; 46 47public class GoogleAccountLogin implements Runnable, 48 AccountManagerCallback<Bundle>, OnCancelListener { 49 50 private static final String LOGTAG = "BrowserLogin"; 51 52 // Url for issuing the uber token. 53 private Uri ISSUE_AUTH_TOKEN_URL = Uri.parse( 54 "https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false"); 55 // Url for signing into a particular service. 56 private static final Uri TOKEN_AUTH_URL = Uri.parse( 57 "https://www.google.com/accounts/TokenAuth"); 58 // Google account type 59 private static final String GOOGLE = "com.google"; 60 // Last auto login time 61 public static final String PREF_AUTOLOGIN_TIME = "last_autologin_time"; 62 63 private final Activity mActivity; 64 private final Account mAccount; 65 private final WebView mWebView; 66 private Runnable mRunnable; 67 private ProgressDialog mProgressDialog; 68 69 // SID and LSID retrieval process. 70 private String mSid; 71 private String mLsid; 72 private int mState; // {NONE(0), SID(1), LSID(2)} 73 private boolean mTokensInvalidated; 74 private String mUserAgent; 75 76 private GoogleAccountLogin(Activity activity, Account account, 77 Runnable runnable) { 78 mActivity = activity; 79 mAccount = account; 80 mWebView = new WebView(mActivity); 81 mRunnable = runnable; 82 mUserAgent = mWebView.getSettings().getUserAgentString(); 83 84 // XXX: Doing pre-login causes onResume to skip calling 85 // resumeWebViewTimers. So to avoid problems with timers not running, we 86 // duplicate the work here using the off-screen WebView. 87 CookieSyncManager.getInstance().startSync(); 88 WebViewTimersControl.getInstance().onBrowserActivityResume(mWebView); 89 90 mWebView.setWebViewClient(new WebViewClient() { 91 @Override 92 public boolean shouldOverrideUrlLoading(WebView view, String url) { 93 return false; 94 } 95 @Override 96 public void onPageFinished(WebView view, String url) { 97 done(); 98 } 99 }); 100 } 101 102 private void saveLoginTime() { 103 Editor ed = BrowserSettings.getInstance().getPreferences().edit(); 104 ed.putLong(PREF_AUTOLOGIN_TIME, System.currentTimeMillis()); 105 ed.apply(); 106 } 107 108 // Runnable 109 @Override 110 public void run() { 111 String urlString = ISSUE_AUTH_TOKEN_URL.buildUpon() 112 .appendQueryParameter("SID", mSid) 113 .appendQueryParameter("LSID", mLsid) 114 .build().toString(); 115 116 HttpURLConnection connection = null; 117 String authToken = null; 118 try { 119 URL url = new URL(urlString); 120 // Intentionally not using Proxy. 121 connection = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY); 122 connection.setRequestMethod("POST"); 123 connection.setRequestProperty("User-Agent", mUserAgent); 124 125 int status = connection.getResponseCode(); 126 if (status != HttpURLConnection.HTTP_OK) { 127 Log.d(LOGTAG, "LOGIN_FAIL: Bad status from auth url " 128 + status + ": " + connection.getResponseMessage()); 129 // Invalidate the tokens once just in case the 403 was for other 130 // reasons. 131 if (status == HttpURLConnection.HTTP_FORBIDDEN && !mTokensInvalidated) { 132 Log.d(LOGTAG, "LOGIN_FAIL: Invalidating tokens..."); 133 // Need to regenerate the auth tokens and try again. 134 invalidateTokens(); 135 // XXX: Do not touch any more member variables from this 136 // thread as a second thread will handle the next login 137 // attempt. 138 return; 139 } 140 done(); 141 return; 142 } 143 144 final Charset responseCharset = ResponseUtils.responseCharset( 145 connection.getContentType()); 146 byte[] responseBytes = Streams.readFully(connection.getInputStream()); 147 authToken = new String(responseBytes, responseCharset); 148 } catch (Exception e) { 149 Log.d(LOGTAG, "LOGIN_FAIL: Exception acquiring uber token " + e); 150 done(); 151 return; 152 } finally { 153 if (connection != null) { 154 connection.disconnect(); 155 } 156 } 157 158 final String newUrl = TOKEN_AUTH_URL.buildUpon() 159 .appendQueryParameter("source", "android-browser") 160 .appendQueryParameter("auth", authToken) 161 .appendQueryParameter("continue", 162 BrowserSettings.getFactoryResetHomeUrl(mActivity)) 163 .build().toString(); 164 mActivity.runOnUiThread(new Runnable() { 165 @Override public void run() { 166 // Check mRunnable in case the request has been canceled. This 167 // is most likely not necessary as run() is the only non-UI 168 // thread that calls done() but I am paranoid. 169 synchronized (GoogleAccountLogin.this) { 170 if (mRunnable == null) { 171 return; 172 } 173 mWebView.loadUrl(newUrl); 174 } 175 } 176 }); 177 } 178 179 private void invalidateTokens() { 180 AccountManager am = AccountManager.get(mActivity); 181 am.invalidateAuthToken(GOOGLE, mSid); 182 am.invalidateAuthToken(GOOGLE, mLsid); 183 mTokensInvalidated = true; 184 mState = 1; // SID 185 am.getAuthToken(mAccount, "SID", null, mActivity, this, null); 186 } 187 188 // AccountManager callbacks. 189 @Override 190 public void run(AccountManagerFuture<Bundle> value) { 191 try { 192 String id = value.getResult().getString( 193 AccountManager.KEY_AUTHTOKEN); 194 switch (mState) { 195 default: 196 case 0: 197 throw new IllegalStateException( 198 "Impossible to get into this state"); 199 case 1: 200 mSid = id; 201 mState = 2; // LSID 202 AccountManager.get(mActivity).getAuthToken( 203 mAccount, "LSID", null, mActivity, this, null); 204 break; 205 case 2: 206 mLsid = id; 207 new Thread(this).start(); 208 break; 209 } 210 } catch (Exception e) { 211 Log.d(LOGTAG, "LOGIN_FAIL: Exception in state " + mState + " " + e); 212 // For all exceptions load the original signin page. 213 // TODO: toast login failed? 214 done(); 215 } 216 } 217 218 // Start the login process if auto-login is enabled and the user is not 219 // already logged in. 220 public static void startLoginIfNeeded(Activity activity, 221 Runnable runnable) { 222 // Already logged in? 223 if (isLoggedIn()) { 224 runnable.run(); 225 return; 226 } 227 228 // No account found? 229 Account[] accounts = getAccounts(activity); 230 if (accounts == null || accounts.length == 0) { 231 runnable.run(); 232 return; 233 } 234 235 GoogleAccountLogin login = 236 new GoogleAccountLogin(activity, accounts[0], runnable); 237 login.startLogin(); 238 } 239 240 private void startLogin() { 241 saveLoginTime(); 242 mProgressDialog = ProgressDialog.show(mActivity, 243 mActivity.getString(R.string.pref_autologin_title), 244 mActivity.getString(R.string.pref_autologin_progress, 245 mAccount.name), 246 true /* indeterminate */, 247 true /* cancelable */, 248 this); 249 mState = 1; // SID 250 AccountManager.get(mActivity).getAuthToken( 251 mAccount, "SID", null, mActivity, this, null); 252 } 253 254 private static Account[] getAccounts(Context ctx) { 255 return AccountManager.get(ctx).getAccountsByType(GOOGLE); 256 } 257 258 // Checks if we already did pre-login. 259 private static boolean isLoggedIn() { 260 // See if we last logged in less than a week ago. 261 long lastLogin = BrowserSettings.getInstance().getPreferences() 262 .getLong(PREF_AUTOLOGIN_TIME, -1); 263 if (lastLogin == -1) { 264 return false; 265 } 266 return true; 267 } 268 269 // Used to indicate that the Browser should continue loading the main page. 270 // This can happen on success, error, or timeout. 271 private synchronized void done() { 272 if (mRunnable != null) { 273 Log.d(LOGTAG, "Finished login attempt for " + mAccount.name); 274 mActivity.runOnUiThread(mRunnable); 275 276 try { 277 mProgressDialog.dismiss(); 278 } catch (Exception e) { 279 // TODO: Switch to a managed dialog solution (DialogFragment?) 280 // Also refactor this class, it doesn't 281 // play nice with the activity lifecycle, leading to issues 282 // with the dialog it manages 283 Log.w(LOGTAG, "Failed to dismiss mProgressDialog: " + e.getMessage()); 284 } 285 mRunnable = null; 286 mActivity.runOnUiThread(new Runnable() { 287 @Override 288 public void run() { 289 mWebView.destroy(); 290 } 291 }); 292 } 293 } 294 295 // Called by the progress dialog on startup. 296 public void onCancel(DialogInterface unused) { 297 done(); 298 } 299 300} 301