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