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