blob: f39ac4b6b75655b1bdf627ca44848109a0781dd4 [file] [log] [blame]
Michael Kolb8233fac2010-10-26 16:08:53 -07001/*
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
Patrick Scottd05faa62010-12-16 09:15:34 -050019import org.apache.http.Header;
20import org.apache.http.HeaderIterator;
21import org.apache.http.HttpEntity;
22import org.apache.http.HttpResponse;
23import org.apache.http.HttpStatus;
24import org.apache.http.client.methods.HttpPost;
25import org.apache.http.util.EntityUtils;
26
27import android.accounts.Account;
28import android.accounts.AccountManager;
29import android.accounts.AccountManagerCallback;
30import android.accounts.AccountManagerFuture;
Michael Kolb8233fac2010-10-26 16:08:53 -070031import android.app.Activity;
Patrick Scottd05faa62010-12-16 09:15:34 -050032import android.app.AlertDialog;
Michael Kolb8233fac2010-10-26 16:08:53 -070033import android.content.ActivityNotFoundException;
Patrick Scottd05faa62010-12-16 09:15:34 -050034import android.content.DialogInterface;
35import android.content.DialogInterface.OnCancelListener;
36import android.content.DialogInterface.OnClickListener;
Michael Kolb8233fac2010-10-26 16:08:53 -070037import android.content.Intent;
38import android.content.pm.PackageManager;
39import android.database.Cursor;
40import android.net.Uri;
Patrick Scottd05faa62010-12-16 09:15:34 -050041import android.net.http.AndroidHttpClient;
Ben Murdocha941f6e2010-12-07 16:09:16 +000042import android.os.AsyncTask;
Patrick Scottd05faa62010-12-16 09:15:34 -050043import android.os.Bundle;
Michael Kolb8233fac2010-10-26 16:08:53 -070044import android.util.Log;
45import android.webkit.WebView;
46
47import java.net.URISyntaxException;
48
49/**
50 *
51 */
52public class UrlHandler {
53
54 // Use in overrideUrlLoading
55 /* package */ final static String SCHEME_WTAI = "wtai://wp/";
56 /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;";
57 /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;";
58 /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;";
59
60 Controller mController;
61 Activity mActivity;
62
63 private Boolean mIsProviderPresent = null;
64 private Uri mRlzUri = null;
65
66 public UrlHandler(Controller controller) {
67 mController = controller;
68 mActivity = mController.getActivity();
69 }
70
Michael Kolb18eb3772010-12-10 14:29:51 -080071 boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
Michael Kolb8233fac2010-10-26 16:08:53 -070072 if (view.isPrivateBrowsingEnabled()) {
73 // Don't allow urls to leave the browser app when in
74 // private browsing mode
Patrick Scottd05faa62010-12-16 09:15:34 -050075 return false;
Michael Kolb8233fac2010-10-26 16:08:53 -070076 }
77
78 if (url.startsWith(SCHEME_WTAI)) {
79 // wtai://wp/mc;number
80 // number=string(phone-number)
81 if (url.startsWith(SCHEME_WTAI_MC)) {
82 Intent intent = new Intent(Intent.ACTION_VIEW,
83 Uri.parse(WebView.SCHEME_TEL +
84 url.substring(SCHEME_WTAI_MC.length())));
85 mActivity.startActivity(intent);
86 // before leaving BrowserActivity, close the empty child tab.
87 // If a new tab is created through JavaScript open to load this
88 // url, we would like to close it as we will load this url in a
89 // different Activity.
90 mController.closeEmptyChildTab();
91 return true;
92 }
93 // wtai://wp/sd;dtmf
94 // dtmf=string(dialstring)
95 if (url.startsWith(SCHEME_WTAI_SD)) {
96 // TODO: only send when there is active voice connection
97 return false;
98 }
99 // wtai://wp/ap;number;name
100 // number=string(phone-number)
101 // name=string
102 if (url.startsWith(SCHEME_WTAI_AP)) {
103 // TODO
104 return false;
105 }
106 }
107
108 // The "about:" schemes are internal to the browser; don't want these to
109 // be dispatched to other apps.
110 if (url.startsWith("about:")) {
111 return false;
112 }
113
114 // If this is a Google search, attempt to add an RLZ string
115 // (if one isn't already present).
116 if (rlzProviderPresent()) {
117 Uri siteUri = Uri.parse(url);
118 if (needsRlzString(siteUri)) {
Ben Murdocha941f6e2010-12-07 16:09:16 +0000119 // Need to look up the RLZ info from a database, so do it in an
120 // AsyncTask. Although we are not overriding the URL load synchronously,
121 // we guarantee that we will handle this URL load after the task executes,
122 // so it's safe to just return true to WebCore now to stop its own loading.
Russell Brenner14e1ae22011-01-12 14:54:23 -0800123 new RLZTask(tab, siteUri, view).execute();
Michael Kolb8233fac2010-10-26 16:08:53 -0700124 return true;
125 }
126 }
127
Patrick Scottd05faa62010-12-16 09:15:34 -0500128 // Check for service login and prompt the user for an account to use.
129 if (url.startsWith("https://www.google.com/accounts/ServiceLogin?") ||
130 url.startsWith("https://www.google.com/accounts/Login?")) {
131 if (loginWithDeviceAccount(view, url)) {
132 return true;
133 }
134 }
135
Russell Brennerd4afde12011-01-07 11:09:36 -0800136 if (startActivityForUrl(url)) {
137 return true;
Michael Kolb8233fac2010-10-26 16:08:53 -0700138 }
139
Russell Brenner14e1ae22011-01-12 14:54:23 -0800140 if (handleMenuClick(tab, url)) {
Michael Kolb8233fac2010-10-26 16:08:53 -0700141 return true;
142 }
Russell Brennerd4afde12011-01-07 11:09:36 -0800143
Michael Kolb8233fac2010-10-26 16:08:53 -0700144 return false;
145 }
146
Russell Brennerd4afde12011-01-07 11:09:36 -0800147 boolean startActivityForUrl(String url)
148 {
149 Intent intent;
150 // perform generic parsing of the URI to turn it into an Intent.
151 try {
152 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
153 } catch (URISyntaxException ex) {
154 Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
155 return false;
156 }
157
158 // check whether the intent can be resolved. If not, we will see
159 // whether we can download it from the Market.
160 if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) {
161 String packagename = intent.getPackage();
162 if (packagename != null) {
163 intent = new Intent(Intent.ACTION_VIEW, Uri
164 .parse("market://search?q=pname:" + packagename));
165 intent.addCategory(Intent.CATEGORY_BROWSABLE);
166 mActivity.startActivity(intent);
167 // before leaving BrowserActivity, close the empty child tab.
168 // If a new tab is created through JavaScript open to load this
169 // url, we would like to close it as we will load this url in a
170 // different Activity.
171 mController.closeEmptyChildTab();
172 return true;
173 } else {
174 return false;
175 }
176 }
177
178 // sanitize the Intent, ensuring web pages can not bypass browser
179 // security (only access to BROWSABLE activities).
180 intent.addCategory(Intent.CATEGORY_BROWSABLE);
181 intent.setComponent(null);
182 try {
183 if (mActivity.startActivityIfNeeded(intent, -1)) {
184 // before leaving BrowserActivity, close the empty child tab.
185 // If a new tab is created through JavaScript open to load this
186 // url, we would like to close it as we will load this url in a
187 // different Activity.
188 mController.closeEmptyChildTab();
189 return true;
190 }
191 } catch (ActivityNotFoundException ex) {
192 // ignore the error. If no application can handle the URL,
193 // eg about:blank, assume the browser can handle it.
194 }
195
196 return false;
197 }
198
Russell Brenner14e1ae22011-01-12 14:54:23 -0800199 // In case a physical keyboard is attached, handle clicks with the menu key
200 // depressed by opening in a new tab
201 boolean handleMenuClick(Tab tab, String url)
202 {
203 if (mController.isMenuDown()) {
204 mController.openTab(tab, url, false);
205 mActivity.closeOptionsMenu();
206 return true;
207 }
208
209 return false;
210 }
211
Patrick Scottd05faa62010-12-16 09:15:34 -0500212 // Url for issuing the uber token.
213 private final static Uri ISSUE_AUTH_TOKEN_URL = Uri.parse(
214 "https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false");
215 // Url for signing into a particular service.
216 private final static Uri TOKEN_AUTH_URL = Uri.parse(
217 "https://www.google.com/accounts/TokenAuth");
218
219 private class GoogleServiceLogin extends Thread implements
220 AccountManagerCallback<Bundle>, OnClickListener, OnCancelListener {
221 // For choosing the account.
222 private final Account[] mAccounts;
223 private int mCurrentAccount; // initially 0 for the first account
224
225 // For loading the auth token urls or the original url on error.
226 private final WebView mWebView;
227 private final String mUrl;
228
229 // SID and LSID retrieval process.
230 private String mSid;
231 private String mLsid;
232 private int mState; // {NONE(0), SID(1), LSID(2)}
233
234 GoogleServiceLogin(Account[] accounts, WebView view, String url) {
235 mAccounts = accounts;
236 mWebView = view;
237 mUrl = url;
238 }
239
240 // Thread
241 public void run() {
242 String url = ISSUE_AUTH_TOKEN_URL.buildUpon()
243 .appendQueryParameter("SID", mSid)
244 .appendQueryParameter("LSID", mLsid)
245 .build().toString();
246 // Intentionally not using Proxy.
247 AndroidHttpClient client = AndroidHttpClient.newInstance(
248 mWebView.getSettings().getUserAgentString());
249 HttpPost request = new HttpPost(url);
250
251 String result = null;
252 try {
253 HttpResponse response = client.execute(request);
254 if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
255 onCancel(null);
256 return;
257 }
258 HttpEntity entity = response.getEntity();
259 if (entity == null) {
260 onCancel(null);
261 return;
262 }
263 result = EntityUtils.toString(entity, "UTF-8");
264 } catch (Exception e) {
265 request.abort();
266 onCancel(null);
267 } finally {
268 client.close();
269 }
270 Uri parsedUri = Uri.parse(mUrl);
271 String service = parsedUri.getQueryParameter("service");
272 String redirect = parsedUri.getQueryParameter("continue");
273 final String newUrl = TOKEN_AUTH_URL.buildUpon()
274 .appendQueryParameter("service", service)
275 .appendQueryParameter("source", "android-browser")
276 .appendQueryParameter("auth", result)
277 .appendQueryParameter("continue", redirect)
278 .build().toString();
279 mActivity.runOnUiThread(new Runnable() {
280 @Override public void run() {
281 mController.loadUrl(mWebView, newUrl);
282 }
283 });
284 }
285
286 // AccountManager callbacks.
287 public void run(AccountManagerFuture<Bundle> value) {
288 try {
289 String id = value.getResult().getString(
290 AccountManager.KEY_AUTHTOKEN);
291 switch (mState) {
292 default:
293 case 0:
294 throw new IllegalStateException(
295 "Impossible to get into this state");
296 case 1:
297 mSid = id;
298 mState = 2; // LSID
299 AccountManager.get(mActivity).getAuthToken(
300 mAccounts[mCurrentAccount], "LSID", null,
301 mActivity, this, null);
302 break;
303 case 2:
304 mLsid = id;
305 this.start();
306 break;
307 }
308 } catch (Exception e) {
309 // For all exceptions load the original signin page.
310 // TODO: toast login failed?
311 onCancel(null);
312 }
313 }
314
315 // Handle picking an account and "OK."
316 public void onClick(DialogInterface unused, int which) {
317 if (which == DialogInterface.BUTTON_POSITIVE) {
318 // TODO: toast loading...?
319 Account current = mAccounts[mCurrentAccount];
320 mState = 1; // SID
321 AccountManager.get(mActivity).getAuthToken(
322 mAccounts[mCurrentAccount], "SID", null,
323 mActivity, this, null);
324 } else if (which == DialogInterface.BUTTON_NEGATIVE) {
325 onCancel(null);
326 } else {
327 mCurrentAccount = which;
328 }
329 }
330
331 // Handle "cancel."
332 public void onCancel(DialogInterface unusued) {
333 // load the original url to login manually.
334 mController.loadUrl(mWebView, mUrl);
335 }
336 }
337
338 private boolean loginWithDeviceAccount(WebView view, String url) {
339 Uri parsedUri = Uri.parse(url);
340 if ("true".equals(parsedUri.getQueryParameter("go"))) {
341 return false;
342 }
343 Account[] accounts =
344 AccountManager.get(mActivity).getAccountsByType("com.google");
345 if (accounts.length == 0) {
346 return false;
347 }
348
349 // Populate the account list.
350 CharSequence[] names = new CharSequence[accounts.length];
351 int i = 0;
352 for (Account a : accounts) {
353 names[i++] = a.name;
354 }
355
356 GoogleServiceLogin login = new GoogleServiceLogin(accounts, view, url);
357 new AlertDialog.Builder(mActivity)
358 .setTitle(R.string.account_picker_title)
359 .setSingleChoiceItems(names, 0 /* first choice */, login)
360 .setPositiveButton(R.string.ok, login)
361 .setNegativeButton(R.string.cancel, login)
362 .setCancelable(true)
363 .setOnCancelListener(login)
364 .show();
365 return true;
366 }
367
Ben Murdocha941f6e2010-12-07 16:09:16 +0000368 private class RLZTask extends AsyncTask<Void, Void, String> {
Russell Brenner14e1ae22011-01-12 14:54:23 -0800369 private Tab mTab;
Ben Murdocha941f6e2010-12-07 16:09:16 +0000370 private Uri mSiteUri;
371 private WebView mWebView;
372
Russell Brenner14e1ae22011-01-12 14:54:23 -0800373 public RLZTask(Tab tab, Uri uri, WebView webView) {
374 mTab = tab;
Ben Murdocha941f6e2010-12-07 16:09:16 +0000375 mSiteUri = uri;
376 mWebView = webView;
377 }
378
379 protected String doInBackground(Void... unused) {
380 String result = mSiteUri.toString();
381 Cursor cur = null;
382 try {
383 cur = mActivity.getContentResolver()
384 .query(getRlzUri(), null, null, null, null);
385 if (cur != null && cur.moveToFirst() && !cur.isNull(0)) {
386 result = mSiteUri.buildUpon()
387 .appendQueryParameter("rlz", cur.getString(0))
388 .build().toString();
389 }
390 } finally {
391 if (cur != null) {
392 cur.close();
393 }
394 }
395 return result;
396 }
397
398 protected void onPostExecute(String result) {
Russell Brenner14e1ae22011-01-12 14:54:23 -0800399 // If the Activity Manager is not invoked, load the URL directly
400 if (!startActivityForUrl(result)) {
401 if (!handleMenuClick(mTab, result)) {
402 mController.loadUrl(mWebView, result);
403 }
404 }
Ben Murdocha941f6e2010-12-07 16:09:16 +0000405 }
406 }
407
Michael Kolb8233fac2010-10-26 16:08:53 -0700408 // Determine whether the RLZ provider is present on the system.
409 private boolean rlzProviderPresent() {
410 if (mIsProviderPresent == null) {
411 PackageManager pm = mActivity.getPackageManager();
412 mIsProviderPresent = pm.resolveContentProvider(
413 BrowserSettings.RLZ_PROVIDER, 0) != null;
414 }
415 return mIsProviderPresent;
416 }
417
418 // Retrieve the RLZ access point string and cache the URI used to
419 // retrieve RLZ values.
420 private Uri getRlzUri() {
421 if (mRlzUri == null) {
422 String ap = mActivity.getResources()
423 .getString(R.string.rlz_access_point);
424 mRlzUri = Uri.withAppendedPath(BrowserSettings.RLZ_PROVIDER_URI, ap);
425 }
426 return mRlzUri;
427 }
428
429 // Determine if this URI appears to be for a Google search
430 // and does not have an RLZ parameter.
431 // Taken largely from Chrome source, src/chrome/browser/google_url_tracker.cc
432 private static boolean needsRlzString(Uri uri) {
433 String scheme = uri.getScheme();
434 if (("http".equals(scheme) || "https".equals(scheme)) &&
435 (uri.getQueryParameter("q") != null) &&
436 (uri.getQueryParameter("rlz") == null)) {
437 String host = uri.getHost();
438 if (host == null) {
439 return false;
440 }
441 String[] hostComponents = host.split("\\.");
442
443 if (hostComponents.length < 2) {
444 return false;
445 }
446 int googleComponent = hostComponents.length - 2;
447 String component = hostComponents[googleComponent];
448 if (!"google".equals(component)) {
449 if (hostComponents.length < 3 ||
450 (!"co".equals(component) && !"com".equals(component))) {
451 return false;
452 }
453 googleComponent = hostComponents.length - 3;
454 if (!"google".equals(hostComponents[googleComponent])) {
455 return false;
456 }
457 }
458
459 // Google corp network handling.
460 if (googleComponent > 0 && "corp".equals(
461 hostComponents[googleComponent - 1])) {
462 return false;
463 }
464
465 return true;
466 }
467 return false;
468 }
469
470}