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