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