blob: 93b10a6bf1e11cc0b5897c7834ffa15753459ff9 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.browser;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.webkit.CookieSyncManager;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import libcore.io.Streams;
import libcore.net.http.ResponseUtils;
public class GoogleAccountLogin implements Runnable,
AccountManagerCallback<Bundle>, OnCancelListener {
private static final String LOGTAG = "BrowserLogin";
// Url for issuing the uber token.
private Uri ISSUE_AUTH_TOKEN_URL = Uri.parse(
"https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false");
// Url for signing into a particular service.
private static final Uri TOKEN_AUTH_URL = Uri.parse(
"https://www.google.com/accounts/TokenAuth");
// Google account type
private static final String GOOGLE = "com.google";
// Last auto login time
public static final String PREF_AUTOLOGIN_TIME = "last_autologin_time";
private final Activity mActivity;
private final Account mAccount;
private final WebView mWebView;
private Runnable mRunnable;
private ProgressDialog mProgressDialog;
// SID and LSID retrieval process.
private String mSid;
private String mLsid;
private int mState; // {NONE(0), SID(1), LSID(2)}
private boolean mTokensInvalidated;
private String mUserAgent;
private GoogleAccountLogin(Activity activity, Account account,
Runnable runnable) {
mActivity = activity;
mAccount = account;
mWebView = new WebView(mActivity);
mRunnable = runnable;
mUserAgent = mWebView.getSettings().getUserAgentString();
// XXX: Doing pre-login causes onResume to skip calling
// resumeWebViewTimers. So to avoid problems with timers not running, we
// duplicate the work here using the off-screen WebView.
CookieSyncManager.getInstance().startSync();
WebViewTimersControl.getInstance().onBrowserActivityResume(mWebView);
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
@Override
public void onPageFinished(WebView view, String url) {
done();
}
});
}
private void saveLoginTime() {
Editor ed = BrowserSettings.getInstance().getPreferences().edit();
ed.putLong(PREF_AUTOLOGIN_TIME, System.currentTimeMillis());
ed.apply();
}
// Runnable
@Override
public void run() {
String urlString = ISSUE_AUTH_TOKEN_URL.buildUpon()
.appendQueryParameter("SID", mSid)
.appendQueryParameter("LSID", mLsid)
.build().toString();
HttpURLConnection connection = null;
String authToken = null;
try {
URL url = new URL(urlString);
// Intentionally not using Proxy.
connection = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
connection.setRequestMethod("POST");
connection.setRequestProperty("User-Agent", mUserAgent);
int status = connection.getResponseCode();
if (status != HttpURLConnection.HTTP_OK) {
Log.d(LOGTAG, "LOGIN_FAIL: Bad status from auth url "
+ status + ": " + connection.getResponseMessage());
// Invalidate the tokens once just in case the 403 was for other
// reasons.
if (status == HttpURLConnection.HTTP_FORBIDDEN && !mTokensInvalidated) {
Log.d(LOGTAG, "LOGIN_FAIL: Invalidating tokens...");
// Need to regenerate the auth tokens and try again.
invalidateTokens();
// XXX: Do not touch any more member variables from this
// thread as a second thread will handle the next login
// attempt.
return;
}
done();
return;
}
final Charset responseCharset = ResponseUtils.responseCharset(
connection.getContentType());
byte[] responseBytes = Streams.readFully(connection.getInputStream());
authToken = new String(responseBytes, responseCharset);
} catch (Exception e) {
Log.d(LOGTAG, "LOGIN_FAIL: Exception acquiring uber token " + e);
done();
return;
} finally {
if (connection != null) {
connection.disconnect();
}
}
final String newUrl = TOKEN_AUTH_URL.buildUpon()
.appendQueryParameter("source", "android-browser")
.appendQueryParameter("auth", authToken)
.appendQueryParameter("continue",
BrowserSettings.getFactoryResetHomeUrl(mActivity))
.build().toString();
mActivity.runOnUiThread(new Runnable() {
@Override public void run() {
// Check mRunnable in case the request has been canceled. This
// is most likely not necessary as run() is the only non-UI
// thread that calls done() but I am paranoid.
synchronized (GoogleAccountLogin.this) {
if (mRunnable == null) {
return;
}
mWebView.loadUrl(newUrl);
}
}
});
}
private void invalidateTokens() {
AccountManager am = AccountManager.get(mActivity);
am.invalidateAuthToken(GOOGLE, mSid);
am.invalidateAuthToken(GOOGLE, mLsid);
mTokensInvalidated = true;
mState = 1; // SID
am.getAuthToken(mAccount, "SID", null, mActivity, this, null);
}
// AccountManager callbacks.
@Override
public void run(AccountManagerFuture<Bundle> value) {
try {
String id = value.getResult().getString(
AccountManager.KEY_AUTHTOKEN);
switch (mState) {
default:
case 0:
throw new IllegalStateException(
"Impossible to get into this state");
case 1:
mSid = id;
mState = 2; // LSID
AccountManager.get(mActivity).getAuthToken(
mAccount, "LSID", null, mActivity, this, null);
break;
case 2:
mLsid = id;
new Thread(this).start();
break;
}
} catch (Exception e) {
Log.d(LOGTAG, "LOGIN_FAIL: Exception in state " + mState + " " + e);
// For all exceptions load the original signin page.
// TODO: toast login failed?
done();
}
}
// Start the login process if auto-login is enabled and the user is not
// already logged in.
public static void startLoginIfNeeded(Activity activity,
Runnable runnable) {
// Already logged in?
if (isLoggedIn()) {
runnable.run();
return;
}
// No account found?
Account[] accounts = getAccounts(activity);
if (accounts == null || accounts.length == 0) {
runnable.run();
return;
}
GoogleAccountLogin login =
new GoogleAccountLogin(activity, accounts[0], runnable);
login.startLogin();
}
private void startLogin() {
saveLoginTime();
mProgressDialog = ProgressDialog.show(mActivity,
mActivity.getString(R.string.pref_autologin_title),
mActivity.getString(R.string.pref_autologin_progress,
mAccount.name),
true /* indeterminate */,
true /* cancelable */,
this);
mState = 1; // SID
AccountManager.get(mActivity).getAuthToken(
mAccount, "SID", null, mActivity, this, null);
}
private static Account[] getAccounts(Context ctx) {
return AccountManager.get(ctx).getAccountsByType(GOOGLE);
}
// Checks if we already did pre-login.
private static boolean isLoggedIn() {
// See if we last logged in less than a week ago.
long lastLogin = BrowserSettings.getInstance().getPreferences()
.getLong(PREF_AUTOLOGIN_TIME, -1);
if (lastLogin == -1) {
return false;
}
return true;
}
// Used to indicate that the Browser should continue loading the main page.
// This can happen on success, error, or timeout.
private synchronized void done() {
if (mRunnable != null) {
Log.d(LOGTAG, "Finished login attempt for " + mAccount.name);
mActivity.runOnUiThread(mRunnable);
try {
mProgressDialog.dismiss();
} catch (Exception e) {
// TODO: Switch to a managed dialog solution (DialogFragment?)
// Also refactor this class, it doesn't
// play nice with the activity lifecycle, leading to issues
// with the dialog it manages
Log.w(LOGTAG, "Failed to dismiss mProgressDialog: " + e.getMessage());
}
mRunnable = null;
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
mWebView.destroy();
}
});
}
}
// Called by the progress dialog on startup.
public void onCancel(DialogInterface unused) {
done();
}
}