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