Home | History | Annotate | Download | only in browser
      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       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