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 
GoogleAccountLogin(Activity activity, Account account, Runnable runnable)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 
saveLoginTime()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
run()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 
invalidateTokens()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
run(AccountManagerFuture<Bundle> value)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.
startLoginIfNeeded(Activity activity, Runnable runnable)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 
startLogin()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 
getAccounts(Context ctx)248     private static Account[] getAccounts(Context ctx) {
249         return AccountManager.get(ctx).getAccountsByType(GOOGLE);
250     }
251 
252     // Checks if we already did pre-login.
isLoggedIn()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.
done()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.
onCancel(DialogInterface unused)290     public void onCancel(DialogInterface unused) {
291         done();
292     }
293 
294 }
295