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 17package com.android.browser; 18 19import android.app.Activity; 20import android.content.ActivityNotFoundException; 21import android.content.Intent; 22import android.content.IntentFilter; 23import android.content.pm.PackageManager; 24import android.content.pm.ResolveInfo; 25import android.database.Cursor; 26import android.net.Uri; 27import android.os.AsyncTask; 28import android.provider.Browser; 29import android.util.Log; 30import android.webkit.WebView; 31 32import java.net.URISyntaxException; 33import java.util.List; 34import java.util.regex.Matcher; 35 36/** 37 * 38 */ 39public 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 56 public UrlHandler(Controller controller) { 57 mController = controller; 58 mActivity = mController.getActivity(); 59 } 60 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 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 */ 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 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 259 public RLZTask(Tab tab, Uri uri, WebView webView) { 260 mTab = tab; 261 mSiteUri = uri; 262 mWebView = webView; 263 } 264 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 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. 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. 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 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