• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.browser;
18 
19 import android.app.Activity;
20 import android.content.ActivityNotFoundException;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.provider.Browser;
29 import android.util.Log;
30 import android.webkit.WebView;
31 
32 import java.net.URISyntaxException;
33 import java.util.List;
34 import java.util.regex.Matcher;
35 
36 /**
37  *
38  */
39 public class UrlHandler {
40 
41     static final String RLZ_PROVIDER = "com.google.android.partnersetup.rlzappprovider";
42     static final Uri RLZ_PROVIDER_URI = Uri.parse("content://" + RLZ_PROVIDER + "/");
43 
44     // Use in overrideUrlLoading
45     /* package */ final static String SCHEME_WTAI = "wtai://wp/";
46     /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;";
47     /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;";
48     /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;";
49 
50     Controller mController;
51     Activity mActivity;
52 
53     private Boolean mIsProviderPresent = null;
54     private Uri mRlzUri = null;
55 
UrlHandler(Controller controller)56     public UrlHandler(Controller controller) {
57         mController = controller;
58         mActivity = mController.getActivity();
59     }
60 
shouldOverrideUrlLoading(Tab tab, WebView view, String url)61     boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
62         if (view.isPrivateBrowsingEnabled()) {
63             // Don't allow urls to leave the browser app when in
64             // private browsing mode
65             return false;
66         }
67 
68         if (url.startsWith(SCHEME_WTAI)) {
69             // wtai://wp/mc;number
70             // number=string(phone-number)
71             if (url.startsWith(SCHEME_WTAI_MC)) {
72                 Intent intent = new Intent(Intent.ACTION_VIEW,
73                         Uri.parse(WebView.SCHEME_TEL +
74                         url.substring(SCHEME_WTAI_MC.length())));
75                 mActivity.startActivity(intent);
76                 // before leaving BrowserActivity, close the empty child tab.
77                 // If a new tab is created through JavaScript open to load this
78                 // url, we would like to close it as we will load this url in a
79                 // different Activity.
80                 mController.closeEmptyTab();
81                 return true;
82             }
83             // wtai://wp/sd;dtmf
84             // dtmf=string(dialstring)
85             if (url.startsWith(SCHEME_WTAI_SD)) {
86                 // TODO: only send when there is active voice connection
87                 return false;
88             }
89             // wtai://wp/ap;number;name
90             // number=string(phone-number)
91             // name=string
92             if (url.startsWith(SCHEME_WTAI_AP)) {
93                 // TODO
94                 return false;
95             }
96         }
97 
98         // The "about:" schemes are internal to the browser; don't want these to
99         // be dispatched to other apps.
100         if (url.startsWith("about:")) {
101             return false;
102         }
103 
104         // If this is a Google search, attempt to add an RLZ string
105         // (if one isn't already present).
106         if (rlzProviderPresent()) {
107             Uri siteUri = Uri.parse(url);
108             if (needsRlzString(siteUri)) {
109                 // Need to look up the RLZ info from a database, so do it in an
110                 // AsyncTask. Although we are not overriding the URL load synchronously,
111                 // we guarantee that we will handle this URL load after the task executes,
112                 // so it's safe to just return true to WebCore now to stop its own loading.
113                 new RLZTask(tab, siteUri, view).execute();
114                 return true;
115             }
116         }
117 
118         if (startActivityForUrl(tab, url)) {
119             return true;
120         }
121 
122         if (handleMenuClick(tab, url)) {
123             return true;
124         }
125 
126         return false;
127     }
128 
startActivityForUrl(Tab tab, String url)129     boolean startActivityForUrl(Tab tab, String url) {
130       Intent intent;
131       // perform generic parsing of the URI to turn it into an Intent.
132       try {
133           intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
134       } catch (URISyntaxException ex) {
135           Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
136           return false;
137       }
138 
139       // check whether the intent can be resolved. If not, we will see
140       // whether we can download it from the Market.
141       ResolveInfo r = null;
142       try {
143         r = mActivity.getPackageManager().resolveActivity(intent, 0);
144       } catch (Exception e) {
145         return false;
146       }
147       if (r == 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               try {
154                   mActivity.startActivity(intent);
155                   // before leaving BrowserActivity, close the empty child tab.
156                   // If a new tab is created through JavaScript open to load this
157                   // url, we would like to close it as we will load this url in a
158                   // different Activity.
159                   mController.closeEmptyTab();
160                   return true;
161               } catch (ActivityNotFoundException e) {
162                   Log.w("Browser", "No activity found to handle " + url);
163                   return false;
164               }
165             } else {
166               return false;
167           }
168       }
169 
170       // sanitize the Intent, ensuring web pages can not bypass browser
171       // security (only access to BROWSABLE activities).
172       intent.addCategory(Intent.CATEGORY_BROWSABLE);
173       intent.setComponent(null);
174       Intent selector = intent.getSelector();
175       if (selector != null) {
176           selector.addCategory(Intent.CATEGORY_BROWSABLE);
177           selector.setComponent(null);
178       }
179       // Re-use the existing tab if the intent comes back to us
180       if (tab != null) {
181           if (tab.getAppId() == null) {
182               tab.setAppId(mActivity.getPackageName() + "-" + tab.getId());
183           }
184           intent.putExtra(Browser.EXTRA_APPLICATION_ID, tab.getAppId());
185       }
186       // Make sure webkit can handle it internally before checking for specialized
187       // handlers. If webkit can't handle it internally, we need to call
188       // startActivityIfNeeded
189       Matcher m = UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url);
190       if (m.matches() && !isSpecializedHandlerAvailable(intent)) {
191           return false;
192       }
193       try {
194           intent.putExtra(BrowserActivity.EXTRA_DISABLE_URL_OVERRIDE, true);
195           if (mActivity.startActivityIfNeeded(intent, -1)) {
196               // before leaving BrowserActivity, close the empty child tab.
197               // If a new tab is created through JavaScript open to load this
198               // url, we would like to close it as we will load this url in a
199               // different Activity.
200               mController.closeEmptyTab();
201               return true;
202           }
203       } catch (ActivityNotFoundException ex) {
204           // ignore the error. If no application can handle the URL,
205           // eg about:blank, assume the browser can handle it.
206       }
207 
208       return false;
209     }
210 
211     /**
212      * Search for intent handlers that are specific to this URL
213      * aka, specialized apps like google maps or youtube
214      */
isSpecializedHandlerAvailable(Intent intent)215     private boolean isSpecializedHandlerAvailable(Intent intent) {
216         PackageManager pm = mActivity.getPackageManager();
217           List<ResolveInfo> handlers = pm.queryIntentActivities(intent,
218                   PackageManager.GET_RESOLVED_FILTER);
219           if (handlers == null || handlers.size() == 0) {
220               return false;
221           }
222           for (ResolveInfo resolveInfo : handlers) {
223               IntentFilter filter = resolveInfo.filter;
224               if (filter == null) {
225                   // No intent filter matches this intent?
226                   // Error on the side of staying in the browser, ignore
227                   continue;
228               }
229               if (filter.countDataAuthorities() == 0 && filter.countDataPaths() == 0) {
230                   // Generic handler, skip
231                   continue;
232               }
233               return true;
234           }
235           return false;
236     }
237 
238     // In case a physical keyboard is attached, handle clicks with the menu key
239     // depressed by opening in a new tab
handleMenuClick(Tab tab, String url)240     boolean handleMenuClick(Tab tab, String url) {
241         if (mController.isMenuDown()) {
242             mController.openTab(url,
243                     (tab != null) && tab.isPrivateBrowsingEnabled(),
244                     !BrowserSettings.getInstance().openInBackground(), true);
245             mActivity.closeOptionsMenu();
246             return true;
247         }
248 
249         return false;
250     }
251 
252     // TODO: Move this class into Tab, where it can be properly stopped upon
253     // closure of the tab
254     private class RLZTask extends AsyncTask<Void, Void, String> {
255         private Tab mTab;
256         private Uri mSiteUri;
257         private WebView mWebView;
258 
RLZTask(Tab tab, Uri uri, WebView webView)259         public RLZTask(Tab tab, Uri uri, WebView webView) {
260             mTab = tab;
261             mSiteUri = uri;
262             mWebView = webView;
263         }
264 
doInBackground(Void... unused)265         protected String doInBackground(Void... unused) {
266             String result = mSiteUri.toString();
267             Cursor cur = null;
268             try {
269                 cur = mActivity.getContentResolver()
270                         .query(getRlzUri(), null, null, null, null);
271                 if (cur != null && cur.moveToFirst() && !cur.isNull(0)) {
272                     result = mSiteUri.buildUpon()
273                            .appendQueryParameter("rlz", cur.getString(0))
274                            .build().toString();
275                 }
276             } finally {
277                 if (cur != null) {
278                     cur.close();
279                 }
280             }
281             return result;
282         }
283 
onPostExecute(String result)284         protected void onPostExecute(String result) {
285             // abort if we left browser already
286             if (mController.isActivityPaused()) return;
287             // Make sure the Tab was not closed while handling the task
288             if (mController.getTabControl().getTabPosition(mTab) != -1) {
289                 // If the Activity Manager is not invoked, load the URL directly
290                 if (!startActivityForUrl(mTab, result)) {
291                     if (!handleMenuClick(mTab, result)) {
292                         mController.loadUrl(mTab, result);
293                     }
294                 }
295             }
296         }
297     }
298 
299     // Determine whether the RLZ provider is present on the system.
rlzProviderPresent()300     private boolean rlzProviderPresent() {
301         if (mIsProviderPresent == null) {
302             PackageManager pm = mActivity.getPackageManager();
303             mIsProviderPresent = pm.resolveContentProvider(RLZ_PROVIDER, 0) != null;
304         }
305         return mIsProviderPresent;
306     }
307 
308     // Retrieve the RLZ access point string and cache the URI used to
309     // retrieve RLZ values.
getRlzUri()310     private Uri getRlzUri() {
311         if (mRlzUri == null) {
312             String ap = mActivity.getResources()
313                     .getString(R.string.rlz_access_point);
314             mRlzUri = Uri.withAppendedPath(RLZ_PROVIDER_URI, ap);
315         }
316         return mRlzUri;
317     }
318 
319     // Determine if this URI appears to be for a Google search
320     // and does not have an RLZ parameter.
321     // Taken largely from Chrome source, src/chrome/browser/google_url_tracker.cc
needsRlzString(Uri uri)322     private static boolean needsRlzString(Uri uri) {
323         String scheme = uri.getScheme();
324         if (("http".equals(scheme) || "https".equals(scheme)) &&
325             (uri.getQueryParameter("q") != null) &&
326                     (uri.getQueryParameter("rlz") == null)) {
327             String host = uri.getHost();
328             if (host == null) {
329                 return false;
330             }
331             String[] hostComponents = host.split("\\.");
332 
333             if (hostComponents.length < 2) {
334                 return false;
335             }
336             int googleComponent = hostComponents.length - 2;
337             String component = hostComponents[googleComponent];
338             if (!"google".equals(component)) {
339                 if (hostComponents.length < 3 ||
340                         (!"co".equals(component) && !"com".equals(component))) {
341                     return false;
342                 }
343                 googleComponent = hostComponents.length - 3;
344                 if (!"google".equals(hostComponents[googleComponent])) {
345                     return false;
346                 }
347             }
348 
349             // Google corp network handling.
350             if (googleComponent > 0 && "corp".equals(
351                     hostComponents[googleComponent - 1])) {
352                 return false;
353             }
354 
355             return true;
356         }
357         return false;
358     }
359 
360 }
361