blob: fbbc3cf5dc29b5f751a60ffbf8cd328e3b85ecb6 [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()) {
217 mController.openTab(tab, url, false);
218 mActivity.closeOptionsMenu();
219 return true;
220 }
221
222 return false;
223 }
224
Russell Brennerca9898e2011-01-21 13:34:02 -0800225 // TODO: Move this class into Tab, where it can be properly stopped upon
226 // closure of the tab
Ben Murdocha941f6e2010-12-07 16:09:16 +0000227 private class RLZTask extends AsyncTask<Void, Void, String> {
Russell Brenner14e1ae22011-01-12 14:54:23 -0800228 private Tab mTab;
Ben Murdocha941f6e2010-12-07 16:09:16 +0000229 private Uri mSiteUri;
230 private WebView mWebView;
231
Russell Brenner14e1ae22011-01-12 14:54:23 -0800232 public RLZTask(Tab tab, Uri uri, WebView webView) {
233 mTab = tab;
Ben Murdocha941f6e2010-12-07 16:09:16 +0000234 mSiteUri = uri;
235 mWebView = webView;
236 }
237
238 protected String doInBackground(Void... unused) {
239 String result = mSiteUri.toString();
240 Cursor cur = null;
241 try {
242 cur = mActivity.getContentResolver()
243 .query(getRlzUri(), null, null, null, null);
244 if (cur != null && cur.moveToFirst() && !cur.isNull(0)) {
245 result = mSiteUri.buildUpon()
246 .appendQueryParameter("rlz", cur.getString(0))
247 .build().toString();
248 }
249 } finally {
250 if (cur != null) {
251 cur.close();
252 }
253 }
254 return result;
255 }
256
257 protected void onPostExecute(String result) {
Russell Brennerca9898e2011-01-21 13:34:02 -0800258 // Make sure the Tab was not closed while handling the task
259 if (mController.getTabControl().getTabIndex(mTab) != -1) {
260 // If the Activity Manager is not invoked, load the URL directly
261 if (!startActivityForUrl(result)) {
262 if (!handleMenuClick(mTab, result)) {
263 mController.loadUrl(mWebView, result);
264 }
Russell Brenner14e1ae22011-01-12 14:54:23 -0800265 }
266 }
Ben Murdocha941f6e2010-12-07 16:09:16 +0000267 }
268 }
269
Michael Kolb8233fac2010-10-26 16:08:53 -0700270 // Determine whether the RLZ provider is present on the system.
271 private boolean rlzProviderPresent() {
272 if (mIsProviderPresent == null) {
273 PackageManager pm = mActivity.getPackageManager();
John Reck35e9dd62011-04-25 09:01:54 -0700274 mIsProviderPresent = pm.resolveContentProvider(RLZ_PROVIDER, 0) != null;
Michael Kolb8233fac2010-10-26 16:08:53 -0700275 }
276 return mIsProviderPresent;
277 }
278
279 // Retrieve the RLZ access point string and cache the URI used to
280 // retrieve RLZ values.
281 private Uri getRlzUri() {
282 if (mRlzUri == null) {
283 String ap = mActivity.getResources()
284 .getString(R.string.rlz_access_point);
John Reck35e9dd62011-04-25 09:01:54 -0700285 mRlzUri = Uri.withAppendedPath(RLZ_PROVIDER_URI, ap);
Michael Kolb8233fac2010-10-26 16:08:53 -0700286 }
287 return mRlzUri;
288 }
289
290 // Determine if this URI appears to be for a Google search
291 // and does not have an RLZ parameter.
292 // Taken largely from Chrome source, src/chrome/browser/google_url_tracker.cc
293 private static boolean needsRlzString(Uri uri) {
294 String scheme = uri.getScheme();
295 if (("http".equals(scheme) || "https".equals(scheme)) &&
296 (uri.getQueryParameter("q") != null) &&
297 (uri.getQueryParameter("rlz") == null)) {
298 String host = uri.getHost();
299 if (host == null) {
300 return false;
301 }
302 String[] hostComponents = host.split("\\.");
303
304 if (hostComponents.length < 2) {
305 return false;
306 }
307 int googleComponent = hostComponents.length - 2;
308 String component = hostComponents[googleComponent];
309 if (!"google".equals(component)) {
310 if (hostComponents.length < 3 ||
311 (!"co".equals(component) && !"com".equals(component))) {
312 return false;
313 }
314 googleComponent = hostComponents.length - 3;
315 if (!"google".equals(hostComponents[googleComponent])) {
316 return false;
317 }
318 }
319
320 // Google corp network handling.
321 if (googleComponent > 0 && "corp".equals(
322 hostComponents[googleComponent - 1])) {
323 return false;
324 }
325
326 return true;
327 }
328 return false;
329 }
330
331}