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