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