blob: 03bab9bcdb6a15f59c018ae403db555fa16f091d [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
19import android.app.Activity;
20import android.content.ActivityNotFoundException;
21import android.content.Intent;
John Reck95a49ff2011-02-08 18:23:22 -080022import android.content.IntentFilter;
Michael Kolb8233fac2010-10-26 16:08:53 -070023import android.content.pm.PackageManager;
John Reck95a49ff2011-02-08 18:23:22 -080024import android.content.pm.ResolveInfo;
Michael Kolb8233fac2010-10-26 16:08:53 -070025import android.database.Cursor;
26import android.net.Uri;
Ben Murdocha941f6e2010-12-07 16:09:16 +000027import android.os.AsyncTask;
Michael Kolb8233fac2010-10-26 16:08:53 -070028import android.util.Log;
29import android.webkit.WebView;
30
31import java.net.URISyntaxException;
John Reck95a49ff2011-02-08 18:23:22 -080032import java.util.List;
John Reckdb3d43d2011-02-11 11:56:38 -080033import java.util.regex.Matcher;
Michael Kolb8233fac2010-10-26 16:08:53 -070034
35/**
36 *
37 */
38public class UrlHandler {
39
40 // Use in overrideUrlLoading
41 /* package */ final static String SCHEME_WTAI = "wtai://wp/";
42 /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;";
43 /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;";
44 /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;";
45
46 Controller mController;
47 Activity mActivity;
48
49 private Boolean mIsProviderPresent = null;
50 private Uri mRlzUri = null;
51
52 public UrlHandler(Controller controller) {
53 mController = controller;
54 mActivity = mController.getActivity();
55 }
56
Michael Kolb18eb3772010-12-10 14:29:51 -080057 boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
Michael Kolb8233fac2010-10-26 16:08:53 -070058 if (view.isPrivateBrowsingEnabled()) {
59 // Don't allow urls to leave the browser app when in
60 // private browsing mode
Patrick Scottd05faa62010-12-16 09:15:34 -050061 return false;
Michael Kolb8233fac2010-10-26 16:08:53 -070062 }
63
64 if (url.startsWith(SCHEME_WTAI)) {
65 // wtai://wp/mc;number
66 // number=string(phone-number)
67 if (url.startsWith(SCHEME_WTAI_MC)) {
68 Intent intent = new Intent(Intent.ACTION_VIEW,
69 Uri.parse(WebView.SCHEME_TEL +
70 url.substring(SCHEME_WTAI_MC.length())));
71 mActivity.startActivity(intent);
72 // before leaving BrowserActivity, close the empty child tab.
73 // If a new tab is created through JavaScript open to load this
74 // url, we would like to close it as we will load this url in a
75 // different Activity.
76 mController.closeEmptyChildTab();
77 return true;
78 }
79 // wtai://wp/sd;dtmf
80 // dtmf=string(dialstring)
81 if (url.startsWith(SCHEME_WTAI_SD)) {
82 // TODO: only send when there is active voice connection
83 return false;
84 }
85 // wtai://wp/ap;number;name
86 // number=string(phone-number)
87 // name=string
88 if (url.startsWith(SCHEME_WTAI_AP)) {
89 // TODO
90 return false;
91 }
92 }
93
94 // The "about:" schemes are internal to the browser; don't want these to
95 // be dispatched to other apps.
96 if (url.startsWith("about:")) {
97 return false;
98 }
99
100 // If this is a Google search, attempt to add an RLZ string
101 // (if one isn't already present).
102 if (rlzProviderPresent()) {
103 Uri siteUri = Uri.parse(url);
104 if (needsRlzString(siteUri)) {
Ben Murdocha941f6e2010-12-07 16:09:16 +0000105 // Need to look up the RLZ info from a database, so do it in an
106 // AsyncTask. Although we are not overriding the URL load synchronously,
107 // we guarantee that we will handle this URL load after the task executes,
108 // so it's safe to just return true to WebCore now to stop its own loading.
Russell Brenner14e1ae22011-01-12 14:54:23 -0800109 new RLZTask(tab, siteUri, view).execute();
Michael Kolb8233fac2010-10-26 16:08:53 -0700110 return true;
111 }
112 }
113
Russell Brennerd4afde12011-01-07 11:09:36 -0800114 if (startActivityForUrl(url)) {
115 return true;
Michael Kolb8233fac2010-10-26 16:08:53 -0700116 }
117
Russell Brenner14e1ae22011-01-12 14:54:23 -0800118 if (handleMenuClick(tab, url)) {
Michael Kolb8233fac2010-10-26 16:08:53 -0700119 return true;
120 }
Russell Brennerd4afde12011-01-07 11:09:36 -0800121
Michael Kolb8233fac2010-10-26 16:08:53 -0700122 return false;
123 }
124
Russell Brennerca9898e2011-01-21 13:34:02 -0800125 boolean startActivityForUrl(String url) {
Russell Brennerd4afde12011-01-07 11:09:36 -0800126 Intent intent;
127 // perform generic parsing of the URI to turn it into an Intent.
128 try {
129 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
130 } catch (URISyntaxException ex) {
131 Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
132 return false;
133 }
134
135 // check whether the intent can be resolved. If not, we will see
136 // whether we can download it from the Market.
137 if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) {
138 String packagename = intent.getPackage();
139 if (packagename != null) {
140 intent = new Intent(Intent.ACTION_VIEW, Uri
141 .parse("market://search?q=pname:" + packagename));
142 intent.addCategory(Intent.CATEGORY_BROWSABLE);
143 mActivity.startActivity(intent);
144 // before leaving BrowserActivity, close the empty child tab.
145 // If a new tab is created through JavaScript open to load this
146 // url, we would like to close it as we will load this url in a
147 // different Activity.
148 mController.closeEmptyChildTab();
149 return true;
150 } else {
151 return false;
152 }
153 }
154
155 // sanitize the Intent, ensuring web pages can not bypass browser
156 // security (only access to BROWSABLE activities).
157 intent.addCategory(Intent.CATEGORY_BROWSABLE);
158 intent.setComponent(null);
John Reckdb3d43d2011-02-11 11:56:38 -0800159 // Make sure webkit can handle it internally before checking for specialized
160 // handlers. If webkit can't handle it internally, we need to call
161 // startActivityIfNeeded
162 Matcher m = UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url);
163 if (m.matches() && !isSpecializedHandlerAvailable(intent)) {
John Reck95a49ff2011-02-08 18:23:22 -0800164 return false;
165 }
Russell Brennerd4afde12011-01-07 11:09:36 -0800166 try {
167 if (mActivity.startActivityIfNeeded(intent, -1)) {
168 // before leaving BrowserActivity, close the empty child tab.
169 // If a new tab is created through JavaScript open to load this
170 // url, we would like to close it as we will load this url in a
171 // different Activity.
172 mController.closeEmptyChildTab();
173 return true;
174 }
175 } catch (ActivityNotFoundException ex) {
176 // ignore the error. If no application can handle the URL,
177 // eg about:blank, assume the browser can handle it.
178 }
179
180 return false;
181 }
182
John Reck95a49ff2011-02-08 18:23:22 -0800183 /**
184 * Search for intent handlers that are specific to this URL
185 * aka, specialized apps like google maps or youtube
186 */
187 private boolean isSpecializedHandlerAvailable(Intent intent) {
188 PackageManager pm = mActivity.getPackageManager();
189 List<ResolveInfo> handlers = pm.queryIntentActivities(intent,
190 PackageManager.GET_RESOLVED_FILTER);
191 if (handlers == null || handlers.size() == 0) {
192 return false;
193 }
194 for (ResolveInfo resolveInfo : handlers) {
195 IntentFilter filter = resolveInfo.filter;
196 if (filter == null) {
197 // No intent filter matches this intent?
198 // Error on the side of staying in the browser, ignore
199 continue;
200 }
201 if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) {
202 // Generic handler, skip
203 continue;
204 }
205 return true;
206 }
207 return false;
208 }
209
Russell Brenner14e1ae22011-01-12 14:54:23 -0800210 // In case a physical keyboard is attached, handle clicks with the menu key
211 // depressed by opening in a new tab
Russell Brennerca9898e2011-01-21 13:34:02 -0800212 boolean handleMenuClick(Tab tab, String url) {
Russell Brenner14e1ae22011-01-12 14:54:23 -0800213 if (mController.isMenuDown()) {
214 mController.openTab(tab, url, false);
215 mActivity.closeOptionsMenu();
216 return true;
217 }
218
219 return false;
220 }
221
Russell Brennerca9898e2011-01-21 13:34:02 -0800222 // TODO: Move this class into Tab, where it can be properly stopped upon
223 // closure of the tab
Ben Murdocha941f6e2010-12-07 16:09:16 +0000224 private class RLZTask extends AsyncTask<Void, Void, String> {
Russell Brenner14e1ae22011-01-12 14:54:23 -0800225 private Tab mTab;
Ben Murdocha941f6e2010-12-07 16:09:16 +0000226 private Uri mSiteUri;
227 private WebView mWebView;
228
Russell Brenner14e1ae22011-01-12 14:54:23 -0800229 public RLZTask(Tab tab, Uri uri, WebView webView) {
230 mTab = tab;
Ben Murdocha941f6e2010-12-07 16:09:16 +0000231 mSiteUri = uri;
232 mWebView = webView;
233 }
234
235 protected String doInBackground(Void... unused) {
236 String result = mSiteUri.toString();
237 Cursor cur = null;
238 try {
239 cur = mActivity.getContentResolver()
240 .query(getRlzUri(), null, null, null, null);
241 if (cur != null && cur.moveToFirst() && !cur.isNull(0)) {
242 result = mSiteUri.buildUpon()
243 .appendQueryParameter("rlz", cur.getString(0))
244 .build().toString();
245 }
246 } finally {
247 if (cur != null) {
248 cur.close();
249 }
250 }
251 return result;
252 }
253
254 protected void onPostExecute(String result) {
Russell Brennerca9898e2011-01-21 13:34:02 -0800255 // Make sure the Tab was not closed while handling the task
256 if (mController.getTabControl().getTabIndex(mTab) != -1) {
257 // If the Activity Manager is not invoked, load the URL directly
258 if (!startActivityForUrl(result)) {
259 if (!handleMenuClick(mTab, result)) {
260 mController.loadUrl(mWebView, result);
261 }
Russell Brenner14e1ae22011-01-12 14:54:23 -0800262 }
263 }
Ben Murdocha941f6e2010-12-07 16:09:16 +0000264 }
265 }
266
Michael Kolb8233fac2010-10-26 16:08:53 -0700267 // Determine whether the RLZ provider is present on the system.
268 private boolean rlzProviderPresent() {
269 if (mIsProviderPresent == null) {
270 PackageManager pm = mActivity.getPackageManager();
271 mIsProviderPresent = pm.resolveContentProvider(
272 BrowserSettings.RLZ_PROVIDER, 0) != null;
273 }
274 return mIsProviderPresent;
275 }
276
277 // Retrieve the RLZ access point string and cache the URI used to
278 // retrieve RLZ values.
279 private Uri getRlzUri() {
280 if (mRlzUri == null) {
281 String ap = mActivity.getResources()
282 .getString(R.string.rlz_access_point);
283 mRlzUri = Uri.withAppendedPath(BrowserSettings.RLZ_PROVIDER_URI, ap);
284 }
285 return mRlzUri;
286 }
287
288 // Determine if this URI appears to be for a Google search
289 // and does not have an RLZ parameter.
290 // Taken largely from Chrome source, src/chrome/browser/google_url_tracker.cc
291 private static boolean needsRlzString(Uri uri) {
292 String scheme = uri.getScheme();
293 if (("http".equals(scheme) || "https".equals(scheme)) &&
294 (uri.getQueryParameter("q") != null) &&
295 (uri.getQueryParameter("rlz") == null)) {
296 String host = uri.getHost();
297 if (host == null) {
298 return false;
299 }
300 String[] hostComponents = host.split("\\.");
301
302 if (hostComponents.length < 2) {
303 return false;
304 }
305 int googleComponent = hostComponents.length - 2;
306 String component = hostComponents[googleComponent];
307 if (!"google".equals(component)) {
308 if (hostComponents.length < 3 ||
309 (!"co".equals(component) && !"com".equals(component))) {
310 return false;
311 }
312 googleComponent = hostComponents.length - 3;
313 if (!"google".equals(hostComponents[googleComponent])) {
314 return false;
315 }
316 }
317
318 // Google corp network handling.
319 if (googleComponent > 0 && "corp".equals(
320 hostComponents[googleComponent - 1])) {
321 return false;
322 }
323
324 return true;
325 }
326 return false;
327 }
328
329}