BrowserActivity.java revision 0c33ba3800d105e094eb1bc5c5af9892ab21733f
1/*
2 * Copyright (C) 2006 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.app.AlertDialog;
21import android.app.ProgressDialog;
22import android.app.SearchManager;
23import android.content.ActivityNotFoundException;
24import android.content.BroadcastReceiver;
25import android.content.ComponentName;
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.content.ContentValues;
29import android.content.Context;
30import android.content.DialogInterface;
31import android.content.Intent;
32import android.content.IntentFilter;
33import android.content.pm.PackageInfo;
34import android.content.pm.PackageManager;
35import android.content.pm.ResolveInfo;
36import android.content.res.Configuration;
37import android.content.res.Resources;
38import android.database.Cursor;
39import android.graphics.Bitmap;
40import android.graphics.BitmapFactory;
41import android.graphics.Canvas;
42import android.graphics.Picture;
43import android.graphics.PixelFormat;
44import android.graphics.Rect;
45import android.graphics.drawable.Drawable;
46import android.net.ConnectivityManager;
47import android.net.Uri;
48import android.net.WebAddress;
49import android.net.http.SslCertificate;
50import android.net.http.SslError;
51import android.os.AsyncTask;
52import android.os.Bundle;
53import android.os.Debug;
54import android.os.Environment;
55import android.os.Handler;
56import android.os.Message;
57import android.os.PowerManager;
58import android.os.Process;
59import android.os.ServiceManager;
60import android.os.SystemClock;
61import android.provider.Browser;
62import android.provider.ContactsContract;
63import android.provider.ContactsContract.Intents.Insert;
64import android.provider.Downloads;
65import android.provider.MediaStore;
66import android.text.IClipboard;
67import android.text.TextUtils;
68import android.text.format.DateFormat;
69import android.util.AttributeSet;
70import android.util.Log;
71import android.view.ContextMenu;
72import android.view.Gravity;
73import android.view.KeyEvent;
74import android.view.LayoutInflater;
75import android.view.Menu;
76import android.view.MenuInflater;
77import android.view.MenuItem;
78import android.view.View;
79import android.view.ViewGroup;
80import android.view.Window;
81import android.view.WindowManager;
82import android.view.ContextMenu.ContextMenuInfo;
83import android.view.MenuItem.OnMenuItemClickListener;
84import android.webkit.CookieManager;
85import android.webkit.CookieSyncManager;
86import android.webkit.DownloadListener;
87import android.webkit.HttpAuthHandler;
88import android.webkit.PluginManager;
89import android.webkit.SslErrorHandler;
90import android.webkit.URLUtil;
91import android.webkit.ValueCallback;
92import android.webkit.WebChromeClient;
93import android.webkit.WebHistoryItem;
94import android.webkit.WebIconDatabase;
95import android.webkit.WebView;
96import android.widget.EditText;
97import android.widget.FrameLayout;
98import android.widget.LinearLayout;
99import android.widget.TextView;
100import android.widget.Toast;
101import android.accounts.Account;
102import android.accounts.AccountManager;
103import android.accounts.AccountManagerFuture;
104import android.accounts.AuthenticatorException;
105import android.accounts.OperationCanceledException;
106import android.accounts.AccountManagerCallback;
107
108import com.android.common.Patterns;
109
110import com.google.android.googlelogin.GoogleLoginServiceConstants;
111
112import java.io.ByteArrayOutputStream;
113import java.io.File;
114import java.io.IOException;
115import java.io.InputStream;
116import java.net.MalformedURLException;
117import java.net.URI;
118import java.net.URISyntaxException;
119import java.net.URL;
120import java.net.URLEncoder;
121import java.text.ParseException;
122import java.util.Date;
123import java.util.HashMap;
124import java.util.regex.Matcher;
125import java.util.regex.Pattern;
126
127public class BrowserActivity extends Activity
128    implements View.OnCreateContextMenuListener, DownloadListener,
129        AccountManagerCallback<Account[]> {
130
131    /* Define some aliases to make these debugging flags easier to refer to.
132     * This file imports android.provider.Browser, so we can't just refer to "Browser.DEBUG".
133     */
134    private final static boolean DEBUG = com.android.browser.Browser.DEBUG;
135    private final static boolean LOGV_ENABLED = com.android.browser.Browser.LOGV_ENABLED;
136    private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
137
138    // These are single-character shortcuts for searching popular sources.
139    private static final int SHORTCUT_INVALID = 0;
140    private static final int SHORTCUT_GOOGLE_SEARCH = 1;
141    private static final int SHORTCUT_WIKIPEDIA_SEARCH = 2;
142    private static final int SHORTCUT_DICTIONARY_SEARCH = 3;
143    private static final int SHORTCUT_GOOGLE_MOBILE_LOCAL_SEARCH = 4;
144
145    private Account[] mAccountsGoogle;
146    private Account[] mAccountsPreferHosted;
147
148    private void startReadOfGoogleAccounts() {
149        mAccountsGoogle = null;
150        mAccountsPreferHosted = null;
151
152        AccountManager.get(this).getAccountsByTypeAndFeatures(
153                GoogleLoginServiceConstants.ACCOUNT_TYPE,
154                new String[]{GoogleLoginServiceConstants.FEATURE_LEGACY_HOSTED_OR_GOOGLE},
155                this, null);
156    }
157
158    /** This implements AccountManagerCallback<Account[]> */
159    public void run(AccountManagerFuture<Account[]> accountManagerFuture) {
160        try {
161            if (mAccountsGoogle == null) {
162                mAccountsGoogle = accountManagerFuture.getResult();
163
164                AccountManager.get(this).getAccountsByTypeAndFeatures(
165                        GoogleLoginServiceConstants.ACCOUNT_TYPE,
166                        new String[]{GoogleLoginServiceConstants.FEATURE_LEGACY_GOOGLE},
167                        this, null);
168            } else {
169                mAccountsPreferHosted = accountManagerFuture.getResult();
170                setupHomePage();
171            }
172        } catch (OperationCanceledException e) {
173            setupHomePage();
174        } catch (IOException e) {
175            setupHomePage();
176        } catch (AuthenticatorException e) {
177            setupHomePage();
178        }
179    }
180
181    private void setupHomePage() {
182        // get the default home page
183        String homepage = mSettings.getHomePage();
184
185        if (mAccountsPreferHosted != null && mAccountsGoogle != null) {
186            // three cases:
187            //
188            //   hostedUser == googleUser
189            //      The device has only a google account
190            //
191            //   hostedUser != googleUser
192            //      The device has a hosted account and a google account
193            //
194            //   hostedUser != null, googleUser == null
195            //      The device has only a hosted account (so far)
196            String hostedUser = mAccountsPreferHosted.length == 0
197                    ? null
198                    : mAccountsPreferHosted[0].name;
199            String googleUser = mAccountsGoogle.length == 0 ? null : mAccountsGoogle[0].name;
200
201            // developers might have no accounts at all
202            if (hostedUser == null) return;
203
204            if (googleUser == null || !hostedUser.equals(googleUser)) {
205                String domain = hostedUser.substring(hostedUser.lastIndexOf('@')+1);
206                homepage = homepage.replace("?", "/a/" + domain + "?");
207            }
208        }
209
210        mSettings.setHomePage(BrowserActivity.this, homepage);
211        resumeAfterCredentials();
212    }
213
214    private static class ClearThumbnails extends AsyncTask<File, Void, Void> {
215        @Override
216        public Void doInBackground(File... files) {
217            if (files != null) {
218                for (File f : files) {
219                    if (!f.delete()) {
220                      Log.e(LOGTAG, f.getPath() + " was not deleted");
221                    }
222                }
223            }
224            return null;
225        }
226    }
227
228    /**
229     * This layout holds everything you see below the status bar, including the
230     * error console, the custom view container, and the webviews.
231     */
232    private FrameLayout mBrowserFrameLayout;
233
234    @Override
235    public void onCreate(Bundle icicle) {
236        if (LOGV_ENABLED) {
237            Log.v(LOGTAG, this + " onStart");
238        }
239        super.onCreate(icicle);
240        // test the browser in OpenGL
241        // requestWindowFeature(Window.FEATURE_OPENGL);
242
243        setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
244
245        mResolver = getContentResolver();
246
247        // If this was a web search request, pass it on to the default web
248        // search provider and finish this activity.
249        if (handleWebSearchIntent(getIntent())) {
250            finish();
251            return;
252        }
253
254        mSecLockIcon = Resources.getSystem().getDrawable(
255                android.R.drawable.ic_secure);
256        mMixLockIcon = Resources.getSystem().getDrawable(
257                android.R.drawable.ic_partial_secure);
258
259        FrameLayout frameLayout = (FrameLayout) getWindow().getDecorView()
260                .findViewById(com.android.internal.R.id.content);
261        mBrowserFrameLayout = (FrameLayout) LayoutInflater.from(this)
262                .inflate(R.layout.custom_screen, null);
263        mContentView = (FrameLayout) mBrowserFrameLayout.findViewById(
264                R.id.main_content);
265        mErrorConsoleContainer = (LinearLayout) mBrowserFrameLayout
266                .findViewById(R.id.error_console);
267        mCustomViewContainer = (FrameLayout) mBrowserFrameLayout
268                .findViewById(R.id.fullscreen_custom_content);
269        frameLayout.addView(mBrowserFrameLayout, COVER_SCREEN_PARAMS);
270        mTitleBar = new TitleBar(this);
271        mFakeTitleBar = new TitleBar(this);
272
273        // Create the tab control and our initial tab
274        mTabControl = new TabControl(this);
275
276        // Open the icon database and retain all the bookmark urls for favicons
277        retainIconsOnStartup();
278
279        // Keep a settings instance handy.
280        mSettings = BrowserSettings.getInstance();
281        mSettings.setTabControl(mTabControl);
282        mSettings.loadFromDb(this);
283
284        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
285        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Browser");
286
287        /* enables registration for changes in network status from
288           http stack */
289        mNetworkStateChangedFilter = new IntentFilter();
290        mNetworkStateChangedFilter.addAction(
291                ConnectivityManager.CONNECTIVITY_ACTION);
292        mNetworkStateIntentReceiver = new BroadcastReceiver() {
293                @Override
294                public void onReceive(Context context, Intent intent) {
295                    if (intent.getAction().equals(
296                            ConnectivityManager.CONNECTIVITY_ACTION)) {
297                        boolean noConnectivity = intent.getBooleanExtra(
298                                ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
299                        onNetworkToggle(!noConnectivity);
300                    }
301                }
302            };
303
304        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
305        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
306        filter.addDataScheme("package");
307        mPackageInstallationReceiver = new BroadcastReceiver() {
308            @Override
309            public void onReceive(Context context, Intent intent) {
310                final String action = intent.getAction();
311                final String packageName = intent.getData()
312                        .getSchemeSpecificPart();
313                final boolean replacing = intent.getBooleanExtra(
314                        Intent.EXTRA_REPLACING, false);
315                if (Intent.ACTION_PACKAGE_REMOVED.equals(action) && replacing) {
316                    // if it is replacing, refreshPlugins() when adding
317                    return;
318                }
319                PackageManager pm = BrowserActivity.this.getPackageManager();
320                PackageInfo pkgInfo = null;
321                try {
322                    pkgInfo = pm.getPackageInfo(packageName,
323                            PackageManager.GET_PERMISSIONS);
324                } catch (PackageManager.NameNotFoundException e) {
325                    return;
326                }
327                if (pkgInfo != null) {
328                    String permissions[] = pkgInfo.requestedPermissions;
329                    if (permissions == null) {
330                        return;
331                    }
332                    boolean permissionOk = false;
333                    for (String permit : permissions) {
334                        if (PluginManager.PLUGIN_PERMISSION.equals(permit)) {
335                            permissionOk = true;
336                            break;
337                        }
338                    }
339                    if (permissionOk) {
340                        PluginManager.getInstance(BrowserActivity.this)
341                                .refreshPlugins(
342                                        Intent.ACTION_PACKAGE_ADDED
343                                                .equals(action));
344                    }
345                }
346            }
347        };
348        registerReceiver(mPackageInstallationReceiver, filter);
349
350        if (!mTabControl.restoreState(icicle)) {
351            // clear up the thumbnail directory if we can't restore the state as
352            // none of the files in the directory are referenced any more.
353            new ClearThumbnails().execute(
354                    mTabControl.getThumbnailDir().listFiles());
355            // there is no quit on Android. But if we can't restore the state,
356            // we can treat it as a new Browser, remove the old session cookies.
357            CookieManager.getInstance().removeSessionCookie();
358            final Intent intent = getIntent();
359            final Bundle extra = intent.getExtras();
360            // Create an initial tab.
361            // If the intent is ACTION_VIEW and data is not null, the Browser is
362            // invoked to view the content by another application. In this case,
363            // the tab will be close when exit.
364            UrlData urlData = getUrlDataFromIntent(intent);
365
366            final Tab t = mTabControl.createNewTab(
367                    Intent.ACTION_VIEW.equals(intent.getAction()) &&
368                    intent.getData() != null,
369                    intent.getStringExtra(Browser.EXTRA_APPLICATION_ID), urlData.mUrl);
370            mTabControl.setCurrentTab(t);
371            attachTabToContentView(t);
372            WebView webView = t.getWebView();
373            if (extra != null) {
374                int scale = extra.getInt(Browser.INITIAL_ZOOM_LEVEL, 0);
375                if (scale > 0 && scale <= 1000) {
376                    webView.setInitialScale(scale);
377                }
378            }
379            // If we are not restoring from an icicle, then there is a high
380            // likely hood this is the first run. So, check to see if the
381            // homepage needs to be configured and copy any plugins from our
382            // asset directory to the data partition.
383            if ((extra == null || !extra.getBoolean("testing"))
384                    && !mSettings.isLoginInitialized()) {
385                startReadOfGoogleAccounts();
386            }
387
388            if (urlData.isEmpty()) {
389                if (mSettings.isLoginInitialized()) {
390                    webView.loadUrl(mSettings.getHomePage());
391                } else {
392                    waitForCredentials();
393                }
394            } else {
395                if (extra != null) {
396                    urlData.setPostData(extra
397                            .getByteArray(Browser.EXTRA_POST_DATA));
398                }
399                urlData.loadIn(webView);
400            }
401        } else {
402            // TabControl.restoreState() will create a new tab even if
403            // restoring the state fails.
404            attachTabToContentView(mTabControl.getCurrentTab());
405        }
406
407        // Read JavaScript flags if it exists.
408        String jsFlags = mSettings.getJsFlags();
409        if (jsFlags.trim().length() != 0) {
410            mTabControl.getCurrentWebView().setJsFlags(jsFlags);
411        }
412    }
413
414    @Override
415    protected void onNewIntent(Intent intent) {
416        Tab current = mTabControl.getCurrentTab();
417        // When a tab is closed on exit, the current tab index is set to -1.
418        // Reset before proceed as Browser requires the current tab to be set.
419        if (current == null) {
420            // Try to reset the tab in case the index was incorrect.
421            current = mTabControl.getTab(0);
422            if (current == null) {
423                // No tabs at all so just ignore this intent.
424                return;
425            }
426            mTabControl.setCurrentTab(current);
427            attachTabToContentView(current);
428            resetTitleAndIcon(current.getWebView());
429        }
430        final String action = intent.getAction();
431        final int flags = intent.getFlags();
432        if (Intent.ACTION_MAIN.equals(action) ||
433                (flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) {
434            // just resume the browser
435            return;
436        }
437        if (Intent.ACTION_VIEW.equals(action)
438                || Intent.ACTION_SEARCH.equals(action)
439                || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
440                || Intent.ACTION_WEB_SEARCH.equals(action)) {
441            // If this was a search request (e.g. search query directly typed into the address bar),
442            // pass it on to the default web search provider.
443            if (handleWebSearchIntent(intent)) {
444                return;
445            }
446
447            UrlData urlData = getUrlDataFromIntent(intent);
448            if (urlData.isEmpty()) {
449                urlData = new UrlData(mSettings.getHomePage());
450            }
451            urlData.setPostData(intent
452                    .getByteArrayExtra(Browser.EXTRA_POST_DATA));
453
454            final String appId = intent
455                    .getStringExtra(Browser.EXTRA_APPLICATION_ID);
456            if (Intent.ACTION_VIEW.equals(action)
457                    && !getPackageName().equals(appId)
458                    && (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
459                Tab appTab = mTabControl.getTabFromId(appId);
460                if (appTab != null) {
461                    Log.i(LOGTAG, "Reusing tab for " + appId);
462                    // Dismiss the subwindow if applicable.
463                    dismissSubWindow(appTab);
464                    // Since we might kill the WebView, remove it from the
465                    // content view first.
466                    removeTabFromContentView(appTab);
467                    // Recreate the main WebView after destroying the old one.
468                    // If the WebView has the same original url and is on that
469                    // page, it can be reused.
470                    boolean needsLoad =
471                            mTabControl.recreateWebView(appTab, urlData.mUrl);
472
473                    if (current != appTab) {
474                        switchToTab(mTabControl.getTabIndex(appTab));
475                        if (needsLoad) {
476                            urlData.loadIn(appTab.getWebView());
477                        }
478                    } else {
479                        // If the tab was the current tab, we have to attach
480                        // it to the view system again.
481                        attachTabToContentView(appTab);
482                        if (needsLoad) {
483                            urlData.loadIn(appTab.getWebView());
484                        }
485                    }
486                    return;
487                } else {
488                    // No matching application tab, try to find a regular tab
489                    // with a matching url.
490                    appTab = mTabControl.findUnusedTabWithUrl(urlData.mUrl);
491                    if (appTab != null) {
492                        if (current != appTab) {
493                            switchToTab(mTabControl.getTabIndex(appTab));
494                        }
495                        // Otherwise, we are already viewing the correct tab.
496                    } else {
497                        // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url
498                        // will be opened in a new tab unless we have reached
499                        // MAX_TABS. Then the url will be opened in the current
500                        // tab. If a new tab is created, it will have "true" for
501                        // exit on close.
502                        openTabAndShow(urlData, true, appId);
503                    }
504                }
505            } else {
506                if (!urlData.isEmpty()
507                        && urlData.mUrl.startsWith("about:debug")) {
508                    if ("about:debug.dom".equals(urlData.mUrl)) {
509                        current.getWebView().dumpDomTree(false);
510                    } else if ("about:debug.dom.file".equals(urlData.mUrl)) {
511                        current.getWebView().dumpDomTree(true);
512                    } else if ("about:debug.render".equals(urlData.mUrl)) {
513                        current.getWebView().dumpRenderTree(false);
514                    } else if ("about:debug.render.file".equals(urlData.mUrl)) {
515                        current.getWebView().dumpRenderTree(true);
516                    } else if ("about:debug.display".equals(urlData.mUrl)) {
517                        current.getWebView().dumpDisplayTree();
518                    } else if ("about:debug.drag0".equals(urlData.mUrl)) {
519                        current.getWebView().setDragTracker(null);
520                    } else if ("about:debug.drag1".equals(urlData.mUrl)) {
521                        current.getWebView().setDragTracker(new MeshTracker(1));
522                    } else if ("about:debug.drag2".equals(urlData.mUrl)) {
523                        current.getWebView().setDragTracker(new MeshTracker(2));
524                    } else {
525                        mSettings.toggleDebugSettings();
526                    }
527                    return;
528                }
529                // Get rid of the subwindow if it exists
530                dismissSubWindow(current);
531                urlData.loadIn(current.getWebView());
532            }
533        }
534    }
535
536    private int parseUrlShortcut(String url) {
537        if (url == null) return SHORTCUT_INVALID;
538
539        // FIXME: quick search, need to be customized by setting
540        if (url.length() > 2 && url.charAt(1) == ' ') {
541            switch (url.charAt(0)) {
542            case 'g': return SHORTCUT_GOOGLE_SEARCH;
543            case 'w': return SHORTCUT_WIKIPEDIA_SEARCH;
544            case 'd': return SHORTCUT_DICTIONARY_SEARCH;
545            case 'l': return SHORTCUT_GOOGLE_MOBILE_LOCAL_SEARCH;
546            }
547        }
548        return SHORTCUT_INVALID;
549    }
550
551    /**
552     * Launches the default web search activity with the query parameters if the given intent's data
553     * are identified as plain search terms and not URLs/shortcuts.
554     * @return true if the intent was handled and web search activity was launched, false if not.
555     */
556    private boolean handleWebSearchIntent(Intent intent) {
557        if (intent == null) return false;
558
559        String url = null;
560        final String action = intent.getAction();
561        if (Intent.ACTION_VIEW.equals(action)) {
562            Uri data = intent.getData();
563            if (data != null) url = data.toString();
564        } else if (Intent.ACTION_SEARCH.equals(action)
565                || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
566                || Intent.ACTION_WEB_SEARCH.equals(action)) {
567            url = intent.getStringExtra(SearchManager.QUERY);
568        }
569        return handleWebSearchRequest(url, intent.getBundleExtra(SearchManager.APP_DATA),
570                intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
571    }
572
573    /**
574     * Launches the default web search activity with the query parameters if the given url string
575     * was identified as plain search terms and not URL/shortcut.
576     * @return true if the request was handled and web search activity was launched, false if not.
577     */
578    private boolean handleWebSearchRequest(String inUrl, Bundle appData, String extraData) {
579        if (inUrl == null) return false;
580
581        // In general, we shouldn't modify URL from Intent.
582        // But currently, we get the user-typed URL from search box as well.
583        String url = fixUrl(inUrl).trim();
584
585        // URLs and site specific search shortcuts are handled by the regular flow of control, so
586        // return early.
587        if (Patterns.WEB_URL.matcher(url).matches()
588                || ACCEPTED_URI_SCHEMA.matcher(url).matches()
589                || parseUrlShortcut(url) != SHORTCUT_INVALID) {
590            return false;
591        }
592
593        Browser.updateVisitedHistory(mResolver, url, false);
594        Browser.addSearchUrl(mResolver, url);
595
596        Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
597        intent.addCategory(Intent.CATEGORY_DEFAULT);
598        intent.putExtra(SearchManager.QUERY, url);
599        if (appData != null) {
600            intent.putExtra(SearchManager.APP_DATA, appData);
601        }
602        if (extraData != null) {
603            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
604        }
605        intent.putExtra(Browser.EXTRA_APPLICATION_ID, getPackageName());
606        startActivity(intent);
607
608        return true;
609    }
610
611    private UrlData getUrlDataFromIntent(Intent intent) {
612        String url = null;
613        if (intent != null) {
614            final String action = intent.getAction();
615            if (Intent.ACTION_VIEW.equals(action)) {
616                url = smartUrlFilter(intent.getData());
617                if (url != null && url.startsWith("content:")) {
618                    /* Append mimetype so webview knows how to display */
619                    String mimeType = intent.resolveType(getContentResolver());
620                    if (mimeType != null) {
621                        url += "?" + mimeType;
622                    }
623                }
624                if ("inline:".equals(url)) {
625                    return new InlinedUrlData(
626                            intent.getStringExtra(Browser.EXTRA_INLINE_CONTENT),
627                            intent.getType(),
628                            intent.getStringExtra(Browser.EXTRA_INLINE_ENCODING),
629                            intent.getStringExtra(Browser.EXTRA_INLINE_FAILURL));
630                }
631            } else if (Intent.ACTION_SEARCH.equals(action)
632                    || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
633                    || Intent.ACTION_WEB_SEARCH.equals(action)) {
634                url = intent.getStringExtra(SearchManager.QUERY);
635                if (url != null) {
636                    mLastEnteredUrl = url;
637                    Browser.updateVisitedHistory(mResolver, url, false);
638                    // In general, we shouldn't modify URL from Intent.
639                    // But currently, we get the user-typed URL from search box as well.
640                    url = fixUrl(url);
641                    url = smartUrlFilter(url);
642                    String searchSource = "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&";
643                    if (url.contains(searchSource)) {
644                        String source = null;
645                        final Bundle appData = intent.getBundleExtra(SearchManager.APP_DATA);
646                        if (appData != null) {
647                            source = appData.getString(SearchManager.SOURCE);
648                        }
649                        if (TextUtils.isEmpty(source)) {
650                            source = GOOGLE_SEARCH_SOURCE_UNKNOWN;
651                        }
652                        url = url.replace(searchSource, "&source=android-"+source+"&");
653                    }
654                }
655            }
656        }
657        return new UrlData(url);
658    }
659
660    /* package */ static String fixUrl(String inUrl) {
661        // FIXME: Converting the url to lower case
662        // duplicates functionality in smartUrlFilter().
663        // However, changing all current callers of fixUrl to
664        // call smartUrlFilter in addition may have unwanted
665        // consequences, and is deferred for now.
666        int colon = inUrl.indexOf(':');
667        boolean allLower = true;
668        for (int index = 0; index < colon; index++) {
669            char ch = inUrl.charAt(index);
670            if (!Character.isLetter(ch)) {
671                break;
672            }
673            allLower &= Character.isLowerCase(ch);
674            if (index == colon - 1 && !allLower) {
675                inUrl = inUrl.substring(0, colon).toLowerCase()
676                        + inUrl.substring(colon);
677            }
678        }
679        if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
680            return inUrl;
681        if (inUrl.startsWith("http:") ||
682                inUrl.startsWith("https:")) {
683            if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
684                inUrl = inUrl.replaceFirst("/", "//");
685            } else inUrl = inUrl.replaceFirst(":", "://");
686        }
687        return inUrl;
688    }
689
690    @Override
691    protected void onResume() {
692        super.onResume();
693        if (LOGV_ENABLED) {
694            Log.v(LOGTAG, "BrowserActivity.onResume: this=" + this);
695        }
696
697        if (!mActivityInPause) {
698            Log.e(LOGTAG, "BrowserActivity is already resumed.");
699            return;
700        }
701
702        mTabControl.resumeCurrentTab();
703        mActivityInPause = false;
704        resumeWebViewTimers();
705
706        if (mWakeLock.isHeld()) {
707            mHandler.removeMessages(RELEASE_WAKELOCK);
708            mWakeLock.release();
709        }
710
711        if (mCredsDlg != null) {
712            if (!mHandler.hasMessages(CANCEL_CREDS_REQUEST)) {
713             // In case credential request never comes back
714                mHandler.sendEmptyMessageDelayed(CANCEL_CREDS_REQUEST, 6000);
715            }
716        }
717
718        registerReceiver(mNetworkStateIntentReceiver,
719                         mNetworkStateChangedFilter);
720        WebView.enablePlatformNotifications();
721    }
722
723    /**
724     * Since the actual title bar is embedded in the WebView, and removing it
725     * would change its appearance, use a different TitleBar to show overlayed
726     * at the top of the screen, when the menu is open or the page is loading.
727     */
728    private TitleBar mFakeTitleBar;
729
730    /**
731     * Holder for the fake title bar.  It will have a foreground shadow, as well
732     * as a white background, so the fake title bar looks like the real one.
733     */
734    private ViewGroup mFakeTitleBarHolder;
735
736    /**
737     * Layout parameters for the fake title bar within mFakeTitleBarHolder
738     */
739    private FrameLayout.LayoutParams mFakeTitleBarParams
740            = new FrameLayout.LayoutParams(
741            ViewGroup.LayoutParams.MATCH_PARENT,
742            ViewGroup.LayoutParams.WRAP_CONTENT);
743    /**
744     * Keeps track of whether the options menu is open.  This is important in
745     * determining whether to show or hide the title bar overlay.
746     */
747    private boolean mOptionsMenuOpen;
748
749    /**
750     * Only meaningful when mOptionsMenuOpen is true.  This variable keeps track
751     * of whether the configuration has changed.  The first onMenuOpened call
752     * after a configuration change is simply a reopening of the same menu
753     * (i.e. mIconView did not change).
754     */
755    private boolean mConfigChanged;
756
757    /**
758     * Whether or not the options menu is in its smaller, icon menu form.  When
759     * true, we want the title bar overlay to be up.  When false, we do not.
760     * Only meaningful if mOptionsMenuOpen is true.
761     */
762    private boolean mIconView;
763
764    @Override
765    public boolean onMenuOpened(int featureId, Menu menu) {
766        if (Window.FEATURE_OPTIONS_PANEL == featureId) {
767            if (mOptionsMenuOpen) {
768                if (mConfigChanged) {
769                    // We do not need to make any changes to the state of the
770                    // title bar, since the only thing that happened was a
771                    // change in orientation
772                    mConfigChanged = false;
773                } else {
774                    if (mIconView) {
775                        // Switching the menu to expanded view, so hide the
776                        // title bar.
777                        hideFakeTitleBar();
778                        mIconView = false;
779                    } else {
780                        // Switching the menu back to icon view, so show the
781                        // title bar once again.
782                        showFakeTitleBar();
783                        mIconView = true;
784                    }
785                }
786            } else {
787                // The options menu is closed, so open it, and show the title
788                showFakeTitleBar();
789                mOptionsMenuOpen = true;
790                mConfigChanged = false;
791                mIconView = true;
792            }
793        }
794        return true;
795    }
796
797    /**
798     * Special class used exclusively for the shadow drawn underneath the fake
799     * title bar.  The shadow does not need to be drawn if the WebView
800     * underneath is scrolled to the top, because it will draw directly on top
801     * of the embedded shadow.
802     */
803    private static class Shadow extends View {
804        private WebView mWebView;
805
806        public Shadow(Context context, AttributeSet attrs) {
807            super(context, attrs);
808        }
809
810        public void setWebView(WebView view) {
811            mWebView = view;
812        }
813
814        @Override
815        public void draw(Canvas canvas) {
816            // In general onDraw is the method to override, but we care about
817            // whether or not the background gets drawn, which happens in draw()
818            if (mWebView == null || mWebView.getScrollY() > getHeight()) {
819                super.draw(canvas);
820            }
821            // Need to invalidate so that if the scroll position changes, we
822            // still draw as appropriate.
823            invalidate();
824        }
825    }
826
827    private void showFakeTitleBar() {
828        final View decor = getWindow().peekDecorView();
829        if (mFakeTitleBar.getParent() == null && mActiveTabsPage == null
830                && !mActivityInPause && decor != null
831                && decor.getWindowToken() != null) {
832            Rect visRect = new Rect();
833            if (!mBrowserFrameLayout.getGlobalVisibleRect(visRect)) {
834                if (LOGD_ENABLED) {
835                    Log.d(LOGTAG, "showFakeTitleBar visRect failed");
836                }
837                return;
838            }
839
840            WindowManager manager
841                    = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
842
843            // Add the title bar to the window manager so it can receive touches
844            // while the menu is up
845            WindowManager.LayoutParams params
846                    = new WindowManager.LayoutParams(
847                    ViewGroup.LayoutParams.MATCH_PARENT,
848                    ViewGroup.LayoutParams.WRAP_CONTENT,
849                    WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL,
850                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
851                    PixelFormat.TRANSLUCENT);
852            params.gravity = Gravity.TOP;
853            WebView mainView = mTabControl.getCurrentWebView();
854            boolean atTop = mainView != null && mainView.getScrollY() == 0;
855            params.windowAnimations = atTop ? 0 : R.style.TitleBar;
856            // XXX : Without providing an offset, the fake title bar will be
857            // placed underneath the status bar.  Use the global visible rect
858            // of mBrowserFrameLayout to determine the bottom of the status bar
859            params.y = visRect.top;
860            // Add a holder for the title bar.  It also holds a shadow to show
861            // below the title bar.
862            if (mFakeTitleBarHolder == null) {
863                mFakeTitleBarHolder = (ViewGroup) LayoutInflater.from(this)
864                    .inflate(R.layout.title_bar_bg, null);
865            }
866            Shadow shadow = (Shadow) mFakeTitleBarHolder.findViewById(
867                    R.id.shadow);
868            shadow.setWebView(mainView);
869            mFakeTitleBarHolder.addView(mFakeTitleBar, 0, mFakeTitleBarParams);
870            manager.addView(mFakeTitleBarHolder, params);
871        }
872    }
873
874    @Override
875    public void onOptionsMenuClosed(Menu menu) {
876        mOptionsMenuOpen = false;
877        if (!mInLoad) {
878            hideFakeTitleBar();
879        } else if (!mIconView) {
880            // The page is currently loading, and we are in expanded mode, so
881            // we were not showing the menu.  Show it once again.  It will be
882            // removed when the page finishes.
883            showFakeTitleBar();
884        }
885    }
886
887    private void hideFakeTitleBar() {
888        if (mFakeTitleBar.getParent() == null) return;
889        WindowManager.LayoutParams params = (WindowManager.LayoutParams)
890                mFakeTitleBarHolder.getLayoutParams();
891        WebView mainView = mTabControl.getCurrentWebView();
892        // Although we decided whether or not to animate based on the current
893        // scroll position, the scroll position may have changed since the
894        // fake title bar was displayed.  Make sure it has the appropriate
895        // animation/lack thereof before removing.
896        params.windowAnimations = mainView != null && mainView.getScrollY() == 0
897                ? 0 : R.style.TitleBar;
898        WindowManager manager
899                    = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
900        manager.updateViewLayout(mFakeTitleBarHolder, params);
901        mFakeTitleBarHolder.removeView(mFakeTitleBar);
902        manager.removeView(mFakeTitleBarHolder);
903    }
904
905    /**
906     * Special method for the fake title bar to call when displaying its context
907     * menu, since it is in its own Window, and its parent does not show a
908     * context menu.
909     */
910    /* package */ void showTitleBarContextMenu() {
911        if (null == mTitleBar.getParent()) {
912            return;
913        }
914        openContextMenu(mTitleBar);
915    }
916
917    @Override
918    public void onContextMenuClosed(Menu menu) {
919        super.onContextMenuClosed(menu);
920        if (mInLoad) {
921            showFakeTitleBar();
922        }
923    }
924
925    /**
926     *  onSaveInstanceState(Bundle map)
927     *  onSaveInstanceState is called right before onStop(). The map contains
928     *  the saved state.
929     */
930    @Override
931    protected void onSaveInstanceState(Bundle outState) {
932        if (LOGV_ENABLED) {
933            Log.v(LOGTAG, "BrowserActivity.onSaveInstanceState: this=" + this);
934        }
935        // the default implementation requires each view to have an id. As the
936        // browser handles the state itself and it doesn't use id for the views,
937        // don't call the default implementation. Otherwise it will trigger the
938        // warning like this, "couldn't save which view has focus because the
939        // focused view XXX has no id".
940
941        // Save all the tabs
942        mTabControl.saveState(outState);
943    }
944
945    @Override
946    protected void onPause() {
947        super.onPause();
948
949        if (mActivityInPause) {
950            Log.e(LOGTAG, "BrowserActivity is already paused.");
951            return;
952        }
953
954        mTabControl.pauseCurrentTab();
955        mActivityInPause = true;
956        if (mTabControl.getCurrentIndex() >= 0 && !pauseWebViewTimers()) {
957            mWakeLock.acquire();
958            mHandler.sendMessageDelayed(mHandler
959                    .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT);
960        }
961
962        // Clear the credentials toast if it is up
963        if (mCredsDlg != null && mCredsDlg.isShowing()) {
964            mCredsDlg.dismiss();
965        }
966        mCredsDlg = null;
967
968        // FIXME: This removes the active tabs page and resets the menu to
969        // MAIN_MENU.  A better solution might be to do this work in onNewIntent
970        // but then we would need to save it in onSaveInstanceState and restore
971        // it in onCreate/onRestoreInstanceState
972        if (mActiveTabsPage != null) {
973            removeActiveTabPage(true);
974        }
975
976        cancelStopToast();
977
978        // unregister network state listener
979        unregisterReceiver(mNetworkStateIntentReceiver);
980        WebView.disablePlatformNotifications();
981    }
982
983    @Override
984    protected void onDestroy() {
985        if (LOGV_ENABLED) {
986            Log.v(LOGTAG, "BrowserActivity.onDestroy: this=" + this);
987        }
988        super.onDestroy();
989
990        if (mUploadMessage != null) {
991            mUploadMessage.onReceiveValue(null);
992            mUploadMessage = null;
993        }
994
995        if (mTabControl == null) return;
996
997        // Remove the fake title bar if it is there
998        hideFakeTitleBar();
999
1000        // Remove the current tab and sub window
1001        Tab t = mTabControl.getCurrentTab();
1002        if (t != null) {
1003            dismissSubWindow(t);
1004            removeTabFromContentView(t);
1005        }
1006        // Destroy all the tabs
1007        mTabControl.destroy();
1008        WebIconDatabase.getInstance().close();
1009
1010        unregisterReceiver(mPackageInstallationReceiver);
1011    }
1012
1013    @Override
1014    public void onConfigurationChanged(Configuration newConfig) {
1015        mConfigChanged = true;
1016        super.onConfigurationChanged(newConfig);
1017
1018        if (mPageInfoDialog != null) {
1019            mPageInfoDialog.dismiss();
1020            showPageInfo(
1021                mPageInfoView,
1022                mPageInfoFromShowSSLCertificateOnError);
1023        }
1024        if (mSSLCertificateDialog != null) {
1025            mSSLCertificateDialog.dismiss();
1026            showSSLCertificate(
1027                mSSLCertificateView);
1028        }
1029        if (mSSLCertificateOnErrorDialog != null) {
1030            mSSLCertificateOnErrorDialog.dismiss();
1031            showSSLCertificateOnError(
1032                mSSLCertificateOnErrorView,
1033                mSSLCertificateOnErrorHandler,
1034                mSSLCertificateOnErrorError);
1035        }
1036        if (mHttpAuthenticationDialog != null) {
1037            String title = ((TextView) mHttpAuthenticationDialog
1038                    .findViewById(com.android.internal.R.id.alertTitle)).getText()
1039                    .toString();
1040            String name = ((TextView) mHttpAuthenticationDialog
1041                    .findViewById(R.id.username_edit)).getText().toString();
1042            String password = ((TextView) mHttpAuthenticationDialog
1043                    .findViewById(R.id.password_edit)).getText().toString();
1044            int focusId = mHttpAuthenticationDialog.getCurrentFocus()
1045                    .getId();
1046            mHttpAuthenticationDialog.dismiss();
1047            showHttpAuthentication(mHttpAuthHandler, null, null, title,
1048                    name, password, focusId);
1049        }
1050    }
1051
1052    @Override
1053    public void onLowMemory() {
1054        super.onLowMemory();
1055        mTabControl.freeMemory();
1056    }
1057
1058    private boolean resumeWebViewTimers() {
1059        Tab tab = mTabControl.getCurrentTab();
1060        boolean inLoad = tab.inLoad();
1061        if ((!mActivityInPause && !inLoad) || (mActivityInPause && inLoad)) {
1062            CookieSyncManager.getInstance().startSync();
1063            WebView w = tab.getWebView();
1064            if (w != null) {
1065                w.resumeTimers();
1066            }
1067            return true;
1068        } else {
1069            return false;
1070        }
1071    }
1072
1073    private boolean pauseWebViewTimers() {
1074        Tab tab = mTabControl.getCurrentTab();
1075        boolean inLoad = tab.inLoad();
1076        if (mActivityInPause && !inLoad) {
1077            CookieSyncManager.getInstance().stopSync();
1078            WebView w = mTabControl.getCurrentWebView();
1079            if (w != null) {
1080                w.pauseTimers();
1081            }
1082            return true;
1083        } else {
1084            return false;
1085        }
1086    }
1087
1088    // FIXME: Do we want to call this when loading google for the first time?
1089    /*
1090     * This function is called when we are launching for the first time. We
1091     * are waiting for the login credentials before loading Google home
1092     * pages. This way the user will be logged in straight away.
1093     */
1094    private void waitForCredentials() {
1095        // Show a toast
1096        mCredsDlg = new ProgressDialog(this);
1097        mCredsDlg.setIndeterminate(true);
1098        mCredsDlg.setMessage(getText(R.string.retrieving_creds_dlg_msg));
1099        // If the user cancels the operation, then cancel the Google
1100        // Credentials request.
1101        mCredsDlg.setCancelMessage(mHandler.obtainMessage(CANCEL_CREDS_REQUEST));
1102        mCredsDlg.show();
1103
1104        // We set a timeout for the retrieval of credentials in onResume()
1105        // as that is when we have freed up some CPU time to get
1106        // the login credentials.
1107    }
1108
1109    /*
1110     * If we have received the credentials or we have timed out and we are
1111     * showing the credentials dialog, then it is time to move on.
1112     */
1113    private void resumeAfterCredentials() {
1114        if (mCredsDlg == null) {
1115            return;
1116        }
1117
1118        // Clear the toast
1119        if (mCredsDlg.isShowing()) {
1120            mCredsDlg.dismiss();
1121        }
1122        mCredsDlg = null;
1123
1124        // Clear any pending timeout
1125        mHandler.removeMessages(CANCEL_CREDS_REQUEST);
1126
1127        // Load the page
1128        WebView w = mTabControl.getCurrentWebView();
1129        if (w != null) {
1130            w.loadUrl(mSettings.getHomePage());
1131        }
1132
1133        // Update the settings, need to do this last as it can take a moment
1134        // to persist the settings. In the mean time we could be loading
1135        // content.
1136        mSettings.setLoginInitialized(this);
1137    }
1138
1139    // Open the icon database and retain all the icons for visited sites.
1140    private void retainIconsOnStartup() {
1141        final WebIconDatabase db = WebIconDatabase.getInstance();
1142        db.open(getDir("icons", 0).getPath());
1143        try {
1144            Cursor c = Browser.getAllBookmarks(mResolver);
1145            if (!c.moveToFirst()) {
1146                c.deactivate();
1147                return;
1148            }
1149            int urlIndex = c.getColumnIndex(Browser.BookmarkColumns.URL);
1150            do {
1151                String url = c.getString(urlIndex);
1152                db.retainIconForPageUrl(url);
1153            } while (c.moveToNext());
1154            c.deactivate();
1155        } catch (IllegalStateException e) {
1156            Log.e(LOGTAG, "retainIconsOnStartup", e);
1157        }
1158    }
1159
1160    // Helper method for getting the top window.
1161    WebView getTopWindow() {
1162        return mTabControl.getCurrentTopWebView();
1163    }
1164
1165    TabControl getTabControl() {
1166        return mTabControl;
1167    }
1168
1169    @Override
1170    public boolean onCreateOptionsMenu(Menu menu) {
1171        super.onCreateOptionsMenu(menu);
1172
1173        MenuInflater inflater = getMenuInflater();
1174        inflater.inflate(R.menu.browser, menu);
1175        mMenu = menu;
1176        updateInLoadMenuItems();
1177        return true;
1178    }
1179
1180    /**
1181     * As the menu can be open when loading state changes
1182     * we must manually update the state of the stop/reload menu
1183     * item
1184     */
1185    private void updateInLoadMenuItems() {
1186        if (mMenu == null) {
1187            return;
1188        }
1189        MenuItem src = mInLoad ?
1190                mMenu.findItem(R.id.stop_menu_id):
1191                    mMenu.findItem(R.id.reload_menu_id);
1192        MenuItem dest = mMenu.findItem(R.id.stop_reload_menu_id);
1193        dest.setIcon(src.getIcon());
1194        dest.setTitle(src.getTitle());
1195    }
1196
1197    @Override
1198    public boolean onContextItemSelected(MenuItem item) {
1199        // chording is not an issue with context menus, but we use the same
1200        // options selector, so set mCanChord to true so we can access them.
1201        mCanChord = true;
1202        int id = item.getItemId();
1203        switch (id) {
1204            // For the context menu from the title bar
1205            case R.id.title_bar_share_page_url:
1206            case R.id.title_bar_copy_page_url:
1207                WebView mainView = mTabControl.getCurrentWebView();
1208                if (null == mainView) {
1209                    return false;
1210                }
1211                if (id == R.id.title_bar_share_page_url) {
1212                    Browser.sendString(this, mainView.getUrl());
1213                } else {
1214                    copy(mainView.getUrl());
1215                }
1216                break;
1217            // -- Browser context menu
1218            case R.id.open_context_menu_id:
1219            case R.id.open_newtab_context_menu_id:
1220            case R.id.bookmark_context_menu_id:
1221            case R.id.save_link_context_menu_id:
1222            case R.id.share_link_context_menu_id:
1223            case R.id.copy_link_context_menu_id:
1224                final WebView webView = getTopWindow();
1225                if (null == webView) {
1226                    return false;
1227                }
1228                final HashMap hrefMap = new HashMap();
1229                hrefMap.put("webview", webView);
1230                final Message msg = mHandler.obtainMessage(
1231                        FOCUS_NODE_HREF, id, 0, hrefMap);
1232                webView.requestFocusNodeHref(msg);
1233                break;
1234
1235            default:
1236                // For other context menus
1237                return onOptionsItemSelected(item);
1238        }
1239        mCanChord = false;
1240        return true;
1241    }
1242
1243    private Bundle createGoogleSearchSourceBundle(String source) {
1244        Bundle bundle = new Bundle();
1245        bundle.putString(SearchManager.SOURCE, source);
1246        return bundle;
1247    }
1248
1249    /**
1250     * Overriding this to insert a local information bundle
1251     */
1252    @Override
1253    public boolean onSearchRequested() {
1254        if (mOptionsMenuOpen) closeOptionsMenu();
1255        String url = (getTopWindow() == null) ? null : getTopWindow().getUrl();
1256        startSearch(mSettings.getHomePage().equals(url) ? null : url, true,
1257                createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_SEARCHKEY), false);
1258        return true;
1259    }
1260
1261    @Override
1262    public void startSearch(String initialQuery, boolean selectInitialQuery,
1263            Bundle appSearchData, boolean globalSearch) {
1264        if (appSearchData == null) {
1265            appSearchData = createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_TYPE);
1266        }
1267        super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
1268    }
1269
1270    /**
1271     * Switch tabs.  Called by the TitleBarSet when sliding the title bar
1272     * results in changing tabs.
1273     * @param index Index of the tab to change to, as defined by
1274     *              mTabControl.getTabIndex(Tab t).
1275     * @return boolean True if we successfully switched to a different tab.  If
1276     *                 the indexth tab is null, or if that tab is the same as
1277     *                 the current one, return false.
1278     */
1279    /* package */ boolean switchToTab(int index) {
1280        Tab tab = mTabControl.getTab(index);
1281        Tab currentTab = mTabControl.getCurrentTab();
1282        if (tab == null || tab == currentTab) {
1283            return false;
1284        }
1285        if (currentTab != null) {
1286            // currentTab may be null if it was just removed.  In that case,
1287            // we do not need to remove it
1288            removeTabFromContentView(currentTab);
1289        }
1290        mTabControl.setCurrentTab(tab);
1291        attachTabToContentView(tab);
1292        resetTitleIconAndProgress();
1293        updateLockIconToLatest();
1294        return true;
1295    }
1296
1297    /* package */ Tab openTabToHomePage() {
1298        return openTabAndShow(mSettings.getHomePage(), false, null);
1299    }
1300
1301    /* package */ void closeCurrentWindow() {
1302        final Tab current = mTabControl.getCurrentTab();
1303        if (mTabControl.getTabCount() == 1) {
1304            // This is the last tab.  Open a new one, with the home
1305            // page and close the current one.
1306            openTabToHomePage();
1307            closeTab(current);
1308            return;
1309        }
1310        final Tab parent = current.getParentTab();
1311        int indexToShow = -1;
1312        if (parent != null) {
1313            indexToShow = mTabControl.getTabIndex(parent);
1314        } else {
1315            final int currentIndex = mTabControl.getCurrentIndex();
1316            // Try to move to the tab to the right
1317            indexToShow = currentIndex + 1;
1318            if (indexToShow > mTabControl.getTabCount() - 1) {
1319                // Try to move to the tab to the left
1320                indexToShow = currentIndex - 1;
1321            }
1322        }
1323        if (switchToTab(indexToShow)) {
1324            // Close window
1325            closeTab(current);
1326        }
1327    }
1328
1329    private ActiveTabsPage mActiveTabsPage;
1330
1331    /**
1332     * Remove the active tabs page.
1333     * @param needToAttach If true, the active tabs page did not attach a tab
1334     *                     to the content view, so we need to do that here.
1335     */
1336    /* package */ void removeActiveTabPage(boolean needToAttach) {
1337        mContentView.removeView(mActiveTabsPage);
1338        mActiveTabsPage = null;
1339        mMenuState = R.id.MAIN_MENU;
1340        if (needToAttach) {
1341            attachTabToContentView(mTabControl.getCurrentTab());
1342        }
1343        getTopWindow().requestFocus();
1344    }
1345
1346    @Override
1347    public boolean onOptionsItemSelected(MenuItem item) {
1348        if (!mCanChord) {
1349            // The user has already fired a shortcut with this hold down of the
1350            // menu key.
1351            return false;
1352        }
1353        if (null == getTopWindow()) {
1354            return false;
1355        }
1356        if (mMenuIsDown) {
1357            // The shortcut action consumes the MENU. Even if it is still down,
1358            // it won't trigger the next shortcut action. In the case of the
1359            // shortcut action triggering a new activity, like Bookmarks, we
1360            // won't get onKeyUp for MENU. So it is important to reset it here.
1361            mMenuIsDown = false;
1362        }
1363        switch (item.getItemId()) {
1364            // -- Main menu
1365            case R.id.new_tab_menu_id:
1366                openTabToHomePage();
1367                break;
1368
1369            case R.id.goto_menu_id:
1370                onSearchRequested();
1371                break;
1372
1373            case R.id.bookmarks_menu_id:
1374                bookmarksOrHistoryPicker(false);
1375                break;
1376
1377            case R.id.active_tabs_menu_id:
1378                mActiveTabsPage = new ActiveTabsPage(this, mTabControl);
1379                removeTabFromContentView(mTabControl.getCurrentTab());
1380                hideFakeTitleBar();
1381                mContentView.addView(mActiveTabsPage, COVER_SCREEN_PARAMS);
1382                mActiveTabsPage.requestFocus();
1383                mMenuState = EMPTY_MENU;
1384                break;
1385
1386            case R.id.add_bookmark_menu_id:
1387                Intent i = new Intent(BrowserActivity.this,
1388                        AddBookmarkPage.class);
1389                WebView w = getTopWindow();
1390                i.putExtra("url", w.getUrl());
1391                i.putExtra("title", w.getTitle());
1392                i.putExtra("touch_icon_url", w.getTouchIconUrl());
1393                i.putExtra("thumbnail", createScreenshot(w));
1394                startActivity(i);
1395                break;
1396
1397            case R.id.stop_reload_menu_id:
1398                if (mInLoad) {
1399                    stopLoading();
1400                } else {
1401                    getTopWindow().reload();
1402                }
1403                break;
1404
1405            case R.id.back_menu_id:
1406                getTopWindow().goBack();
1407                break;
1408
1409            case R.id.forward_menu_id:
1410                getTopWindow().goForward();
1411                break;
1412
1413            case R.id.close_menu_id:
1414                // Close the subwindow if it exists.
1415                if (mTabControl.getCurrentSubWindow() != null) {
1416                    dismissSubWindow(mTabControl.getCurrentTab());
1417                    break;
1418                }
1419                closeCurrentWindow();
1420                break;
1421
1422            case R.id.homepage_menu_id:
1423                Tab current = mTabControl.getCurrentTab();
1424                if (current != null) {
1425                    dismissSubWindow(current);
1426                    current.getWebView().loadUrl(mSettings.getHomePage());
1427                }
1428                break;
1429
1430            case R.id.preferences_menu_id:
1431                Intent intent = new Intent(this,
1432                        BrowserPreferencesPage.class);
1433                intent.putExtra(BrowserPreferencesPage.CURRENT_PAGE,
1434                        getTopWindow().getUrl());
1435                startActivityForResult(intent, PREFERENCES_PAGE);
1436                break;
1437
1438            case R.id.find_menu_id:
1439                if (null == mFindDialog) {
1440                    mFindDialog = new FindDialog(this);
1441                }
1442                mFindDialog.setWebView(getTopWindow());
1443                mFindDialog.show();
1444                mMenuState = EMPTY_MENU;
1445                break;
1446
1447            case R.id.select_text_id:
1448                getTopWindow().emulateShiftHeld();
1449                break;
1450            case R.id.page_info_menu_id:
1451                showPageInfo(mTabControl.getCurrentTab(), false);
1452                break;
1453
1454            case R.id.classic_history_menu_id:
1455                bookmarksOrHistoryPicker(true);
1456                break;
1457
1458            case R.id.share_page_menu_id:
1459                Browser.sendString(this, getTopWindow().getUrl(),
1460                        getText(R.string.choosertitle_sharevia).toString());
1461                break;
1462
1463            case R.id.dump_nav_menu_id:
1464                getTopWindow().debugDump();
1465                break;
1466
1467            case R.id.zoom_in_menu_id:
1468                getTopWindow().zoomIn();
1469                break;
1470
1471            case R.id.zoom_out_menu_id:
1472                getTopWindow().zoomOut();
1473                break;
1474
1475            case R.id.view_downloads_menu_id:
1476                viewDownloads(null);
1477                break;
1478
1479            case R.id.window_one_menu_id:
1480            case R.id.window_two_menu_id:
1481            case R.id.window_three_menu_id:
1482            case R.id.window_four_menu_id:
1483            case R.id.window_five_menu_id:
1484            case R.id.window_six_menu_id:
1485            case R.id.window_seven_menu_id:
1486            case R.id.window_eight_menu_id:
1487                {
1488                    int menuid = item.getItemId();
1489                    for (int id = 0; id < WINDOW_SHORTCUT_ID_ARRAY.length; id++) {
1490                        if (WINDOW_SHORTCUT_ID_ARRAY[id] == menuid) {
1491                            Tab desiredTab = mTabControl.getTab(id);
1492                            if (desiredTab != null &&
1493                                    desiredTab != mTabControl.getCurrentTab()) {
1494                                switchToTab(id);
1495                            }
1496                            break;
1497                        }
1498                    }
1499                }
1500                break;
1501
1502            default:
1503                if (!super.onOptionsItemSelected(item)) {
1504                    return false;
1505                }
1506                // Otherwise fall through.
1507        }
1508        mCanChord = false;
1509        return true;
1510    }
1511
1512    public void closeFind() {
1513        mMenuState = R.id.MAIN_MENU;
1514    }
1515
1516    @Override
1517    public boolean onPrepareOptionsMenu(Menu menu) {
1518        // This happens when the user begins to hold down the menu key, so
1519        // allow them to chord to get a shortcut.
1520        mCanChord = true;
1521        // Note: setVisible will decide whether an item is visible; while
1522        // setEnabled() will decide whether an item is enabled, which also means
1523        // whether the matching shortcut key will function.
1524        super.onPrepareOptionsMenu(menu);
1525        switch (mMenuState) {
1526            case EMPTY_MENU:
1527                if (mCurrentMenuState != mMenuState) {
1528                    menu.setGroupVisible(R.id.MAIN_MENU, false);
1529                    menu.setGroupEnabled(R.id.MAIN_MENU, false);
1530                    menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, false);
1531                }
1532                break;
1533            default:
1534                if (mCurrentMenuState != mMenuState) {
1535                    menu.setGroupVisible(R.id.MAIN_MENU, true);
1536                    menu.setGroupEnabled(R.id.MAIN_MENU, true);
1537                    menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, true);
1538                }
1539                final WebView w = getTopWindow();
1540                boolean canGoBack = false;
1541                boolean canGoForward = false;
1542                boolean isHome = false;
1543                if (w != null) {
1544                    canGoBack = w.canGoBack();
1545                    canGoForward = w.canGoForward();
1546                    isHome = mSettings.getHomePage().equals(w.getUrl());
1547                }
1548                final MenuItem back = menu.findItem(R.id.back_menu_id);
1549                back.setEnabled(canGoBack);
1550
1551                final MenuItem home = menu.findItem(R.id.homepage_menu_id);
1552                home.setEnabled(!isHome);
1553
1554                menu.findItem(R.id.forward_menu_id)
1555                        .setEnabled(canGoForward);
1556
1557                menu.findItem(R.id.new_tab_menu_id).setEnabled(
1558                        mTabControl.canCreateNewTab());
1559
1560                // decide whether to show the share link option
1561                PackageManager pm = getPackageManager();
1562                Intent send = new Intent(Intent.ACTION_SEND);
1563                send.setType("text/plain");
1564                ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
1565                menu.findItem(R.id.share_page_menu_id).setVisible(ri != null);
1566
1567                boolean isNavDump = mSettings.isNavDump();
1568                final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id);
1569                nav.setVisible(isNavDump);
1570                nav.setEnabled(isNavDump);
1571                break;
1572        }
1573        mCurrentMenuState = mMenuState;
1574        return true;
1575    }
1576
1577    @Override
1578    public void onCreateContextMenu(ContextMenu menu, View v,
1579            ContextMenuInfo menuInfo) {
1580        WebView webview = (WebView) v;
1581        WebView.HitTestResult result = webview.getHitTestResult();
1582        if (result == null) {
1583            return;
1584        }
1585
1586        int type = result.getType();
1587        if (type == WebView.HitTestResult.UNKNOWN_TYPE) {
1588            Log.w(LOGTAG,
1589                    "We should not show context menu when nothing is touched");
1590            return;
1591        }
1592        if (type == WebView.HitTestResult.EDIT_TEXT_TYPE) {
1593            // let TextView handles context menu
1594            return;
1595        }
1596
1597        // Note, http://b/issue?id=1106666 is requesting that
1598        // an inflated menu can be used again. This is not available
1599        // yet, so inflate each time (yuk!)
1600        MenuInflater inflater = getMenuInflater();
1601        inflater.inflate(R.menu.browsercontext, menu);
1602
1603        // Show the correct menu group
1604        String extra = result.getExtra();
1605        menu.setGroupVisible(R.id.PHONE_MENU,
1606                type == WebView.HitTestResult.PHONE_TYPE);
1607        menu.setGroupVisible(R.id.EMAIL_MENU,
1608                type == WebView.HitTestResult.EMAIL_TYPE);
1609        menu.setGroupVisible(R.id.GEO_MENU,
1610                type == WebView.HitTestResult.GEO_TYPE);
1611        menu.setGroupVisible(R.id.IMAGE_MENU,
1612                type == WebView.HitTestResult.IMAGE_TYPE
1613                || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
1614        menu.setGroupVisible(R.id.ANCHOR_MENU,
1615                type == WebView.HitTestResult.SRC_ANCHOR_TYPE
1616                || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
1617
1618        // Setup custom handling depending on the type
1619        switch (type) {
1620            case WebView.HitTestResult.PHONE_TYPE:
1621                menu.setHeaderTitle(Uri.decode(extra));
1622                menu.findItem(R.id.dial_context_menu_id).setIntent(
1623                        new Intent(Intent.ACTION_VIEW, Uri
1624                                .parse(WebView.SCHEME_TEL + extra)));
1625                Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
1626                addIntent.putExtra(Insert.PHONE, Uri.decode(extra));
1627                addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
1628                menu.findItem(R.id.add_contact_context_menu_id).setIntent(
1629                        addIntent);
1630                menu.findItem(R.id.copy_phone_context_menu_id).setOnMenuItemClickListener(
1631                        new Copy(extra));
1632                break;
1633
1634            case WebView.HitTestResult.EMAIL_TYPE:
1635                menu.setHeaderTitle(extra);
1636                menu.findItem(R.id.email_context_menu_id).setIntent(
1637                        new Intent(Intent.ACTION_VIEW, Uri
1638                                .parse(WebView.SCHEME_MAILTO + extra)));
1639                menu.findItem(R.id.copy_mail_context_menu_id).setOnMenuItemClickListener(
1640                        new Copy(extra));
1641                break;
1642
1643            case WebView.HitTestResult.GEO_TYPE:
1644                menu.setHeaderTitle(extra);
1645                menu.findItem(R.id.map_context_menu_id).setIntent(
1646                        new Intent(Intent.ACTION_VIEW, Uri
1647                                .parse(WebView.SCHEME_GEO
1648                                        + URLEncoder.encode(extra))));
1649                menu.findItem(R.id.copy_geo_context_menu_id).setOnMenuItemClickListener(
1650                        new Copy(extra));
1651                break;
1652
1653            case WebView.HitTestResult.SRC_ANCHOR_TYPE:
1654            case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
1655                TextView titleView = (TextView) LayoutInflater.from(this)
1656                        .inflate(android.R.layout.browser_link_context_header,
1657                        null);
1658                titleView.setText(extra);
1659                menu.setHeaderView(titleView);
1660                // decide whether to show the open link in new tab option
1661                menu.findItem(R.id.open_newtab_context_menu_id).setVisible(
1662                        mTabControl.canCreateNewTab());
1663                menu.findItem(R.id.bookmark_context_menu_id).setVisible(
1664                        Bookmarks.urlHasAcceptableScheme(extra));
1665                PackageManager pm = getPackageManager();
1666                Intent send = new Intent(Intent.ACTION_SEND);
1667                send.setType("text/plain");
1668                ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
1669                menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null);
1670                if (type == WebView.HitTestResult.SRC_ANCHOR_TYPE) {
1671                    break;
1672                }
1673                // otherwise fall through to handle image part
1674            case WebView.HitTestResult.IMAGE_TYPE:
1675                if (type == WebView.HitTestResult.IMAGE_TYPE) {
1676                    menu.setHeaderTitle(extra);
1677                }
1678                menu.findItem(R.id.view_image_context_menu_id).setIntent(
1679                        new Intent(Intent.ACTION_VIEW, Uri.parse(extra)));
1680                menu.findItem(R.id.download_context_menu_id).
1681                        setOnMenuItemClickListener(new Download(extra));
1682                menu.findItem(R.id.set_wallpaper_context_menu_id).
1683                        setOnMenuItemClickListener(new SetAsWallpaper(extra));
1684                break;
1685
1686            default:
1687                Log.w(LOGTAG, "We should not get here.");
1688                break;
1689        }
1690        hideFakeTitleBar();
1691    }
1692
1693    // Attach the given tab to the content view.
1694    // this should only be called for the current tab.
1695    private void attachTabToContentView(Tab t) {
1696        // Attach the container that contains the main WebView and any other UI
1697        // associated with the tab.
1698        t.attachTabToContentView(mContentView);
1699
1700        if (mShouldShowErrorConsole) {
1701            ErrorConsoleView errorConsole = t.getErrorConsole(true);
1702            if (errorConsole.numberOfErrors() == 0) {
1703                errorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
1704            } else {
1705                errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
1706            }
1707
1708            mErrorConsoleContainer.addView(errorConsole,
1709                    new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
1710                                                  ViewGroup.LayoutParams.WRAP_CONTENT));
1711        }
1712
1713        WebView view = t.getWebView();
1714        view.setEmbeddedTitleBar(mTitleBar);
1715        // Request focus on the top window.
1716        t.getTopWindow().requestFocus();
1717    }
1718
1719    // Attach a sub window to the main WebView of the given tab.
1720    void attachSubWindow(Tab t) {
1721        t.attachSubWindow(mContentView);
1722        getTopWindow().requestFocus();
1723    }
1724
1725    // Remove the given tab from the content view.
1726    private void removeTabFromContentView(Tab t) {
1727        // Remove the container that contains the main WebView.
1728        t.removeTabFromContentView(mContentView);
1729
1730        ErrorConsoleView errorConsole = t.getErrorConsole(false);
1731        if (errorConsole != null) {
1732            mErrorConsoleContainer.removeView(errorConsole);
1733        }
1734
1735        WebView view = t.getWebView();
1736        if (view != null) {
1737            view.setEmbeddedTitleBar(null);
1738        }
1739    }
1740
1741    // Remove the sub window if it exists. Also called by TabControl when the
1742    // user clicks the 'X' to dismiss a sub window.
1743    /* package */ void dismissSubWindow(Tab t) {
1744        t.removeSubWindow(mContentView);
1745        // dismiss the subwindow. This will destroy the WebView.
1746        t.dismissSubWindow();
1747        getTopWindow().requestFocus();
1748    }
1749
1750    // A wrapper function of {@link #openTabAndShow(UrlData, boolean, String)}
1751    // that accepts url as string.
1752    private Tab openTabAndShow(String url, boolean closeOnExit, String appId) {
1753        return openTabAndShow(new UrlData(url), closeOnExit, appId);
1754    }
1755
1756    // This method does a ton of stuff. It will attempt to create a new tab
1757    // if we haven't reached MAX_TABS. Otherwise it uses the current tab. If
1758    // url isn't null, it will load the given url.
1759    /* package */Tab openTabAndShow(UrlData urlData, boolean closeOnExit,
1760            String appId) {
1761        final Tab currentTab = mTabControl.getCurrentTab();
1762        if (mTabControl.canCreateNewTab()) {
1763            final Tab tab = mTabControl.createNewTab(closeOnExit, appId,
1764                    urlData.mUrl);
1765            WebView webview = tab.getWebView();
1766            // If the last tab was removed from the active tabs page, currentTab
1767            // will be null.
1768            if (currentTab != null) {
1769                removeTabFromContentView(currentTab);
1770            }
1771            // We must set the new tab as the current tab to reflect the old
1772            // animation behavior.
1773            mTabControl.setCurrentTab(tab);
1774            attachTabToContentView(tab);
1775            if (!urlData.isEmpty()) {
1776                urlData.loadIn(webview);
1777            }
1778            return tab;
1779        } else {
1780            // Get rid of the subwindow if it exists
1781            dismissSubWindow(currentTab);
1782            if (!urlData.isEmpty()) {
1783                // Load the given url.
1784                urlData.loadIn(currentTab.getWebView());
1785            }
1786        }
1787        return currentTab;
1788    }
1789
1790    private Tab openTab(String url) {
1791        if (mSettings.openInBackground()) {
1792            Tab t = mTabControl.createNewTab();
1793            if (t != null) {
1794                WebView view = t.getWebView();
1795                view.loadUrl(url);
1796            }
1797            return t;
1798        } else {
1799            return openTabAndShow(url, false, null);
1800        }
1801    }
1802
1803    private class Copy implements OnMenuItemClickListener {
1804        private CharSequence mText;
1805
1806        public boolean onMenuItemClick(MenuItem item) {
1807            copy(mText);
1808            return true;
1809        }
1810
1811        public Copy(CharSequence toCopy) {
1812            mText = toCopy;
1813        }
1814    }
1815
1816    private class Download implements OnMenuItemClickListener {
1817        private String mText;
1818
1819        public boolean onMenuItemClick(MenuItem item) {
1820            onDownloadStartNoStream(mText, null, null, null, -1);
1821            return true;
1822        }
1823
1824        public Download(String toDownload) {
1825            mText = toDownload;
1826        }
1827    }
1828
1829    private class SetAsWallpaper extends Thread implements
1830            OnMenuItemClickListener, DialogInterface.OnCancelListener {
1831        private URL mUrl;
1832        private ProgressDialog mWallpaperProgress;
1833        private boolean mCanceled = false;
1834
1835        public SetAsWallpaper(String url) {
1836            try {
1837                mUrl = new URL(url);
1838            } catch (MalformedURLException e) {
1839                mUrl = null;
1840            }
1841        }
1842
1843        public void onCancel(DialogInterface dialog) {
1844            mCanceled = true;
1845        }
1846
1847        public boolean onMenuItemClick(MenuItem item) {
1848            if (mUrl != null) {
1849                // The user may have tried to set a image with a large file size as their
1850                // background so it may take a few moments to perform the operation. Display
1851                // a progress spinner while it is working.
1852                mWallpaperProgress = new ProgressDialog(BrowserActivity.this);
1853                mWallpaperProgress.setIndeterminate(true);
1854                mWallpaperProgress.setMessage(getText(R.string.progress_dialog_setting_wallpaper));
1855                mWallpaperProgress.setCancelable(true);
1856                mWallpaperProgress.setOnCancelListener(this);
1857                mWallpaperProgress.show();
1858                start();
1859            }
1860            return true;
1861        }
1862
1863        public void run() {
1864            Drawable oldWallpaper = BrowserActivity.this.getWallpaper();
1865            try {
1866                // TODO: This will cause the resource to be downloaded again, when we
1867                // should in most cases be able to grab it from the cache. To fix this
1868                // we should query WebCore to see if we can access a cached version and
1869                // instead open an input stream on that. This pattern could also be used
1870                // in the download manager where the same problem exists.
1871                InputStream inputstream = mUrl.openStream();
1872                if (inputstream != null) {
1873                    setWallpaper(inputstream);
1874                }
1875            } catch (IOException e) {
1876                Log.e(LOGTAG, "Unable to set new wallpaper");
1877                // Act as though the user canceled the operation so we try to
1878                // restore the old wallpaper.
1879                mCanceled = true;
1880            }
1881
1882            if (mCanceled) {
1883                // Restore the old wallpaper if the user cancelled whilst we were setting
1884                // the new wallpaper.
1885                int width = oldWallpaper.getIntrinsicWidth();
1886                int height = oldWallpaper.getIntrinsicHeight();
1887                Bitmap bm = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
1888                Canvas canvas = new Canvas(bm);
1889                oldWallpaper.setBounds(0, 0, width, height);
1890                oldWallpaper.draw(canvas);
1891                try {
1892                    setWallpaper(bm);
1893                } catch (IOException e) {
1894                    Log.e(LOGTAG, "Unable to restore old wallpaper.");
1895                }
1896                mCanceled = false;
1897            }
1898
1899            if (mWallpaperProgress.isShowing()) {
1900                mWallpaperProgress.dismiss();
1901            }
1902        }
1903    }
1904
1905    private void copy(CharSequence text) {
1906        try {
1907            IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
1908            if (clip != null) {
1909                clip.setClipboardText(text);
1910            }
1911        } catch (android.os.RemoteException e) {
1912            Log.e(LOGTAG, "Copy failed", e);
1913        }
1914    }
1915
1916    /**
1917     * Resets the browser title-view to whatever it must be
1918     * (for example, if we had a loading error)
1919     * When we have a new page, we call resetTitle, when we
1920     * have to reset the titlebar to whatever it used to be
1921     * (for example, if the user chose to stop loading), we
1922     * call resetTitleAndRevertLockIcon.
1923     */
1924    /* package */ void resetTitleAndRevertLockIcon() {
1925        mTabControl.getCurrentTab().revertLockIcon();
1926        updateLockIconToLatest();
1927        resetTitleIconAndProgress();
1928    }
1929
1930    /**
1931     * Reset the title, favicon, and progress.
1932     */
1933    private void resetTitleIconAndProgress() {
1934        WebView current = mTabControl.getCurrentWebView();
1935        if (current == null) {
1936            return;
1937        }
1938        resetTitleAndIcon(current);
1939        int progress = current.getProgress();
1940        current.getWebChromeClient().onProgressChanged(current, progress);
1941    }
1942
1943    // Reset the title and the icon based on the given item.
1944    private void resetTitleAndIcon(WebView view) {
1945        WebHistoryItem item = view.copyBackForwardList().getCurrentItem();
1946        if (item != null) {
1947            setUrlTitle(item.getUrl(), item.getTitle());
1948            setFavicon(item.getFavicon());
1949        } else {
1950            setUrlTitle(null, null);
1951            setFavicon(null);
1952        }
1953    }
1954
1955    /**
1956     * Sets a title composed of the URL and the title string.
1957     * @param url The URL of the site being loaded.
1958     * @param title The title of the site being loaded.
1959     */
1960    void setUrlTitle(String url, String title) {
1961        mUrl = url;
1962        mTitle = title;
1963
1964        mTitleBar.setTitleAndUrl(title, url);
1965        mFakeTitleBar.setTitleAndUrl(title, url);
1966    }
1967
1968    /**
1969     * @param url The URL to build a title version of the URL from.
1970     * @return The title version of the URL or null if fails.
1971     * The title version of the URL can be either the URL hostname,
1972     * or the hostname with an "https://" prefix (for secure URLs),
1973     * or an empty string if, for example, the URL in question is a
1974     * file:// URL with no hostname.
1975     */
1976    /* package */ static String buildTitleUrl(String url) {
1977        String titleUrl = null;
1978
1979        if (url != null) {
1980            try {
1981                // parse the url string
1982                URL urlObj = new URL(url);
1983                if (urlObj != null) {
1984                    titleUrl = "";
1985
1986                    String protocol = urlObj.getProtocol();
1987                    String host = urlObj.getHost();
1988
1989                    if (host != null && 0 < host.length()) {
1990                        titleUrl = host;
1991                        if (protocol != null) {
1992                            // if a secure site, add an "https://" prefix!
1993                            if (protocol.equalsIgnoreCase("https")) {
1994                                titleUrl = protocol + "://" + host;
1995                            }
1996                        }
1997                    }
1998                }
1999            } catch (MalformedURLException e) {}
2000        }
2001
2002        return titleUrl;
2003    }
2004
2005    // Set the favicon in the title bar.
2006    void setFavicon(Bitmap icon) {
2007        mTitleBar.setFavicon(icon);
2008        mFakeTitleBar.setFavicon(icon);
2009    }
2010
2011    /**
2012     * Close the tab, remove its associated title bar, and adjust mTabControl's
2013     * current tab to a valid value.
2014     */
2015    /* package */ void closeTab(Tab t) {
2016        int currentIndex = mTabControl.getCurrentIndex();
2017        int removeIndex = mTabControl.getTabIndex(t);
2018        mTabControl.removeTab(t);
2019        if (currentIndex >= removeIndex && currentIndex != 0) {
2020            currentIndex--;
2021        }
2022        mTabControl.setCurrentTab(mTabControl.getTab(currentIndex));
2023        resetTitleIconAndProgress();
2024    }
2025
2026    private void goBackOnePageOrQuit() {
2027        Tab current = mTabControl.getCurrentTab();
2028        if (current == null) {
2029            /*
2030             * Instead of finishing the activity, simply push this to the back
2031             * of the stack and let ActivityManager to choose the foreground
2032             * activity. As BrowserActivity is singleTask, it will be always the
2033             * root of the task. So we can use either true or false for
2034             * moveTaskToBack().
2035             */
2036            moveTaskToBack(true);
2037            return;
2038        }
2039        WebView w = current.getWebView();
2040        if (w.canGoBack()) {
2041            w.goBack();
2042        } else {
2043            // Check to see if we are closing a window that was created by
2044            // another window. If so, we switch back to that window.
2045            Tab parent = current.getParentTab();
2046            if (parent != null) {
2047                switchToTab(mTabControl.getTabIndex(parent));
2048                // Now we close the other tab
2049                closeTab(current);
2050            } else {
2051                if (current.closeOnExit()) {
2052                    // force the tab's inLoad() to be false as we are going to
2053                    // either finish the activity or remove the tab. This will
2054                    // ensure pauseWebViewTimers() taking action.
2055                    mTabControl.getCurrentTab().clearInLoad();
2056                    if (mTabControl.getTabCount() == 1) {
2057                        finish();
2058                        return;
2059                    }
2060                    // call pauseWebViewTimers() now, we won't be able to call
2061                    // it in onPause() as the WebView won't be valid.
2062                    // Temporarily change mActivityInPause to be true as
2063                    // pauseWebViewTimers() will do nothing if mActivityInPause
2064                    // is false.
2065                    boolean savedState = mActivityInPause;
2066                    if (savedState) {
2067                        Log.e(LOGTAG, "BrowserActivity is already paused "
2068                                + "while handing goBackOnePageOrQuit.");
2069                    }
2070                    mActivityInPause = true;
2071                    pauseWebViewTimers();
2072                    mActivityInPause = savedState;
2073                    removeTabFromContentView(current);
2074                    mTabControl.removeTab(current);
2075                }
2076                /*
2077                 * Instead of finishing the activity, simply push this to the back
2078                 * of the stack and let ActivityManager to choose the foreground
2079                 * activity. As BrowserActivity is singleTask, it will be always the
2080                 * root of the task. So we can use either true or false for
2081                 * moveTaskToBack().
2082                 */
2083                moveTaskToBack(true);
2084            }
2085        }
2086    }
2087
2088    boolean isMenuDown() {
2089        return mMenuIsDown;
2090    }
2091
2092    @Override
2093    public boolean onKeyDown(int keyCode, KeyEvent event) {
2094        // Even if MENU is already held down, we need to call to super to open
2095        // the IME on long press.
2096        if (KeyEvent.KEYCODE_MENU == keyCode) {
2097            mMenuIsDown = true;
2098            return super.onKeyDown(keyCode, event);
2099        }
2100        // The default key mode is DEFAULT_KEYS_SEARCH_LOCAL. As the MENU is
2101        // still down, we don't want to trigger the search. Pretend to consume
2102        // the key and do nothing.
2103        if (mMenuIsDown) return true;
2104
2105        switch(keyCode) {
2106            case KeyEvent.KEYCODE_SPACE:
2107                // WebView/WebTextView handle the keys in the KeyDown. As
2108                // the Activity's shortcut keys are only handled when WebView
2109                // doesn't, have to do it in onKeyDown instead of onKeyUp.
2110                if (event.isShiftPressed()) {
2111                    getTopWindow().pageUp(false);
2112                } else {
2113                    getTopWindow().pageDown(false);
2114                }
2115                return true;
2116            case KeyEvent.KEYCODE_BACK:
2117                if (event.getRepeatCount() == 0) {
2118                    event.startTracking();
2119                    return true;
2120                } else if (mCustomView == null && mActiveTabsPage == null
2121                        && event.isLongPress()) {
2122                    bookmarksOrHistoryPicker(true);
2123                    return true;
2124                }
2125                break;
2126        }
2127        return super.onKeyDown(keyCode, event);
2128    }
2129
2130    @Override
2131    public boolean onKeyUp(int keyCode, KeyEvent event) {
2132        switch(keyCode) {
2133            case KeyEvent.KEYCODE_MENU:
2134                mMenuIsDown = false;
2135                break;
2136            case KeyEvent.KEYCODE_BACK:
2137                if (event.isTracking() && !event.isCanceled()) {
2138                    if (mCustomView != null) {
2139                        // if a custom view is showing, hide it
2140                        mTabControl.getCurrentWebView().getWebChromeClient()
2141                                .onHideCustomView();
2142                    } else if (mActiveTabsPage != null) {
2143                        // if tab page is showing, hide it
2144                        removeActiveTabPage(true);
2145                    } else {
2146                        WebView subwindow = mTabControl.getCurrentSubWindow();
2147                        if (subwindow != null) {
2148                            if (subwindow.canGoBack()) {
2149                                subwindow.goBack();
2150                            } else {
2151                                dismissSubWindow(mTabControl.getCurrentTab());
2152                            }
2153                        } else {
2154                            goBackOnePageOrQuit();
2155                        }
2156                    }
2157                    return true;
2158                }
2159                break;
2160        }
2161        return super.onKeyUp(keyCode, event);
2162    }
2163
2164    /* package */ void stopLoading() {
2165        mDidStopLoad = true;
2166        resetTitleAndRevertLockIcon();
2167        WebView w = getTopWindow();
2168        w.stopLoading();
2169        // FIXME: before refactor, it is using mWebViewClient. So I keep the
2170        // same logic here. But for subwindow case, should we call into the main
2171        // WebView's onPageFinished as we never call its onPageStarted and if
2172        // the page finishes itself, we don't call onPageFinished.
2173        mTabControl.getCurrentWebView().getWebViewClient().onPageFinished(w,
2174                w.getUrl());
2175
2176        cancelStopToast();
2177        mStopToast = Toast
2178                .makeText(this, R.string.stopping, Toast.LENGTH_SHORT);
2179        mStopToast.show();
2180    }
2181
2182    boolean didUserStopLoading() {
2183        return mDidStopLoad;
2184    }
2185
2186    private void cancelStopToast() {
2187        if (mStopToast != null) {
2188            mStopToast.cancel();
2189            mStopToast = null;
2190        }
2191    }
2192
2193    // called by a UI or non-UI thread to post the message
2194    public void postMessage(int what, int arg1, int arg2, Object obj,
2195            long delayMillis) {
2196        mHandler.sendMessageDelayed(mHandler.obtainMessage(what, arg1, arg2,
2197                obj), delayMillis);
2198    }
2199
2200    // called by a UI or non-UI thread to remove the message
2201    void removeMessages(int what, Object object) {
2202        mHandler.removeMessages(what, object);
2203    }
2204
2205    // public message ids
2206    public final static int LOAD_URL                = 1001;
2207    public final static int STOP_LOAD               = 1002;
2208
2209    // Message Ids
2210    private static final int FOCUS_NODE_HREF         = 102;
2211    private static final int CANCEL_CREDS_REQUEST    = 103;
2212    private static final int RELEASE_WAKELOCK        = 107;
2213
2214    static final int UPDATE_BOOKMARK_THUMBNAIL       = 108;
2215
2216    // Private handler for handling javascript and saving passwords
2217    private Handler mHandler = new Handler() {
2218
2219        public void handleMessage(Message msg) {
2220            switch (msg.what) {
2221                case FOCUS_NODE_HREF:
2222                {
2223                    String url = (String) msg.getData().get("url");
2224                    String title = (String) msg.getData().get("title");
2225                    if (url == null || url.length() == 0) {
2226                        break;
2227                    }
2228                    HashMap focusNodeMap = (HashMap) msg.obj;
2229                    WebView view = (WebView) focusNodeMap.get("webview");
2230                    // Only apply the action if the top window did not change.
2231                    if (getTopWindow() != view) {
2232                        break;
2233                    }
2234                    switch (msg.arg1) {
2235                        case R.id.open_context_menu_id:
2236                        case R.id.view_image_context_menu_id:
2237                            loadURL(getTopWindow(), url);
2238                            break;
2239                        case R.id.open_newtab_context_menu_id:
2240                            final Tab parent = mTabControl.getCurrentTab();
2241                            final Tab newTab = openTab(url);
2242                            if (newTab != parent) {
2243                                parent.addChildTab(newTab);
2244                            }
2245                            break;
2246                        case R.id.bookmark_context_menu_id:
2247                            Intent intent = new Intent(BrowserActivity.this,
2248                                    AddBookmarkPage.class);
2249                            intent.putExtra("url", url);
2250                            intent.putExtra("title", title);
2251                            startActivity(intent);
2252                            break;
2253                        case R.id.share_link_context_menu_id:
2254                            Browser.sendString(BrowserActivity.this, url,
2255                                    getText(R.string.choosertitle_sharevia).toString());
2256                            break;
2257                        case R.id.copy_link_context_menu_id:
2258                            copy(url);
2259                            break;
2260                        case R.id.save_link_context_menu_id:
2261                        case R.id.download_context_menu_id:
2262                            onDownloadStartNoStream(url, null, null, null, -1);
2263                            break;
2264                    }
2265                    break;
2266                }
2267
2268                case LOAD_URL:
2269                    loadURL(getTopWindow(), (String) msg.obj);
2270                    break;
2271
2272                case STOP_LOAD:
2273                    stopLoading();
2274                    break;
2275
2276                case CANCEL_CREDS_REQUEST:
2277                    resumeAfterCredentials();
2278                    break;
2279
2280                case RELEASE_WAKELOCK:
2281                    if (mWakeLock.isHeld()) {
2282                        mWakeLock.release();
2283                        // if we reach here, Browser should be still in the
2284                        // background loading after WAKELOCK_TIMEOUT (5-min).
2285                        // To avoid burning the battery, stop loading.
2286                        mTabControl.stopAllLoading();
2287                    }
2288                    break;
2289
2290                case UPDATE_BOOKMARK_THUMBNAIL:
2291                    WebView view = (WebView) msg.obj;
2292                    if (view != null) {
2293                        updateScreenshot(view);
2294                    }
2295                    break;
2296            }
2297        }
2298    };
2299
2300    private void updateScreenshot(WebView view) {
2301        // If this is a bookmarked site, add a screenshot to the database.
2302        // FIXME: When should we update?  Every time?
2303        // FIXME: Would like to make sure there is actually something to
2304        // draw, but the API for that (WebViewCore.pictureReady()) is not
2305        // currently accessible here.
2306
2307        ContentResolver cr = getContentResolver();
2308        final Cursor c = BrowserBookmarksAdapter.queryBookmarksForUrl(
2309                cr, view.getOriginalUrl(), view.getUrl(), true);
2310        if (c != null) {
2311            boolean succeed = c.moveToFirst();
2312            ContentValues values = null;
2313            while (succeed) {
2314                if (values == null) {
2315                    final ByteArrayOutputStream os
2316                            = new ByteArrayOutputStream();
2317                    Bitmap bm = createScreenshot(view);
2318                    if (bm == null) {
2319                        c.close();
2320                        return;
2321                    }
2322                    bm.compress(Bitmap.CompressFormat.PNG, 100, os);
2323                    values = new ContentValues();
2324                    values.put(Browser.BookmarkColumns.THUMBNAIL,
2325                            os.toByteArray());
2326                }
2327                cr.update(ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
2328                        c.getInt(0)), values, null, null);
2329                succeed = c.moveToNext();
2330            }
2331            c.close();
2332        }
2333    }
2334
2335    /**
2336     * Values for the size of the thumbnail created when taking a screenshot.
2337     * Lazily initialized.  Instead of using these directly, use
2338     * getDesiredThumbnailWidth() or getDesiredThumbnailHeight().
2339     */
2340    private static int THUMBNAIL_WIDTH = 0;
2341    private static int THUMBNAIL_HEIGHT = 0;
2342
2343    /**
2344     * Return the desired width for thumbnail screenshots, which are stored in
2345     * the database, and used on the bookmarks screen.
2346     * @param context Context for finding out the density of the screen.
2347     * @return int desired width for thumbnail screenshot.
2348     */
2349    /* package */ static int getDesiredThumbnailWidth(Context context) {
2350        if (THUMBNAIL_WIDTH == 0) {
2351            float density = context.getResources().getDisplayMetrics().density;
2352            THUMBNAIL_WIDTH = (int) (90 * density);
2353            THUMBNAIL_HEIGHT = (int) (80 * density);
2354        }
2355        return THUMBNAIL_WIDTH;
2356    }
2357
2358    /**
2359     * Return the desired height for thumbnail screenshots, which are stored in
2360     * the database, and used on the bookmarks screen.
2361     * @param context Context for finding out the density of the screen.
2362     * @return int desired height for thumbnail screenshot.
2363     */
2364    /* package */ static int getDesiredThumbnailHeight(Context context) {
2365        // To ensure that they are both initialized.
2366        getDesiredThumbnailWidth(context);
2367        return THUMBNAIL_HEIGHT;
2368    }
2369
2370    private Bitmap createScreenshot(WebView view) {
2371        Picture thumbnail = view.capturePicture();
2372        if (thumbnail == null) {
2373            return null;
2374        }
2375        Bitmap bm = Bitmap.createBitmap(getDesiredThumbnailWidth(this),
2376                getDesiredThumbnailHeight(this), Bitmap.Config.ARGB_4444);
2377        Canvas canvas = new Canvas(bm);
2378        // May need to tweak these values to determine what is the
2379        // best scale factor
2380        int thumbnailWidth = thumbnail.getWidth();
2381        int thumbnailHeight = thumbnail.getHeight();
2382        float scaleFactorX = 1.0f;
2383        float scaleFactorY = 1.0f;
2384        if (thumbnailWidth > 0) {
2385            scaleFactorX = (float) getDesiredThumbnailWidth(this) /
2386                    (float)thumbnailWidth;
2387        } else {
2388            return null;
2389        }
2390
2391        if (view.getWidth() > view.getHeight() &&
2392                thumbnailHeight < view.getHeight() && thumbnailHeight > 0) {
2393            // If the device is in landscape and the page is shorter
2394            // than the height of the view, stretch the thumbnail to fill the
2395            // space.
2396            scaleFactorY = (float) getDesiredThumbnailHeight(this) /
2397                    (float)thumbnailHeight;
2398        } else {
2399            // In the portrait case, this looks nice.
2400            scaleFactorY = scaleFactorX;
2401        }
2402
2403        canvas.scale(scaleFactorX, scaleFactorY);
2404
2405        thumbnail.draw(canvas);
2406        return bm;
2407    }
2408
2409    // -------------------------------------------------------------------------
2410    // Helper function for WebViewClient.
2411    //-------------------------------------------------------------------------
2412
2413    // Use in overrideUrlLoading
2414    /* package */ final static String SCHEME_WTAI = "wtai://wp/";
2415    /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;";
2416    /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;";
2417    /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;";
2418
2419    void onPageStarted(WebView view, String url, Bitmap favicon) {
2420        // when BrowserActivity just starts, onPageStarted may be called before
2421        // onResume as it is triggered from onCreate. Call resumeWebViewTimers
2422        // to start the timer. As we won't switch tabs while an activity is in
2423        // pause state, we can ensure calling resume and pause in pair.
2424        if (mActivityInPause) resumeWebViewTimers();
2425
2426        resetLockIcon(url);
2427        setUrlTitle(url, null);
2428        setFavicon(favicon);
2429        // Keep this initial progress in sync with initialProgressValue (* 100)
2430        // in ProgressTracker.cpp
2431        // Show some progress so that the user knows the page is beginning to
2432        // load
2433        onProgressChanged(view, 10);
2434        mDidStopLoad = false;
2435        if (!mIsNetworkUp) createAndShowNetworkDialog();
2436
2437        if (mSettings.isTracing()) {
2438            String host;
2439            try {
2440                WebAddress uri = new WebAddress(url);
2441                host = uri.mHost;
2442            } catch (android.net.ParseException ex) {
2443                host = "browser";
2444            }
2445            host = host.replace('.', '_');
2446            host += ".trace";
2447            mInTrace = true;
2448            Debug.startMethodTracing(host, 20 * 1024 * 1024);
2449        }
2450
2451        // Performance probe
2452        if (false) {
2453            mStart = SystemClock.uptimeMillis();
2454            mProcessStart = Process.getElapsedCpuTime();
2455            long[] sysCpu = new long[7];
2456            if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null,
2457                    sysCpu, null)) {
2458                mUserStart = sysCpu[0] + sysCpu[1];
2459                mSystemStart = sysCpu[2];
2460                mIdleStart = sysCpu[3];
2461                mIrqStart = sysCpu[4] + sysCpu[5] + sysCpu[6];
2462            }
2463            mUiStart = SystemClock.currentThreadTimeMillis();
2464        }
2465    }
2466
2467    void onPageFinished(WebView view, String url) {
2468        // Reset the title and icon in case we stopped a provisional load.
2469        resetTitleAndIcon(view);
2470        // Update the lock icon image only once we are done loading
2471        updateLockIconToLatest();
2472        // pause the WebView timer and release the wake lock if it is finished
2473        // while BrowserActivity is in pause state.
2474        if (mActivityInPause && pauseWebViewTimers()) {
2475            if (mWakeLock.isHeld()) {
2476                mHandler.removeMessages(RELEASE_WAKELOCK);
2477                mWakeLock.release();
2478            }
2479        }
2480
2481        // Performance probe
2482        if (false) {
2483            long[] sysCpu = new long[7];
2484            if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null,
2485                    sysCpu, null)) {
2486                String uiInfo = "UI thread used "
2487                        + (SystemClock.currentThreadTimeMillis() - mUiStart)
2488                        + " ms";
2489                if (LOGD_ENABLED) {
2490                    Log.d(LOGTAG, uiInfo);
2491                }
2492                //The string that gets written to the log
2493                String performanceString = "It took total "
2494                        + (SystemClock.uptimeMillis() - mStart)
2495                        + " ms clock time to load the page."
2496                        + "\nbrowser process used "
2497                        + (Process.getElapsedCpuTime() - mProcessStart)
2498                        + " ms, user processes used "
2499                        + (sysCpu[0] + sysCpu[1] - mUserStart) * 10
2500                        + " ms, kernel used "
2501                        + (sysCpu[2] - mSystemStart) * 10
2502                        + " ms, idle took " + (sysCpu[3] - mIdleStart) * 10
2503                        + " ms and irq took "
2504                        + (sysCpu[4] + sysCpu[5] + sysCpu[6] - mIrqStart)
2505                        * 10 + " ms, " + uiInfo;
2506                if (LOGD_ENABLED) {
2507                    Log.d(LOGTAG, performanceString + "\nWebpage: " + url);
2508                }
2509                if (url != null) {
2510                    // strip the url to maintain consistency
2511                    String newUrl = new String(url);
2512                    if (newUrl.startsWith("http://www.")) {
2513                        newUrl = newUrl.substring(11);
2514                    } else if (newUrl.startsWith("http://")) {
2515                        newUrl = newUrl.substring(7);
2516                    } else if (newUrl.startsWith("https://www.")) {
2517                        newUrl = newUrl.substring(12);
2518                    } else if (newUrl.startsWith("https://")) {
2519                        newUrl = newUrl.substring(8);
2520                    }
2521                    if (LOGD_ENABLED) {
2522                        Log.d(LOGTAG, newUrl + " loaded");
2523                    }
2524                }
2525            }
2526         }
2527
2528        if (mInTrace) {
2529            mInTrace = false;
2530            Debug.stopMethodTracing();
2531        }
2532    }
2533
2534    boolean shouldOverrideUrlLoading(WebView view, String url) {
2535        if (url.startsWith(SCHEME_WTAI)) {
2536            // wtai://wp/mc;number
2537            // number=string(phone-number)
2538            if (url.startsWith(SCHEME_WTAI_MC)) {
2539                Intent intent = new Intent(Intent.ACTION_VIEW,
2540                        Uri.parse(WebView.SCHEME_TEL +
2541                        url.substring(SCHEME_WTAI_MC.length())));
2542                startActivity(intent);
2543                return true;
2544            }
2545            // wtai://wp/sd;dtmf
2546            // dtmf=string(dialstring)
2547            if (url.startsWith(SCHEME_WTAI_SD)) {
2548                // TODO: only send when there is active voice connection
2549                return false;
2550            }
2551            // wtai://wp/ap;number;name
2552            // number=string(phone-number)
2553            // name=string
2554            if (url.startsWith(SCHEME_WTAI_AP)) {
2555                // TODO
2556                return false;
2557            }
2558        }
2559
2560        // The "about:" schemes are internal to the browser; don't want these to
2561        // be dispatched to other apps.
2562        if (url.startsWith("about:")) {
2563            return false;
2564        }
2565
2566        Intent intent;
2567        // perform generic parsing of the URI to turn it into an Intent.
2568        try {
2569            intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
2570        } catch (URISyntaxException ex) {
2571            Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
2572            return false;
2573        }
2574
2575        // check whether the intent can be resolved. If not, we will see
2576        // whether we can download it from the Market.
2577        if (getPackageManager().resolveActivity(intent, 0) == null) {
2578            String packagename = intent.getPackage();
2579            if (packagename != null) {
2580                intent = new Intent(Intent.ACTION_VIEW, Uri
2581                        .parse("market://search?q=pname:" + packagename));
2582                intent.addCategory(Intent.CATEGORY_BROWSABLE);
2583                startActivity(intent);
2584                return true;
2585            } else {
2586                return false;
2587            }
2588        }
2589
2590        // sanitize the Intent, ensuring web pages can not bypass browser
2591        // security (only access to BROWSABLE activities).
2592        intent.addCategory(Intent.CATEGORY_BROWSABLE);
2593        intent.setComponent(null);
2594        try {
2595            if (startActivityIfNeeded(intent, -1)) {
2596                return true;
2597            }
2598        } catch (ActivityNotFoundException ex) {
2599            // ignore the error. If no application can handle the URL,
2600            // eg about:blank, assume the browser can handle it.
2601        }
2602
2603        if (mMenuIsDown) {
2604            openTab(url);
2605            closeOptionsMenu();
2606            return true;
2607        }
2608        return false;
2609    }
2610
2611    // -------------------------------------------------------------------------
2612    // Helper function for WebChromeClient
2613    // -------------------------------------------------------------------------
2614
2615    void onProgressChanged(WebView view, int newProgress) {
2616        mTitleBar.setProgress(newProgress);
2617        mFakeTitleBar.setProgress(newProgress);
2618
2619        if (newProgress == 100) {
2620            // onProgressChanged() may continue to be called after the main
2621            // frame has finished loading, as any remaining sub frames continue
2622            // to load. We'll only get called once though with newProgress as
2623            // 100 when everything is loaded. (onPageFinished is called once
2624            // when the main frame completes loading regardless of the state of
2625            // any sub frames so calls to onProgressChanges may continue after
2626            // onPageFinished has executed)
2627            if (mInLoad) {
2628                mInLoad = false;
2629                updateInLoadMenuItems();
2630                // If the options menu is open, leave the title bar
2631                if (!mOptionsMenuOpen || !mIconView) {
2632                    hideFakeTitleBar();
2633                }
2634            }
2635        } else if (!mInLoad) {
2636            // onPageFinished may have already been called but a subframe is
2637            // still loading and updating the progress. Reset mInLoad and update
2638            // the menu items.
2639            mInLoad = true;
2640            updateInLoadMenuItems();
2641            if (!mOptionsMenuOpen || mIconView) {
2642                // This page has begun to load, so show the title bar
2643                showFakeTitleBar();
2644            }
2645        }
2646    }
2647
2648    void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) {
2649        // if a view already exists then immediately terminate the new one
2650        if (mCustomView != null) {
2651            callback.onCustomViewHidden();
2652            return;
2653        }
2654
2655        // Add the custom view to its container.
2656        mCustomViewContainer.addView(view, COVER_SCREEN_GRAVITY_CENTER);
2657        mCustomView = view;
2658        mCustomViewCallback = callback;
2659        // Save the menu state and set it to empty while the custom
2660        // view is showing.
2661        mOldMenuState = mMenuState;
2662        mMenuState = EMPTY_MENU;
2663        // Hide the content view.
2664        mContentView.setVisibility(View.GONE);
2665        // Finally show the custom view container.
2666        setStatusBarVisibility(false);
2667        mCustomViewContainer.setVisibility(View.VISIBLE);
2668        mCustomViewContainer.bringToFront();
2669    }
2670
2671    void onHideCustomView() {
2672        if (mCustomView == null)
2673            return;
2674
2675        // Hide the custom view.
2676        mCustomView.setVisibility(View.GONE);
2677        // Remove the custom view from its container.
2678        mCustomViewContainer.removeView(mCustomView);
2679        mCustomView = null;
2680        // Reset the old menu state.
2681        mMenuState = mOldMenuState;
2682        mOldMenuState = EMPTY_MENU;
2683        mCustomViewContainer.setVisibility(View.GONE);
2684        mCustomViewCallback.onCustomViewHidden();
2685        // Show the content view.
2686        setStatusBarVisibility(true);
2687        mContentView.setVisibility(View.VISIBLE);
2688    }
2689
2690    Bitmap getDefaultVideoPoster() {
2691        if (mDefaultVideoPoster == null) {
2692            mDefaultVideoPoster = BitmapFactory.decodeResource(
2693                    getResources(), R.drawable.default_video_poster);
2694        }
2695        return mDefaultVideoPoster;
2696    }
2697
2698    View getVideoLoadingProgressView() {
2699        if (mVideoProgressView == null) {
2700            LayoutInflater inflater = LayoutInflater.from(BrowserActivity.this);
2701            mVideoProgressView = inflater.inflate(
2702                    R.layout.video_loading_progress, null);
2703        }
2704        return mVideoProgressView;
2705    }
2706
2707    /*
2708     * The Object used to inform the WebView of the file to upload.
2709     */
2710    private ValueCallback<Uri> mUploadMessage;
2711
2712    void openFileChooser(ValueCallback<Uri> uploadMsg) {
2713        if (mUploadMessage != null) return;
2714        mUploadMessage = uploadMsg;
2715        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2716        i.addCategory(Intent.CATEGORY_OPENABLE);
2717        i.setType("*/*");
2718        BrowserActivity.this.startActivityForResult(Intent.createChooser(i,
2719                getString(R.string.choose_upload)), FILE_SELECTED);
2720    }
2721
2722    // -------------------------------------------------------------------------
2723    // Implement functions for DownloadListener
2724    // -------------------------------------------------------------------------
2725
2726    /**
2727     * Notify the host application a download should be done, or that
2728     * the data should be streamed if a streaming viewer is available.
2729     * @param url The full url to the content that should be downloaded
2730     * @param contentDisposition Content-disposition http header, if
2731     *                           present.
2732     * @param mimetype The mimetype of the content reported by the server
2733     * @param contentLength The file size reported by the server
2734     */
2735    public void onDownloadStart(String url, String userAgent,
2736            String contentDisposition, String mimetype, long contentLength) {
2737        // if we're dealing wih A/V content that's not explicitly marked
2738        //     for download, check if it's streamable.
2739        if (contentDisposition == null
2740                || !contentDisposition.regionMatches(
2741                        true, 0, "attachment", 0, 10)) {
2742            // query the package manager to see if there's a registered handler
2743            //     that matches.
2744            Intent intent = new Intent(Intent.ACTION_VIEW);
2745            intent.setDataAndType(Uri.parse(url), mimetype);
2746            ResolveInfo info = getPackageManager().resolveActivity(intent,
2747                    PackageManager.MATCH_DEFAULT_ONLY);
2748            if (info != null) {
2749                ComponentName myName = getComponentName();
2750                // If we resolved to ourselves, we don't want to attempt to
2751                // load the url only to try and download it again.
2752                if (!myName.getPackageName().equals(
2753                        info.activityInfo.packageName)
2754                        || !myName.getClassName().equals(
2755                                info.activityInfo.name)) {
2756                    // someone (other than us) knows how to handle this mime
2757                    // type with this scheme, don't download.
2758                    try {
2759                        startActivity(intent);
2760                        return;
2761                    } catch (ActivityNotFoundException ex) {
2762                        if (LOGD_ENABLED) {
2763                            Log.d(LOGTAG, "activity not found for " + mimetype
2764                                    + " over " + Uri.parse(url).getScheme(),
2765                                    ex);
2766                        }
2767                        // Best behavior is to fall back to a download in this
2768                        // case
2769                    }
2770                }
2771            }
2772        }
2773        onDownloadStartNoStream(url, userAgent, contentDisposition, mimetype, contentLength);
2774    }
2775
2776    /**
2777     * Notify the host application a download should be done, even if there
2778     * is a streaming viewer available for thise type.
2779     * @param url The full url to the content that should be downloaded
2780     * @param contentDisposition Content-disposition http header, if
2781     *                           present.
2782     * @param mimetype The mimetype of the content reported by the server
2783     * @param contentLength The file size reported by the server
2784     */
2785    /*package */ void onDownloadStartNoStream(String url, String userAgent,
2786            String contentDisposition, String mimetype, long contentLength) {
2787
2788        String filename = URLUtil.guessFileName(url,
2789                contentDisposition, mimetype);
2790
2791        // Check to see if we have an SDCard
2792        String status = Environment.getExternalStorageState();
2793        if (!status.equals(Environment.MEDIA_MOUNTED)) {
2794            int title;
2795            String msg;
2796
2797            // Check to see if the SDCard is busy, same as the music app
2798            if (status.equals(Environment.MEDIA_SHARED)) {
2799                msg = getString(R.string.download_sdcard_busy_dlg_msg);
2800                title = R.string.download_sdcard_busy_dlg_title;
2801            } else {
2802                msg = getString(R.string.download_no_sdcard_dlg_msg, filename);
2803                title = R.string.download_no_sdcard_dlg_title;
2804            }
2805
2806            new AlertDialog.Builder(this)
2807                .setTitle(title)
2808                .setIcon(android.R.drawable.ic_dialog_alert)
2809                .setMessage(msg)
2810                .setPositiveButton(R.string.ok, null)
2811                .show();
2812            return;
2813        }
2814
2815        // java.net.URI is a lot stricter than KURL so we have to undo
2816        // KURL's percent-encoding and redo the encoding using java.net.URI.
2817        URI uri = null;
2818        try {
2819            // Undo the percent-encoding that KURL may have done.
2820            String newUrl = new String(URLUtil.decode(url.getBytes()));
2821            // Parse the url into pieces
2822            WebAddress w = new WebAddress(newUrl);
2823            String frag = null;
2824            String query = null;
2825            String path = w.mPath;
2826            // Break the path into path, query, and fragment
2827            if (path.length() > 0) {
2828                // Strip the fragment
2829                int idx = path.lastIndexOf('#');
2830                if (idx != -1) {
2831                    frag = path.substring(idx + 1);
2832                    path = path.substring(0, idx);
2833                }
2834                idx = path.lastIndexOf('?');
2835                if (idx != -1) {
2836                    query = path.substring(idx + 1);
2837                    path = path.substring(0, idx);
2838                }
2839            }
2840            uri = new URI(w.mScheme, w.mAuthInfo, w.mHost, w.mPort, path,
2841                    query, frag);
2842        } catch (Exception e) {
2843            Log.e(LOGTAG, "Could not parse url for download: " + url, e);
2844            return;
2845        }
2846
2847        // XXX: Have to use the old url since the cookies were stored using the
2848        // old percent-encoded url.
2849        String cookies = CookieManager.getInstance().getCookie(url);
2850
2851        ContentValues values = new ContentValues();
2852        values.put(Downloads.COLUMN_URI, uri.toString());
2853        values.put(Downloads.COLUMN_COOKIE_DATA, cookies);
2854        values.put(Downloads.COLUMN_USER_AGENT, userAgent);
2855        values.put(Downloads.COLUMN_NOTIFICATION_PACKAGE,
2856                getPackageName());
2857        values.put(Downloads.COLUMN_NOTIFICATION_CLASS,
2858                BrowserDownloadPage.class.getCanonicalName());
2859        values.put(Downloads.COLUMN_VISIBILITY, Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
2860        values.put(Downloads.COLUMN_MIME_TYPE, mimetype);
2861        values.put(Downloads.COLUMN_FILE_NAME_HINT, filename);
2862        values.put(Downloads.COLUMN_DESCRIPTION, uri.getHost());
2863        if (contentLength > 0) {
2864            values.put(Downloads.COLUMN_TOTAL_BYTES, contentLength);
2865        }
2866        if (mimetype == null) {
2867            // We must have long pressed on a link or image to download it. We
2868            // are not sure of the mimetype in this case, so do a head request
2869            new FetchUrlMimeType(this).execute(values);
2870        } else {
2871            final Uri contentUri =
2872                    getContentResolver().insert(Downloads.CONTENT_URI, values);
2873            viewDownloads(contentUri);
2874        }
2875
2876    }
2877
2878    // -------------------------------------------------------------------------
2879
2880    /**
2881     * Resets the lock icon. This method is called when we start a new load and
2882     * know the url to be loaded.
2883     */
2884    private void resetLockIcon(String url) {
2885        // Save the lock-icon state (we revert to it if the load gets cancelled)
2886        mTabControl.getCurrentTab().resetLockIcon(url);
2887        updateLockIconImage(LOCK_ICON_UNSECURE);
2888    }
2889
2890    /**
2891     * Update the lock icon to correspond to our latest state.
2892     */
2893    private void updateLockIconToLatest() {
2894        updateLockIconImage(mTabControl.getCurrentTab().getLockIconType());
2895    }
2896
2897    /**
2898     * Updates the lock-icon image in the title-bar.
2899     */
2900    private void updateLockIconImage(int lockIconType) {
2901        Drawable d = null;
2902        if (lockIconType == LOCK_ICON_SECURE) {
2903            d = mSecLockIcon;
2904        } else if (lockIconType == LOCK_ICON_MIXED) {
2905            d = mMixLockIcon;
2906        }
2907        mTitleBar.setLock(d);
2908        mFakeTitleBar.setLock(d);
2909    }
2910
2911    /**
2912     * Displays a page-info dialog.
2913     * @param tab The tab to show info about
2914     * @param fromShowSSLCertificateOnError The flag that indicates whether
2915     * this dialog was opened from the SSL-certificate-on-error dialog or
2916     * not. This is important, since we need to know whether to return to
2917     * the parent dialog or simply dismiss.
2918     */
2919    private void showPageInfo(final Tab tab,
2920                              final boolean fromShowSSLCertificateOnError) {
2921        final LayoutInflater factory = LayoutInflater
2922                .from(this);
2923
2924        final View pageInfoView = factory.inflate(R.layout.page_info, null);
2925
2926        final WebView view = tab.getWebView();
2927
2928        String url = null;
2929        String title = null;
2930
2931        if (view == null) {
2932            url = tab.getUrl();
2933            title = tab.getTitle();
2934        } else if (view == mTabControl.getCurrentWebView()) {
2935             // Use the cached title and url if this is the current WebView
2936            url = mUrl;
2937            title = mTitle;
2938        } else {
2939            url = view.getUrl();
2940            title = view.getTitle();
2941        }
2942
2943        if (url == null) {
2944            url = "";
2945        }
2946        if (title == null) {
2947            title = "";
2948        }
2949
2950        ((TextView) pageInfoView.findViewById(R.id.address)).setText(url);
2951        ((TextView) pageInfoView.findViewById(R.id.title)).setText(title);
2952
2953        mPageInfoView = tab;
2954        mPageInfoFromShowSSLCertificateOnError = fromShowSSLCertificateOnError;
2955
2956        AlertDialog.Builder alertDialogBuilder =
2957            new AlertDialog.Builder(this)
2958            .setTitle(R.string.page_info).setIcon(android.R.drawable.ic_dialog_info)
2959            .setView(pageInfoView)
2960            .setPositiveButton(
2961                R.string.ok,
2962                new DialogInterface.OnClickListener() {
2963                    public void onClick(DialogInterface dialog,
2964                                        int whichButton) {
2965                        mPageInfoDialog = null;
2966                        mPageInfoView = null;
2967
2968                        // if we came here from the SSL error dialog
2969                        if (fromShowSSLCertificateOnError) {
2970                            // go back to the SSL error dialog
2971                            showSSLCertificateOnError(
2972                                mSSLCertificateOnErrorView,
2973                                mSSLCertificateOnErrorHandler,
2974                                mSSLCertificateOnErrorError);
2975                        }
2976                    }
2977                })
2978            .setOnCancelListener(
2979                new DialogInterface.OnCancelListener() {
2980                    public void onCancel(DialogInterface dialog) {
2981                        mPageInfoDialog = null;
2982                        mPageInfoView = null;
2983
2984                        // if we came here from the SSL error dialog
2985                        if (fromShowSSLCertificateOnError) {
2986                            // go back to the SSL error dialog
2987                            showSSLCertificateOnError(
2988                                mSSLCertificateOnErrorView,
2989                                mSSLCertificateOnErrorHandler,
2990                                mSSLCertificateOnErrorError);
2991                        }
2992                    }
2993                });
2994
2995        // if we have a main top-level page SSL certificate set or a certificate
2996        // error
2997        if (fromShowSSLCertificateOnError ||
2998                (view != null && view.getCertificate() != null)) {
2999            // add a 'View Certificate' button
3000            alertDialogBuilder.setNeutralButton(
3001                R.string.view_certificate,
3002                new DialogInterface.OnClickListener() {
3003                    public void onClick(DialogInterface dialog,
3004                                        int whichButton) {
3005                        mPageInfoDialog = null;
3006                        mPageInfoView = null;
3007
3008                        // if we came here from the SSL error dialog
3009                        if (fromShowSSLCertificateOnError) {
3010                            // go back to the SSL error dialog
3011                            showSSLCertificateOnError(
3012                                mSSLCertificateOnErrorView,
3013                                mSSLCertificateOnErrorHandler,
3014                                mSSLCertificateOnErrorError);
3015                        } else {
3016                            // otherwise, display the top-most certificate from
3017                            // the chain
3018                            if (view.getCertificate() != null) {
3019                                showSSLCertificate(tab);
3020                            }
3021                        }
3022                    }
3023                });
3024        }
3025
3026        mPageInfoDialog = alertDialogBuilder.show();
3027    }
3028
3029       /**
3030     * Displays the main top-level page SSL certificate dialog
3031     * (accessible from the Page-Info dialog).
3032     * @param tab The tab to show certificate for.
3033     */
3034    private void showSSLCertificate(final Tab tab) {
3035        final View certificateView =
3036                inflateCertificateView(tab.getWebView().getCertificate());
3037        if (certificateView == null) {
3038            return;
3039        }
3040
3041        LayoutInflater factory = LayoutInflater.from(this);
3042
3043        final LinearLayout placeholder =
3044                (LinearLayout)certificateView.findViewById(R.id.placeholder);
3045
3046        LinearLayout ll = (LinearLayout) factory.inflate(
3047            R.layout.ssl_success, placeholder);
3048        ((TextView)ll.findViewById(R.id.success))
3049            .setText(R.string.ssl_certificate_is_valid);
3050
3051        mSSLCertificateView = tab;
3052        mSSLCertificateDialog =
3053            new AlertDialog.Builder(this)
3054                .setTitle(R.string.ssl_certificate).setIcon(
3055                    R.drawable.ic_dialog_browser_certificate_secure)
3056                .setView(certificateView)
3057                .setPositiveButton(R.string.ok,
3058                        new DialogInterface.OnClickListener() {
3059                            public void onClick(DialogInterface dialog,
3060                                    int whichButton) {
3061                                mSSLCertificateDialog = null;
3062                                mSSLCertificateView = null;
3063
3064                                showPageInfo(tab, false);
3065                            }
3066                        })
3067                .setOnCancelListener(
3068                        new DialogInterface.OnCancelListener() {
3069                            public void onCancel(DialogInterface dialog) {
3070                                mSSLCertificateDialog = null;
3071                                mSSLCertificateView = null;
3072
3073                                showPageInfo(tab, false);
3074                            }
3075                        })
3076                .show();
3077    }
3078
3079    /**
3080     * Displays the SSL error certificate dialog.
3081     * @param view The target web-view.
3082     * @param handler The SSL error handler responsible for cancelling the
3083     * connection that resulted in an SSL error or proceeding per user request.
3084     * @param error The SSL error object.
3085     */
3086    void showSSLCertificateOnError(
3087        final WebView view, final SslErrorHandler handler, final SslError error) {
3088
3089        final View certificateView =
3090            inflateCertificateView(error.getCertificate());
3091        if (certificateView == null) {
3092            return;
3093        }
3094
3095        LayoutInflater factory = LayoutInflater.from(this);
3096
3097        final LinearLayout placeholder =
3098                (LinearLayout)certificateView.findViewById(R.id.placeholder);
3099
3100        if (error.hasError(SslError.SSL_UNTRUSTED)) {
3101            LinearLayout ll = (LinearLayout)factory
3102                .inflate(R.layout.ssl_warning, placeholder);
3103            ((TextView)ll.findViewById(R.id.warning))
3104                .setText(R.string.ssl_untrusted);
3105        }
3106
3107        if (error.hasError(SslError.SSL_IDMISMATCH)) {
3108            LinearLayout ll = (LinearLayout)factory
3109                .inflate(R.layout.ssl_warning, placeholder);
3110            ((TextView)ll.findViewById(R.id.warning))
3111                .setText(R.string.ssl_mismatch);
3112        }
3113
3114        if (error.hasError(SslError.SSL_EXPIRED)) {
3115            LinearLayout ll = (LinearLayout)factory
3116                .inflate(R.layout.ssl_warning, placeholder);
3117            ((TextView)ll.findViewById(R.id.warning))
3118                .setText(R.string.ssl_expired);
3119        }
3120
3121        if (error.hasError(SslError.SSL_NOTYETVALID)) {
3122            LinearLayout ll = (LinearLayout)factory
3123                .inflate(R.layout.ssl_warning, placeholder);
3124            ((TextView)ll.findViewById(R.id.warning))
3125                .setText(R.string.ssl_not_yet_valid);
3126        }
3127
3128        mSSLCertificateOnErrorHandler = handler;
3129        mSSLCertificateOnErrorView = view;
3130        mSSLCertificateOnErrorError = error;
3131        mSSLCertificateOnErrorDialog =
3132            new AlertDialog.Builder(this)
3133                .setTitle(R.string.ssl_certificate).setIcon(
3134                    R.drawable.ic_dialog_browser_certificate_partially_secure)
3135                .setView(certificateView)
3136                .setPositiveButton(R.string.ok,
3137                        new DialogInterface.OnClickListener() {
3138                            public void onClick(DialogInterface dialog,
3139                                    int whichButton) {
3140                                mSSLCertificateOnErrorDialog = null;
3141                                mSSLCertificateOnErrorView = null;
3142                                mSSLCertificateOnErrorHandler = null;
3143                                mSSLCertificateOnErrorError = null;
3144
3145                                view.getWebViewClient().onReceivedSslError(
3146                                                view, handler, error);
3147                            }
3148                        })
3149                 .setNeutralButton(R.string.page_info_view,
3150                        new DialogInterface.OnClickListener() {
3151                            public void onClick(DialogInterface dialog,
3152                                    int whichButton) {
3153                                mSSLCertificateOnErrorDialog = null;
3154
3155                                // do not clear the dialog state: we will
3156                                // need to show the dialog again once the
3157                                // user is done exploring the page-info details
3158
3159                                showPageInfo(mTabControl.getTabFromView(view),
3160                                        true);
3161                            }
3162                        })
3163                .setOnCancelListener(
3164                        new DialogInterface.OnCancelListener() {
3165                            public void onCancel(DialogInterface dialog) {
3166                                mSSLCertificateOnErrorDialog = null;
3167                                mSSLCertificateOnErrorView = null;
3168                                mSSLCertificateOnErrorHandler = null;
3169                                mSSLCertificateOnErrorError = null;
3170
3171                                view.getWebViewClient().onReceivedSslError(
3172                                                view, handler, error);
3173                            }
3174                        })
3175                .show();
3176    }
3177
3178    /**
3179     * Inflates the SSL certificate view (helper method).
3180     * @param certificate The SSL certificate.
3181     * @return The resultant certificate view with issued-to, issued-by,
3182     * issued-on, expires-on, and possibly other fields set.
3183     * If the input certificate is null, returns null.
3184     */
3185    private View inflateCertificateView(SslCertificate certificate) {
3186        if (certificate == null) {
3187            return null;
3188        }
3189
3190        LayoutInflater factory = LayoutInflater.from(this);
3191
3192        View certificateView = factory.inflate(
3193            R.layout.ssl_certificate, null);
3194
3195        // issued to:
3196        SslCertificate.DName issuedTo = certificate.getIssuedTo();
3197        if (issuedTo != null) {
3198            ((TextView) certificateView.findViewById(R.id.to_common))
3199                .setText(issuedTo.getCName());
3200            ((TextView) certificateView.findViewById(R.id.to_org))
3201                .setText(issuedTo.getOName());
3202            ((TextView) certificateView.findViewById(R.id.to_org_unit))
3203                .setText(issuedTo.getUName());
3204        }
3205
3206        // issued by:
3207        SslCertificate.DName issuedBy = certificate.getIssuedBy();
3208        if (issuedBy != null) {
3209            ((TextView) certificateView.findViewById(R.id.by_common))
3210                .setText(issuedBy.getCName());
3211            ((TextView) certificateView.findViewById(R.id.by_org))
3212                .setText(issuedBy.getOName());
3213            ((TextView) certificateView.findViewById(R.id.by_org_unit))
3214                .setText(issuedBy.getUName());
3215        }
3216
3217        // issued on:
3218        String issuedOn = reformatCertificateDate(
3219            certificate.getValidNotBefore());
3220        ((TextView) certificateView.findViewById(R.id.issued_on))
3221            .setText(issuedOn);
3222
3223        // expires on:
3224        String expiresOn = reformatCertificateDate(
3225            certificate.getValidNotAfter());
3226        ((TextView) certificateView.findViewById(R.id.expires_on))
3227            .setText(expiresOn);
3228
3229        return certificateView;
3230    }
3231
3232    /**
3233     * Re-formats the certificate date (Date.toString()) string to
3234     * a properly localized date string.
3235     * @return Properly localized version of the certificate date string and
3236     * the original certificate date string if fails to localize.
3237     * If the original string is null, returns an empty string "".
3238     */
3239    private String reformatCertificateDate(String certificateDate) {
3240      String reformattedDate = null;
3241
3242      if (certificateDate != null) {
3243          Date date = null;
3244          try {
3245              date = java.text.DateFormat.getInstance().parse(certificateDate);
3246          } catch (ParseException e) {
3247              date = null;
3248          }
3249
3250          if (date != null) {
3251              reformattedDate =
3252                  DateFormat.getDateFormat(this).format(date);
3253          }
3254      }
3255
3256      return reformattedDate != null ? reformattedDate :
3257          (certificateDate != null ? certificateDate : "");
3258    }
3259
3260    /**
3261     * Displays an http-authentication dialog.
3262     */
3263    void showHttpAuthentication(final HttpAuthHandler handler,
3264            final String host, final String realm, final String title,
3265            final String name, final String password, int focusId) {
3266        LayoutInflater factory = LayoutInflater.from(this);
3267        final View v = factory
3268                .inflate(R.layout.http_authentication, null);
3269        if (name != null) {
3270            ((EditText) v.findViewById(R.id.username_edit)).setText(name);
3271        }
3272        if (password != null) {
3273            ((EditText) v.findViewById(R.id.password_edit)).setText(password);
3274        }
3275
3276        String titleText = title;
3277        if (titleText == null) {
3278            titleText = getText(R.string.sign_in_to).toString().replace(
3279                    "%s1", host).replace("%s2", realm);
3280        }
3281
3282        mHttpAuthHandler = handler;
3283        AlertDialog dialog = new AlertDialog.Builder(this)
3284                .setTitle(titleText)
3285                .setIcon(android.R.drawable.ic_dialog_alert)
3286                .setView(v)
3287                .setPositiveButton(R.string.action,
3288                        new DialogInterface.OnClickListener() {
3289                             public void onClick(DialogInterface dialog,
3290                                     int whichButton) {
3291                                String nm = ((EditText) v
3292                                        .findViewById(R.id.username_edit))
3293                                        .getText().toString();
3294                                String pw = ((EditText) v
3295                                        .findViewById(R.id.password_edit))
3296                                        .getText().toString();
3297                                BrowserActivity.this.setHttpAuthUsernamePassword
3298                                        (host, realm, nm, pw);
3299                                handler.proceed(nm, pw);
3300                                mHttpAuthenticationDialog = null;
3301                                mHttpAuthHandler = null;
3302                            }})
3303                .setNegativeButton(R.string.cancel,
3304                        new DialogInterface.OnClickListener() {
3305                            public void onClick(DialogInterface dialog,
3306                                    int whichButton) {
3307                                handler.cancel();
3308                                BrowserActivity.this.resetTitleAndRevertLockIcon();
3309                                mHttpAuthenticationDialog = null;
3310                                mHttpAuthHandler = null;
3311                            }})
3312                .setOnCancelListener(new DialogInterface.OnCancelListener() {
3313                        public void onCancel(DialogInterface dialog) {
3314                            handler.cancel();
3315                            BrowserActivity.this.resetTitleAndRevertLockIcon();
3316                            mHttpAuthenticationDialog = null;
3317                            mHttpAuthHandler = null;
3318                        }})
3319                .create();
3320        // Make the IME appear when the dialog is displayed if applicable.
3321        dialog.getWindow().setSoftInputMode(
3322                WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
3323        dialog.show();
3324        if (focusId != 0) {
3325            dialog.findViewById(focusId).requestFocus();
3326        } else {
3327            v.findViewById(R.id.username_edit).requestFocus();
3328        }
3329        mHttpAuthenticationDialog = dialog;
3330    }
3331
3332    public int getProgress() {
3333        WebView w = mTabControl.getCurrentWebView();
3334        if (w != null) {
3335            return w.getProgress();
3336        } else {
3337            return 100;
3338        }
3339    }
3340
3341    /**
3342     * Set HTTP authentication password.
3343     *
3344     * @param host The host for the password
3345     * @param realm The realm for the password
3346     * @param username The username for the password. If it is null, it means
3347     *            password can't be saved.
3348     * @param password The password
3349     */
3350    public void setHttpAuthUsernamePassword(String host, String realm,
3351                                            String username,
3352                                            String password) {
3353        WebView w = mTabControl.getCurrentWebView();
3354        if (w != null) {
3355            w.setHttpAuthUsernamePassword(host, realm, username, password);
3356        }
3357    }
3358
3359    /**
3360     * connectivity manager says net has come or gone... inform the user
3361     * @param up true if net has come up, false if net has gone down
3362     */
3363    public void onNetworkToggle(boolean up) {
3364        if (up == mIsNetworkUp) {
3365            return;
3366        } else if (up) {
3367            mIsNetworkUp = true;
3368            if (mAlertDialog != null) {
3369                mAlertDialog.cancel();
3370                mAlertDialog = null;
3371            }
3372        } else {
3373            mIsNetworkUp = false;
3374            if (mInLoad) {
3375                createAndShowNetworkDialog();
3376           }
3377        }
3378        WebView w = mTabControl.getCurrentWebView();
3379        if (w != null) {
3380            w.setNetworkAvailable(up);
3381        }
3382    }
3383
3384    boolean isNetworkUp() {
3385        return mIsNetworkUp;
3386    }
3387
3388    // This method shows the network dialog alerting the user that the net is
3389    // down. It will only show the dialog if mAlertDialog is null.
3390    private void createAndShowNetworkDialog() {
3391        if (mAlertDialog == null) {
3392            mAlertDialog = new AlertDialog.Builder(this)
3393                    .setTitle(R.string.loadSuspendedTitle)
3394                    .setMessage(R.string.loadSuspended)
3395                    .setPositiveButton(R.string.ok, null)
3396                    .show();
3397        }
3398    }
3399
3400    @Override
3401    protected void onActivityResult(int requestCode, int resultCode,
3402                                    Intent intent) {
3403        if (getTopWindow() == null) return;
3404
3405        switch (requestCode) {
3406            case COMBO_PAGE:
3407                if (resultCode == RESULT_OK && intent != null) {
3408                    String data = intent.getAction();
3409                    Bundle extras = intent.getExtras();
3410                    if (extras != null && extras.getBoolean("new_window", false)) {
3411                        openTab(data);
3412                    } else {
3413                        final Tab currentTab =
3414                                mTabControl.getCurrentTab();
3415                        dismissSubWindow(currentTab);
3416                        if (data != null && data.length() != 0) {
3417                            getTopWindow().loadUrl(data);
3418                        }
3419                    }
3420                }
3421                // Deliberately fall through to PREFERENCES_PAGE, since the
3422                // same extra may be attached to the COMBO_PAGE
3423            case PREFERENCES_PAGE:
3424                if (resultCode == RESULT_OK && intent != null) {
3425                    String action = intent.getStringExtra(Intent.EXTRA_TEXT);
3426                    if (BrowserSettings.PREF_CLEAR_HISTORY.equals(action)) {
3427                        mTabControl.removeParentChildRelationShips();
3428                    }
3429                }
3430                break;
3431            // Choose a file from the file picker.
3432            case FILE_SELECTED:
3433                if (null == mUploadMessage) break;
3434                Uri result = intent == null || resultCode != RESULT_OK ? null
3435                        : intent.getData();
3436                mUploadMessage.onReceiveValue(result);
3437                mUploadMessage = null;
3438                break;
3439            default:
3440                break;
3441        }
3442        getTopWindow().requestFocus();
3443    }
3444
3445    /*
3446     * This method is called as a result of the user selecting the options
3447     * menu to see the download window, or when a download changes state. It
3448     * shows the download window ontop of the current window.
3449     */
3450    /* package */ void viewDownloads(Uri downloadRecord) {
3451        Intent intent = new Intent(this,
3452                BrowserDownloadPage.class);
3453        intent.setData(downloadRecord);
3454        startActivityForResult(intent, BrowserActivity.DOWNLOAD_PAGE);
3455
3456    }
3457
3458    /**
3459     * Open the Go page.
3460     * @param startWithHistory If true, open starting on the history tab.
3461     *                         Otherwise, start with the bookmarks tab.
3462     */
3463    /* package */ void bookmarksOrHistoryPicker(boolean startWithHistory) {
3464        WebView current = mTabControl.getCurrentWebView();
3465        if (current == null) {
3466            return;
3467        }
3468        Intent intent = new Intent(this,
3469                CombinedBookmarkHistoryActivity.class);
3470        String title = current.getTitle();
3471        String url = current.getUrl();
3472        Bitmap thumbnail = createScreenshot(current);
3473
3474        // Just in case the user opens bookmarks before a page finishes loading
3475        // so the current history item, and therefore the page, is null.
3476        if (null == url) {
3477            url = mLastEnteredUrl;
3478            // This can happen.
3479            if (null == url) {
3480                url = mSettings.getHomePage();
3481            }
3482        }
3483        // In case the web page has not yet received its associated title.
3484        if (title == null) {
3485            title = url;
3486        }
3487        intent.putExtra("title", title);
3488        intent.putExtra("url", url);
3489        intent.putExtra("thumbnail", thumbnail);
3490        // Disable opening in a new window if we have maxed out the windows
3491        intent.putExtra("disable_new_window", !mTabControl.canCreateNewTab());
3492        intent.putExtra("touch_icon_url", current.getTouchIconUrl());
3493        if (startWithHistory) {
3494            intent.putExtra(CombinedBookmarkHistoryActivity.STARTING_TAB,
3495                    CombinedBookmarkHistoryActivity.HISTORY_TAB);
3496        }
3497        startActivityForResult(intent, COMBO_PAGE);
3498    }
3499
3500    // Called when loading from context menu or LOAD_URL message
3501    private void loadURL(WebView view, String url) {
3502        // In case the user enters nothing.
3503        if (url != null && url.length() != 0 && view != null) {
3504            url = smartUrlFilter(url);
3505            if (!view.getWebViewClient().shouldOverrideUrlLoading(view, url)) {
3506                view.loadUrl(url);
3507            }
3508        }
3509    }
3510
3511    private String smartUrlFilter(Uri inUri) {
3512        if (inUri != null) {
3513            return smartUrlFilter(inUri.toString());
3514        }
3515        return null;
3516    }
3517
3518    protected static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
3519            "(?i)" + // switch on case insensitive matching
3520            "(" +    // begin group for schema
3521            "(?:http|https|file):\\/\\/" +
3522            "|(?:inline|data|about|content|javascript):" +
3523            ")" +
3524            "(.*)" );
3525
3526    /**
3527     * Attempts to determine whether user input is a URL or search
3528     * terms.  Anything with a space is passed to search.
3529     *
3530     * Converts to lowercase any mistakenly uppercased schema (i.e.,
3531     * "Http://" converts to "http://"
3532     *
3533     * @return Original or modified URL
3534     *
3535     */
3536    String smartUrlFilter(String url) {
3537
3538        String inUrl = url.trim();
3539        boolean hasSpace = inUrl.indexOf(' ') != -1;
3540
3541        Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl);
3542        if (matcher.matches()) {
3543            // force scheme to lowercase
3544            String scheme = matcher.group(1);
3545            String lcScheme = scheme.toLowerCase();
3546            if (!lcScheme.equals(scheme)) {
3547                inUrl = lcScheme + matcher.group(2);
3548            }
3549            if (hasSpace) {
3550                inUrl = inUrl.replace(" ", "%20");
3551            }
3552            return inUrl;
3553        }
3554        if (hasSpace) {
3555            // FIXME: Is this the correct place to add to searches?
3556            // what if someone else calls this function?
3557            int shortcut = parseUrlShortcut(inUrl);
3558            if (shortcut != SHORTCUT_INVALID) {
3559                Browser.addSearchUrl(mResolver, inUrl);
3560                String query = inUrl.substring(2);
3561                switch (shortcut) {
3562                case SHORTCUT_GOOGLE_SEARCH:
3563                    return URLUtil.composeSearchUrl(query, QuickSearch_G, QUERY_PLACE_HOLDER);
3564                case SHORTCUT_WIKIPEDIA_SEARCH:
3565                    return URLUtil.composeSearchUrl(query, QuickSearch_W, QUERY_PLACE_HOLDER);
3566                case SHORTCUT_DICTIONARY_SEARCH:
3567                    return URLUtil.composeSearchUrl(query, QuickSearch_D, QUERY_PLACE_HOLDER);
3568                case SHORTCUT_GOOGLE_MOBILE_LOCAL_SEARCH:
3569                    // FIXME: we need location in this case
3570                    return URLUtil.composeSearchUrl(query, QuickSearch_L, QUERY_PLACE_HOLDER);
3571                }
3572            }
3573        } else {
3574            if (Patterns.WEB_URL.matcher(inUrl).matches()) {
3575                return URLUtil.guessUrl(inUrl);
3576            }
3577        }
3578
3579        Browser.addSearchUrl(mResolver, inUrl);
3580        return URLUtil.composeSearchUrl(inUrl, QuickSearch_G, QUERY_PLACE_HOLDER);
3581    }
3582
3583    /* package */ void setShouldShowErrorConsole(boolean flag) {
3584        if (flag == mShouldShowErrorConsole) {
3585            // Nothing to do.
3586            return;
3587        }
3588
3589        mShouldShowErrorConsole = flag;
3590
3591        ErrorConsoleView errorConsole = mTabControl.getCurrentTab()
3592                .getErrorConsole(true);
3593
3594        if (flag) {
3595            // Setting the show state of the console will cause it's the layout to be inflated.
3596            if (errorConsole.numberOfErrors() > 0) {
3597                errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
3598            } else {
3599                errorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
3600            }
3601
3602            // Now we can add it to the main view.
3603            mErrorConsoleContainer.addView(errorConsole,
3604                    new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
3605                                                  ViewGroup.LayoutParams.WRAP_CONTENT));
3606        } else {
3607            mErrorConsoleContainer.removeView(errorConsole);
3608        }
3609
3610    }
3611
3612    boolean shouldShowErrorConsole() {
3613        return mShouldShowErrorConsole;
3614    }
3615
3616    private void setStatusBarVisibility(boolean visible) {
3617        int flag = visible ? 0 : WindowManager.LayoutParams.FLAG_FULLSCREEN;
3618        getWindow().setFlags(flag, WindowManager.LayoutParams.FLAG_FULLSCREEN);
3619    }
3620
3621    final static int LOCK_ICON_UNSECURE = 0;
3622    final static int LOCK_ICON_SECURE   = 1;
3623    final static int LOCK_ICON_MIXED    = 2;
3624
3625    private BrowserSettings mSettings;
3626    private TabControl      mTabControl;
3627    private ContentResolver mResolver;
3628    private FrameLayout     mContentView;
3629    private View            mCustomView;
3630    private FrameLayout     mCustomViewContainer;
3631    private WebChromeClient.CustomViewCallback mCustomViewCallback;
3632
3633    // FIXME, temp address onPrepareMenu performance problem. When we move everything out of
3634    // view, we should rewrite this.
3635    private int mCurrentMenuState = 0;
3636    private int mMenuState = R.id.MAIN_MENU;
3637    private int mOldMenuState = EMPTY_MENU;
3638    private static final int EMPTY_MENU = -1;
3639    private Menu mMenu;
3640
3641    private FindDialog mFindDialog;
3642    // Used to prevent chording to result in firing two shortcuts immediately
3643    // one after another.  Fixes bug 1211714.
3644    boolean mCanChord;
3645
3646    private boolean mInLoad;
3647    private boolean mIsNetworkUp;
3648    private boolean mDidStopLoad;
3649
3650    private boolean mActivityInPause = true;
3651
3652    private boolean mMenuIsDown;
3653
3654    private static boolean mInTrace;
3655
3656    // Performance probe
3657    private static final int[] SYSTEM_CPU_FORMAT = new int[] {
3658            Process.PROC_SPACE_TERM | Process.PROC_COMBINE,
3659            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 1: user time
3660            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 2: nice time
3661            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 3: sys time
3662            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 4: idle time
3663            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 5: iowait time
3664            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 6: irq time
3665            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG  // 7: softirq time
3666    };
3667
3668    private long mStart;
3669    private long mProcessStart;
3670    private long mUserStart;
3671    private long mSystemStart;
3672    private long mIdleStart;
3673    private long mIrqStart;
3674
3675    private long mUiStart;
3676
3677    private Drawable    mMixLockIcon;
3678    private Drawable    mSecLockIcon;
3679
3680    /* hold a ref so we can auto-cancel if necessary */
3681    private AlertDialog mAlertDialog;
3682
3683    // Wait for credentials before loading google.com
3684    private ProgressDialog mCredsDlg;
3685
3686    // The up-to-date URL and title (these can be different from those stored
3687    // in WebView, since it takes some time for the information in WebView to
3688    // get updated)
3689    private String mUrl;
3690    private String mTitle;
3691
3692    // As PageInfo has different style for landscape / portrait, we have
3693    // to re-open it when configuration changed
3694    private AlertDialog mPageInfoDialog;
3695    private Tab mPageInfoView;
3696    // If the Page-Info dialog is launched from the SSL-certificate-on-error
3697    // dialog, we should not just dismiss it, but should get back to the
3698    // SSL-certificate-on-error dialog. This flag is used to store this state
3699    private boolean mPageInfoFromShowSSLCertificateOnError;
3700
3701    // as SSLCertificateOnError has different style for landscape / portrait,
3702    // we have to re-open it when configuration changed
3703    private AlertDialog mSSLCertificateOnErrorDialog;
3704    private WebView mSSLCertificateOnErrorView;
3705    private SslErrorHandler mSSLCertificateOnErrorHandler;
3706    private SslError mSSLCertificateOnErrorError;
3707
3708    // as SSLCertificate has different style for landscape / portrait, we
3709    // have to re-open it when configuration changed
3710    private AlertDialog mSSLCertificateDialog;
3711    private Tab mSSLCertificateView;
3712
3713    // as HttpAuthentication has different style for landscape / portrait, we
3714    // have to re-open it when configuration changed
3715    private AlertDialog mHttpAuthenticationDialog;
3716    private HttpAuthHandler mHttpAuthHandler;
3717
3718    /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_PARAMS =
3719                                            new FrameLayout.LayoutParams(
3720                                            ViewGroup.LayoutParams.MATCH_PARENT,
3721                                            ViewGroup.LayoutParams.MATCH_PARENT);
3722    /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER =
3723                                            new FrameLayout.LayoutParams(
3724                                            ViewGroup.LayoutParams.MATCH_PARENT,
3725                                            ViewGroup.LayoutParams.MATCH_PARENT,
3726                                            Gravity.CENTER);
3727    // Google search
3728    final static String QuickSearch_G = "http://www.google.com/m?q=%s";
3729    // Wikipedia search
3730    final static String QuickSearch_W = "http://en.wikipedia.org/w/index.php?search=%s&go=Go";
3731    // Dictionary search
3732    final static String QuickSearch_D = "http://dictionary.reference.com/search?q=%s";
3733    // Google Mobile Local search
3734    final static String QuickSearch_L = "http://www.google.com/m/search?site=local&q=%s&near=mountain+view";
3735
3736    final static String QUERY_PLACE_HOLDER = "%s";
3737
3738    // "source" parameter for Google search through search key
3739    final static String GOOGLE_SEARCH_SOURCE_SEARCHKEY = "browser-key";
3740    // "source" parameter for Google search through goto menu
3741    final static String GOOGLE_SEARCH_SOURCE_GOTO = "browser-goto";
3742    // "source" parameter for Google search through simplily type
3743    final static String GOOGLE_SEARCH_SOURCE_TYPE = "browser-type";
3744    // "source" parameter for Google search suggested by the browser
3745    final static String GOOGLE_SEARCH_SOURCE_SUGGEST = "browser-suggest";
3746    // "source" parameter for Google search from unknown source
3747    final static String GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown";
3748
3749    private final static String LOGTAG = "browser";
3750
3751    private String mLastEnteredUrl;
3752
3753    private PowerManager.WakeLock mWakeLock;
3754    private final static int WAKELOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes
3755
3756    private Toast mStopToast;
3757
3758    private TitleBar mTitleBar;
3759
3760    private LinearLayout mErrorConsoleContainer = null;
3761    private boolean mShouldShowErrorConsole = false;
3762
3763    // As the ids are dynamically created, we can't guarantee that they will
3764    // be in sequence, so this static array maps ids to a window number.
3765    final static private int[] WINDOW_SHORTCUT_ID_ARRAY =
3766    { R.id.window_one_menu_id, R.id.window_two_menu_id, R.id.window_three_menu_id,
3767      R.id.window_four_menu_id, R.id.window_five_menu_id, R.id.window_six_menu_id,
3768      R.id.window_seven_menu_id, R.id.window_eight_menu_id };
3769
3770    // monitor platform changes
3771    private IntentFilter mNetworkStateChangedFilter;
3772    private BroadcastReceiver mNetworkStateIntentReceiver;
3773
3774    private BroadcastReceiver mPackageInstallationReceiver;
3775
3776    // activity requestCode
3777    final static int COMBO_PAGE                 = 1;
3778    final static int DOWNLOAD_PAGE              = 2;
3779    final static int PREFERENCES_PAGE           = 3;
3780    final static int FILE_SELECTED              = 4;
3781
3782    // the default <video> poster
3783    private Bitmap mDefaultVideoPoster;
3784    // the video progress view
3785    private View mVideoProgressView;
3786
3787    /**
3788     * A UrlData class to abstract how the content will be set to WebView.
3789     * This base class uses loadUrl to show the content.
3790     */
3791    private static class UrlData {
3792        String mUrl;
3793        byte[] mPostData;
3794
3795        UrlData(String url) {
3796            this.mUrl = url;
3797        }
3798
3799        void setPostData(byte[] postData) {
3800            mPostData = postData;
3801        }
3802
3803        boolean isEmpty() {
3804            return mUrl == null || mUrl.length() == 0;
3805        }
3806
3807        public void loadIn(WebView webView) {
3808            if (mPostData != null) {
3809                webView.postUrl(mUrl, mPostData);
3810            } else {
3811                webView.loadUrl(mUrl);
3812            }
3813        }
3814    };
3815
3816    /**
3817     * A subclass of UrlData class that can display inlined content using
3818     * {@link WebView#loadDataWithBaseURL(String, String, String, String, String)}.
3819     */
3820    private static class InlinedUrlData extends UrlData {
3821        InlinedUrlData(String inlined, String mimeType, String encoding, String failUrl) {
3822            super(failUrl);
3823            mInlined = inlined;
3824            mMimeType = mimeType;
3825            mEncoding = encoding;
3826        }
3827        String mMimeType;
3828        String mInlined;
3829        String mEncoding;
3830        @Override
3831        boolean isEmpty() {
3832            return mInlined == null || mInlined.length() == 0 || super.isEmpty();
3833        }
3834
3835        @Override
3836        public void loadIn(WebView webView) {
3837            webView.loadDataWithBaseURL(null, mInlined, mMimeType, mEncoding, mUrl);
3838        }
3839    }
3840
3841    /* package */ static final UrlData EMPTY_URL_DATA = new UrlData(null);
3842}
3843