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