blob: ac8442ac79bd1d49ff030a6d89910d3bde49b2b1 [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;
kaiyizc4ada322013-07-30 09:58:07 +080028import android.os.SystemProperties;
John Reck8bcafc12011-08-29 16:43:02 -070029import android.provider.Browser;
Michael Kolb8233fac2010-10-26 16:08:53 -070030import android.util.Log;
31import android.webkit.WebView;
kaiyizc4ada322013-07-30 09:58:07 +080032import android.widget.Toast;
Michael Kolb8233fac2010-10-26 16:08:53 -070033
kaiyizc4ada322013-07-30 09:58:07 +080034import java.io.UnsupportedEncodingException;
Michael Kolb8233fac2010-10-26 16:08:53 -070035import java.net.URISyntaxException;
John Reck95a49ff2011-02-08 18:23:22 -080036import java.util.List;
John Reckdb3d43d2011-02-11 11:56:38 -080037import java.util.regex.Matcher;
Michael Kolb8233fac2010-10-26 16:08:53 -070038
39/**
40 *
41 */
42public class UrlHandler {
43
kaiyizc4ada322013-07-30 09:58:07 +080044 private final static String TAG = "UrlHandler";
John Reck35e9dd62011-04-25 09:01:54 -070045 static final String RLZ_PROVIDER = "com.google.android.partnersetup.rlzappprovider";
46 static final Uri RLZ_PROVIDER_URI = Uri.parse("content://" + RLZ_PROVIDER + "/");
47
Michael Kolb8233fac2010-10-26 16:08:53 -070048 // Use in overrideUrlLoading
49 /* package */ final static String SCHEME_WTAI = "wtai://wp/";
50 /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;";
51 /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;";
52 /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;";
53
54 Controller mController;
55 Activity mActivity;
56
57 private Boolean mIsProviderPresent = null;
58 private Uri mRlzUri = null;
59
60 public UrlHandler(Controller controller) {
61 mController = controller;
62 mActivity = mController.getActivity();
63 }
64
Michael Kolb18eb3772010-12-10 14:29:51 -080065 boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
Michael Kolb8233fac2010-10-26 16:08:53 -070066 if (view.isPrivateBrowsingEnabled()) {
67 // Don't allow urls to leave the browser app when in
68 // private browsing mode
Patrick Scottd05faa62010-12-16 09:15:34 -050069 return false;
Michael Kolb8233fac2010-10-26 16:08:53 -070070 }
71
72 if (url.startsWith(SCHEME_WTAI)) {
73 // wtai://wp/mc;number
74 // number=string(phone-number)
75 if (url.startsWith(SCHEME_WTAI_MC)) {
76 Intent intent = new Intent(Intent.ACTION_VIEW,
77 Uri.parse(WebView.SCHEME_TEL +
78 url.substring(SCHEME_WTAI_MC.length())));
79 mActivity.startActivity(intent);
80 // before leaving BrowserActivity, close the empty child tab.
81 // If a new tab is created through JavaScript open to load this
82 // url, we would like to close it as we will load this url in a
83 // different Activity.
John Reck8bcafc12011-08-29 16:43:02 -070084 mController.closeEmptyTab();
Michael Kolb8233fac2010-10-26 16:08:53 -070085 return true;
86 }
87 // wtai://wp/sd;dtmf
88 // dtmf=string(dialstring)
89 if (url.startsWith(SCHEME_WTAI_SD)) {
90 // TODO: only send when there is active voice connection
91 return false;
92 }
93 // wtai://wp/ap;number;name
94 // number=string(phone-number)
95 // name=string
96 if (url.startsWith(SCHEME_WTAI_AP)) {
97 // TODO
98 return false;
99 }
100 }
101
102 // The "about:" schemes are internal to the browser; don't want these to
103 // be dispatched to other apps.
104 if (url.startsWith("about:")) {
105 return false;
106 }
107
kaiyiz6e5b3e02013-08-19 20:02:01 +0800108 if (url.startsWith("ae://") && url.endsWith("add-fav")) {
109 mController.startAddMyNavigation(url);
110 return true;
111 }
Michael Kolb8233fac2010-10-26 16:08:53 -0700112 // If this is a Google search, attempt to add an RLZ string
113 // (if one isn't already present).
114 if (rlzProviderPresent()) {
115 Uri siteUri = Uri.parse(url);
116 if (needsRlzString(siteUri)) {
Ben Murdocha941f6e2010-12-07 16:09:16 +0000117 // Need to look up the RLZ info from a database, so do it in an
118 // AsyncTask. Although we are not overriding the URL load synchronously,
119 // we guarantee that we will handle this URL load after the task executes,
120 // so it's safe to just return true to WebCore now to stop its own loading.
Russell Brenner14e1ae22011-01-12 14:54:23 -0800121 new RLZTask(tab, siteUri, view).execute();
Michael Kolb8233fac2010-10-26 16:08:53 -0700122 return true;
123 }
124 }
125
kaiyizc4ada322013-07-30 09:58:07 +0800126 // add for carrier wap2estore feature
127 boolean wap2estore = SystemProperties.getBoolean(
128 "persist.env.browser.wap2estore", false);
129 if (wap2estore && isEstoreTypeUrl(url)) {
130 handleEstoreTypeUrl(url);
131 return true;
132 }
133
John Reck8bcafc12011-08-29 16:43:02 -0700134 if (startActivityForUrl(tab, url)) {
Russell Brennerd4afde12011-01-07 11:09:36 -0800135 return true;
Michael Kolb8233fac2010-10-26 16:08:53 -0700136 }
137
Russell Brenner14e1ae22011-01-12 14:54:23 -0800138 if (handleMenuClick(tab, url)) {
Michael Kolb8233fac2010-10-26 16:08:53 -0700139 return true;
140 }
Russell Brennerd4afde12011-01-07 11:09:36 -0800141
Michael Kolb8233fac2010-10-26 16:08:53 -0700142 return false;
143 }
144
kaiyizc4ada322013-07-30 09:58:07 +0800145 private boolean isEstoreTypeUrl(String url) {
146 String utf8Url = null;
147 try {
148 utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
149 } catch (UnsupportedEncodingException e) {
150 Log.e(TAG, "err " + e);
151 }
152 if (utf8Url != null && utf8Url.startsWith("estore:")) {
153 return true;
154 }
155 return false;
156 }
157
158 private void handleEstoreTypeUrl(String url) {
159 String utf8Url = null, finalUrl = null;
160 try {
161 utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
162 } catch (UnsupportedEncodingException e) {
163 Log.e(TAG, "err " + e);
164 }
165 if (utf8Url != null) {
166 finalUrl = utf8Url;
167 } else {
168 finalUrl = url;
169 }
170 if (finalUrl.replaceFirst("estore:", "").length() > 256) {
171 Toast.makeText(mActivity, R.string.estore_url_warning, Toast.LENGTH_LONG).show();
172 return;
173 }
174 Intent intent = new Intent(Intent.ACTION_VIEW);
175 intent.setData(Uri.parse(finalUrl));
176 try {
177 mActivity.startActivity(intent);
178 } catch (ActivityNotFoundException ex) {
179 String downloadUrl = mActivity.getResources().getString(R.string.estore_homepage);
180 mController.loadUrl(mController.getCurrentTab(), downloadUrl);
181 Toast.makeText(mActivity, R.string.download_estore_app, Toast.LENGTH_LONG).show();
182 }
183 }
184
John Reck8bcafc12011-08-29 16:43:02 -0700185 boolean startActivityForUrl(Tab tab, String url) {
Russell Brennerd4afde12011-01-07 11:09:36 -0800186 Intent intent;
187 // perform generic parsing of the URI to turn it into an Intent.
188 try {
189 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
190 } catch (URISyntaxException ex) {
191 Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
192 return false;
193 }
194
195 // check whether the intent can be resolved. If not, we will see
196 // whether we can download it from the Market.
197 if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) {
198 String packagename = intent.getPackage();
199 if (packagename != null) {
200 intent = new Intent(Intent.ACTION_VIEW, Uri
201 .parse("market://search?q=pname:" + packagename));
202 intent.addCategory(Intent.CATEGORY_BROWSABLE);
203 mActivity.startActivity(intent);
204 // before leaving BrowserActivity, close the empty child tab.
205 // If a new tab is created through JavaScript open to load this
206 // url, we would like to close it as we will load this url in a
207 // different Activity.
John Reck8bcafc12011-08-29 16:43:02 -0700208 mController.closeEmptyTab();
Russell Brennerd4afde12011-01-07 11:09:36 -0800209 return true;
210 } else {
211 return false;
212 }
213 }
214
215 // sanitize the Intent, ensuring web pages can not bypass browser
216 // security (only access to BROWSABLE activities).
217 intent.addCategory(Intent.CATEGORY_BROWSABLE);
218 intent.setComponent(null);
John Reck8bcafc12011-08-29 16:43:02 -0700219 // Re-use the existing tab if the intent comes back to us
220 if (tab != null) {
221 if (tab.getAppId() == null) {
Michael Kolbf1286a42012-04-17 14:10:52 -0700222 tab.setAppId(mActivity.getPackageName() + "-" + tab.getId());
John Reck8bcafc12011-08-29 16:43:02 -0700223 }
224 intent.putExtra(Browser.EXTRA_APPLICATION_ID, tab.getAppId());
225 }
John Reckdb3d43d2011-02-11 11:56:38 -0800226 // Make sure webkit can handle it internally before checking for specialized
227 // handlers. If webkit can't handle it internally, we need to call
228 // startActivityIfNeeded
229 Matcher m = UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url);
230 if (m.matches() && !isSpecializedHandlerAvailable(intent)) {
John Reck95a49ff2011-02-08 18:23:22 -0800231 return false;
232 }
Russell Brennerd4afde12011-01-07 11:09:36 -0800233 try {
John Reck38b39652012-06-05 09:22:59 -0700234 intent.putExtra(BrowserActivity.EXTRA_DISABLE_URL_OVERRIDE, true);
Russell Brennerd4afde12011-01-07 11:09:36 -0800235 if (mActivity.startActivityIfNeeded(intent, -1)) {
236 // before leaving BrowserActivity, close the empty child tab.
237 // If a new tab is created through JavaScript open to load this
238 // url, we would like to close it as we will load this url in a
239 // different Activity.
John Reck8bcafc12011-08-29 16:43:02 -0700240 mController.closeEmptyTab();
Russell Brennerd4afde12011-01-07 11:09:36 -0800241 return true;
242 }
243 } catch (ActivityNotFoundException ex) {
244 // ignore the error. If no application can handle the URL,
245 // eg about:blank, assume the browser can handle it.
246 }
247
248 return false;
249 }
250
John Reck95a49ff2011-02-08 18:23:22 -0800251 /**
252 * Search for intent handlers that are specific to this URL
253 * aka, specialized apps like google maps or youtube
254 */
255 private boolean isSpecializedHandlerAvailable(Intent intent) {
256 PackageManager pm = mActivity.getPackageManager();
257 List<ResolveInfo> handlers = pm.queryIntentActivities(intent,
258 PackageManager.GET_RESOLVED_FILTER);
259 if (handlers == null || handlers.size() == 0) {
260 return false;
261 }
262 for (ResolveInfo resolveInfo : handlers) {
263 IntentFilter filter = resolveInfo.filter;
264 if (filter == null) {
265 // No intent filter matches this intent?
266 // Error on the side of staying in the browser, ignore
267 continue;
268 }
John Reck1e6a2872011-12-15 14:35:02 -0800269 if (filter.countDataAuthorities() == 0 && filter.countDataPaths() == 0) {
John Reck95a49ff2011-02-08 18:23:22 -0800270 // Generic handler, skip
271 continue;
272 }
273 return true;
274 }
275 return false;
276 }
277
Russell Brenner14e1ae22011-01-12 14:54:23 -0800278 // In case a physical keyboard is attached, handle clicks with the menu key
279 // depressed by opening in a new tab
Russell Brennerca9898e2011-01-21 13:34:02 -0800280 boolean handleMenuClick(Tab tab, String url) {
Russell Brenner14e1ae22011-01-12 14:54:23 -0800281 if (mController.isMenuDown()) {
Michael Kolb7bcafde2011-05-09 13:55:59 -0700282 mController.openTab(url,
283 (tab != null) && tab.isPrivateBrowsingEnabled(),
284 !BrowserSettings.getInstance().openInBackground(), true);
Russell Brenner14e1ae22011-01-12 14:54:23 -0800285 mActivity.closeOptionsMenu();
286 return true;
287 }
288
289 return false;
290 }
291
Russell Brennerca9898e2011-01-21 13:34:02 -0800292 // TODO: Move this class into Tab, where it can be properly stopped upon
293 // closure of the tab
Ben Murdocha941f6e2010-12-07 16:09:16 +0000294 private class RLZTask extends AsyncTask<Void, Void, String> {
Russell Brenner14e1ae22011-01-12 14:54:23 -0800295 private Tab mTab;
Ben Murdocha941f6e2010-12-07 16:09:16 +0000296 private Uri mSiteUri;
297 private WebView mWebView;
298
Russell Brenner14e1ae22011-01-12 14:54:23 -0800299 public RLZTask(Tab tab, Uri uri, WebView webView) {
300 mTab = tab;
Ben Murdocha941f6e2010-12-07 16:09:16 +0000301 mSiteUri = uri;
302 mWebView = webView;
303 }
304
305 protected String doInBackground(Void... unused) {
306 String result = mSiteUri.toString();
307 Cursor cur = null;
308 try {
309 cur = mActivity.getContentResolver()
310 .query(getRlzUri(), null, null, null, null);
311 if (cur != null && cur.moveToFirst() && !cur.isNull(0)) {
312 result = mSiteUri.buildUpon()
313 .appendQueryParameter("rlz", cur.getString(0))
314 .build().toString();
315 }
316 } finally {
317 if (cur != null) {
318 cur.close();
319 }
320 }
321 return result;
322 }
323
324 protected void onPostExecute(String result) {
Michael Kolbe57c7092011-07-19 11:56:56 -0700325 // abort if we left browser already
326 if (mController.isActivityPaused()) return;
Russell Brennerca9898e2011-01-21 13:34:02 -0800327 // Make sure the Tab was not closed while handling the task
Michael Kolbc831b632011-05-11 09:30:34 -0700328 if (mController.getTabControl().getTabPosition(mTab) != -1) {
Russell Brennerca9898e2011-01-21 13:34:02 -0800329 // If the Activity Manager is not invoked, load the URL directly
John Reck8bcafc12011-08-29 16:43:02 -0700330 if (!startActivityForUrl(mTab, result)) {
Russell Brennerca9898e2011-01-21 13:34:02 -0800331 if (!handleMenuClick(mTab, result)) {
John Reck26b18322011-06-21 13:08:58 -0700332 mController.loadUrl(mTab, result);
Russell Brennerca9898e2011-01-21 13:34:02 -0800333 }
Russell Brenner14e1ae22011-01-12 14:54:23 -0800334 }
335 }
Ben Murdocha941f6e2010-12-07 16:09:16 +0000336 }
337 }
338
Michael Kolb8233fac2010-10-26 16:08:53 -0700339 // Determine whether the RLZ provider is present on the system.
340 private boolean rlzProviderPresent() {
341 if (mIsProviderPresent == null) {
342 PackageManager pm = mActivity.getPackageManager();
John Reck35e9dd62011-04-25 09:01:54 -0700343 mIsProviderPresent = pm.resolveContentProvider(RLZ_PROVIDER, 0) != null;
Michael Kolb8233fac2010-10-26 16:08:53 -0700344 }
345 return mIsProviderPresent;
346 }
347
348 // Retrieve the RLZ access point string and cache the URI used to
349 // retrieve RLZ values.
350 private Uri getRlzUri() {
351 if (mRlzUri == null) {
352 String ap = mActivity.getResources()
353 .getString(R.string.rlz_access_point);
John Reck35e9dd62011-04-25 09:01:54 -0700354 mRlzUri = Uri.withAppendedPath(RLZ_PROVIDER_URI, ap);
Michael Kolb8233fac2010-10-26 16:08:53 -0700355 }
356 return mRlzUri;
357 }
358
359 // Determine if this URI appears to be for a Google search
360 // and does not have an RLZ parameter.
361 // Taken largely from Chrome source, src/chrome/browser/google_url_tracker.cc
362 private static boolean needsRlzString(Uri uri) {
363 String scheme = uri.getScheme();
364 if (("http".equals(scheme) || "https".equals(scheme)) &&
365 (uri.getQueryParameter("q") != null) &&
366 (uri.getQueryParameter("rlz") == null)) {
367 String host = uri.getHost();
368 if (host == null) {
369 return false;
370 }
371 String[] hostComponents = host.split("\\.");
372
373 if (hostComponents.length < 2) {
374 return false;
375 }
376 int googleComponent = hostComponents.length - 2;
377 String component = hostComponents[googleComponent];
378 if (!"google".equals(component)) {
379 if (hostComponents.length < 3 ||
380 (!"co".equals(component) && !"com".equals(component))) {
381 return false;
382 }
383 googleComponent = hostComponents.length - 3;
384 if (!"google".equals(hostComponents[googleComponent])) {
385 return false;
386 }
387 }
388
389 // Google corp network handling.
390 if (googleComponent > 0 && "corp".equals(
391 hostComponents[googleComponent - 1])) {
392 return false;
393 }
394
395 return true;
396 }
397 return false;
398 }
399
400}