blob: 17d4062bcbec9c239bf22919e9dfbf7f36ac5abd [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
John Reck35e9dd62011-04-25 09:01:54 -070040 static final String RLZ_PROVIDER = "com.google.android.partnersetup.rlzappprovider";
41 static final Uri RLZ_PROVIDER_URI = Uri.parse("content://" + RLZ_PROVIDER + "/");
42
Michael Kolb8233fac2010-10-26 16:08:53 -070043 // Use in overrideUrlLoading
44 /* package */ final static String SCHEME_WTAI = "wtai://wp/";
45 /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;";
46 /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;";
47 /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;";
48
49 Controller mController;
50 Activity mActivity;
51
52 private Boolean mIsProviderPresent = null;
53 private Uri mRlzUri = null;
54
55 public UrlHandler(Controller controller) {
56 mController = controller;
57 mActivity = mController.getActivity();
58 }
59
Michael Kolb18eb3772010-12-10 14:29:51 -080060 boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
Michael Kolb8233fac2010-10-26 16:08:53 -070061 if (view.isPrivateBrowsingEnabled()) {
62 // Don't allow urls to leave the browser app when in
63 // private browsing mode
Patrick Scottd05faa62010-12-16 09:15:34 -050064 return false;
Michael Kolb8233fac2010-10-26 16:08:53 -070065 }
66
67 if (url.startsWith(SCHEME_WTAI)) {
68 // wtai://wp/mc;number
69 // number=string(phone-number)
70 if (url.startsWith(SCHEME_WTAI_MC)) {
71 Intent intent = new Intent(Intent.ACTION_VIEW,
72 Uri.parse(WebView.SCHEME_TEL +
73 url.substring(SCHEME_WTAI_MC.length())));
74 mActivity.startActivity(intent);
75 // before leaving BrowserActivity, close the empty child tab.
76 // If a new tab is created through JavaScript open to load this
77 // url, we would like to close it as we will load this url in a
78 // different Activity.
79 mController.closeEmptyChildTab();
80 return true;
81 }
82 // wtai://wp/sd;dtmf
83 // dtmf=string(dialstring)
84 if (url.startsWith(SCHEME_WTAI_SD)) {
85 // TODO: only send when there is active voice connection
86 return false;
87 }
88 // wtai://wp/ap;number;name
89 // number=string(phone-number)
90 // name=string
91 if (url.startsWith(SCHEME_WTAI_AP)) {
92 // TODO
93 return false;
94 }
95 }
96
97 // The "about:" schemes are internal to the browser; don't want these to
98 // be dispatched to other apps.
99 if (url.startsWith("about:")) {
100 return false;
101 }
102
103 // If this is a Google search, attempt to add an RLZ string
104 // (if one isn't already present).
105 if (rlzProviderPresent()) {
106 Uri siteUri = Uri.parse(url);
107 if (needsRlzString(siteUri)) {
Ben Murdocha941f6e2010-12-07 16:09:16 +0000108 // Need to look up the RLZ info from a database, so do it in an
109 // AsyncTask. Although we are not overriding the URL load synchronously,
110 // we guarantee that we will handle this URL load after the task executes,
111 // so it's safe to just return true to WebCore now to stop its own loading.
Russell Brenner14e1ae22011-01-12 14:54:23 -0800112 new RLZTask(tab, siteUri, view).execute();
Michael Kolb8233fac2010-10-26 16:08:53 -0700113 return true;
114 }
115 }
116
Russell Brennerd4afde12011-01-07 11:09:36 -0800117 if (startActivityForUrl(url)) {
118 return true;
Michael Kolb8233fac2010-10-26 16:08:53 -0700119 }
120
Russell Brenner14e1ae22011-01-12 14:54:23 -0800121 if (handleMenuClick(tab, url)) {
Michael Kolb8233fac2010-10-26 16:08:53 -0700122 return true;
123 }
Russell Brennerd4afde12011-01-07 11:09:36 -0800124
Michael Kolb8233fac2010-10-26 16:08:53 -0700125 return false;
126 }
127
Russell Brennerca9898e2011-01-21 13:34:02 -0800128 boolean startActivityForUrl(String url) {
Russell Brennerd4afde12011-01-07 11:09:36 -0800129 Intent intent;
130 // perform generic parsing of the URI to turn it into an Intent.
131 try {
132 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
133 } catch (URISyntaxException ex) {
134 Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
135 return false;
136 }
137
138 // check whether the intent can be resolved. If not, we will see
139 // whether we can download it from the Market.
140 if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) {
141 String packagename = intent.getPackage();
142 if (packagename != null) {
143 intent = new Intent(Intent.ACTION_VIEW, Uri
144 .parse("market://search?q=pname:" + packagename));
145 intent.addCategory(Intent.CATEGORY_BROWSABLE);
146 mActivity.startActivity(intent);
147 // before leaving BrowserActivity, close the empty child tab.
148 // If a new tab is created through JavaScript open to load this
149 // url, we would like to close it as we will load this url in a
150 // different Activity.
151 mController.closeEmptyChildTab();
152 return true;
153 } else {
154 return false;
155 }
156 }
157
158 // sanitize the Intent, ensuring web pages can not bypass browser
159 // security (only access to BROWSABLE activities).
160 intent.addCategory(Intent.CATEGORY_BROWSABLE);
161 intent.setComponent(null);
John Reckdb3d43d2011-02-11 11:56:38 -0800162 // Make sure webkit can handle it internally before checking for specialized
163 // handlers. If webkit can't handle it internally, we need to call
164 // startActivityIfNeeded
165 Matcher m = UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url);
166 if (m.matches() && !isSpecializedHandlerAvailable(intent)) {
John Reck95a49ff2011-02-08 18:23:22 -0800167 return false;
168 }
Russell Brennerd4afde12011-01-07 11:09:36 -0800169 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 return false;
184 }
185
John Reck95a49ff2011-02-08 18:23:22 -0800186 /**
187 * Search for intent handlers that are specific to this URL
188 * aka, specialized apps like google maps or youtube
189 */
190 private boolean isSpecializedHandlerAvailable(Intent intent) {
191 PackageManager pm = mActivity.getPackageManager();
192 List<ResolveInfo> handlers = pm.queryIntentActivities(intent,
193 PackageManager.GET_RESOLVED_FILTER);
194 if (handlers == null || handlers.size() == 0) {
195 return false;
196 }
197 for (ResolveInfo resolveInfo : handlers) {
198 IntentFilter filter = resolveInfo.filter;
199 if (filter == null) {
200 // No intent filter matches this intent?
201 // Error on the side of staying in the browser, ignore
202 continue;
203 }
204 if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) {
205 // Generic handler, skip
206 continue;
207 }
208 return true;
209 }
210 return false;
211 }
212
Russell Brenner14e1ae22011-01-12 14:54:23 -0800213 // In case a physical keyboard is attached, handle clicks with the menu key
214 // depressed by opening in a new tab
Russell Brennerca9898e2011-01-21 13:34:02 -0800215 boolean handleMenuClick(Tab tab, String url) {
Russell Brenner14e1ae22011-01-12 14:54:23 -0800216 if (mController.isMenuDown()) {
Michael Kolb7bcafde2011-05-09 13:55:59 -0700217 mController.openTab(url,
218 (tab != null) && tab.isPrivateBrowsingEnabled(),
219 !BrowserSettings.getInstance().openInBackground(), true);
Russell Brenner14e1ae22011-01-12 14:54:23 -0800220 mActivity.closeOptionsMenu();
221 return true;
222 }
223
224 return false;
225 }
226
Russell Brennerca9898e2011-01-21 13:34:02 -0800227 // TODO: Move this class into Tab, where it can be properly stopped upon
228 // closure of the tab
Ben Murdocha941f6e2010-12-07 16:09:16 +0000229 private class RLZTask extends AsyncTask<Void, Void, String> {
Russell Brenner14e1ae22011-01-12 14:54:23 -0800230 private Tab mTab;
Ben Murdocha941f6e2010-12-07 16:09:16 +0000231 private Uri mSiteUri;
232 private WebView mWebView;
233
Russell Brenner14e1ae22011-01-12 14:54:23 -0800234 public RLZTask(Tab tab, Uri uri, WebView webView) {
235 mTab = tab;
Ben Murdocha941f6e2010-12-07 16:09:16 +0000236 mSiteUri = uri;
237 mWebView = webView;
238 }
239
240 protected String doInBackground(Void... unused) {
241 String result = mSiteUri.toString();
242 Cursor cur = null;
243 try {
244 cur = mActivity.getContentResolver()
245 .query(getRlzUri(), null, null, null, null);
246 if (cur != null && cur.moveToFirst() && !cur.isNull(0)) {
247 result = mSiteUri.buildUpon()
248 .appendQueryParameter("rlz", cur.getString(0))
249 .build().toString();
250 }
251 } finally {
252 if (cur != null) {
253 cur.close();
254 }
255 }
256 return result;
257 }
258
259 protected void onPostExecute(String result) {
Russell Brennerca9898e2011-01-21 13:34:02 -0800260 // Make sure the Tab was not closed while handling the task
Michael Kolbc831b632011-05-11 09:30:34 -0700261 if (mController.getTabControl().getTabPosition(mTab) != -1) {
Russell Brennerca9898e2011-01-21 13:34:02 -0800262 // If the Activity Manager is not invoked, load the URL directly
263 if (!startActivityForUrl(result)) {
264 if (!handleMenuClick(mTab, result)) {
John Reck26b18322011-06-21 13:08:58 -0700265 mController.loadUrl(mTab, result);
Russell Brennerca9898e2011-01-21 13:34:02 -0800266 }
Russell Brenner14e1ae22011-01-12 14:54:23 -0800267 }
268 }
Ben Murdocha941f6e2010-12-07 16:09:16 +0000269 }
270 }
271
Michael Kolb8233fac2010-10-26 16:08:53 -0700272 // Determine whether the RLZ provider is present on the system.
273 private boolean rlzProviderPresent() {
274 if (mIsProviderPresent == null) {
275 PackageManager pm = mActivity.getPackageManager();
John Reck35e9dd62011-04-25 09:01:54 -0700276 mIsProviderPresent = pm.resolveContentProvider(RLZ_PROVIDER, 0) != null;
Michael Kolb8233fac2010-10-26 16:08:53 -0700277 }
278 return mIsProviderPresent;
279 }
280
281 // Retrieve the RLZ access point string and cache the URI used to
282 // retrieve RLZ values.
283 private Uri getRlzUri() {
284 if (mRlzUri == null) {
285 String ap = mActivity.getResources()
286 .getString(R.string.rlz_access_point);
John Reck35e9dd62011-04-25 09:01:54 -0700287 mRlzUri = Uri.withAppendedPath(RLZ_PROVIDER_URI, ap);
Michael Kolb8233fac2010-10-26 16:08:53 -0700288 }
289 return mRlzUri;
290 }
291
292 // Determine if this URI appears to be for a Google search
293 // and does not have an RLZ parameter.
294 // Taken largely from Chrome source, src/chrome/browser/google_url_tracker.cc
295 private static boolean needsRlzString(Uri uri) {
296 String scheme = uri.getScheme();
297 if (("http".equals(scheme) || "https".equals(scheme)) &&
298 (uri.getQueryParameter("q") != null) &&
299 (uri.getQueryParameter("rlz") == null)) {
300 String host = uri.getHost();
301 if (host == null) {
302 return false;
303 }
304 String[] hostComponents = host.split("\\.");
305
306 if (hostComponents.length < 2) {
307 return false;
308 }
309 int googleComponent = hostComponents.length - 2;
310 String component = hostComponents[googleComponent];
311 if (!"google".equals(component)) {
312 if (hostComponents.length < 3 ||
313 (!"co".equals(component) && !"com".equals(component))) {
314 return false;
315 }
316 googleComponent = hostComponents.length - 3;
317 if (!"google".equals(hostComponents[googleComponent])) {
318 return false;
319 }
320 }
321
322 // Google corp network handling.
323 if (googleComponent > 0 && "corp".equals(
324 hostComponents[googleComponent - 1])) {
325 return false;
326 }
327
328 return true;
329 }
330 return false;
331 }
332
333}