IntentHandler.java revision b4e831bf1ff62945199b89a77ad039be7bd16545
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
18package com.android.browser;
19
20import android.app.Activity;
21import android.app.PendingIntent;
22import android.app.PendingIntent.CanceledException;
23import android.app.SearchManager;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.Intent;
27import android.net.Uri;
28import android.nfc.NfcAdapter;
29import android.os.AsyncTask;
30import android.os.Bundle;
31import android.provider.Browser;
32import android.provider.MediaStore;
33import android.speech.RecognizerResultsIntent;
34import android.text.TextUtils;
35import android.util.Log;
36import android.util.Patterns;
37
38import com.android.browser.search.SearchEngine;
39import com.android.common.Search;
40import com.android.common.speech.LoggingEvents;
41
42import java.util.HashMap;
43import java.util.Iterator;
44import java.util.Map;
45
46/**
47 * Handle all browser related intents
48 */
49public class IntentHandler {
50
51    // "source" parameter for Google search suggested by the browser
52    final static String GOOGLE_SEARCH_SOURCE_SUGGEST = "browser-suggest";
53    // "source" parameter for Google search from unknown source
54    final static String GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown";
55
56    // Pending intent extra attached to browser intents that is broadcast when the page load
57    // completes.
58    // TODO move to android.provider.Browser & make public?
59    private static final String EXTRA_LOAD_COMPLETE_PENDINGINTENT = "load_complete_intent";
60    // extra attached to intent received via EXTRA_LOAD_COMPLETE_PENDINGINTENT indicating the
61    // time at which the load completed.
62    public static final String EXTRA_LOAD_COMPLETION_TIME = "completets";
63    // extra attached to intent received via EXTRA_LOAD_COMPLETE_PENDINGINTENT indicating if
64    // preloading was attempted.
65    public static final String EXTRA_PREFETCH_ATTEMPTED = "prefattempt";
66    // extra attached to intent received via EXTRA_LOAD_COMPLETE_PENDINGINTENT indicating if
67    // preloading succeeded.
68    public static final String EXTRA_PREFETCH_SUCCESS = "prefsuccess";
69
70
71
72    /* package */ static final UrlData EMPTY_URL_DATA = new UrlData(null);
73
74    private Activity mActivity;
75    private Controller mController;
76    private TabControl mTabControl;
77    private BrowserSettings mSettings;
78
79    public IntentHandler(Activity browser, Controller controller) {
80        mActivity = browser;
81        mController = controller;
82        mTabControl = mController.getTabControl();
83        mSettings = controller.getSettings();
84    }
85
86    void onNewIntent(Intent intent) {
87        mActivity.setIntent(null);
88        Tab current = mTabControl.getCurrentTab();
89        // When a tab is closed on exit, the current tab index is set to -1.
90        // Reset before proceed as Browser requires the current tab to be set.
91        if (current == null) {
92            // Try to reset the tab in case the index was incorrect.
93            current = mTabControl.getTab(0);
94            if (current == null) {
95                // No tabs at all so just ignore this intent.
96                return;
97            }
98            mController.setActiveTab(current);
99        }
100        final String action = intent.getAction();
101        final int flags = intent.getFlags();
102        if (Intent.ACTION_MAIN.equals(action) ||
103                (flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) {
104            // just resume the browser
105            return;
106        }
107        if (BrowserActivity.ACTION_SHOW_BOOKMARKS.equals(action)) {
108            mController.bookmarksOrHistoryPicker(false);
109            return;
110        }
111
112        // In case the SearchDialog is open.
113        ((SearchManager) mActivity.getSystemService(Context.SEARCH_SERVICE))
114                .stopSearch();
115        boolean activateVoiceSearch = RecognizerResultsIntent
116                .ACTION_VOICE_SEARCH_RESULTS.equals(action);
117        if (Intent.ACTION_VIEW.equals(action)
118                || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)
119                || Intent.ACTION_SEARCH.equals(action)
120                || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
121                || Intent.ACTION_WEB_SEARCH.equals(action)
122                || activateVoiceSearch) {
123            if (current.isInVoiceSearchMode()) {
124                String title = current.getVoiceDisplayTitle();
125                if (title != null && title.equals(intent.getStringExtra(
126                        SearchManager.QUERY))) {
127                    // The user submitted the same search as the last voice
128                    // search, so do nothing.
129                    return;
130                }
131                if (Intent.ACTION_SEARCH.equals(action)
132                        && current.voiceSearchSourceIsGoogle()) {
133                    Intent logIntent = new Intent(
134                            LoggingEvents.ACTION_LOG_EVENT);
135                    logIntent.putExtra(LoggingEvents.EXTRA_EVENT,
136                            LoggingEvents.VoiceSearch.QUERY_UPDATED);
137                    logIntent.putExtra(
138                            LoggingEvents.VoiceSearch.EXTRA_QUERY_UPDATED_VALUE,
139                            intent.getDataString());
140                    mActivity.sendBroadcast(logIntent);
141                    // Note, onPageStarted will revert the voice title bar
142                    // When http://b/issue?id=2379215 is fixed, we should update
143                    // the title bar here.
144                }
145            }
146            // If this was a search request (e.g. search query directly typed into the address bar),
147            // pass it on to the default web search provider.
148            if (handleWebSearchIntent(mActivity, mController, intent)) {
149                return;
150            }
151
152            UrlData urlData = getUrlDataFromIntent(intent);
153            if (urlData.isEmpty()) {
154                urlData = new UrlData(mSettings.getHomePage());
155            }
156
157            if (intent.getBooleanExtra(Browser.EXTRA_CREATE_NEW_TAB, false)
158                  || urlData.isPreloaded()) {
159                Tab t = mController.openTab(urlData);
160                return;
161            }
162            /*
163             * TODO: Don't allow javascript URIs
164             * 0) If this is a javascript: URI, *always* open a new tab
165             * 1) If this is a voice search, re-use tab for appId
166             *    If there is no appId, use current tab
167             * 2) If the URL is already opened, switch to that tab
168             * 3-phone) Reuse tab with same appId
169             * 3-tablet) Open new tab
170             */
171            final String appId = intent
172                    .getStringExtra(Browser.EXTRA_APPLICATION_ID);
173            if (!TextUtils.isEmpty(urlData.mUrl) &&
174                    urlData.mUrl.startsWith("javascript:")) {
175                // Always open javascript: URIs in new tabs
176                mController.openTab(urlData);
177                return;
178            }
179            if ((Intent.ACTION_VIEW.equals(action)
180                    // If a voice search has no appId, it means that it came
181                    // from the browser.  In that case, reuse the current tab.
182                    || (activateVoiceSearch && appId != null))
183                    && !mActivity.getPackageName().equals(appId)) {
184                if (activateVoiceSearch || !BrowserActivity.isTablet(mActivity)) {
185                    Tab appTab = mTabControl.getTabFromAppId(appId);
186                    if (appTab != null) {
187                        mController.reuseTab(appTab, urlData);
188                        return;
189                    }
190                }
191                // No matching application tab, try to find a regular tab
192                // with a matching url.
193                Tab appTab = mTabControl.findTabWithUrl(urlData.mUrl);
194                if (appTab != null) {
195                    // Transfer ownership
196                    appTab.setAppId(appId);
197                    if (current != appTab) {
198                        mController.switchToTab(appTab);
199                    }
200                    // Otherwise, we are already viewing the correct tab.
201                } else {
202                    // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url
203                    // will be opened in a new tab unless we have reached
204                    // MAX_TABS. Then the url will be opened in the current
205                    // tab. If a new tab is created, it will have "true" for
206                    // exit on close.
207                    Tab tab = mController.openTab(urlData);
208                    if (tab != null) {
209                        tab.setAppId(appId);
210                        if ((intent.getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
211                            tab.setCloseOnBack(true);
212                        }
213                    }
214                }
215            } else {
216                if (!urlData.isEmpty()
217                        && urlData.mUrl.startsWith("about:debug")) {
218                    if ("about:debug.dom".equals(urlData.mUrl)) {
219                        current.getWebView().dumpDomTree(false);
220                    } else if ("about:debug.dom.file".equals(urlData.mUrl)) {
221                        current.getWebView().dumpDomTree(true);
222                    } else if ("about:debug.render".equals(urlData.mUrl)) {
223                        current.getWebView().dumpRenderTree(false);
224                    } else if ("about:debug.render.file".equals(urlData.mUrl)) {
225                        current.getWebView().dumpRenderTree(true);
226                    } else if ("about:debug.display".equals(urlData.mUrl)) {
227                        current.getWebView().dumpDisplayTree();
228                    } else if ("about:debug.nav".equals(urlData.mUrl)) {
229                        current.getWebView().debugDump();
230                    } else {
231                        mSettings.toggleDebugSettings();
232                    }
233                    return;
234                }
235                // Get rid of the subwindow if it exists
236                mController.dismissSubWindow(current);
237                // If the current Tab is being used as an application tab,
238                // remove the association, since the new Intent means that it is
239                // no longer associated with that application.
240                current.setAppId(null);
241                mController.loadUrlDataIn(current, urlData);
242            }
243        }
244    }
245
246    /**
247     * Send a pending intent received in a page view intent. This should be called when the page
248     * has finished loading.
249     *
250     * @param prefetchAttempted Indicates if prefetching was attempted, {@code null} if prefetching
251     *      was not requested or is disabled.
252     * @param prefetchSucceeded Indicates if prefetching succeeded, {@code null} if prefetching
253     *      was not requested or is disabled.
254     */
255    public static void sendPageLoadCompletePendingIntent(Context context, PendingIntent pi,
256            Boolean prefetchAttempted, Boolean prefetchSucceeded) {
257        if (pi == null) return;
258        Intent fillIn = new Intent();
259        fillIn.putExtra(EXTRA_LOAD_COMPLETION_TIME, System.currentTimeMillis());
260        if (prefetchAttempted != null) {
261            fillIn.putExtra(EXTRA_PREFETCH_ATTEMPTED, prefetchAttempted.booleanValue());
262        }
263        if (prefetchSucceeded != null) {
264            fillIn.putExtra(EXTRA_PREFETCH_SUCCESS, prefetchSucceeded.booleanValue());
265        }
266        try {
267            pi.send(context, Activity.RESULT_OK, fillIn);
268        } catch (CanceledException e) {
269            // ignore
270        }
271    }
272
273    protected static UrlData getUrlDataFromIntent(Intent intent) {
274        String url = "";
275        Map<String, String> headers = null;
276        PreloadedTabControl preloaded = null;
277        String preloadedSearchBoxQuery = null;
278        PendingIntent loadCompletePendingIntent = null;
279        if (intent != null
280                && (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
281            final String action = intent.getAction();
282            if (Intent.ACTION_VIEW.equals(action) ||
283                    NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
284                url = UrlUtils.smartUrlFilter(intent.getData());
285                if (url != null && url.startsWith("http")) {
286                    final Bundle pairs = intent
287                            .getBundleExtra(Browser.EXTRA_HEADERS);
288                    if (pairs != null && !pairs.isEmpty()) {
289                        Iterator<String> iter = pairs.keySet().iterator();
290                        headers = new HashMap<String, String>();
291                        while (iter.hasNext()) {
292                            String key = iter.next();
293                            headers.put(key, pairs.getString(key));
294                        }
295                    }
296                }
297                if (intent.hasExtra(PreloadRequestReceiver.EXTRA_PRELOAD_ID)) {
298                    String id = intent.getStringExtra(PreloadRequestReceiver.EXTRA_PRELOAD_ID);
299                    preloadedSearchBoxQuery = intent.getStringExtra(
300                            PreloadRequestReceiver.EXTRA_SEARCHBOX_SETQUERY);
301                    preloaded = Preloader.getInstance().getPreloadedTab(id);
302                }
303                if (intent.hasExtra(EXTRA_LOAD_COMPLETE_PENDINGINTENT)) {
304                    loadCompletePendingIntent =
305                        intent.getParcelableExtra(EXTRA_LOAD_COMPLETE_PENDINGINTENT);
306                }
307            } else if (Intent.ACTION_SEARCH.equals(action)
308                    || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
309                    || Intent.ACTION_WEB_SEARCH.equals(action)) {
310                url = intent.getStringExtra(SearchManager.QUERY);
311                if (url != null) {
312                    // In general, we shouldn't modify URL from Intent.
313                    // But currently, we get the user-typed URL from search box as well.
314                    url = UrlUtils.fixUrl(url);
315                    url = UrlUtils.smartUrlFilter(url);
316                    String searchSource = "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&";
317                    if (url.contains(searchSource)) {
318                        String source = null;
319                        final Bundle appData = intent.getBundleExtra(SearchManager.APP_DATA);
320                        if (appData != null) {
321                            source = appData.getString(Search.SOURCE);
322                        }
323                        if (TextUtils.isEmpty(source)) {
324                            source = GOOGLE_SEARCH_SOURCE_UNKNOWN;
325                        }
326                        url = url.replace(searchSource, "&source=android-"+source+"&");
327                    }
328                }
329            }
330        }
331        return new UrlData(url, headers, intent, preloaded, preloadedSearchBoxQuery,
332                loadCompletePendingIntent);
333    }
334
335    /**
336     * Launches the default web search activity with the query parameters if the given intent's data
337     * are identified as plain search terms and not URLs/shortcuts.
338     * @return true if the intent was handled and web search activity was launched, false if not.
339     */
340    static boolean handleWebSearchIntent(Activity activity,
341            Controller controller, Intent intent) {
342        if (intent == null) return false;
343
344        String url = null;
345        final String action = intent.getAction();
346        if (RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS.equals(
347                action)) {
348            return false;
349        }
350        if (Intent.ACTION_VIEW.equals(action)) {
351            Uri data = intent.getData();
352            if (data != null) url = data.toString();
353        } else if (Intent.ACTION_SEARCH.equals(action)
354                || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
355                || Intent.ACTION_WEB_SEARCH.equals(action)) {
356            url = intent.getStringExtra(SearchManager.QUERY);
357        }
358        return handleWebSearchRequest(activity, controller, url,
359                intent.getBundleExtra(SearchManager.APP_DATA),
360                intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
361    }
362
363    /**
364     * Launches the default web search activity with the query parameters if the given url string
365     * was identified as plain search terms and not URL/shortcut.
366     * @return true if the request was handled and web search activity was launched, false if not.
367     */
368    private static boolean handleWebSearchRequest(Activity activity,
369            Controller controller, String inUrl, Bundle appData,
370            String extraData) {
371        if (inUrl == null) return false;
372
373        // In general, we shouldn't modify URL from Intent.
374        // But currently, we get the user-typed URL from search box as well.
375        String url = UrlUtils.fixUrl(inUrl).trim();
376        if (TextUtils.isEmpty(url)) return false;
377
378        // URLs are handled by the regular flow of control, so
379        // return early.
380        if (Patterns.WEB_URL.matcher(url).matches()
381                || UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url).matches()) {
382            return false;
383        }
384
385        final ContentResolver cr = activity.getContentResolver();
386        final String newUrl = url;
387        if (controller == null || controller.getTabControl() == null
388                || controller.getTabControl().getCurrentWebView() == null
389                || !controller.getTabControl().getCurrentWebView()
390                .isPrivateBrowsingEnabled()) {
391            new AsyncTask<Void, Void, Void>() {
392                @Override
393                protected Void doInBackground(Void... unused) {
394                        Browser.addSearchUrl(cr, newUrl);
395                    return null;
396                }
397            }.execute();
398        }
399
400        SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine();
401        if (searchEngine == null) return false;
402        searchEngine.startSearch(activity, url, appData, extraData);
403
404        return true;
405    }
406
407    /**
408     * A UrlData class to abstract how the content will be set to WebView.
409     * This base class uses loadUrl to show the content.
410     */
411    static class UrlData {
412        final String mUrl;
413        final Map<String, String> mHeaders;
414        final Intent mVoiceIntent;
415        final PreloadedTabControl mPreloadedTab;
416        final String mSearchBoxQueryToSubmit;
417        final PendingIntent mOnLoadCompletePendingIntent;
418
419        UrlData(String url) {
420            this.mUrl = url;
421            this.mHeaders = null;
422            this.mVoiceIntent = null;
423            this.mPreloadedTab = null;
424            this.mSearchBoxQueryToSubmit = null;
425            this.mOnLoadCompletePendingIntent = null;
426        }
427
428        UrlData(String url, Map<String, String> headers, Intent intent) {
429            this(url, headers, intent, null, null, null);
430        }
431
432        UrlData(String url, Map<String, String> headers, Intent intent,
433                PreloadedTabControl preloaded, String searchBoxQueryToSubmit,
434                PendingIntent onLoadCompletePendingIntent) {
435            this.mUrl = url;
436            this.mHeaders = headers;
437            if (RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS
438                    .equals(intent.getAction())) {
439                this.mVoiceIntent = intent;
440            } else {
441                this.mVoiceIntent = null;
442            }
443            this.mPreloadedTab = preloaded;
444            this.mSearchBoxQueryToSubmit = searchBoxQueryToSubmit;
445            this.mOnLoadCompletePendingIntent = onLoadCompletePendingIntent;
446        }
447
448        boolean isEmpty() {
449            return mVoiceIntent == null && (mUrl == null || mUrl.length() == 0);
450        }
451
452        boolean isPreloaded() {
453            return mPreloadedTab != null;
454        }
455
456        PreloadedTabControl getPreloadedTab() {
457            return mPreloadedTab;
458        }
459
460        String getSearchBoxQueryToSubmit() {
461            return mSearchBoxQueryToSubmit;
462        }
463
464        PendingIntent getOnLoadCompletePendingIntent() {
465            return mOnLoadCompletePendingIntent;
466        }
467    }
468
469}
470