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