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