blob: cd0afeb3cfe4c46d5bd4187e9ad698a752911538 [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.
123 new RLZTask(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
140 if (mController.isMenuDown()) {
Michael Kolb18eb3772010-12-10 14:29:51 -0800141 mController.openTab(tab, url, false);
Michael Kolb8233fac2010-10-26 16:08:53 -0700142 mActivity.closeOptionsMenu();
143 return true;
144 }
Russell Brennerd4afde12011-01-07 11:09:36 -0800145
Michael Kolb8233fac2010-10-26 16:08:53 -0700146 return false;
147 }
148
Russell Brennerd4afde12011-01-07 11:09:36 -0800149 boolean startActivityForUrl(String url)
150 {
151 Intent intent;
152 // perform generic parsing of the URI to turn it into an Intent.
153 try {
154 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
155 } catch (URISyntaxException ex) {
156 Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
157 return false;
158 }
159
160 // check whether the intent can be resolved. If not, we will see
161 // whether we can download it from the Market.
162 if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) {
163 String packagename = intent.getPackage();
164 if (packagename != null) {
165 intent = new Intent(Intent.ACTION_VIEW, Uri
166 .parse("market://search?q=pname:" + packagename));
167 intent.addCategory(Intent.CATEGORY_BROWSABLE);
168 mActivity.startActivity(intent);
169 // before leaving BrowserActivity, close the empty child tab.
170 // If a new tab is created through JavaScript open to load this
171 // url, we would like to close it as we will load this url in a
172 // different Activity.
173 mController.closeEmptyChildTab();
174 return true;
175 } else {
176 return false;
177 }
178 }
179
180 // sanitize the Intent, ensuring web pages can not bypass browser
181 // security (only access to BROWSABLE activities).
182 intent.addCategory(Intent.CATEGORY_BROWSABLE);
183 intent.setComponent(null);
184 try {
185 if (mActivity.startActivityIfNeeded(intent, -1)) {
186 // before leaving BrowserActivity, close the empty child tab.
187 // If a new tab is created through JavaScript open to load this
188 // url, we would like to close it as we will load this url in a
189 // different Activity.
190 mController.closeEmptyChildTab();
191 return true;
192 }
193 } catch (ActivityNotFoundException ex) {
194 // ignore the error. If no application can handle the URL,
195 // eg about:blank, assume the browser can handle it.
196 }
197
198 return false;
199 }
200
Patrick Scottd05faa62010-12-16 09:15:34 -0500201 // Url for issuing the uber token.
202 private final static Uri ISSUE_AUTH_TOKEN_URL = Uri.parse(
203 "https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false");
204 // Url for signing into a particular service.
205 private final static Uri TOKEN_AUTH_URL = Uri.parse(
206 "https://www.google.com/accounts/TokenAuth");
207
208 private class GoogleServiceLogin extends Thread implements
209 AccountManagerCallback<Bundle>, OnClickListener, OnCancelListener {
210 // For choosing the account.
211 private final Account[] mAccounts;
212 private int mCurrentAccount; // initially 0 for the first account
213
214 // For loading the auth token urls or the original url on error.
215 private final WebView mWebView;
216 private final String mUrl;
217
218 // SID and LSID retrieval process.
219 private String mSid;
220 private String mLsid;
221 private int mState; // {NONE(0), SID(1), LSID(2)}
222
223 GoogleServiceLogin(Account[] accounts, WebView view, String url) {
224 mAccounts = accounts;
225 mWebView = view;
226 mUrl = url;
227 }
228
229 // Thread
230 public void run() {
231 String url = ISSUE_AUTH_TOKEN_URL.buildUpon()
232 .appendQueryParameter("SID", mSid)
233 .appendQueryParameter("LSID", mLsid)
234 .build().toString();
235 // Intentionally not using Proxy.
236 AndroidHttpClient client = AndroidHttpClient.newInstance(
237 mWebView.getSettings().getUserAgentString());
238 HttpPost request = new HttpPost(url);
239
240 String result = null;
241 try {
242 HttpResponse response = client.execute(request);
243 if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
244 onCancel(null);
245 return;
246 }
247 HttpEntity entity = response.getEntity();
248 if (entity == null) {
249 onCancel(null);
250 return;
251 }
252 result = EntityUtils.toString(entity, "UTF-8");
253 } catch (Exception e) {
254 request.abort();
255 onCancel(null);
256 } finally {
257 client.close();
258 }
259 Uri parsedUri = Uri.parse(mUrl);
260 String service = parsedUri.getQueryParameter("service");
261 String redirect = parsedUri.getQueryParameter("continue");
262 final String newUrl = TOKEN_AUTH_URL.buildUpon()
263 .appendQueryParameter("service", service)
264 .appendQueryParameter("source", "android-browser")
265 .appendQueryParameter("auth", result)
266 .appendQueryParameter("continue", redirect)
267 .build().toString();
268 mActivity.runOnUiThread(new Runnable() {
269 @Override public void run() {
270 mController.loadUrl(mWebView, newUrl);
271 }
272 });
273 }
274
275 // AccountManager callbacks.
276 public void run(AccountManagerFuture<Bundle> value) {
277 try {
278 String id = value.getResult().getString(
279 AccountManager.KEY_AUTHTOKEN);
280 switch (mState) {
281 default:
282 case 0:
283 throw new IllegalStateException(
284 "Impossible to get into this state");
285 case 1:
286 mSid = id;
287 mState = 2; // LSID
288 AccountManager.get(mActivity).getAuthToken(
289 mAccounts[mCurrentAccount], "LSID", null,
290 mActivity, this, null);
291 break;
292 case 2:
293 mLsid = id;
294 this.start();
295 break;
296 }
297 } catch (Exception e) {
298 // For all exceptions load the original signin page.
299 // TODO: toast login failed?
300 onCancel(null);
301 }
302 }
303
304 // Handle picking an account and "OK."
305 public void onClick(DialogInterface unused, int which) {
306 if (which == DialogInterface.BUTTON_POSITIVE) {
307 // TODO: toast loading...?
308 Account current = mAccounts[mCurrentAccount];
309 mState = 1; // SID
310 AccountManager.get(mActivity).getAuthToken(
311 mAccounts[mCurrentAccount], "SID", null,
312 mActivity, this, null);
313 } else if (which == DialogInterface.BUTTON_NEGATIVE) {
314 onCancel(null);
315 } else {
316 mCurrentAccount = which;
317 }
318 }
319
320 // Handle "cancel."
321 public void onCancel(DialogInterface unusued) {
322 // load the original url to login manually.
323 mController.loadUrl(mWebView, mUrl);
324 }
325 }
326
327 private boolean loginWithDeviceAccount(WebView view, String url) {
328 Uri parsedUri = Uri.parse(url);
329 if ("true".equals(parsedUri.getQueryParameter("go"))) {
330 return false;
331 }
332 Account[] accounts =
333 AccountManager.get(mActivity).getAccountsByType("com.google");
334 if (accounts.length == 0) {
335 return false;
336 }
337
338 // Populate the account list.
339 CharSequence[] names = new CharSequence[accounts.length];
340 int i = 0;
341 for (Account a : accounts) {
342 names[i++] = a.name;
343 }
344
345 GoogleServiceLogin login = new GoogleServiceLogin(accounts, view, url);
346 new AlertDialog.Builder(mActivity)
347 .setTitle(R.string.account_picker_title)
348 .setSingleChoiceItems(names, 0 /* first choice */, login)
349 .setPositiveButton(R.string.ok, login)
350 .setNegativeButton(R.string.cancel, login)
351 .setCancelable(true)
352 .setOnCancelListener(login)
353 .show();
354 return true;
355 }
356
Ben Murdocha941f6e2010-12-07 16:09:16 +0000357 private class RLZTask extends AsyncTask<Void, Void, String> {
358 private Uri mSiteUri;
359 private WebView mWebView;
360
361 public RLZTask(Uri uri, WebView webView) {
362 mSiteUri = uri;
363 mWebView = webView;
364 }
365
366 protected String doInBackground(Void... unused) {
367 String result = mSiteUri.toString();
368 Cursor cur = null;
369 try {
370 cur = mActivity.getContentResolver()
371 .query(getRlzUri(), null, null, null, null);
372 if (cur != null && cur.moveToFirst() && !cur.isNull(0)) {
373 result = mSiteUri.buildUpon()
374 .appendQueryParameter("rlz", cur.getString(0))
375 .build().toString();
376 }
377 } finally {
378 if (cur != null) {
379 cur.close();
380 }
381 }
382 return result;
383 }
384
385 protected void onPostExecute(String result) {
Russell Brennerd4afde12011-01-07 11:09:36 -0800386 startActivityForUrl(result);
Ben Murdocha941f6e2010-12-07 16:09:16 +0000387 }
388 }
389
Michael Kolb8233fac2010-10-26 16:08:53 -0700390 // Determine whether the RLZ provider is present on the system.
391 private boolean rlzProviderPresent() {
392 if (mIsProviderPresent == null) {
393 PackageManager pm = mActivity.getPackageManager();
394 mIsProviderPresent = pm.resolveContentProvider(
395 BrowserSettings.RLZ_PROVIDER, 0) != null;
396 }
397 return mIsProviderPresent;
398 }
399
400 // Retrieve the RLZ access point string and cache the URI used to
401 // retrieve RLZ values.
402 private Uri getRlzUri() {
403 if (mRlzUri == null) {
404 String ap = mActivity.getResources()
405 .getString(R.string.rlz_access_point);
406 mRlzUri = Uri.withAppendedPath(BrowserSettings.RLZ_PROVIDER_URI, ap);
407 }
408 return mRlzUri;
409 }
410
411 // Determine if this URI appears to be for a Google search
412 // and does not have an RLZ parameter.
413 // Taken largely from Chrome source, src/chrome/browser/google_url_tracker.cc
414 private static boolean needsRlzString(Uri uri) {
415 String scheme = uri.getScheme();
416 if (("http".equals(scheme) || "https".equals(scheme)) &&
417 (uri.getQueryParameter("q") != null) &&
418 (uri.getQueryParameter("rlz") == null)) {
419 String host = uri.getHost();
420 if (host == null) {
421 return false;
422 }
423 String[] hostComponents = host.split("\\.");
424
425 if (hostComponents.length < 2) {
426 return false;
427 }
428 int googleComponent = hostComponents.length - 2;
429 String component = hostComponents[googleComponent];
430 if (!"google".equals(component)) {
431 if (hostComponents.length < 3 ||
432 (!"co".equals(component) && !"com".equals(component))) {
433 return false;
434 }
435 googleComponent = hostComponents.length - 3;
436 if (!"google".equals(hostComponents[googleComponent])) {
437 return false;
438 }
439 }
440
441 // Google corp network handling.
442 if (googleComponent > 0 && "corp".equals(
443 hostComponents[googleComponent - 1])) {
444 return false;
445 }
446
447 return true;
448 }
449 return false;
450 }
451
452}