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