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 if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) { 142 String packagename = intent.getPackage(); 143 if (packagename != null) { 144 intent = new Intent(Intent.ACTION_VIEW, Uri 145 .parse("market://search?q=pname:" + packagename)); 146 intent.addCategory(Intent.CATEGORY_BROWSABLE); 147 mActivity.startActivity(intent); 148 // before leaving BrowserActivity, close the empty child tab. 149 // If a new tab is created through JavaScript open to load this 150 // url, we would like to close it as we will load this url in a 151 // different Activity. 152 mController.closeEmptyTab(); 153 return true; 154 } else { 155 return false; 156 } 157 } 158 159 // sanitize the Intent, ensuring web pages can not bypass browser 160 // security (only access to BROWSABLE activities). 161 intent.addCategory(Intent.CATEGORY_BROWSABLE); 162 intent.setComponent(null); 163 Intent selector = intent.getSelector(); 164 if (selector != null) { 165 selector.addCategory(Intent.CATEGORY_BROWSABLE); 166 selector.setComponent(null); 167 } 168 // Re-use the existing tab if the intent comes back to us 169 if (tab != null) { 170 if (tab.getAppId() == null) { 171 tab.setAppId(mActivity.getPackageName() + "-" + tab.getId()); 172 } 173 intent.putExtra(Browser.EXTRA_APPLICATION_ID, tab.getAppId()); 174 } 175 // Make sure webkit can handle it internally before checking for specialized 176 // handlers. If webkit can't handle it internally, we need to call 177 // startActivityIfNeeded 178 Matcher m = UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url); 179 if (m.matches() && !isSpecializedHandlerAvailable(intent)) { 180 return false; 181 } 182 try { 183 intent.putExtra(BrowserActivity.EXTRA_DISABLE_URL_OVERRIDE, true); 184 if (mActivity.startActivityIfNeeded(intent, -1)) { 185 // before leaving BrowserActivity, close the empty child tab. 186 // If a new tab is created through JavaScript open to load this 187 // url, we would like to close it as we will load this url in a 188 // different Activity. 189 mController.closeEmptyTab(); 190 return true; 191 } 192 } catch (ActivityNotFoundException ex) { 193 // ignore the error. If no application can handle the URL, 194 // eg about:blank, assume the browser can handle it. 195 } 196 197 return false; 198 } 199 200 /** 201 * Search for intent handlers that are specific to this URL 202 * aka, specialized apps like google maps or youtube 203 */ isSpecializedHandlerAvailable(Intent intent)204 private boolean isSpecializedHandlerAvailable(Intent intent) { 205 PackageManager pm = mActivity.getPackageManager(); 206 List<ResolveInfo> handlers = pm.queryIntentActivities(intent, 207 PackageManager.GET_RESOLVED_FILTER); 208 if (handlers == null || handlers.size() == 0) { 209 return false; 210 } 211 for (ResolveInfo resolveInfo : handlers) { 212 IntentFilter filter = resolveInfo.filter; 213 if (filter == null) { 214 // No intent filter matches this intent? 215 // Error on the side of staying in the browser, ignore 216 continue; 217 } 218 if (filter.countDataAuthorities() == 0 && filter.countDataPaths() == 0) { 219 // Generic handler, skip 220 continue; 221 } 222 return true; 223 } 224 return false; 225 } 226 227 // In case a physical keyboard is attached, handle clicks with the menu key 228 // depressed by opening in a new tab handleMenuClick(Tab tab, String url)229 boolean handleMenuClick(Tab tab, String url) { 230 if (mController.isMenuDown()) { 231 mController.openTab(url, 232 (tab != null) && tab.isPrivateBrowsingEnabled(), 233 !BrowserSettings.getInstance().openInBackground(), true); 234 mActivity.closeOptionsMenu(); 235 return true; 236 } 237 238 return false; 239 } 240 241 // TODO: Move this class into Tab, where it can be properly stopped upon 242 // closure of the tab 243 private class RLZTask extends AsyncTask<Void, Void, String> { 244 private Tab mTab; 245 private Uri mSiteUri; 246 private WebView mWebView; 247 RLZTask(Tab tab, Uri uri, WebView webView)248 public RLZTask(Tab tab, Uri uri, WebView webView) { 249 mTab = tab; 250 mSiteUri = uri; 251 mWebView = webView; 252 } 253 doInBackground(Void... unused)254 protected String doInBackground(Void... unused) { 255 String result = mSiteUri.toString(); 256 Cursor cur = null; 257 try { 258 cur = mActivity.getContentResolver() 259 .query(getRlzUri(), null, null, null, null); 260 if (cur != null && cur.moveToFirst() && !cur.isNull(0)) { 261 result = mSiteUri.buildUpon() 262 .appendQueryParameter("rlz", cur.getString(0)) 263 .build().toString(); 264 } 265 } finally { 266 if (cur != null) { 267 cur.close(); 268 } 269 } 270 return result; 271 } 272 onPostExecute(String result)273 protected void onPostExecute(String result) { 274 // abort if we left browser already 275 if (mController.isActivityPaused()) return; 276 // Make sure the Tab was not closed while handling the task 277 if (mController.getTabControl().getTabPosition(mTab) != -1) { 278 // If the Activity Manager is not invoked, load the URL directly 279 if (!startActivityForUrl(mTab, result)) { 280 if (!handleMenuClick(mTab, result)) { 281 mController.loadUrl(mTab, result); 282 } 283 } 284 } 285 } 286 } 287 288 // Determine whether the RLZ provider is present on the system. rlzProviderPresent()289 private boolean rlzProviderPresent() { 290 if (mIsProviderPresent == null) { 291 PackageManager pm = mActivity.getPackageManager(); 292 mIsProviderPresent = pm.resolveContentProvider(RLZ_PROVIDER, 0) != null; 293 } 294 return mIsProviderPresent; 295 } 296 297 // Retrieve the RLZ access point string and cache the URI used to 298 // retrieve RLZ values. getRlzUri()299 private Uri getRlzUri() { 300 if (mRlzUri == null) { 301 String ap = mActivity.getResources() 302 .getString(R.string.rlz_access_point); 303 mRlzUri = Uri.withAppendedPath(RLZ_PROVIDER_URI, ap); 304 } 305 return mRlzUri; 306 } 307 308 // Determine if this URI appears to be for a Google search 309 // and does not have an RLZ parameter. 310 // Taken largely from Chrome source, src/chrome/browser/google_url_tracker.cc needsRlzString(Uri uri)311 private static boolean needsRlzString(Uri uri) { 312 String scheme = uri.getScheme(); 313 if (("http".equals(scheme) || "https".equals(scheme)) && 314 (uri.getQueryParameter("q") != null) && 315 (uri.getQueryParameter("rlz") == null)) { 316 String host = uri.getHost(); 317 if (host == null) { 318 return false; 319 } 320 String[] hostComponents = host.split("\\."); 321 322 if (hostComponents.length < 2) { 323 return false; 324 } 325 int googleComponent = hostComponents.length - 2; 326 String component = hostComponents[googleComponent]; 327 if (!"google".equals(component)) { 328 if (hostComponents.length < 3 || 329 (!"co".equals(component) && !"com".equals(component))) { 330 return false; 331 } 332 googleComponent = hostComponents.length - 3; 333 if (!"google".equals(hostComponents[googleComponent])) { 334 return false; 335 } 336 } 337 338 // Google corp network handling. 339 if (googleComponent > 0 && "corp".equals( 340 hostComponents[googleComponent - 1])) { 341 return false; 342 } 343 344 return true; 345 } 346 return false; 347 } 348 349 } 350